@bolloon/bolloon-agent 0.4.24 → 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 +446 -0
- package/dist/agents/external-events.js +162 -0
- package/dist/agents/goal-criteria.js +124 -0
- package/dist/agents/goal-store.js +526 -0
- package/dist/agents/pi-harness.js +263 -0
- package/dist/agents/pi-sdk.js +607 -126
- package/dist/agents/run-store.js +772 -0
- package/dist/agents/runner-resolver.js +225 -0
- package/dist/agents/skill-readiness.js +133 -0
- package/dist/agents/skill-supervisor-link.js +70 -0
- package/dist/agents/skills-manager.js +717 -0
- package/dist/agents/supervisor-host.js +249 -0
- package/dist/cli/setup-wizard.js +96 -127
- package/dist/cron/tick-lock.js +1 -1
- 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 +549 -26
- package/dist/ios/agent-delegate-server.js +58 -12
- package/dist/ios/icons/icon-1024x1024.png +0 -0
- package/dist/ios/icons/icon-1024x1024.webp +0 -0
- package/dist/ios/icons/icon-216x216.png +0 -0
- package/dist/ios/icons/icon-216x216.webp +0 -0
- package/dist/ios/index.html +21 -1
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +195 -1
- package/dist/ios/mobile-core.js +24876 -24723
- package/dist/ios/mobile.css +15 -0
- package/dist/ios/mobile.html +21 -1
- package/dist/ios/mobile.js +143 -0
- package/dist/ios/server.js +51 -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/icons/icon-1024x1024.png +0 -0
- package/dist/web/icons/icon-1024x1024.webp +0 -0
- package/dist/web/icons/icon-216x216.png +0 -0
- package/dist/web/icons/icon-216x216.webp +0 -0
- package/dist/web/manifest.json +1 -1
- package/dist/web/mobile-agent.js +2 -2
- package/dist/web/mobile-core.js +24884 -24726
- package/dist/web/mobile-privacy.js +185 -0
- package/dist/web/mobile.css +15 -0
- package/dist/web/mobile.html +21 -1
- package/dist/web/mobile.js +179 -6
- package/dist/web/server.js +633 -0
- package/package.json +2 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-harness.ts — 唯一 PiAgentHarness 门面 (2026-09-16 Milestone 1-B)
|
|
3
|
+
*
|
|
4
|
+
* 背景 (leo 2026-09-16): 约束层是**散落的多层** —— react-harness / deny-pipeline /
|
|
5
|
+
* pre-tool-validator(human-value-pipeline) / hooks-engine / tool-gate / loop-review 各自被
|
|
6
|
+
* pi-sdk 在不同位置分别调用, 于是出现"某条路径走 deny-pipeline、另一条绕过它", 且各自出错时
|
|
7
|
+
* 的处置不一致 (多数是 fail-open = 出错就放行)。
|
|
8
|
+
*
|
|
9
|
+
* 这一层不删旧模块, 只**收敛入口**: pi-sdk 只认识 PiAgentHarness, 由 Harness 决定约束流程与顺序。
|
|
10
|
+
* 旧模块成为 Harness 的内部实现 (通过构造参数注入, 行为保持不变)。
|
|
11
|
+
*
|
|
12
|
+
* 生命周期 (与计划书一致):
|
|
13
|
+
* sessionStart → beforeModelCall → afterModelCall → beforeToolCall → afterToolCall
|
|
14
|
+
* → checkpoint → recover → pause → sessionEnd
|
|
15
|
+
*
|
|
16
|
+
* 失败分级 (leo 2026-09-16 第 6 条):
|
|
17
|
+
* core_constraint 核心约束自身失效 (e.g. 策略层异常) → **阻止执行** (默认 fail-closed)
|
|
18
|
+
* policy_denied 工具被策略拒绝 → 返回 agent 可处理的拒绝结果 (不崩, 不静默放行)
|
|
19
|
+
* observational 观测/记录失败 → 记降级, 不中断 (决策不因记账失败而改变)
|
|
20
|
+
* goal_review 目标审查未通过 → 不许进 done (由调用方按决策续跑)
|
|
21
|
+
*
|
|
22
|
+
* 设计取舍 (如实记):
|
|
23
|
+
* - 旧行为里 "harness 抛错 → 放行" 是 fail-open; 现在默认 fail-closed (`failClosed: true`)。
|
|
24
|
+
* 这是 Phase 1 的**明确要求** (不允许 harness 出错后默认放行高风险工具), 不是顺带改动。
|
|
25
|
+
* - Harness 事件写 Run 用"观测级"写入: 记账失败绝不改变已做出的决策 (记录是账, 不是闸)。
|
|
26
|
+
*/
|
|
27
|
+
import { decideAfterReview, DEFAULT_MAX_REVIEWS } from './loop-review.js';
|
|
28
|
+
export class PiAgentHarness {
|
|
29
|
+
deps;
|
|
30
|
+
failClosed;
|
|
31
|
+
modelCalls = 0;
|
|
32
|
+
degradedOnce = new Set();
|
|
33
|
+
constructor(deps) {
|
|
34
|
+
this.deps = deps;
|
|
35
|
+
this.failClosed = deps.failClosed !== false;
|
|
36
|
+
}
|
|
37
|
+
/** 事件出口: 记账失败**永不**抛出 (记录是账, 不是闸) */
|
|
38
|
+
emit(e) {
|
|
39
|
+
try {
|
|
40
|
+
const full = { ts: new Date().toISOString(), ...e };
|
|
41
|
+
const out = this.deps.events?.(full);
|
|
42
|
+
if (out && typeof out.catch === 'function') {
|
|
43
|
+
out.catch((err) => console.warn('[PiAgentHarness] 事件写入失败 (非致命):', err?.message));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.warn('[PiAgentHarness] 事件构造/写入失败 (非致命):', err?.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** 约束层自身失效的统一处置: 记降级 + 决定是否放行 (failClosed → 不放行) */
|
|
51
|
+
onLayerError(layer, err, ctx, tool) {
|
|
52
|
+
const message = `${layer} 失效: ${String(err?.message || err).slice(0, 200)}`;
|
|
53
|
+
console.warn(`[PiAgentHarness] ${message}`);
|
|
54
|
+
this.emit({ event: 'beforeToolCall', kind: 'error', failureKind: 'core_constraint', tool, reason: message, source: layer, runId: ctx.runId, goalId: ctx.goalId });
|
|
55
|
+
if (!this.failClosed)
|
|
56
|
+
return null; // 旧语义 (fail-open) — 仅显式配置时
|
|
57
|
+
return {
|
|
58
|
+
allow: false,
|
|
59
|
+
reason: `核心约束层 ${layer} 失效, 已阻止该工具调用 (fail-closed): ${message}`,
|
|
60
|
+
source: 'harness-error',
|
|
61
|
+
kind: 'core_constraint',
|
|
62
|
+
degraded: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ───────────────────────── session ─────────────────────────
|
|
66
|
+
async sessionStart(ctx) {
|
|
67
|
+
const t0 = Date.now();
|
|
68
|
+
// ① react-harness 会话开启 (8-gate 状态复位)
|
|
69
|
+
try {
|
|
70
|
+
await this.deps.reactHarness.onSessionStart(ctx.channelId);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.emit({ event: 'sessionStart', kind: 'degrade', failureKind: 'observational', reason: `reactHarness.onSessionStart 失败: ${err?.message}`, runId: ctx.runId, goalId: ctx.goalId });
|
|
74
|
+
}
|
|
75
|
+
// ② hooks: onLoopStart (一次运行一次; 与旧行为一致)
|
|
76
|
+
let hookDeny = null;
|
|
77
|
+
try {
|
|
78
|
+
const results = await this.deps.hooks.fire('onLoopStart', { event: 'onLoopStart', channelId: ctx.channelId, agentId: ctx.agentId });
|
|
79
|
+
for (const r of results || [])
|
|
80
|
+
if (r?.deny)
|
|
81
|
+
hookDeny = r.reason || 'hook 拒绝';
|
|
82
|
+
}
|
|
83
|
+
catch { /* hook 失败静默 (与旧行为一致: hooks-engine 自身已兜底) */ }
|
|
84
|
+
this.emit({ event: 'sessionStart', kind: hookDeny ? 'deny' : 'allow', reason: hookDeny || undefined, source: 'hooks', ms: Date.now() - t0, runId: ctx.runId, goalId: ctx.goalId });
|
|
85
|
+
}
|
|
86
|
+
async sessionEnd(ctx) {
|
|
87
|
+
try {
|
|
88
|
+
await this.deps.reactHarness.onSessionEnd();
|
|
89
|
+
this.emit({ event: 'sessionEnd', kind: 'note', runId: ctx.runId, goalId: ctx.goalId });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
this.emit({ event: 'sessionEnd', kind: 'degrade', failureKind: 'observational', reason: `reactHarness.onSessionEnd 失败: ${err?.message}`, runId: ctx.runId, goalId: ctx.goalId });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ───────────────────────── model call ─────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* 模型调用前 (扩展点)。默认只计数 + 留事件。
|
|
98
|
+
* 刻意**不**在此 fire 新的 hook 事件: 旧行为里模型调用前没有任何 hook, 加了会改变现有
|
|
99
|
+
* hooks.yaml 用户的实际触发次数 (属于行为变更, 需要单独决策, 见 wiki 记录)。
|
|
100
|
+
*/
|
|
101
|
+
beforeModelCall(ctx) {
|
|
102
|
+
this.modelCalls++;
|
|
103
|
+
this.emit({ event: 'beforeModelCall', kind: 'note', runId: ctx.runId, goalId: ctx.goalId });
|
|
104
|
+
return { hints: [] };
|
|
105
|
+
}
|
|
106
|
+
afterModelCall(ctx, info = {}) {
|
|
107
|
+
if (info.error) {
|
|
108
|
+
this.emit({ event: 'afterModelCall', kind: 'error', failureKind: 'core_constraint', reason: String(info.error?.message || info.error).slice(0, 200), ms: info.ms, runId: ctx.runId, goalId: ctx.goalId });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.emit({ event: 'afterModelCall', kind: 'note', ms: info.ms, runId: ctx.runId, goalId: ctx.goalId });
|
|
112
|
+
}
|
|
113
|
+
get modelCallCount() {
|
|
114
|
+
return this.modelCalls;
|
|
115
|
+
}
|
|
116
|
+
// ───────────────────────── tool call ─────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* 工具调用前的**唯一**决策入口。
|
|
119
|
+
* 顺序严格保持旧实现: deny-pipeline → pre-tool-validator(4 步链) → react-harness(8-gate)。
|
|
120
|
+
* 任一层拒绝即返回 (第一层拒绝后不再检查后续), 与旧行为一致。
|
|
121
|
+
*/
|
|
122
|
+
async beforeToolCall(input) {
|
|
123
|
+
const { tool, args, ctx } = input;
|
|
124
|
+
const t0 = Date.now();
|
|
125
|
+
// ① deny-pipeline (黑名单 / 权限 / hooks 策略)
|
|
126
|
+
let denyResult = null;
|
|
127
|
+
try {
|
|
128
|
+
denyResult = await this.deps.denyPipeline.check({
|
|
129
|
+
toolName: tool,
|
|
130
|
+
toolArgs: args || {},
|
|
131
|
+
permissionMode: input.permissionMode,
|
|
132
|
+
channelId: ctx.channelId,
|
|
133
|
+
agentId: ctx.agentId,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const fail = this.onLayerError('deny-pipeline', err, ctx, tool);
|
|
138
|
+
if (fail)
|
|
139
|
+
return fail;
|
|
140
|
+
}
|
|
141
|
+
if (denyResult?.denied) {
|
|
142
|
+
const decision = {
|
|
143
|
+
allow: false,
|
|
144
|
+
reason: denyResult.reason,
|
|
145
|
+
source: 'deny-pipeline',
|
|
146
|
+
rejectedBy: denyResult.source,
|
|
147
|
+
kind: 'policy_denied',
|
|
148
|
+
};
|
|
149
|
+
this.emit({ event: 'beforeToolCall', kind: 'deny', failureKind: 'policy_denied', tool, reason: denyResult.reason, source: `deny-pipeline:${denyResult.source}`, ms: Date.now() - t0, runId: ctx.runId, goalId: ctx.goalId });
|
|
150
|
+
return decision;
|
|
151
|
+
}
|
|
152
|
+
// ② 参数/危险命令校验 (pre-tool-validator 4 步链)
|
|
153
|
+
if (this.deps.preToolUse) {
|
|
154
|
+
let pre = null;
|
|
155
|
+
try {
|
|
156
|
+
pre = await this.deps.preToolUse({ tool, args: args || {}, permissionMode: input.permissionMode });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const fail = this.onLayerError('pre-tool-validator', err, ctx, tool);
|
|
160
|
+
if (fail)
|
|
161
|
+
return fail;
|
|
162
|
+
}
|
|
163
|
+
if (pre && !pre.allowed) {
|
|
164
|
+
this.emit({ event: 'beforeToolCall', kind: 'deny', failureKind: 'policy_denied', tool, reason: pre.reason, source: 'pre-tool-validator', ms: Date.now() - t0, runId: ctx.runId, goalId: ctx.goalId });
|
|
165
|
+
return {
|
|
166
|
+
allow: false,
|
|
167
|
+
reason: pre.reason || '未通过安全校验',
|
|
168
|
+
source: 'pre-tool-validator',
|
|
169
|
+
rejectedBy: 'pre-tool-validator',
|
|
170
|
+
kind: 'policy_denied',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ③ react-harness (8-gate + builtin-guards, 含路由 hint)
|
|
175
|
+
let preCall = null;
|
|
176
|
+
try {
|
|
177
|
+
preCall = await this.deps.reactHarness.preToolCall(tool, args || {}, ctx.channelId);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
const fail = this.onLayerError('react-harness', err, ctx, tool);
|
|
181
|
+
if (fail)
|
|
182
|
+
return fail;
|
|
183
|
+
}
|
|
184
|
+
if (preCall && !preCall.allowed) {
|
|
185
|
+
this.emit({ event: 'beforeToolCall', kind: 'deny', failureKind: 'policy_denied', tool, reason: preCall.reason, source: `react-harness:${preCall.details?.rejectedBy}`, ms: Date.now() - t0, runId: ctx.runId, goalId: ctx.goalId });
|
|
186
|
+
return {
|
|
187
|
+
allow: false,
|
|
188
|
+
reason: preCall.reason || '未通过安全校验',
|
|
189
|
+
source: 'react-harness',
|
|
190
|
+
rejectedBy: preCall.details?.rejectedBy,
|
|
191
|
+
kind: 'policy_denied',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
this.emit({ event: 'beforeToolCall', kind: 'allow', tool, source: 'harness', ms: Date.now() - t0, runId: ctx.runId, goalId: ctx.goalId });
|
|
195
|
+
return { allow: true, systemAddition: denyResult?.systemAddition };
|
|
196
|
+
}
|
|
197
|
+
/** 工具调用后: 路由 hint + 输出 gate (顺序与旧实现一致: hint 先, output gate 后) */
|
|
198
|
+
async afterToolCall(input) {
|
|
199
|
+
const { tool, ctx } = input;
|
|
200
|
+
const out = {};
|
|
201
|
+
try {
|
|
202
|
+
const routeHint = this.deps.reactHarness.getLastRouteHint();
|
|
203
|
+
if (routeHint && routeHint.systemAddition) {
|
|
204
|
+
out.routeHint = { reason: routeHint.reason, systemAddition: routeHint.systemAddition };
|
|
205
|
+
this.deps.reactHarness.clearRouteHint();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch { /* hint 读失败不影响执行 */ }
|
|
209
|
+
try {
|
|
210
|
+
const post = await this.deps.reactHarness.postToolCall(tool, String(input.output || ''), ctx.channelId);
|
|
211
|
+
if (!post.allowed) {
|
|
212
|
+
out.outputBlocked = { reason: post.reason || '输出含敏感信息' };
|
|
213
|
+
this.emit({ event: 'afterToolCall', kind: 'deny', failureKind: 'policy_denied', tool, reason: out.outputBlocked.reason, source: 'react-harness:output', runId: ctx.runId, goalId: ctx.goalId });
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
// 旧行为: output gate 失败 → 放行原输出 (fail-open)。这里保持放行 (输出已产生, 拦不住源头),
|
|
219
|
+
// 但**必须留痕**: 不把"gate 没跑成"说成"gate 通过了"。
|
|
220
|
+
out.degraded = true;
|
|
221
|
+
this.emit({ event: 'afterToolCall', kind: 'degrade', failureKind: 'observational', tool, reason: `output gate 失效 (输出已按原样放行): ${err?.message}`, source: 'react-harness:output', runId: ctx.runId, goalId: ctx.goalId });
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
this.emit({ event: 'afterToolCall', kind: 'allow', tool, source: 'harness', runId: ctx.runId, goalId: ctx.goalId });
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
// ───────────────────────── run 生命周期 ─────────────────────────
|
|
228
|
+
/** checkpoint: 由 Harness 统一触发 (调用方给实现, 通常是 run-store.saveCheckpoint) */
|
|
229
|
+
async checkpoint(ctx, cp) {
|
|
230
|
+
try {
|
|
231
|
+
await this.deps.events?.({ ts: new Date().toISOString(), event: 'checkpoint', kind: 'note', ms: undefined, runId: ctx.runId, goalId: ctx.goalId, reason: cp.nextAction });
|
|
232
|
+
}
|
|
233
|
+
catch { /* 记账失败不影响 */ }
|
|
234
|
+
}
|
|
235
|
+
/** recover / pause: 接口先立 (Milestone 2/3 接线), 此处只留事件 */
|
|
236
|
+
recover(ctx, info) {
|
|
237
|
+
this.emit({ event: 'recover', kind: 'note', failureKind: 'observational', reason: `${info.errorClass || 'unknown'} → ${info.action || 'none'}`, runId: ctx.runId, goalId: ctx.goalId });
|
|
238
|
+
}
|
|
239
|
+
pause(ctx, reason) {
|
|
240
|
+
this.emit({ event: 'pause', kind: 'note', reason, runId: ctx.runId, goalId: ctx.goalId });
|
|
241
|
+
}
|
|
242
|
+
// ───────────────────────── 目标审查 ─────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* final 前目标对齐审查 (loop-review 的唯一入口)。
|
|
245
|
+
* 审查本身失效时: 记为 goal_review 失败并**按"继续审查"处理**? 不 —— 按协议
|
|
246
|
+
* "目标审查失败: 禁止进入 done", 这里返回 `continue-review` 的等价结果 (不放过收尾),
|
|
247
|
+
* 避免"审查没跑成 → 直接算完成"。
|
|
248
|
+
*/
|
|
249
|
+
reviewFinal(state, maxReviews = DEFAULT_MAX_REVIEWS) {
|
|
250
|
+
try {
|
|
251
|
+
const decision = decideAfterReview(state, maxReviews);
|
|
252
|
+
this.emit({ event: 'review', kind: decision.kind === 'continue-review' ? 'note' : 'allow', failureKind: 'goal_review', reason: decision.kind, runId: state.runId, goalId: state.goalId });
|
|
253
|
+
return decision;
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
this.emit({ event: 'review', kind: 'error', failureKind: 'goal_review', reason: `审查器失效: ${err?.message}`, runId: state.runId, goalId: state.goalId });
|
|
257
|
+
return {
|
|
258
|
+
kind: 'continue-review',
|
|
259
|
+
hint: `[目标审查降级] 审查器执行失败 (${String(err?.message || err).slice(0, 120)}), 按"未确认完成"处理: 请再次核对用户需求是否真的全部满足, 并列出证据。确认完成后再加 <final gen>。`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|