@bolloon/bolloon-agent 0.4.25 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/execution-supervisor.js +57 -2
- package/dist/agents/external-events.js +162 -0
- package/dist/agents/goal-criteria.js +124 -0
- package/dist/agents/goal-store.js +79 -4
- package/dist/agents/pi-sdk.js +39 -0
- package/dist/agents/skill-readiness.js +133 -0
- package/dist/agents/skill-supervisor-link.js +70 -0
- package/dist/agents/skills-manager.js +282 -0
- package/dist/cli/setup-wizard.js +96 -127
- package/dist/electron/first-run.js +33 -2
- package/dist/electron-build/electron/first-run.js +35 -2
- package/dist/electron-build/electron/first-run.js.map +1 -1
- package/dist/index.js +121 -4
- package/dist/llm/config-store.js +35 -4
- package/dist/network/agent-network.js +10 -0
- package/dist/network/goal-event-bridge.js +57 -0
- package/dist/setup/onboard.js +549 -0
- package/dist/setup/setup-store.js +592 -0
- package/dist/web/server.js +248 -1
- package/package.json +1 -1
|
@@ -68,7 +68,7 @@ export function decideGoalOutcome(goal, run, opts = {}) {
|
|
|
68
68
|
reason: `运行被中止 (${run.errorClass || run.error || '预算/人工'}): 目标仍 active, 交给下一个 Run 继续`,
|
|
69
69
|
};
|
|
70
70
|
case 'done': {
|
|
71
|
-
const verdict = evaluateGoalCompletion(goal);
|
|
71
|
+
const verdict = evaluateGoalCompletion(goal, { lastRunStatus: opts.lastRunStatus || run.status });
|
|
72
72
|
if (verdict.complete) {
|
|
73
73
|
return {
|
|
74
74
|
goalStatus: 'completed',
|
|
@@ -205,6 +205,18 @@ export class ExecutionSupervisor {
|
|
|
205
205
|
this.log(`[supervisor] 对账: ${report.reconciled.interrupted.length} 条僵尸 run → interrupted`);
|
|
206
206
|
}
|
|
207
207
|
report.supervised = await superviseRuns();
|
|
208
|
+
// 1.5 (2026-09-16, 2-C.4): 外部等待超时 → 明确转人工 (不允许无限等待)
|
|
209
|
+
try {
|
|
210
|
+
const { expireExternalWaits } = await import('./external-events.js');
|
|
211
|
+
const expired = await expireExternalWaits({ now: this.now() });
|
|
212
|
+
for (const e of expired) {
|
|
213
|
+
this.log(`[supervisor] 外部等待超时 → needs_human: ${e.goalId} (${e.wait.expectedSource})`);
|
|
214
|
+
this.emit({ kind: 'needs_human', goalId: e.goalId, message: e.reason });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
this.log(`[supervisor] 外部等待超时检查失败: ${err?.message}`);
|
|
219
|
+
}
|
|
208
220
|
// 2. 扫描可执行 Goal
|
|
209
221
|
const { runnable, skipped } = await listRunnableGoals({ now: this.now(), owner: this.owner });
|
|
210
222
|
report.skipped = skipped;
|
|
@@ -303,6 +315,27 @@ export class ExecutionSupervisor {
|
|
|
303
315
|
report.skipped.push({ goalId: goal.goalId, reason: '无执行器 (Goal 状态未改动)' });
|
|
304
316
|
return { goalId: goal.goalId, status: 'unresolved' };
|
|
305
317
|
}
|
|
318
|
+
// 2026-09-16 (2-G.2): 执行前技能就绪门禁 —— 缺/未启用/损坏/漂移 → 不启动 Run, Goal → needs_human
|
|
319
|
+
try {
|
|
320
|
+
const { ensureGoalSkillsReady, blockGoalOnSkills } = await import('./skill-readiness.js');
|
|
321
|
+
const ready = await ensureGoalSkillsReady(goal);
|
|
322
|
+
for (const d of ready.degradations)
|
|
323
|
+
this.log(`[supervisor] goal=${goal.goalId} 技能降级: ${d}`);
|
|
324
|
+
if (!ready.ok) {
|
|
325
|
+
await blockGoalOnSkills(goal.goalId, ready);
|
|
326
|
+
report.skipped.push({ goalId: goal.goalId, reason: `技能未就绪: ${ready.reason}` });
|
|
327
|
+
this.emit({ kind: 'needs_human', goalId: goal.goalId, message: ready.reason || '技能未就绪' });
|
|
328
|
+
this.log(`[supervisor] goal=${goal.goalId} 技能门禁拦截 → needs_human (未启动 Run): ${ready.reason}`);
|
|
329
|
+
return { goalId: goal.goalId, runId: prevRunId, status: 'blocked_by_skills', error: ready.reason };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
// 门禁自身失败 → fail-closed: 不执行 (宁可停, 不用未校验的技能跑)
|
|
334
|
+
const why = `技能门禁自身失败 (fail-closed, 未执行): ${String(err?.message || err).slice(0, 140)}`;
|
|
335
|
+
report.skipped.push({ goalId: goal.goalId, reason: why });
|
|
336
|
+
this.log(`[supervisor] ${why}`);
|
|
337
|
+
return { goalId: goal.goalId, runId: prevRunId, status: 'blocked_by_skills', error: why };
|
|
338
|
+
}
|
|
306
339
|
// 执行期间持续续租: 续租失败 = 已被别人接管 → 记录 (不掩盖)
|
|
307
340
|
const hb = setInterval(() => {
|
|
308
341
|
void heartbeatGoal(goal.goalId, leaseId, this.leaseTtlMs).then((r) => {
|
|
@@ -329,7 +362,28 @@ export class ExecutionSupervisor {
|
|
|
329
362
|
// 新 Run 必须挂在同一 Goal 下; 没挂上就是执行器没接住 goalId → 如实记, 不掩盖
|
|
330
363
|
report.errors.push(`${goal.goalId}: 新 Run ${finalRunId} 未绑定本 Goal (goalId=${finalRun.goalId || '空'})`);
|
|
331
364
|
}
|
|
332
|
-
|
|
365
|
+
// 2026-09-16 (2-F): 决策前先做两件事 —— ① 跨 Run 汇总证据; ② 没判据就提候选 (未确认 → 不许完成)。
|
|
366
|
+
// 放在决策之前, 因为"有没有判据/判据是否被满足"正是决策的输入 (之前放在 completed 分支里 = 永远轮不到)。
|
|
367
|
+
try {
|
|
368
|
+
const { aggregateEvidence, proposeForGoal } = await import('./goal-criteria.js');
|
|
369
|
+
await aggregateEvidence(goal.goalId);
|
|
370
|
+
const refreshedBefore = await readGoal(goal.goalId);
|
|
371
|
+
// 只在"这一轮正常跑完"时提候选判据 —— 失败/中断要留给 retry 退避逻辑, 不能把重试变成"交人"
|
|
372
|
+
const ranClean = String(finalRun?.status || '') === 'done';
|
|
373
|
+
if (ranClean && refreshedBefore && !refreshedBefore.successCriteria.length && !['completed', 'failed', 'abandoned'].includes(refreshedBefore.status)) {
|
|
374
|
+
const p = await proposeForGoal(goal.goalId);
|
|
375
|
+
if (p.ok)
|
|
376
|
+
this.log(`[supervisor] goal=${goal.goalId} 已提候选判据 (待确认, 未确认前不会判完成)`);
|
|
377
|
+
else
|
|
378
|
+
this.log(`[supervisor] goal=${goal.goalId} 判据生成失败 → 交人: ${p.reason}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
this.log(`[supervisor] 证据/判据处理失败: ${err?.message}`);
|
|
383
|
+
}
|
|
384
|
+
// 重新读一次 Goal: Run 期间判据可能已被满足 (否则会拿旧快照判决)
|
|
385
|
+
const goalForDecision = (await readGoal(goal.goalId)) || goal;
|
|
386
|
+
const decision = decideGoalOutcome(goalForDecision, finalRun, { now: this.now(), maxAttempts: this.maxRetries, lastRunStatus: finalRun?.status });
|
|
333
387
|
await this.applyDecision(goal, decision, finalRun);
|
|
334
388
|
this.emit({ kind: 'goal_decision', goalId: goal.goalId, runId: finalRunId, message: `${finalRun?.status || result.status || '?'} → ${decision.goalStatus}: ${decision.reason}` });
|
|
335
389
|
this.log(`[supervisor] goal=${goal.goalId} run=${finalRunId || '-'} ${finalRun?.status || result.status || '?'} → goal=${decision.goalStatus} (${decision.reason}) ${Date.now() - t0}ms`);
|
|
@@ -346,6 +400,7 @@ export class ExecutionSupervisor {
|
|
|
346
400
|
}
|
|
347
401
|
// 唯一完成出口: 只有经 completeGoalIfEligible 才能把 Goal 判成 completed
|
|
348
402
|
if (decision.goalStatus === 'completed') {
|
|
403
|
+
// 完成门 (判据存在 + 已确认 + 全满足 + 有证据 + 无未解决项 + 最近 Run 健康)
|
|
349
404
|
const r = await completeGoalIfEligible(goal.goalId);
|
|
350
405
|
if (!r.ok) {
|
|
351
406
|
// 完成门拒绝 → 如实退回 active, 并保留原因 (不许装作完成)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* external-events.ts — 外部等待 / 外部事件唤醒协议 (批次 2-C.4, 2026-09-16)
|
|
3
|
+
*
|
|
4
|
+
* 问题: `notifyExternal()` 之前只是个手动入口 (CLI `/wake`、`POST /api/goals/:id/wake`) ——
|
|
5
|
+
* 真实 P2P / delegate 回包到达时**没有任何东西**知道该唤醒哪个 Goal, 也没有来源/关联/去重/过期校验。
|
|
6
|
+
*
|
|
7
|
+
* 协议 (与 leo 的规格 1:1):
|
|
8
|
+
* Goal 在等待时必须绑定: externalRequestId · continuationId · expectedSource · expectedEvent · createdAt · expiresAt
|
|
9
|
+
* 收到事件时必须依次: ① 校验来源 ② 校验 correlation ③ 校验属于当前 continuation
|
|
10
|
+
* ④ 用 eventId 去重 ⑤ 写入事件事实 (Goal 证据) ⑥ 唤醒 Goal ⑦ 由 Supervisor 下一轮继续
|
|
11
|
+
* **事件处理器不直接启动 agent** —— 它只写事实 + 唤醒; 谁来执行由 Supervisor 决定。
|
|
12
|
+
*/
|
|
13
|
+
import { readGoal, setContinuation, updateGoal, listGoals, addEvidence, } from './goal-store.js';
|
|
14
|
+
// ── 绑定等待 ────────────────────────────────────────────────────────────────
|
|
15
|
+
export function defaultWaitExpiry(now = Date.now(), ttlMs = 30 * 60_000) {
|
|
16
|
+
return new Date(now + ttlMs).toISOString();
|
|
17
|
+
}
|
|
18
|
+
/** 生成 continuationId / requestId (调用方也可自带) */
|
|
19
|
+
export function newContinuationId(goalId) {
|
|
20
|
+
return `${goalId}:c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 把"这个 Goal 在等什么外部事件"写进 Goal continuation (持久化)。
|
|
24
|
+
* 事件处理器只读这份事实, 不猜。
|
|
25
|
+
*/
|
|
26
|
+
export async function bindExternalWait(goalId, wait) {
|
|
27
|
+
const rec = await setContinuation(goalId, { external: wait, needsExternal: wait.note || `${wait.expectedSource}${wait.expectedEvent ? `:${wait.expectedEvent}` : ''} (requestId=${wait.requestId})` });
|
|
28
|
+
// 等待中也要让状态机一致: awaiting_external 由调用方/reducer 设置, 这里只保证等待事实在
|
|
29
|
+
return rec;
|
|
30
|
+
}
|
|
31
|
+
/** 清掉等待事实 (唤醒/超时后) */
|
|
32
|
+
export async function clearExternalWait(goalId) {
|
|
33
|
+
return setContinuation(goalId, { external: undefined });
|
|
34
|
+
}
|
|
35
|
+
// ── 匹配 ────────────────────────────────────────────────────────────────────
|
|
36
|
+
function sourceMatches(expected, actual) {
|
|
37
|
+
if (expected === 'any')
|
|
38
|
+
return true;
|
|
39
|
+
return expected === actual;
|
|
40
|
+
}
|
|
41
|
+
async function candidates(event) {
|
|
42
|
+
const out = [];
|
|
43
|
+
if (event.goalId) {
|
|
44
|
+
const g = await readGoal(event.goalId);
|
|
45
|
+
if (g?.continuation?.external)
|
|
46
|
+
out.push({ goal: g, wait: g.continuation.external });
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
const all = await listGoals({ limit: 200 });
|
|
50
|
+
for (const g of all) {
|
|
51
|
+
const w = g.continuation?.external;
|
|
52
|
+
if (w)
|
|
53
|
+
out.push({ goal: g, wait: w });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 投递一个外部事件。校验顺序固定: 来源 → correlation → 属于当前 continuation → 过期 → 去重。
|
|
59
|
+
* 任何一步不过 → **不唤醒**, 只返回原因 (调用方可以把不匹配的事件当普通消息继续走)。
|
|
60
|
+
*/
|
|
61
|
+
export async function deliverExternalEvent(event, deps = {}) {
|
|
62
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
63
|
+
if (!event?.eventId)
|
|
64
|
+
return { ok: false, reason: 'correlation_mismatch', detail: '事件缺少 eventId (无法去重)' };
|
|
65
|
+
const cands = await candidates(event);
|
|
66
|
+
if (cands.length === 0)
|
|
67
|
+
return { ok: false, reason: 'no_match', detail: '没有 Goal 在等这个事件' };
|
|
68
|
+
// 选出唯一匹配的 Goal (按 requestId/continuationId 精确; 退而按来源+事件名)
|
|
69
|
+
let chosen;
|
|
70
|
+
let failReason;
|
|
71
|
+
let failDetail;
|
|
72
|
+
for (const c of cands) {
|
|
73
|
+
const w = c.wait;
|
|
74
|
+
if (!sourceMatches(w.expectedSource, event.source)) {
|
|
75
|
+
failReason ??= 'source_mismatch';
|
|
76
|
+
failDetail ??= `等待 ${w.expectedSource}, 收到 ${event.source}`;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (event.requestId && event.requestId !== w.requestId) {
|
|
80
|
+
failReason ??= 'correlation_mismatch';
|
|
81
|
+
failDetail ??= `requestId 不匹配 (等 ${w.requestId}, 收到 ${event.requestId})`;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!event.requestId && event.continuationId && event.continuationId !== w.continuationId) {
|
|
85
|
+
failReason ??= 'correlation_mismatch';
|
|
86
|
+
failDetail ??= 'continuationId 不匹配';
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!event.requestId && !event.continuationId) {
|
|
90
|
+
failReason ??= 'correlation_mismatch';
|
|
91
|
+
failDetail ??= '事件没有 requestId/continuationId, 不能确认属于这次等待';
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (w.expectedEvent && event.eventName && event.eventName !== w.expectedEvent) {
|
|
95
|
+
failReason ??= 'event_mismatch';
|
|
96
|
+
failDetail ??= `等 ${w.expectedEvent}, 收到 ${event.eventName}`;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
chosen = c;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (!chosen)
|
|
103
|
+
return { ok: false, reason: failReason || 'no_match', detail: failDetail };
|
|
104
|
+
const { goal, wait } = chosen;
|
|
105
|
+
// 过期
|
|
106
|
+
if (Date.parse(wait.expiresAt) <= now) {
|
|
107
|
+
return { ok: false, reason: 'expired', goalId: goal.goalId, detail: `等待已过期 (${wait.expiresAt})` };
|
|
108
|
+
}
|
|
109
|
+
// 去重 (同一 eventId 只处理一次; 跨进程看盘上事实)
|
|
110
|
+
const seen = goal.continuation?.deliveredEventIds || [];
|
|
111
|
+
if (seen.includes(event.eventId))
|
|
112
|
+
return { ok: false, reason: 'duplicate', goalId: goal.goalId, detail: `eventId ${event.eventId} 已处理过` };
|
|
113
|
+
// 写入事件事实 (证据 + 去重表) —— 不在这里启动 agent
|
|
114
|
+
await addEvidence(goal.goalId, [
|
|
115
|
+
`外部事件 (${event.source}${event.fromDid ? `, ${String(event.fromDid).slice(0, 16)}…` : ''}): ${event.eventName || '结果'} eventId=${event.eventId} requestId=${event.requestId || '-'} payload=${JSON.stringify(event.payload ?? null).slice(0, 300)}`,
|
|
116
|
+
]).catch(() => { });
|
|
117
|
+
await setContinuation(goal.goalId, {
|
|
118
|
+
deliveredEventIds: [...seen, event.eventId].slice(-20),
|
|
119
|
+
external: undefined,
|
|
120
|
+
externalResult: { eventId: event.eventId, source: event.source, fromDid: event.fromDid, at: new Date(now).toISOString(), payload: event.payload },
|
|
121
|
+
wakeReason: 'active',
|
|
122
|
+
needsExternal: undefined,
|
|
123
|
+
autoContinue: true,
|
|
124
|
+
wakeAt: undefined,
|
|
125
|
+
});
|
|
126
|
+
// 状态拉回 active (只有还在"等外部"的状态才动它) —— Supervisor 下一轮才会真的执行
|
|
127
|
+
try {
|
|
128
|
+
if (['awaiting_external', 'retry_wait', 'recovering'].includes(String(goal.status))) {
|
|
129
|
+
await updateGoal(goal.goalId, { status: 'active' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { /* 状态写失败 → 下一轮仍会跳过等待; 事件事实已写入, 不丢 */ }
|
|
133
|
+
// 唤醒 (由 Supervisor 下一轮真正执行)
|
|
134
|
+
let woke = false;
|
|
135
|
+
if (deps.wake)
|
|
136
|
+
woke = await deps.wake(goal.goalId).catch(() => false);
|
|
137
|
+
return { ok: true, reason: 'delivered', goalId: goal.goalId, woke };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 把所有"等待已过期"的 Goal 转成明确状态 (needs_human, autoContinue=false)。
|
|
141
|
+
* 由 Supervisor 每轮 tick 开头调用 —— 超时不是失败, 但不能无限等。
|
|
142
|
+
*/
|
|
143
|
+
export async function expireExternalWaits(opts = {}) {
|
|
144
|
+
const now = opts.now ?? Date.now();
|
|
145
|
+
const goals = opts.goalIds
|
|
146
|
+
? (await Promise.all(opts.goalIds.map((id) => readGoal(id)))).filter(Boolean)
|
|
147
|
+
: await listGoals({ limit: 200 });
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const g of goals) {
|
|
150
|
+
const w = g.continuation?.external;
|
|
151
|
+
if (!w)
|
|
152
|
+
continue;
|
|
153
|
+
if (Date.parse(w.expiresAt) > now)
|
|
154
|
+
continue;
|
|
155
|
+
const reason = `外部事件超时 (${w.expectedSource}${w.expectedEvent ? `:${w.expectedEvent}` : ''}, requestId=${w.requestId}, 过期于 ${w.expiresAt}) → 转人工`;
|
|
156
|
+
out.push({ goalId: g.goalId, wait: w, reason });
|
|
157
|
+
await setContinuation(g.goalId, { external: undefined, wakeReason: 'needs_human', needsExternal: undefined, autoContinue: false, lastExternalTimeout: reason });
|
|
158
|
+
await updateGoal(g.goalId, { status: 'needs_human' }).catch(() => { });
|
|
159
|
+
await addEvidence(g.goalId, [`外部事件超时: ${reason}`]).catch(() => { });
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-criteria.ts — 判据生成 / 确认 / 跨 Run 证据汇总 (批次 2-F, 2026-09-16)
|
|
3
|
+
*
|
|
4
|
+
* 规则 (与 leo 的规格一致):
|
|
5
|
+
* · 用户明确给出完成条件 → 直接使用 (criteriaSource = 'user', 已确认);
|
|
6
|
+
* · 用户没给 → **agent 提候选判据** (criteriaSource = 'agent_proposed', 未确认);
|
|
7
|
+
* 候选判据**不能**直接成为最终完成条件 —— 未确认就永不自动完成;
|
|
8
|
+
* · 目标过于模糊 / 候选生成失败 → **不自动完成**, 交人 (needs_human);
|
|
9
|
+
* · 证据跨 Run 汇总 (Run 证据 → Goal 证据, 去重), unresolvedItems 跨 Run 保留。
|
|
10
|
+
*/
|
|
11
|
+
import { readGoal, setCriteria, addEvidence, updateGoal, } from './goal-store.js';
|
|
12
|
+
import { readRun } from './run-store.js';
|
|
13
|
+
/** 太模糊的目标: 判据无从谈起 (短且没有可核验的动作词) */
|
|
14
|
+
export function looksVague(objective) {
|
|
15
|
+
const o = String(objective || '').trim();
|
|
16
|
+
if (!o)
|
|
17
|
+
return { vague: true, reason: '目标为空' };
|
|
18
|
+
const hasVerb = /(写|改|建|加|删|修|跑|测|验证|发布|提交|导出|生成|读|查|部署|完成|实现|接入|更新|安装|配置|整理|对比|汇总)/.test(o);
|
|
19
|
+
if (o.length <= 4 && !hasVerb)
|
|
20
|
+
return { vague: true, reason: `目标太短且没有可核验动作: "${o}"` };
|
|
21
|
+
if (/^(优化|改进|提升|完善|处理|搞|弄|看看|研究一下)/.test(o) && o.length < 12) {
|
|
22
|
+
return { vague: true, reason: `目标过于笼统: "${o}" (缺可核验的产出)` };
|
|
23
|
+
}
|
|
24
|
+
return { vague: false };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 从目标推导候选判据 (确定性启发式, 不依赖 LLM —— 生成失败也不会伪造成功)。
|
|
28
|
+
* 候选**必须**经人确认才能用作完成条件。
|
|
29
|
+
*/
|
|
30
|
+
export function proposeCriteria(objective) {
|
|
31
|
+
const v = looksVague(objective);
|
|
32
|
+
if (v.vague)
|
|
33
|
+
return { ok: false, criteria: [], needsHuman: true, reason: `无法生成可核验判据: ${v.reason} → 请补一句"做完的样子"` };
|
|
34
|
+
const o = String(objective).trim();
|
|
35
|
+
const out = [];
|
|
36
|
+
// 动作拆解: 目标里出现的"动作 + 对象"直接成为候选
|
|
37
|
+
const actions = ['写出', '创建', '修改', '删除', '跑通', '验证', '发布', '提交', '导出', '生成', '读入', '部署', '安装', '配置', '整理', '汇总', '对比'];
|
|
38
|
+
const hits = actions.filter((a) => o.includes(a));
|
|
39
|
+
for (const h of hits.slice(0, 3))
|
|
40
|
+
out.push(`完成"${h}"这一步并留下可核验产物`);
|
|
41
|
+
if (!out.length)
|
|
42
|
+
out.push(`产出与目标一致的结果: ${o.slice(0, 60)}`);
|
|
43
|
+
out.push('有证据可复现 (命令/文件/输出留痕)');
|
|
44
|
+
out.push('没有遗留未解决项');
|
|
45
|
+
return { ok: true, criteria: out.slice(0, 5) };
|
|
46
|
+
}
|
|
47
|
+
/** 给 Goal 提候选判据 (不确认; 留痕) */
|
|
48
|
+
export async function proposeForGoal(goalId) {
|
|
49
|
+
const goal = await readGoal(goalId);
|
|
50
|
+
if (!goal)
|
|
51
|
+
return { ok: false, criteria: [], needsHuman: true, reason: 'Goal 不存在' };
|
|
52
|
+
if (goal.successCriteria.length && goal.criteriaSource === 'user') {
|
|
53
|
+
return { ok: true, criteria: goal.successCriteria, reason: '用户已给出判据 (不改)' };
|
|
54
|
+
}
|
|
55
|
+
const p = proposeCriteria(goal.objective);
|
|
56
|
+
if (!p.ok) {
|
|
57
|
+
// 太模糊 → 明确交人, 不写假判据
|
|
58
|
+
await addEvidence(goalId, [`判据生成失败: ${p.reason}`]).catch(() => { });
|
|
59
|
+
await updateGoal(goalId, { status: 'needs_human' }).catch(() => { });
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
await setCriteria(goalId, { criteria: p.criteria, source: 'agent_proposed', confirm: false });
|
|
63
|
+
await addEvidence(goalId, [`agent 提出候选判据 (待确认): ${p.criteria.join(' / ')}`]).catch(() => { });
|
|
64
|
+
return p;
|
|
65
|
+
}
|
|
66
|
+
/** 人确认判据 (可同时改内容) */
|
|
67
|
+
export async function confirmCriteria(goalId, opts = {}) {
|
|
68
|
+
const goal = await readGoal(goalId);
|
|
69
|
+
if (!goal)
|
|
70
|
+
return { ok: false, reason: 'Goal 不存在' };
|
|
71
|
+
const criteria = opts.criteria?.length ? opts.criteria : goal.successCriteria;
|
|
72
|
+
if (!criteria.length)
|
|
73
|
+
return { ok: false, reason: '没有可确认的判据 (先给判据或让 agent 提候选)' };
|
|
74
|
+
const next = await setCriteria(goalId, { criteria, source: opts.criteria?.length ? 'user' : (goal.criteriaSource || 'user'), confirm: true, by: opts.by || 'human' });
|
|
75
|
+
await addEvidence(goalId, [`判据已确认 (v${next?.criteriaVersion}): ${criteria.join(' / ')}`]).catch(() => { });
|
|
76
|
+
return { ok: true, goal: next };
|
|
77
|
+
}
|
|
78
|
+
/** 把 Goal 名下所有 Run 的证据汇总进 Goal.evidence (去重, 保留跨 Run 历史) */
|
|
79
|
+
export async function aggregateEvidence(goalId) {
|
|
80
|
+
const goal = await readGoal(goalId);
|
|
81
|
+
if (!goal)
|
|
82
|
+
return { runs: 0, runEvidence: [], goalEvidence: [], unresolved: [] };
|
|
83
|
+
const runEvidence = [];
|
|
84
|
+
let lastRunStatus;
|
|
85
|
+
for (const rid of goal.runs || []) {
|
|
86
|
+
const run = await readRun(rid).catch(() => null);
|
|
87
|
+
if (!run)
|
|
88
|
+
continue;
|
|
89
|
+
lastRunStatus = run.status;
|
|
90
|
+
for (const ev of (run.evidence || []))
|
|
91
|
+
runEvidence.push(`[${rid.slice(0, 8)} ${run.status}] ${String(ev).slice(0, 200)}`);
|
|
92
|
+
}
|
|
93
|
+
const merged = [...new Set([...(goal.evidence || []), ...runEvidence])].slice(-50);
|
|
94
|
+
if (merged.length !== (goal.evidence || []).length) {
|
|
95
|
+
await updateGoal(goalId, { evidence: merged }).catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
return { runs: (goal.runs || []).length, runEvidence, goalEvidence: merged, unresolved: goal.unresolvedItems || [], lastRunStatus };
|
|
98
|
+
}
|
|
99
|
+
/** 长期完成的整体判据 (给 CLI/Web/Supervisor 一个统一解释) */
|
|
100
|
+
export async function longTermStatus(goalId) {
|
|
101
|
+
const goal = await readGoal(goalId);
|
|
102
|
+
if (!goal)
|
|
103
|
+
return { canComplete: false, reason: 'Goal 不存在', checks: {} };
|
|
104
|
+
const sum = await aggregateEvidence(goalId);
|
|
105
|
+
const checks = {
|
|
106
|
+
hasCriteria: goal.successCriteria.length > 0,
|
|
107
|
+
criteriaConfirmed: goal.criteriaConfirmed === true && goal.criteriaSource !== undefined,
|
|
108
|
+
allSatisfied: goal.successCriteria.length > 0 && goal.successCriteria.every((_, i) => goal.completedCriteria.includes(i)),
|
|
109
|
+
hasEvidence: (sum.goalEvidence || []).length > 0,
|
|
110
|
+
noUnresolved: (goal.unresolvedItems || []).length === 0,
|
|
111
|
+
lastRunHealthy: !sum.lastRunStatus || !['failed', 'interrupted', 'stalled'].includes(String(sum.lastRunStatus)),
|
|
112
|
+
};
|
|
113
|
+
const canComplete = Object.values(checks).every(Boolean);
|
|
114
|
+
const firstFail = Object.entries(checks).find(([, v]) => !v)?.[0];
|
|
115
|
+
const reasonMap = {
|
|
116
|
+
hasCriteria: '没有完成判据',
|
|
117
|
+
criteriaConfirmed: '判据未经人确认 (候选判据不算数)',
|
|
118
|
+
allSatisfied: '还有判据没满足',
|
|
119
|
+
hasEvidence: '没有证据',
|
|
120
|
+
noUnresolved: '还有未解决项',
|
|
121
|
+
lastRunHealthy: `最近一条 Run 状态异常 (${sum.lastRunStatus})`,
|
|
122
|
+
};
|
|
123
|
+
return { canComplete, reason: canComplete ? '判据全满足 + 已确认 + 有证据 + 无未解决项' : (reasonMap[firstFail] || '未满足完成条件'), checks };
|
|
124
|
+
}
|
|
@@ -53,12 +53,34 @@ export function newGoalId() {
|
|
|
53
53
|
return `g-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
|
|
54
54
|
}
|
|
55
55
|
export async function createGoal(opts) {
|
|
56
|
+
// 2026-09-16 (Phase 3 硬门禁): 初始化未就绪时**不允许创建长期 Goal**。
|
|
57
|
+
// 生产环境生效; 测试环境跳过 (用例跑在隔离 HOME, 本来就没有真实配置)。
|
|
58
|
+
if (!process.env.VITEST && process.env.BOLLOON_SETUP_IN_PROGRESS !== '1') {
|
|
59
|
+
try {
|
|
60
|
+
const { getSetupGateCached } = await import('../setup/setup-store.js');
|
|
61
|
+
const { gate, state } = await getSetupGateCached();
|
|
62
|
+
if (gate !== 'ready') {
|
|
63
|
+
throw new Error(`初始化未就绪 (${gate}, 阶段 ${state.stage}) — 不允许创建 Goal; 先 \`bolloon setup\``);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (/初始化未就绪/.test(String(err?.message || '')))
|
|
68
|
+
throw err;
|
|
69
|
+
// 门禁自身不可读 → fail-closed (不放行)
|
|
70
|
+
throw new Error(`初始化状态不可读, 拒绝创建 Goal (fail-closed): ${String(err?.message || err).slice(0, 120)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
56
73
|
const now = new Date().toISOString();
|
|
57
74
|
const rec = {
|
|
58
75
|
goalId: newGoalId(),
|
|
59
76
|
objective: String(opts.objective || '').slice(0, 500),
|
|
60
77
|
successCriteria: (opts.successCriteria || []).map((c) => String(c).slice(0, 200)).slice(0, 20),
|
|
78
|
+
// 用户明确给出判据 → 直接视为已确认 (criteriaSource=user); 没给 → unknown, 之后由 agent 提候选
|
|
79
|
+
criteriaSource: (opts.successCriteria && opts.successCriteria.length) ? 'user' : 'unknown',
|
|
80
|
+
criteriaConfirmed: !!(opts.successCriteria && opts.successCriteria.length),
|
|
81
|
+
criteriaVersion: 1,
|
|
61
82
|
constraints: (opts.constraints || []).map((c) => String(c).slice(0, 200)).slice(0, 20),
|
|
83
|
+
requiredSkills: (opts.requiredSkills || []).map((c) => String(c).slice(0, 120)).slice(0, 20),
|
|
62
84
|
budget: opts.budget,
|
|
63
85
|
status: 'open',
|
|
64
86
|
channelId: opts.channelId,
|
|
@@ -180,10 +202,22 @@ export async function addEvidence(goalId, evidence) {
|
|
|
180
202
|
* 全部必要判据满足 + 有证据 + 无未解决项 → 才允许 completed。
|
|
181
203
|
* 未声明 successCriteria 的目标**永不**自动完成 (需要人显式确认) —— 否则"模型说完成"就变成了完成。
|
|
182
204
|
*/
|
|
183
|
-
export function evaluateGoalCompletion(goal) {
|
|
205
|
+
export function evaluateGoalCompletion(goal, opts = {}) {
|
|
184
206
|
if (!goal.successCriteria.length) {
|
|
185
207
|
return { complete: false, reason: '未声明 successCriteria: 不允许自动判完成 (需人工确认)', missing: [] };
|
|
186
208
|
}
|
|
209
|
+
// 2-F: 候选判据 (未确认) 不许当作完成条件
|
|
210
|
+
if (goal.criteriaSource === 'agent_proposed' && goal.criteriaConfirmed !== true) {
|
|
211
|
+
return { complete: false, reason: `判据是 agent 提的候选 (v${goal.criteriaVersion || 1}), 未经人确认 → 不许判完成`, missing: goal.successCriteria };
|
|
212
|
+
}
|
|
213
|
+
if (goal.criteriaConfirmed === false) {
|
|
214
|
+
return { complete: false, reason: '判据未确认 → 不许判完成', missing: goal.successCriteria };
|
|
215
|
+
}
|
|
216
|
+
// 2-F: 最近一条 Run 还处于"没跑完"的状态 (失败/中断/失速) → 不许判完成
|
|
217
|
+
const lastRunStatus = opts.lastRunStatus;
|
|
218
|
+
if (lastRunStatus && ['failed', 'interrupted', 'stalled'].includes(String(lastRunStatus))) {
|
|
219
|
+
return { complete: false, reason: `最近一条 Run 状态是 ${lastRunStatus} (未处理好) → 不许判完成`, missing: [] };
|
|
220
|
+
}
|
|
187
221
|
const missing = goal.successCriteria
|
|
188
222
|
.map((c, i) => ({ c, i }))
|
|
189
223
|
.filter(({ i }) => !goal.completedCriteria.includes(i))
|
|
@@ -199,12 +233,51 @@ export function evaluateGoalCompletion(goal) {
|
|
|
199
233
|
}
|
|
200
234
|
return { complete: true, reason: '全部判据满足 + 有证据 + 无未解决项', missing: [] };
|
|
201
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* 设置/确认判据 (2-F)。
|
|
238
|
+
* source: 谁给的 (user 显式给 / agent_proposed 候选 / imported)
|
|
239
|
+
* confirm: 是否视为已确认 —— **候选判据必须显式 confirm 才可能完成**
|
|
240
|
+
*/
|
|
241
|
+
export async function setCriteria(goalId, opts) {
|
|
242
|
+
return withGoalLock(goalId, async () => {
|
|
243
|
+
const rec = await readGoal(goalId);
|
|
244
|
+
if (!rec)
|
|
245
|
+
return null;
|
|
246
|
+
if (opts.criteria) {
|
|
247
|
+
rec.successCriteria = opts.criteria.map((c) => String(c).slice(0, 200)).slice(0, 20);
|
|
248
|
+
rec.criteriaVersion = (rec.criteriaVersion || 1) + 1;
|
|
249
|
+
rec.completedCriteria = []; // 判据变了 → 之前的满足记录作废 (避免拿旧判据凑完成)
|
|
250
|
+
}
|
|
251
|
+
if (opts.source)
|
|
252
|
+
rec.criteriaSource = opts.source;
|
|
253
|
+
if (opts.confirm !== undefined) {
|
|
254
|
+
rec.criteriaConfirmed = opts.confirm;
|
|
255
|
+
rec.criteriaConfirmedBy = opts.confirm ? (opts.by || 'human') : undefined;
|
|
256
|
+
rec.criteriaConfirmedAt = opts.confirm ? new Date().toISOString() : undefined;
|
|
257
|
+
}
|
|
258
|
+
if (opts.source === 'agent_proposed' && !opts.confirm)
|
|
259
|
+
rec.proposedCriteria = rec.successCriteria;
|
|
260
|
+
rec.updatedAt = new Date().toISOString();
|
|
261
|
+
await writeGoal(rec);
|
|
262
|
+
return rec;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
202
265
|
/** 通过完成门就落 completed, 否则保持原状态并回传原因 (不静默) */
|
|
203
266
|
export async function completeGoalIfEligible(goalId) {
|
|
204
267
|
const goal = await readGoal(goalId);
|
|
205
268
|
if (!goal)
|
|
206
269
|
return { ok: false, reason: `goal 不存在: ${goalId}`, goal: null };
|
|
207
|
-
|
|
270
|
+
// 2-F: 把"最近一条 Run 的真实状态"一起纳入完成门 (失败/中断/失速时不许判完成)
|
|
271
|
+
let lastRunStatus;
|
|
272
|
+
try {
|
|
273
|
+
const lastRunId = goal.currentRunId || goal.runs?.[goal.runs.length - 1];
|
|
274
|
+
if (lastRunId) {
|
|
275
|
+
const { readRun } = await import('./run-store.js');
|
|
276
|
+
lastRunStatus = (await readRun(lastRunId))?.status;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { /* 读不到就不加这一条约束, 其余判据照旧 */ }
|
|
280
|
+
const verdict = evaluateGoalCompletion(goal, { lastRunStatus });
|
|
208
281
|
if (!verdict.complete)
|
|
209
282
|
return { ok: false, reason: verdict.reason, goal, missing: verdict.missing };
|
|
210
283
|
const next = await updateGoal(goalId, {
|
|
@@ -441,8 +514,10 @@ export async function wakeReport(now = Date.now()) {
|
|
|
441
514
|
const left = Math.round((Date.parse(c.wakeAt) - now) / 1000);
|
|
442
515
|
wake = `等时间 (${c.wakeAt}, 还剩 ${left}s${c.attempts ? `, 已自动继续 ${c.attempts} 次` : ''})`;
|
|
443
516
|
}
|
|
444
|
-
else if (c?.wakeReason === 'awaiting_external')
|
|
445
|
-
|
|
517
|
+
else if (g.status === 'awaiting_external' || c?.wakeReason === 'awaiting_external' || c?.external) {
|
|
518
|
+
const what = c?.needsExternal || (c?.external ? `${c.external.expectedSource}${c.external.expectedEvent ? `:${c.external.expectedEvent}` : ''} (requestId=${c.external.requestId}, 过期 ${c.external.expiresAt})` : '');
|
|
519
|
+
wake = `等外部事件${what ? ` (${what})` : ''}`;
|
|
520
|
+
}
|
|
446
521
|
else if (live)
|
|
447
522
|
wake = `已被 ${lease.owner} 认领`;
|
|
448
523
|
out.push({ goalId: g.goalId, status: g.status, wake, autoContinue: c?.autoContinue !== false, lease: live ? lease.owner : undefined });
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -684,6 +684,24 @@ export class PiAgentSession {
|
|
|
684
684
|
})();
|
|
685
685
|
}
|
|
686
686
|
async prompt(input, options) {
|
|
687
|
+
// 2026-09-16 (M4 启动硬门禁): 初始化未就绪 → 不执行 Agent。
|
|
688
|
+
// fail-closed: 读不出初始化状态也按未就绪处理 (与 runnerResolver"解析不到只诊断"一致)。
|
|
689
|
+
// 测试环境 (VITEST) 跳过: 那 1800+ 用例跑在隔离 HOME 里, 本来就没有真实配置。
|
|
690
|
+
if (!process.env.VITEST) {
|
|
691
|
+
try {
|
|
692
|
+
const { getSetupGateCached } = await import('../setup/setup-store.js');
|
|
693
|
+
const { gate, state } = await getSetupGateCached();
|
|
694
|
+
if (gate !== 'ready') {
|
|
695
|
+
const why = `初始化未就绪 (${gate}, 当前阶段 ${state.stage})${state.lastError ? ` — ${state.lastError.message}` : ''}`;
|
|
696
|
+
const hint = (state.actions && state.actions[0]) || '运行 `bolloon setup` 完成初始化';
|
|
697
|
+
this.messageHistory.push({ role: 'user', content: input });
|
|
698
|
+
this.messageHistory.push({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
|
|
699
|
+
console.warn(`[PiAgent] 拒绝执行: ${why}`);
|
|
700
|
+
return `[初始化未就绪] ${why}\n${hint}`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
|
|
704
|
+
}
|
|
687
705
|
this.minimaxAvailable = this.checkMinimax();
|
|
688
706
|
this.currentChannelId = options?.channelId ?? this.currentChannelId;
|
|
689
707
|
// 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
|
|
@@ -1209,6 +1227,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1209
1227
|
/** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
|
|
1210
1228
|
resumeRunId = '';
|
|
1211
1229
|
resumePlan = null;
|
|
1230
|
+
/** 2026-09-16 (2-C.4): 外部请求 id (工具层设; 回包必须带上才能精确匹配) */
|
|
1231
|
+
pendingExternalRequestId;
|
|
1212
1232
|
/** 2026-09-16 (M3): 外部等待中 (awaiting_external) —— 收到成功步骤后回 running */
|
|
1213
1233
|
awaitingExternal = false;
|
|
1214
1234
|
/** 2026-09-16 (M3): 熔断原因 (同一工具连续失败达上限 → needs_human) */
|
|
@@ -1277,6 +1297,25 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1277
1297
|
if (cls === 'external_no_reply') {
|
|
1278
1298
|
this.awaitingExternal = true;
|
|
1279
1299
|
await this.safeSetRunStatus(this.currentRunId, 'awaiting_external');
|
|
1300
|
+
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
1301
|
+
try {
|
|
1302
|
+
if (this.currentGoalId) {
|
|
1303
|
+
const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await import('./external-events.js');
|
|
1304
|
+
const isDelegate = /delegate/i.test(String(tool || ''));
|
|
1305
|
+
await bindExternalWait(this.currentGoalId, {
|
|
1306
|
+
requestId: this.pendingExternalRequestId || `${this.currentRunId}:${Date.now().toString(36)}`,
|
|
1307
|
+
continuationId: newContinuationId(this.currentGoalId),
|
|
1308
|
+
expectedSource: isDelegate ? 'delegate' : 'p2p',
|
|
1309
|
+
expectedEvent: 'result',
|
|
1310
|
+
createdAt: new Date().toISOString(),
|
|
1311
|
+
expiresAt: defaultWaitExpiry(Date.now(), Number(process.env.BOLLOON_EXTERNAL_WAIT_MS || 30 * 60_000)),
|
|
1312
|
+
note: `${isDelegate ? 'delegate 回包' : 'P2P 协作回复'} 等待中 (tool=${tool})`,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
catch (e) {
|
|
1317
|
+
await recordDegradation({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.currentRunId, message: String(e?.message || e) }).catch(() => { });
|
|
1318
|
+
}
|
|
1280
1319
|
return;
|
|
1281
1320
|
}
|
|
1282
1321
|
// 鉴权类: 不重试, 直接交人
|