agent-body-filler.js
6 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
/**
* 招聘后同进程调 agent-creator 填自己的定义文件——带互斥锁 + 有限重试 + 失败落地。
*
* 32 号文档方案 B 的落地。修的是 `company.js` hire 路由原来那发
* `fetch('http://127.0.0.1:${PORT}/api/agent/agent-creator/chat', ...).catch(...)`:
* fire-and-forget,`.catch` 只接网络层错误,不检查响应状态码;agent-creator 是单会话,
* 撞车时 `/api/agent/:agentId/chat` 返回 429,被原样当成"成功"吞掉——新招的角色永久停在
* base 空壳 + `status=training`,唯一痕迹是一行 console.error(撞车走的是 429 正常响应,
* 连这行都不会触发)。
*
* 这里改成同进程直接调 `AgentManager.callAgent`,绕开那次多余的 HTTP 回环,换来:
* - 真实的调用结果/异常,而不是网络层 `.catch`;
* - 撞车时**等一等再试**(有限次数 + 退避),不是当场丢弃;
* - 重试耗尽后在本体目录写一个哨兵文件,而不是一行日志——不动
* `system_agents.status`/`config.json.status`(那两个字段还有 draft→active 的
* 毕业逻辑在读,改它们的取值域风险和收益不对等,见 32 号文档 §四.3)。
*
* ⚠️ 互斥锁复用的是 `agent-session-lock.js`,跟 `routes/agent-chat.js` 的
* `POST /:agentId/chat` 共享同一个 Map——不能各起一把。原因见该模块头注:
* `SessionPool.getOrCreate()` 不检查上一次调用有没有跑完,两条路径各自以为自己
* 没有并发的话,会把第二轮 prompt 发进一个还在处理中的 CLI 会话,比现在的
* "被拒绝丢弃"更危险(协议层内容串台,未定义行为)。
*/
const fs = require('fs');
const path = require('path');
const locator = require('./agent-locator');
const { agentChatLocks, AGENT_LOCK_TIMEOUT } = require('../src/core/agent-session-lock');
const MAX_ATTEMPTS = 3;
// 重试之间的等待——不是精算出来的数字,是"给撞车的那一方一点时间跑完"的量级;
// 真正的硬顶是 MAX_ATTEMPTS,不是靠拉长等待时间去覆盖更长的撞车窗口。
const RETRY_DELAY_MS = [5_000, 15_000];
const FAILURE_MARKER_NAME = '.body_fill_failed.json';
function sleep(ms) {
return new Promise((resolve) => { setTimeout(resolve, ms); });
}
function failureMarkerPath(bodyDir) {
return path.join(bodyDir, FAILURE_MARKER_NAME);
}
/** 重试耗尽后落地——**必须能被查询/扫描到**,这是 32 号文档方案 B 的唯一硬要求。 */
function writeFailureMarker(bodyDir, info) {
try {
fs.writeFileSync(failureMarkerPath(bodyDir), JSON.stringify({
...info,
failedAt: new Date().toISOString(),
}, null, 2));
} catch (e) {
// 连哨兵文件都写不出去(目录权限/磁盘满)——这一步再失败也不能让调用方炸,
// 招聘流程本身(scaffoldNewAgent 已经建好目录)不该被这个次要写入拖下水。
console.error(`[BodyFill] ⚠️ 写失败标记本身也失败了: ${e.message}`);
}
}
/** 成功之后必须清掉上一轮可能留下的失败标记——不然人工重试成功了,哨兵文件还在骗人。 */
function clearFailureMarker(bodyDir) {
try {
fs.unlinkSync(failureMarkerPath(bodyDir));
} catch {
// 本来就没有这个文件(多数情况)——不是错误
}
}
/**
* @param {object} a
* @param {object} a.agentManager `AgentManager` 实例(调用方传入,不在这里 new——
* 测试时可以注入假实现,不用真起 Claude CLI)
* @param {string} a.targetAgentId 要填定义的那个新招角色 id(== 本体目录名)
* @param {string} a.message 发给 agent-creator 的完整 brief(调用方已经拼好,
* 这个函数不关心内容,只关心「发出去、等结果、失败重试」)
* @returns {Promise<{ok: boolean, attempts: number, error?: string}>}
*/
async function fillAgentBodyReliably({ agentManager, targetAgentId, message }) {
const bodyDir = locator.bodyDir(targetAgentId);
let lastError = null;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const existingLock = agentChatLocks.get('agent-creator');
if (existingLock && Date.now() - existingLock.startedAt < AGENT_LOCK_TIMEOUT) {
// 忙——等一等,不占位、不硬抢、不当场放弃。这是原来那发 fetch 唯一缺的东西。
console.log(`[BodyFill] 🔒 agent-creator 正忙(conv ${existingLock.convId}),` +
`第 ${attempt}/${MAX_ATTEMPTS} 次尝试改为等待`);
lastError = new Error(`agent-creator 正忙(活跃会话 ${existingLock.convId})`);
if (attempt < MAX_ATTEMPTS) await sleep(RETRY_DELAY_MS[attempt - 1] ?? RETRY_DELAY_MS.at(-1));
continue;
}
const convId = `body-fill-${targetAgentId}-a${attempt}-${Date.now().toString(36)}`;
agentChatLocks.set('agent-creator', { convId, startedAt: Date.now() });
try {
await agentManager.callAgent('agent-creator', message, {
metadata: { taskId: convId },
});
clearFailureMarker(bodyDir);
return { ok: true, attempts: attempt };
} catch (err) {
lastError = err;
console.error(`[BodyFill] ⚠️ agent-creator 第 ${attempt}/${MAX_ATTEMPTS} 次调用失败: ${err.message}`);
if (attempt < MAX_ATTEMPTS) await sleep(RETRY_DELAY_MS[attempt - 1] ?? RETRY_DELAY_MS.at(-1));
} finally {
// 只释放"自己那一把"——万一在途期间锁已经被别的机制清过又被第三方占了,
// 不能凭空把第三方的锁摘掉(同 agent-chat.js releaseLock 的判据)。
const cur = agentChatLocks.get('agent-creator');
if (cur && cur.convId === convId) agentChatLocks.delete('agent-creator');
}
}
writeFailureMarker(bodyDir, {
targetAgentId,
attempts: MAX_ATTEMPTS,
lastError: lastError ? lastError.message : null,
});
return { ok: false, attempts: MAX_ATTEMPTS, error: lastError ? lastError.message : null };
}
module.exports = { fillAgentBodyReliably, FAILURE_MARKER_NAME };