@bolloon/bolloon-agent 0.5.4 → 0.6.0
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/agent-identity-store.js +41 -5
- package/dist/agents/background-notices.js +124 -0
- package/dist/agents/channel-identity.js +54 -0
- package/dist/agents/code-write-gate.js +47 -0
- package/dist/agents/deny-pipeline.js +11 -2
- package/dist/agents/egress-sanitize.js +69 -0
- package/dist/agents/error-classifier.js +67 -5
- package/dist/agents/experience-review.js +226 -0
- package/dist/agents/gateway-group.js +153 -18
- package/dist/agents/iteration-budget.js +64 -0
- package/dist/agents/lesson-to-skill.js +211 -0
- package/dist/agents/managed-services.js +64 -0
- package/dist/agents/parse-tool-call.js +43 -32
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +201 -32
- package/dist/agents/pi-sdk.js +798 -206
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/agents/shell-guard.js +109 -13
- package/dist/agents/skill-health.js +90 -0
- package/dist/agents/skill-ledger.js +129 -0
- package/dist/agents/tool-loop-guard.js +303 -0
- package/dist/agents/tool-path-args.js +53 -0
- package/dist/agents/tool-result-gate.js +50 -0
- package/dist/agents/tool-subset.js +109 -0
- package/dist/agents/tool-telemetry.js +97 -0
- package/dist/bootstrap/context-collector.js +37 -7
- package/dist/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/bootstrap/persona-init.js +174 -0
- package/dist/bootstrap/persona-loader.js +5 -1
- package/dist/cli/ink-app.js +153 -6
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +3 -1
- package/dist/cli/log-gate.js +7 -0
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +87 -2
- package/dist/cli/trace-line.js +54 -0
- package/dist/context-compaction/auto-compact.js +37 -2
- package/dist/electron-build/agents/agent-identity-store.js +43 -5
- package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
- package/dist/electron-build/agents/background-notices.js +138 -0
- package/dist/electron-build/agents/background-notices.js.map +1 -0
- package/dist/electron-build/agents/channel-identity.js +64 -0
- package/dist/electron-build/agents/channel-identity.js.map +1 -0
- package/dist/electron-build/agents/code-write-gate.js +55 -0
- package/dist/electron-build/agents/code-write-gate.js.map +1 -0
- package/dist/electron-build/agents/deny-pipeline.js +11 -2
- package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
- package/dist/electron-build/agents/egress-sanitize.js +76 -0
- package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
- package/dist/electron-build/agents/error-classifier.js +68 -5
- package/dist/electron-build/agents/error-classifier.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +243 -0
- package/dist/electron-build/agents/experience-review.js.map +1 -0
- package/dist/electron-build/agents/iteration-budget.js +73 -0
- package/dist/electron-build/agents/iteration-budget.js.map +1 -0
- package/dist/electron-build/agents/lesson-to-skill.js +228 -0
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
- package/dist/electron-build/agents/managed-services.js +69 -0
- package/dist/electron-build/agents/managed-services.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +43 -32
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
- package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +200 -31
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +799 -206
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/plan-store.js +37 -0
- package/dist/electron-build/agents/plan-store.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +71 -2
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/run-context.js +24 -0
- package/dist/electron-build/agents/run-context.js.map +1 -0
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/shell-guard.js +110 -14
- package/dist/electron-build/agents/shell-guard.js.map +1 -1
- package/dist/electron-build/agents/skill-health.js +103 -0
- package/dist/electron-build/agents/skill-health.js.map +1 -0
- package/dist/electron-build/agents/skill-ledger.js +146 -0
- package/dist/electron-build/agents/skill-ledger.js.map +1 -0
- package/dist/electron-build/agents/tool-loop-guard.js +319 -0
- package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
- package/dist/electron-build/agents/tool-path-args.js +63 -0
- package/dist/electron-build/agents/tool-path-args.js.map +1 -0
- package/dist/electron-build/agents/tool-result-gate.js +59 -0
- package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
- package/dist/electron-build/agents/tool-subset.js +116 -0
- package/dist/electron-build/agents/tool-subset.js.map +1 -0
- package/dist/electron-build/agents/tool-telemetry.js +108 -0
- package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
- package/dist/electron-build/bootstrap/context-collector.js +38 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-init.js +216 -0
- package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
- package/dist/electron-build/bootstrap/persona-loader.js +5 -1
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/context-compaction/auto-compact.js +38 -2
- package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js +22 -0
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +314 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +55 -0
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/web/server-storage.js +26 -5
- package/dist/electron-build/web/server-storage.js.map +1 -1
- package/dist/external-engines/delegate.js +21 -0
- package/dist/index.js +526 -95
- package/dist/kernel/channel-actor.js +297 -0
- package/dist/kernel/control.js +95 -0
- package/dist/kernel/gate-scan.js +1036 -0
- package/dist/kernel/model-runtime.js +472 -0
- package/dist/kernel/plan-channel-actor.js +298 -0
- package/dist/kernel/plan-constraint.js +165 -0
- package/dist/kernel/plan-deletion.js +32 -0
- package/dist/kernel/plan-modelruntime.js +60 -0
- package/dist/kernel/plan-runcontext.js +163 -0
- package/dist/kernel/plan.js +153 -0
- package/dist/kernel/roster.js +242 -0
- package/dist/orbitdb/cid-database.js +55 -0
- package/dist/orbitdb/event-index.js +370 -0
- package/dist/orbitdb/event-query.js +131 -0
- package/dist/orbitdb/event-shell.js +510 -0
- package/dist/orbitdb/group-access.js +381 -0
- package/dist/orbitdb/group-shards.js +140 -0
- package/dist/orbitdb/ipfs-node.js +36 -11
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +4 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/routes-tasks.js +5 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +60 -18
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +473 -29
- package/dist/web/ui/step-timeline.js +13 -12
- package/package.json +2 -1
- package/scripts/build-web.ts +74 -1
- package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
- package/dist/constraint-runtime/_archive_helper.js +0 -9
- package/dist/constraint-runtime/assistant/index.d.ts +0 -11
- package/dist/constraint-runtime/assistant/index.js +0 -12
- package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
- package/dist/constraint-runtime/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/bridge/index.d.ts +0 -11
- package/dist/constraint-runtime/bridge/index.js +0 -12
- package/dist/constraint-runtime/buddy/index.d.ts +0 -11
- package/dist/constraint-runtime/buddy/index.js +0 -12
- package/dist/constraint-runtime/cli/index.d.ts +0 -11
- package/dist/constraint-runtime/cli/index.js +0 -12
- package/dist/constraint-runtime/components/index.d.ts +0 -11
- package/dist/constraint-runtime/components/index.js +0 -12
- package/dist/constraint-runtime/constants/index.d.ts +0 -11
- package/dist/constraint-runtime/constants/index.js +0 -12
- package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
- package/dist/constraint-runtime/coordinator/index.js +0 -12
- package/dist/constraint-runtime/cost_hook.d.ts +0 -2
- package/dist/constraint-runtime/cost_hook.js +0 -4
- package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
- package/dist/constraint-runtime/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/execution_registry.d.ts +0 -20
- package/dist/constraint-runtime/execution_registry.js +0 -44
- package/dist/constraint-runtime/hooks/index.d.ts +0 -11
- package/dist/constraint-runtime/hooks/index.js +0 -12
- package/dist/constraint-runtime/ink.d.ts +0 -1
- package/dist/constraint-runtime/ink.js +0 -4
- package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
- package/dist/constraint-runtime/keybindings/index.js +0 -12
- package/dist/constraint-runtime/memdir/index.d.ts +0 -11
- package/dist/constraint-runtime/memdir/index.js +0 -12
- package/dist/constraint-runtime/migrations/index.d.ts +0 -11
- package/dist/constraint-runtime/migrations/index.js +0 -12
- package/dist/constraint-runtime/moreright/index.d.ts +0 -11
- package/dist/constraint-runtime/moreright/index.js +0 -12
- package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
- package/dist/constraint-runtime/native_ts/index.js +0 -12
- package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
- package/dist/constraint-runtime/output_styles/index.js +0 -12
- package/dist/constraint-runtime/plugins/index.d.ts +0 -11
- package/dist/constraint-runtime/plugins/index.js +0 -12
- package/dist/constraint-runtime/port_manifest.d.ts +0 -7
- package/dist/constraint-runtime/port_manifest.js +0 -11
- package/dist/constraint-runtime/query.d.ts +0 -6
- package/dist/constraint-runtime/query.js +0 -1
- package/dist/constraint-runtime/remote/index.d.ts +0 -11
- package/dist/constraint-runtime/remote/index.js +0 -12
- package/dist/constraint-runtime/schemas/index.d.ts +0 -11
- package/dist/constraint-runtime/schemas/index.js +0 -12
- package/dist/constraint-runtime/screens/index.d.ts +0 -11
- package/dist/constraint-runtime/screens/index.js +0 -12
- package/dist/constraint-runtime/server/index.d.ts +0 -11
- package/dist/constraint-runtime/server/index.js +0 -12
- package/dist/constraint-runtime/services/index.d.ts +0 -11
- package/dist/constraint-runtime/services/index.js +0 -12
- package/dist/constraint-runtime/state/index.d.ts +0 -11
- package/dist/constraint-runtime/state/index.js +0 -12
- package/dist/constraint-runtime/system_init.d.ts +0 -1
- package/dist/constraint-runtime/system_init.js +0 -20
- package/dist/constraint-runtime/types/index.d.ts +0 -11
- package/dist/constraint-runtime/types/index.js +0 -12
- package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
- package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/utils/index.d.ts +0 -11
- package/dist/constraint-runtime/utils/index.js +0 -12
- package/dist/constraint-runtime/vim/index.d.ts +0 -11
- package/dist/constraint-runtime/vim/index.js +0 -12
- package/dist/constraint-runtime/voice/index.d.ts +0 -11
- package/dist/constraint-runtime/voice/index.js +0 -12
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Channel Actor** (K5 第 2 步: 容器)
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它: `messageHistory` / `currentChannelId` / `currentAgentId` / `currentGoalId` 四个字段
|
|
5
|
+
* 在 Pi 实例上被**所有入口共享** —— 这正是"多通道串台 / 同 Channel 并发改 history"的根。
|
|
6
|
+
* K2 已按 leo 口径判定它们**不是 Run 状态**, 该由 Channel Actor 承载 (见 `plan-channel-actor.ts`)。
|
|
7
|
+
*
|
|
8
|
+
* 本文件是**纯新增的容器**: 不改任何现有行为 (Pi 仍按原样跑)。它提供三件事:
|
|
9
|
+
* ① `ActorState` —— Actor 状态容器 (9 项, 与台账逐条对应);
|
|
10
|
+
* ② `SerialMailbox` —— 同 Channel **串行**执行队列 (1 个 channel 同时只有 1 个任务在跑);
|
|
11
|
+
* ③ `ChannelActor` —— 把两者绑起来: `submit()` 入队并串行执行 · `abort()` 取消当前任务。
|
|
12
|
+
*
|
|
13
|
+
* 纪律: 串行语义**真跑验证** (`src/test/kernel-channel-actor.test.ts` 的串行用例), 不靠"看起来对"。
|
|
14
|
+
*/
|
|
15
|
+
/** Actor 状态容器 —— 与 `ACTOR_STATE_ITEMS` 的 9 项一一对应 */
|
|
16
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
17
|
+
/**
|
|
18
|
+
* **当前正在哪个 Actor 的 mailbox 任务里跑** (K5 第 5 步的前置安全网)。
|
|
19
|
+
*
|
|
20
|
+
* 为什么必须有: `SerialMailbox` 是**无重入**的 —— 一个已在 mailbox 里跑的任务若再往同一个 mailbox
|
|
21
|
+
* 投递并 await, 就是**自锁** (新任务排在"自己"后面)。入口投递 (步骤④) 一旦覆盖到"运行中会被调用"
|
|
22
|
+
* 的执行点 (LLM 回调 / judge / 工具内再问一次), 就会踩到这条。
|
|
23
|
+
* ⇒ 用 ALS 记住"我在谁的上下文里", 同 actor 重入时**直跑**, 不同 actor 照常排队。
|
|
24
|
+
*/
|
|
25
|
+
const actorCtx = new AsyncLocalStorage();
|
|
26
|
+
/** 当前是否正跑在某个 Actor 的 mailbox 任务内 (诊断用) */
|
|
27
|
+
export function currentActorContext() {
|
|
28
|
+
return actorCtx.getStore();
|
|
29
|
+
}
|
|
30
|
+
export function createActorState(init = {}) {
|
|
31
|
+
return {
|
|
32
|
+
channelId: init.channelId ?? '',
|
|
33
|
+
agentId: init.agentId ?? '',
|
|
34
|
+
goalBinding: init.goalBinding ?? '',
|
|
35
|
+
messageHistory: init.messageHistory ?? [],
|
|
36
|
+
mailbox: init.mailbox ?? { pending: 0, processed: 0 },
|
|
37
|
+
activeRun: init.activeRun ?? '',
|
|
38
|
+
cancellation: init.cancellation ?? null,
|
|
39
|
+
outboundStream: init.outboundStream ?? null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 同 Channel **串行**邮箱。
|
|
44
|
+
*
|
|
45
|
+
* 契约 (K5 验收标准 ①):
|
|
46
|
+
* · `submit()` 立即返回 promise, 任务**排队**执行;
|
|
47
|
+
* · 同一个 mailbox 内**永不并发** —— 后一个任务在前一个 settle 之后才开始;
|
|
48
|
+
* · 一个任务抛错**不阻塞**队列 (错误交给该任务自己的 promise, 队列继续);
|
|
49
|
+
* · `pending` 是"已入队未完成"的数量, 用于观测 (不是背压实现)。
|
|
50
|
+
*/
|
|
51
|
+
export class SerialMailbox {
|
|
52
|
+
tail = Promise.resolve();
|
|
53
|
+
pendingCount = 0;
|
|
54
|
+
processedCount = 0;
|
|
55
|
+
get pending() { return this.pendingCount; }
|
|
56
|
+
get processed() { return this.processedCount; }
|
|
57
|
+
submit(task) {
|
|
58
|
+
this.pendingCount += 1;
|
|
59
|
+
const run = this.tail.then(async () => {
|
|
60
|
+
try {
|
|
61
|
+
return await task();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.pendingCount -= 1;
|
|
65
|
+
this.processedCount += 1;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// 队列尾巴吞掉错误, 避免一个失败任务毒化后续任务 (错误仍由返回的 promise 抛给调用方)
|
|
69
|
+
this.tail = run.then(() => undefined, () => undefined);
|
|
70
|
+
return run;
|
|
71
|
+
}
|
|
72
|
+
/** 等队列跑空 (测试与收尾用) */
|
|
73
|
+
async drain() {
|
|
74
|
+
await this.tail;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 把一个 Channel 的状态与串行队列绑在一起。
|
|
79
|
+
*
|
|
80
|
+
* 现在**只是容器**: 还没有任何入口往里投递 (K5 第 4 步才接)。
|
|
81
|
+
* 因此它不改变现有行为 —— 现有链路仍走 Pi 实例字段。
|
|
82
|
+
*/
|
|
83
|
+
export class ChannelActor {
|
|
84
|
+
state;
|
|
85
|
+
mailbox = new SerialMailbox();
|
|
86
|
+
constructor(init = {}) {
|
|
87
|
+
this.state = createActorState(init);
|
|
88
|
+
}
|
|
89
|
+
/** 投递一次执行 (串行执行)。执行期间 `state.activeRun` 由调用方/后续步骤写入 */
|
|
90
|
+
submit(fn) {
|
|
91
|
+
// 任务体内建立 ALS 上下文 ⇒ 任务内部的异步续体也能认出"自己在谁的上下文里"
|
|
92
|
+
return this.mailbox.submit(() => actorCtx.run(this, () => fn(this.state)));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* **K5 第 4 步 — hydrate**: 实现体 (load → filter → 截断 → 替换) 住在 Actor, 业务侧只给纯变换。
|
|
96
|
+
* 走邮箱 ⇒ 与 append / persist 串行, 不会再出现"读-改-写"三拍被并发踩掉。
|
|
97
|
+
* 返回灌入条数 (0 = 没历史 / 空 / 加载失败由调用方处理)。
|
|
98
|
+
*/
|
|
99
|
+
hydrateHistory(spec) {
|
|
100
|
+
return this.mailbox.submit(async () => {
|
|
101
|
+
const loaded = await spec.load();
|
|
102
|
+
if (!loaded)
|
|
103
|
+
return 0;
|
|
104
|
+
const next = spec.filter(loaded).slice(-spec.maxMessages);
|
|
105
|
+
if (next.length === 0)
|
|
106
|
+
return 0;
|
|
107
|
+
const arr = this.state.messageHistory;
|
|
108
|
+
arr.length = 0;
|
|
109
|
+
arr.push(...next);
|
|
110
|
+
return next.length;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* **K5 第 4 步 — persist 的取数拍**: 取一份快照 (浅拷贝)。
|
|
115
|
+
* 走邮箱 ⇒ 拿到的是一致的 history, 不会与正在进行的 append 交错 (原来的 `this.messageHistory.map(...)`
|
|
116
|
+
* 是"边写边读", 压缩/工具回灌并发时可能落盘到半截状态)。
|
|
117
|
+
*/
|
|
118
|
+
historySnapshot() {
|
|
119
|
+
return this.mailbox.submit(() => [...this.state.messageHistory]);
|
|
120
|
+
}
|
|
121
|
+
/** **K5 第 4 步 — append (排队版)**: 串行追加 (同 Channel 内排队) */
|
|
122
|
+
appendMessage(msg) {
|
|
123
|
+
return this.mailbox.submit(() => { this.state.messageHistory.push(msg); });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* **K5 第 4 步 — append (同步漏斗版)**: 给"写完立刻要读到"的调用面用 (Pi 的 ReAct 循环里大量
|
|
127
|
+
* `push` 后马上读 `length`/索引) —— 不能改成 await, 否则会改变同拍可见性。
|
|
128
|
+
*
|
|
129
|
+
* 它提供的是**归属与可数性** (所有 history 写入收敛到唯一漏斗, 门可以断言"零处直写"),
|
|
130
|
+
* **不是并发安全** —— 并发安全由"入口投递进 mailbox"负责 (K5 第 5 步 / entriesWired)。
|
|
131
|
+
*/
|
|
132
|
+
appendMessageSync(msg) {
|
|
133
|
+
this.state.messageHistory.push(msg);
|
|
134
|
+
}
|
|
135
|
+
/** **K5 第 4 步 — pop 漏斗** (与 appendMessageSync 同性质: 归属可数, 不负责并发) */
|
|
136
|
+
popMessageSync() {
|
|
137
|
+
return this.state.messageHistory.pop();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* **K5 第 4 步 — compact 的落地拍 (rebase)**: 用压缩结果替换 history, 但**保住**变换期间新追加的尾部。
|
|
141
|
+
*
|
|
142
|
+
* 为什么必须有这一拍: 压缩流水线是 **async** —— 从"取快照"(`snapshotLen`) 到"落地"之间是 await 窗口,
|
|
143
|
+
* 期间 append 进来的消息会被整块替换**丢掉** (lost update)。这里把尾部原样接回去。
|
|
144
|
+
* (同步压缩路径不受影响: 取快照与替换在同一拍相邻两行, 没有窗口 —— 见 pi-sdk 的注释。)
|
|
145
|
+
*/
|
|
146
|
+
rebaseHistory(compacted, snapshotLen) {
|
|
147
|
+
return this.mailbox.submit(() => {
|
|
148
|
+
const arr = this.state.messageHistory;
|
|
149
|
+
const at = Math.max(0, Math.min(snapshotLen, arr.length));
|
|
150
|
+
const tail = arr.slice(at);
|
|
151
|
+
arr.length = 0;
|
|
152
|
+
arr.push(...compacted, ...tail);
|
|
153
|
+
return { keptTail: tail.length };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** **K5 第 4 步 — 整体替换漏斗** (hydrate 回灌 / 压缩后的整体赋值都走这里) */
|
|
157
|
+
replaceHistory(next) {
|
|
158
|
+
const arr = this.state.messageHistory;
|
|
159
|
+
arr.length = 0;
|
|
160
|
+
arr.push(...next);
|
|
161
|
+
}
|
|
162
|
+
/** 取消当前任务: 中断 signal, 队列继续 (后续任务看到的是新的 controller) */
|
|
163
|
+
abort(reason = 'aborted') {
|
|
164
|
+
this.state.cancellation?.abort(reason);
|
|
165
|
+
this.state.cancellation = null;
|
|
166
|
+
}
|
|
167
|
+
/** 开一个新的取消位 (进入一次执行时用) */
|
|
168
|
+
beginCancellation() {
|
|
169
|
+
const ac = new AbortController();
|
|
170
|
+
this.state.cancellation = ac;
|
|
171
|
+
return ac.signal;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
175
|
+
// Actor 注册表 (K5 第 3 步): 一个 channel 一个 actor
|
|
176
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
/**
|
|
178
|
+
* 进程内注册表: **会话身份 → ChannelActor**。
|
|
179
|
+
*
|
|
180
|
+
* ⚠️ 键是**会话身份** (SessionStore key / 整条 peerId), **不是** channel 前缀 —— 这条是被全量回归打出来的:
|
|
181
|
+
* 第一版取 `peerId` 的 `:` 前段当键 ⇒ 同一 channel 下不同会话落进同一个桶 ⇒ 新 session 一构造
|
|
182
|
+
* 就看到别人的 history (4742 测试里 5 红, 症状统一 `expected 2/5/6 to be 0`)。
|
|
183
|
+
* 隔离粒度必须与被迁移状态原有的粒度一致: history 属于**会话**, 不属于 channel 前缀。
|
|
184
|
+
*
|
|
185
|
+
* ⚠️ **没有身份就不归属** (没有 'default' 兜底桶) —— 宁可不共享, 不许串台。这里先把"落点"建起来。
|
|
186
|
+
*
|
|
187
|
+
* ⚠️ 现在的定位 (不许夸大):
|
|
188
|
+
* · 只做**归属**: 同一 channelId 永远拿到同一个 actor 实例, 不同 channelId 互不相干;
|
|
189
|
+
* · **还没有任何入口把执行投递进来** (`submit()` 尚未被业务调用) ⇒ K5 进度里
|
|
190
|
+
* `entriesWired` 仍是 **0/4**, 行为与迁移前一致;
|
|
191
|
+
* · 进程内 (非持久): 重启后 actor 表为空 —— 持久化是后续步骤 (history/Goal/Run 的恢复)。
|
|
192
|
+
*/
|
|
193
|
+
const actors = new Map();
|
|
194
|
+
/**
|
|
195
|
+
* 取(或建)某 channel 的 actor。
|
|
196
|
+
* `init` **只在新建时生效** (既有 actor 不会被 init 覆盖 —— 避免"后到的调用把先建的会话状态冲掉")。
|
|
197
|
+
*/
|
|
198
|
+
export function getOrCreateActor(actorKey, init = {}) {
|
|
199
|
+
const key = actorKey;
|
|
200
|
+
const existing = actors.get(key);
|
|
201
|
+
if (existing)
|
|
202
|
+
return existing;
|
|
203
|
+
// ⚠️ **不拿身份键当 channel 绑定**: `state.channelId` 的语义 = 会话当前绑定的 channel
|
|
204
|
+
// (与 Pi 的 currentChannelId 一致, 由 prompt/入口设置)。工厂若预置成 peerId 前缀,
|
|
205
|
+
// 会让会话在入口设置之前就读到非空值 (snapshot / compaction cacheScope 都会跟着变) —— 静默行为变化。
|
|
206
|
+
const created = new ChannelActor({ ...init });
|
|
207
|
+
actors.set(key, created);
|
|
208
|
+
return created;
|
|
209
|
+
}
|
|
210
|
+
/** 只读探查 (不建) —— 测试与诊断用 */
|
|
211
|
+
export function peekActor(actorKey) {
|
|
212
|
+
return actors.get(actorKey);
|
|
213
|
+
}
|
|
214
|
+
/** 当前 actor 数 (诊断用) */
|
|
215
|
+
export function actorCount() {
|
|
216
|
+
return actors.size;
|
|
217
|
+
}
|
|
218
|
+
/** 清空注册表 —— **仅测试用** (避免测试之间互相污染) */
|
|
219
|
+
export function resetActors() {
|
|
220
|
+
actors.clear();
|
|
221
|
+
channelQueues.clear();
|
|
222
|
+
privateActorsCreated = 0;
|
|
223
|
+
}
|
|
224
|
+
export async function deliverThroughActor(
|
|
225
|
+
// 形参故意放宽成 `unknown`: 调用点的 receiver 类型五花八门 (AgentSession / 结构子集 / any),
|
|
226
|
+
// 写成 `{ actor?: ChannelActor }` 会触发 TS 的弱类型检查 ("no properties in common") 而误报。
|
|
227
|
+
// 实际用法由下面这行运行时取值保证 (取不到 actor 就直跑)。
|
|
228
|
+
holder, run, opts = {}) {
|
|
229
|
+
const actor = holder?.actor;
|
|
230
|
+
if (!actor || typeof actor.submit !== 'function')
|
|
231
|
+
return run();
|
|
232
|
+
// **重入保护**: 已经跑在同一个 actor 的 mailbox 任务里 ⇒ 直跑。
|
|
233
|
+
// 否则"运行中被调用的执行点"(LLM 回调 / judge / 工具内再问) 会往自己的队列尾投递 ⇒ **自锁**。
|
|
234
|
+
if (actorCtx.getStore() === actor)
|
|
235
|
+
return run();
|
|
236
|
+
// channel 级串行 (opt-in): 走 channel 队列; 同样要防重入 (已在同一 channel 队列任务里 ⇒ 直跑)
|
|
237
|
+
if (opts.serializeByChannel) {
|
|
238
|
+
const ch = actor.state.channelId;
|
|
239
|
+
if (ch) {
|
|
240
|
+
const q = getChannelQueue(ch);
|
|
241
|
+
if (channelCtx.getStore() === q)
|
|
242
|
+
return run();
|
|
243
|
+
return q.submit(() => channelCtx.run(q, () => actor.submit(() => actorCtx.run(actor, () => run()))));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return actor.submit(run);
|
|
247
|
+
}
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// channel 级串行锁 (K5 步骤⑤) —— **能力先落地, 默认不启用**
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
/**
|
|
252
|
+
* channel 级队列注册表: `channelId → 串行队列`。
|
|
253
|
+
*
|
|
254
|
+
* 为什么要它 / 为什么默认不启用 (证据在 `plan-channel-actor.ts` 的 `K5_CHANNEL_LOCK`):
|
|
255
|
+
* · **证据**: web 的 channel 只有一个 `currentSessionId` ⇒ 用户消息都投到**同一个会话身份**
|
|
256
|
+
* ⇒ 身份级串行 (mailbox) **已经把该 channel 的输入串起来了** —— 验收①在活跃会话上已成立。
|
|
257
|
+
* · **能力**: 会话切换 (`[新会话] 已切换`) 后旧会话仍在内存, 此时两个身份可能并行 —— 若要"跨会话切换也串行",
|
|
258
|
+
* 需要 channel 级锁。
|
|
259
|
+
* · **代价**: 同 channel 的**多个 agent** (P2P 多智能体) 也会被串起来 ⇒ 吞吐下降。
|
|
260
|
+
* ⇒ 是否全局启用是**意图层**的决定 (leo), 这里只把能力与证据备好, 由台账的 `enabled` 开关控制。
|
|
261
|
+
*/
|
|
262
|
+
const channelQueues = new Map();
|
|
263
|
+
/** 取(或建)某 channel 的串行队列 */
|
|
264
|
+
export function getChannelQueue(channelId) {
|
|
265
|
+
const key = channelId || 'default';
|
|
266
|
+
let q = channelQueues.get(key);
|
|
267
|
+
if (!q) {
|
|
268
|
+
q = new SerialMailbox();
|
|
269
|
+
channelQueues.set(key, q);
|
|
270
|
+
}
|
|
271
|
+
return q;
|
|
272
|
+
}
|
|
273
|
+
/** 当前 channel 队列数 (诊断用) */
|
|
274
|
+
export function channelQueueCount() {
|
|
275
|
+
return channelQueues.size;
|
|
276
|
+
}
|
|
277
|
+
/** channel 队列上下文 (重入判定用; 与 actorCtx 同理 —— 不许自锁) */
|
|
278
|
+
const channelCtx = new AsyncLocalStorage();
|
|
279
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
|
+
// 私有 actor (K5 步骤⑧): 给"没有会话身份"的 session 用
|
|
281
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
282
|
+
let privateActorsCreated = 0;
|
|
283
|
+
/**
|
|
284
|
+
* 造一个**私有** actor: **不注册**进 `actors`, 所以别人拿不到它 ⇒ 不可能串台。
|
|
285
|
+
*
|
|
286
|
+
* 为什么需要它 (K5 步骤⑧ 的前提): 要删掉 Pi 实例侧的"绑定前暂存"字段, 就必须保证**任何 session 都有 actor**。
|
|
287
|
+
* 没有身份时不许猜键 (猜就会共享) —— 那就给它一个**谁也拿不到**的私有 actor:
|
|
288
|
+
* 隔离性比"共享"更保守, 语义与"暂存字段"完全等价 (每个 session 自己一份)。
|
|
289
|
+
*/
|
|
290
|
+
export function createPrivateActor() {
|
|
291
|
+
privateActorsCreated += 1;
|
|
292
|
+
return new ChannelActor();
|
|
293
|
+
}
|
|
294
|
+
/** 已创建的私有 actor 数 (诊断/测试用: 证明"每个无身份 session 各拿一份") */
|
|
295
|
+
export function privateActorCount() {
|
|
296
|
+
return privateActorsCreated;
|
|
297
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const RUN_CONTROL_KINDS = ['record-recovery', 'set-run-status', 'wake-goal'];
|
|
2
|
+
/** 每种请求**必须**携带的定位字段 (校验用; 缺了直接拒, 不许猜) */
|
|
3
|
+
export const RUN_CONTROL_REQUIRED = {
|
|
4
|
+
'record-recovery': 'runId',
|
|
5
|
+
'set-run-status': 'runId',
|
|
6
|
+
'wake-goal': 'goalId',
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* 端口**不抛异常但明确拒绝** (`{ ok: false, reason }`) 的归一化判定。
|
|
10
|
+
* 为什么必须有: store 类原语用返回值表达"状态迁移被拒绝", 而控制面若只看"有没有抛",
|
|
11
|
+
* 就会把**被拒当成成功** (实测: pause/abort 路由依赖 `r.ok`/`r.reason` 回 409)。
|
|
12
|
+
* ⇒ 约定: 端口返回 `{ ok: false }` ⇒ 控制面也报 ok=false, detail = `端口拒绝: <reason>`。
|
|
13
|
+
*/
|
|
14
|
+
function portRefusal(res) {
|
|
15
|
+
if (res && typeof res === 'object' && 'ok' in res) {
|
|
16
|
+
const r = res;
|
|
17
|
+
if (r.ok === false)
|
|
18
|
+
return `端口拒绝: ${String(r.reason ?? '未给原因')}`;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const audit = [];
|
|
23
|
+
const AUDIT_MAX = 200;
|
|
24
|
+
/** 审计流水 (最近 N 条; 测试与运维可读) */
|
|
25
|
+
export function runControlAudit() {
|
|
26
|
+
return audit;
|
|
27
|
+
}
|
|
28
|
+
export function resetRunControlAudit() {
|
|
29
|
+
audit.length = 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 提交一条控制请求。**唯一入口** —— 校验 (kind 合法 / 定位字段齐 / origin 非空) → 派发到 port → 记审计。
|
|
33
|
+
* 一律**返回结果对象**, 不抛 (调用方不该用 try/catch 表达"请求被拒")。
|
|
34
|
+
*/
|
|
35
|
+
export async function submitRunControl(req, ports) {
|
|
36
|
+
const kind = req?.kind;
|
|
37
|
+
const finish = (ok, detail, port, result) => {
|
|
38
|
+
audit.push({
|
|
39
|
+
at: Date.now(),
|
|
40
|
+
kind,
|
|
41
|
+
origin: req?.origin ?? '(anonymous)',
|
|
42
|
+
target: String(req?.runId ?? req?.goalId ?? ''),
|
|
43
|
+
ok,
|
|
44
|
+
detail,
|
|
45
|
+
});
|
|
46
|
+
if (audit.length > AUDIT_MAX)
|
|
47
|
+
audit.splice(0, audit.length - AUDIT_MAX);
|
|
48
|
+
return { ok, kind, via: 'kernel-control', detail, port, result };
|
|
49
|
+
};
|
|
50
|
+
if (!RUN_CONTROL_KINDS.includes(kind))
|
|
51
|
+
return finish(false, `未知请求类型: ${String(req?.kind)}`);
|
|
52
|
+
if (!req.origin)
|
|
53
|
+
return finish(false, '缺少 origin (审计要求: 不许匿名请求)');
|
|
54
|
+
const need = RUN_CONTROL_REQUIRED[kind];
|
|
55
|
+
const target = need === 'runId' ? req.runId : req.goalId;
|
|
56
|
+
if (!target)
|
|
57
|
+
return finish(false, `${kind} 缺少 ${need}`);
|
|
58
|
+
if (kind === 'record-recovery') {
|
|
59
|
+
if (typeof ports.recordRecovery !== 'function')
|
|
60
|
+
return finish(false, 'port 未注入: recordRecovery');
|
|
61
|
+
try {
|
|
62
|
+
const res = await ports.recordRecovery(target, req.payload ?? {});
|
|
63
|
+
const refusal = portRefusal(res);
|
|
64
|
+
return refusal ? finish(false, refusal, 'recordRecovery', res) : finish(true, undefined, 'recordRecovery', res);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return finish(false, `recordRecovery 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (kind === 'set-run-status') {
|
|
71
|
+
if (typeof ports.setRunStatus !== 'function')
|
|
72
|
+
return finish(false, 'port 未注入: setRunStatus');
|
|
73
|
+
const status = String(req.payload?.status ?? '');
|
|
74
|
+
if (!status)
|
|
75
|
+
return finish(false, 'set-run-status 缺少 payload.status');
|
|
76
|
+
try {
|
|
77
|
+
const res = await ports.setRunStatus(target, status, req.payload);
|
|
78
|
+
const refusal = portRefusal(res);
|
|
79
|
+
return refusal ? finish(false, refusal, 'setRunStatus', res) : finish(true, undefined, 'setRunStatus', res);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return finish(false, `setRunStatus 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (typeof ports.setContinuation !== 'function')
|
|
86
|
+
return finish(false, 'port 未注入: setContinuation');
|
|
87
|
+
try {
|
|
88
|
+
const res = await ports.setContinuation(target, req.payload ?? {});
|
|
89
|
+
const refusal = portRefusal(res);
|
|
90
|
+
return refusal ? finish(false, refusal, 'setContinuation', res) : finish(true, undefined, 'setContinuation', res);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return finish(false, `setContinuation 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|