@bolloon/bolloon-agent 0.5.3 → 0.5.5
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 +38 -32
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +268 -47
- package/dist/agents/pi-sdk.js +470 -22
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/session-store.js +144 -3
- 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/agents/wallet-tools.js +63 -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 +231 -32
- package/dist/cli/input-history.js +119 -0
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +69 -7
- package/dist/cli/log-gate.js +46 -1
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/session-summary.js +129 -0
- package/dist/cli/session-tree.js +141 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +155 -0
- package/dist/cli/stores.js +16 -0
- package/dist/cli/trace-line.js +54 -0
- package/dist/cli/wallet-store.js +159 -0
- package/dist/cli/wallet-table.js +46 -0
- package/dist/cli-entry.js +22 -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 +38 -32
- package/dist/electron-build/agents/parse-tool-call.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 +268 -46
- 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 +471 -22
- 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/session-store.js +144 -3
- package/dist/electron-build/agents/session-store.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/agents/wallet-tools.js +102 -0
- package/dist/electron-build/agents/wallet-tools.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/cli/wallet-store.js +200 -0
- package/dist/electron-build/cli/wallet-store.js.map +1 -0
- 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/llm/pi-ai.js +25 -0
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- 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 +1324 -173
- package/dist/llm/pi-ai.js +23 -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 +3 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +2 -0
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* event-shell.ts — 智能体事件网络的「统一事件外壳」(P2, 2026-10-01)
|
|
3
|
+
*
|
|
4
|
+
* 目标 (docs/wiki/agent-event-network-plan.md §P2):
|
|
5
|
+
* 把 message / delegation / proposal / vote / result / observation / payment / task /
|
|
6
|
+
* discovery / error 统一成**一种**事件信封; 事件本体只放 **元数据 + CID**(大内容按 CID 惰性取,
|
|
7
|
+
* 与 `cid-database.ts` 的 lazily-fetch 一致)。
|
|
8
|
+
*
|
|
9
|
+
* 四条硬要求 (每条都能被 `scripts/verify-event-index.ts` 真跑出来, 不是注释里的承诺):
|
|
10
|
+
*
|
|
11
|
+
* ① 严格校验, 不默许:
|
|
12
|
+
* 未知 type / 缺必填 / summary 超预算 / topic·capability 超长 / cid 不是合法 CID
|
|
13
|
+
* → `inspectEvent` 逐条报 issue, `validateEvent` 直接抛 `EventValidationError`。
|
|
14
|
+
* (「默认值时隐式补一个空串然后照常放行」正是本文件要避免的"默许"。)
|
|
15
|
+
*
|
|
16
|
+
* ② 时间非单调必须报出来:
|
|
17
|
+
* 单条事件谈不上"单调", 所以单调性只在**流**上判: `checkMonotonicTs(events)` 给 issue 列表,
|
|
18
|
+
* `inspectEventStream(raws, { monotonic: 'enforce' })` 把它算进 `ok`,
|
|
19
|
+
* `assertMonotonicTs(events)` 抛 `TS_NOT_MONOTONIC`。
|
|
20
|
+
* 默认 `monotonic:'report'` —— 如实报出但不算失败, 因为**多写者合并后的流本来就不保证全局单调**
|
|
21
|
+
* (强行要求会让合法数据永远读不进来)。"如实报" ≠ "强行禁止"。
|
|
22
|
+
*
|
|
23
|
+
* ③ canonical 序列化(键排序) —— 内容寻址的地基:
|
|
24
|
+
* `canonicalJson(v)` 递归键排序 + 丢弃 undefined (dag-cbor 编不了 undefined);
|
|
25
|
+
* **非有限数字直接抛**, 拒绝 `JSON.stringify` 那种把 NaN/Infinity 静默变成 null 的行为。
|
|
26
|
+
* 于是同一语义事件在**任何库 / 任何进程**里序列化出来的字节相同 ⇒ `eventCid` 可作为事件 id。
|
|
27
|
+
*
|
|
28
|
+
* ④ 向后兼容: 老记录 (cid-database.ts 的 `CIDRecord`: memory/context/state/ui/knowledge,
|
|
29
|
+
* 字段是 `{id, agentId, timestamp, type, content, metadata, version, parentId?}`)
|
|
30
|
+
* 走 `readEvent` 自动映射成外壳; **缺的字段逐条报 warning**, 不假装老记录本来就有。
|
|
31
|
+
*/
|
|
32
|
+
import { CID } from 'multiformats/cid';
|
|
33
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
34
|
+
import { sha256 } from 'multiformats/hashes/sha2';
|
|
35
|
+
/** 外壳版本 (加字段/改语义时 +1; 读侧对更高版本报 UNSUPPORTED_VERSION, 不静默降级) */
|
|
36
|
+
export const EVENT_SHELL_VERSION = 1;
|
|
37
|
+
/** 统一事件类型 (P2 要求至少覆盖这十种) */
|
|
38
|
+
export const EVENT_TYPES = [
|
|
39
|
+
'message',
|
|
40
|
+
'delegation',
|
|
41
|
+
'proposal',
|
|
42
|
+
'vote',
|
|
43
|
+
'result',
|
|
44
|
+
'observation',
|
|
45
|
+
'payment',
|
|
46
|
+
'task',
|
|
47
|
+
'discovery',
|
|
48
|
+
'error',
|
|
49
|
+
];
|
|
50
|
+
/** 引用关系 (refs[]): 只放 id, 绝不放正文 */
|
|
51
|
+
export const REF_TYPES = ['event', 'cid', 'did', 'group', 'external'];
|
|
52
|
+
/** 必填字段 (缺一个就是 issue; refs 不在其中 —— 默认 []) */
|
|
53
|
+
export const REQUIRED_FIELDS = ['v', 'id', 'type', 'actor', 'actorId', 'group', 'ts', 'summary'];
|
|
54
|
+
/** 摘要上限 (索引里只允许出现的有界摘要; 正文超预算的部分绝不进索引) */
|
|
55
|
+
export const MAX_SUMMARY_LEN = 160;
|
|
56
|
+
/** topic / capability 长度上限 (它们会变成索引 key 的一部分) */
|
|
57
|
+
export const MAX_LABEL_LEN = 120;
|
|
58
|
+
export class EventValidationError extends Error {
|
|
59
|
+
code;
|
|
60
|
+
issues;
|
|
61
|
+
constructor(code, issues, message) {
|
|
62
|
+
const head = issues[0] ? `${issues[0].field}: ${issues[0].message}` : '无细节';
|
|
63
|
+
super(message ?? `事件校验失败 [${code}] (${issues.length} 条): ${head}`);
|
|
64
|
+
this.name = 'EventValidationError';
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.issues = issues;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
70
|
+
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
71
|
+
export function isKnownEventType(v) {
|
|
72
|
+
return typeof v === 'string' && EVENT_TYPES.includes(v);
|
|
73
|
+
}
|
|
74
|
+
/** 是不是一个合法 CID (bafy… / bafk… 等; 不合法就 false, 不抛) */
|
|
75
|
+
export function isCid(s) {
|
|
76
|
+
if (typeof s !== 'string' || !s.length)
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
CID.parse(s);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 严格校验一个**新外壳**。返回 issue 列表而不是抛 —— 调用方自己决定报/抛。
|
|
88
|
+
* `strictUnknownFields`: 未知顶层字段也报 issue (默认关 —— 前向兼容: 未来版本加的字段不该让旧读侧拒收)。
|
|
89
|
+
*/
|
|
90
|
+
export function inspectEvent(raw, opts) {
|
|
91
|
+
const issues = [];
|
|
92
|
+
const push = (field, code, message) => {
|
|
93
|
+
issues.push({ field, code, message });
|
|
94
|
+
};
|
|
95
|
+
if (!isPlainObject(raw)) {
|
|
96
|
+
return { ok: false, issues: [{ field: '(root)', code: 'NOT_OBJECT', message: `事件必须是对象, 实际是 ${raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw}` }] };
|
|
97
|
+
}
|
|
98
|
+
// v
|
|
99
|
+
if (!('v' in raw))
|
|
100
|
+
push('v', 'MISSING_FIELD', '缺外壳版本 v');
|
|
101
|
+
else if (typeof raw.v !== 'number' || !Number.isInteger(raw.v))
|
|
102
|
+
push('v', 'BAD_FIELD_TYPE', `v 必须是整数, 实际 ${JSON.stringify(raw.v)}`);
|
|
103
|
+
else if (raw.v < 1)
|
|
104
|
+
push('v', 'BAD_VALUE', `v 必须 ≥1, 实际 ${raw.v}`);
|
|
105
|
+
else if (raw.v > EVENT_SHELL_VERSION)
|
|
106
|
+
push('v', 'UNSUPPORTED_VERSION', `外壳版本 ${raw.v} 高于本代码支持的 ${EVENT_SHELL_VERSION}`);
|
|
107
|
+
// id
|
|
108
|
+
if (!('id' in raw))
|
|
109
|
+
push('id', 'MISSING_FIELD', '缺事件 id');
|
|
110
|
+
else if (!isNonEmptyString(raw.id))
|
|
111
|
+
push('id', 'BAD_FIELD_TYPE', 'id 必须是非空字符串');
|
|
112
|
+
// type
|
|
113
|
+
if (!('type' in raw))
|
|
114
|
+
push('type', 'MISSING_FIELD', '缺事件 type');
|
|
115
|
+
else if (typeof raw.type !== 'string')
|
|
116
|
+
push('type', 'BAD_FIELD_TYPE', `type 必须是字符串, 实际 ${typeof raw.type}`);
|
|
117
|
+
else if (!isKnownEventType(raw.type))
|
|
118
|
+
push('type', 'UNKNOWN_TYPE', `未知事件类型 ${JSON.stringify(raw.type)}; 已知: ${EVENT_TYPES.join('/')}`);
|
|
119
|
+
// actor / actorId / group
|
|
120
|
+
for (const f of ['actor', 'actorId', 'group']) {
|
|
121
|
+
if (!(f in raw))
|
|
122
|
+
push(f, 'MISSING_FIELD', `缺 ${f}`);
|
|
123
|
+
else if (!isNonEmptyString(raw[f]))
|
|
124
|
+
push(f, 'BAD_FIELD_TYPE', `${f} 必须是非空字符串`);
|
|
125
|
+
}
|
|
126
|
+
// ts
|
|
127
|
+
if (!('ts' in raw))
|
|
128
|
+
push('ts', 'MISSING_FIELD', '缺时间戳 ts');
|
|
129
|
+
else if (typeof raw.ts !== 'number' || !Number.isFinite(raw.ts))
|
|
130
|
+
push('ts', 'BAD_FIELD_TYPE', `ts 必须是有限数字, 实际 ${JSON.stringify(raw.ts)}`);
|
|
131
|
+
else if (raw.ts < 0)
|
|
132
|
+
push('ts', 'BAD_VALUE', `ts 不能为负, 实际 ${raw.ts}`);
|
|
133
|
+
// summary (有界)
|
|
134
|
+
if (!('summary' in raw))
|
|
135
|
+
push('summary', 'MISSING_FIELD', '缺摘要 summary');
|
|
136
|
+
else if (typeof raw.summary !== 'string')
|
|
137
|
+
push('summary', 'BAD_FIELD_TYPE', 'summary 必须是字符串');
|
|
138
|
+
else if (raw.summary.length > MAX_SUMMARY_LEN)
|
|
139
|
+
push('summary', 'SUMMARY_TOO_LONG', `summary ${raw.summary.length} 字 > 上限 ${MAX_SUMMARY_LEN} (摘要必须是有界的, 否则索引里就装进正文了)`);
|
|
140
|
+
// topic / capability (非空 + 有界)
|
|
141
|
+
for (const f of ['topic', 'capability']) {
|
|
142
|
+
const v = raw[f];
|
|
143
|
+
if (v === undefined || v === null)
|
|
144
|
+
continue;
|
|
145
|
+
if (!isNonEmptyString(v))
|
|
146
|
+
push(f, 'BAD_FIELD_TYPE', `${f} 若出现必须是非空字符串`);
|
|
147
|
+
else if (v.length > MAX_LABEL_LEN)
|
|
148
|
+
push(f, 'LABEL_TOO_LONG', `${f} ${v.length} 字 > 上限 ${MAX_LABEL_LEN}`);
|
|
149
|
+
}
|
|
150
|
+
// cid (正文 CID) —— 出现就必须真能 parse
|
|
151
|
+
if (raw.cid !== undefined && raw.cid !== null) {
|
|
152
|
+
if (!isNonEmptyString(raw.cid))
|
|
153
|
+
push('cid', 'BAD_FIELD_TYPE', 'cid 必须是非空字符串');
|
|
154
|
+
else if (!isCid(raw.cid))
|
|
155
|
+
push('cid', 'INVALID_CID', `cid 不是合法 CID: ${String(raw.cid).slice(0, 80)}`);
|
|
156
|
+
}
|
|
157
|
+
// refs
|
|
158
|
+
const refs = [];
|
|
159
|
+
if (raw.refs !== undefined && raw.refs !== null) {
|
|
160
|
+
if (!Array.isArray(raw.refs))
|
|
161
|
+
push('refs', 'BAD_FIELD_TYPE', 'refs 必须是数组');
|
|
162
|
+
else {
|
|
163
|
+
raw.refs.forEach((r, i) => {
|
|
164
|
+
if (!isPlainObject(r))
|
|
165
|
+
return push(`refs[${i}]`, 'BAD_REF', '引用必须是对象');
|
|
166
|
+
if (!isNonEmptyString(r.id))
|
|
167
|
+
return push(`refs[${i}].id`, 'BAD_REF', '引用 id 必须是非空字符串');
|
|
168
|
+
if (!isNonEmptyString(r.type))
|
|
169
|
+
return push(`refs[${i}].type`, 'BAD_REF', '引用 type 必须是非空字符串');
|
|
170
|
+
if (!REF_TYPES.includes(r.type)) {
|
|
171
|
+
return push(`refs[${i}].type`, 'BAD_REF', `引用 type ${JSON.stringify(r.type)} 不在 ${REF_TYPES.join('/')}`);
|
|
172
|
+
}
|
|
173
|
+
if (r.rel !== undefined && !isNonEmptyString(r.rel))
|
|
174
|
+
return push(`refs[${i}].rel`, 'BAD_REF', 'rel 若出现必须是非空字符串');
|
|
175
|
+
const ref = { type: r.type, id: r.id };
|
|
176
|
+
if (r.rel !== undefined)
|
|
177
|
+
ref.rel = r.rel;
|
|
178
|
+
refs.push(ref);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// metadata
|
|
183
|
+
if (raw.metadata !== undefined && raw.metadata !== null && !isPlainObject(raw.metadata)) {
|
|
184
|
+
push('metadata', 'BAD_FIELD_TYPE', 'metadata 必须是对象');
|
|
185
|
+
}
|
|
186
|
+
// 未知顶层字段 (可选严格)
|
|
187
|
+
if (opts?.strictUnknownFields) {
|
|
188
|
+
const known = new Set([...REQUIRED_FIELDS, 'refs', 'topic', 'capability', 'cid', 'metadata']);
|
|
189
|
+
for (const k of Object.keys(raw))
|
|
190
|
+
if (!known.has(k))
|
|
191
|
+
push(k, 'UNKNOWN_FIELD', `未知顶层字段 ${k}`);
|
|
192
|
+
}
|
|
193
|
+
if (issues.length)
|
|
194
|
+
return { ok: false, issues };
|
|
195
|
+
const event = {
|
|
196
|
+
v: raw.v,
|
|
197
|
+
id: raw.id,
|
|
198
|
+
type: raw.type,
|
|
199
|
+
actor: raw.actor,
|
|
200
|
+
actorId: raw.actorId,
|
|
201
|
+
group: raw.group,
|
|
202
|
+
ts: raw.ts,
|
|
203
|
+
refs,
|
|
204
|
+
summary: raw.summary,
|
|
205
|
+
};
|
|
206
|
+
if (raw.topic !== undefined && raw.topic !== null)
|
|
207
|
+
event.topic = raw.topic;
|
|
208
|
+
if (raw.capability !== undefined && raw.capability !== null)
|
|
209
|
+
event.capability = raw.capability;
|
|
210
|
+
if (raw.cid !== undefined && raw.cid !== null)
|
|
211
|
+
event.cid = raw.cid;
|
|
212
|
+
if (isPlainObject(raw.metadata) && Object.keys(raw.metadata).length)
|
|
213
|
+
event.metadata = raw.metadata;
|
|
214
|
+
return { ok: true, event, issues: [] };
|
|
215
|
+
}
|
|
216
|
+
/** 严格校验, 不合格直接抛 (写侧入口) */
|
|
217
|
+
export function validateEvent(raw, opts) {
|
|
218
|
+
const r = inspectEvent(raw, opts);
|
|
219
|
+
if (!r.ok || !r.event)
|
|
220
|
+
throw new EventValidationError('EVENT_INVALID', r.issues);
|
|
221
|
+
return r.event;
|
|
222
|
+
}
|
|
223
|
+
// ─────────────────────── 时间单调性 (流级) ───────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* 非递减检查。`ts[i] < ts[i-1]` → issue (把它如实报出来, 而不是默默 sort 了事)。
|
|
226
|
+
* 注意: 这是**流**的性质, 不是单条事件的性质。
|
|
227
|
+
*/
|
|
228
|
+
export function checkMonotonicTs(events) {
|
|
229
|
+
const issues = [];
|
|
230
|
+
for (let i = 1; i < events.length; i++) {
|
|
231
|
+
const prev = events[i - 1];
|
|
232
|
+
const cur = events[i];
|
|
233
|
+
if (cur.ts < prev.ts) {
|
|
234
|
+
issues.push({
|
|
235
|
+
field: `[${i}].ts`,
|
|
236
|
+
code: 'TS_NOT_MONOTONIC',
|
|
237
|
+
message: `时间倒退: ts=${cur.ts} < 上一条 ts=${prev.ts} (回退 ${prev.ts - cur.ts}ms; id=${cur.id ?? '?'})`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return issues;
|
|
242
|
+
}
|
|
243
|
+
/** 非递减断言 (要"硬失败"的调用方用这个) */
|
|
244
|
+
export function assertMonotonicTs(events) {
|
|
245
|
+
const issues = checkMonotonicTs(events);
|
|
246
|
+
if (issues.length)
|
|
247
|
+
throw new EventValidationError('TS_NOT_MONOTONIC', issues);
|
|
248
|
+
}
|
|
249
|
+
// ─────────────────────── canonical 序列化 ───────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* 递归 canonical 化: 对象键排序 / 丢弃 undefined 值 / 数组保序。
|
|
252
|
+
* 非有限数字与 bigint **抛** —— 静默转 null 就是在"默许"数据损坏。
|
|
253
|
+
*/
|
|
254
|
+
export function canonicalize(value) {
|
|
255
|
+
if (value === null)
|
|
256
|
+
return null;
|
|
257
|
+
const t = typeof value;
|
|
258
|
+
if (t === 'string' || t === 'boolean')
|
|
259
|
+
return value;
|
|
260
|
+
if (t === 'number') {
|
|
261
|
+
if (!Number.isFinite(value))
|
|
262
|
+
throw new TypeError(`canonicalize: 非有限数字 ${String(value)} (拒绝静默变 null)`);
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
if (t === 'bigint')
|
|
266
|
+
throw new TypeError('canonicalize: bigint 无法 canonical 编码 (会让跨语言字节不一致)');
|
|
267
|
+
if (t === 'undefined')
|
|
268
|
+
return undefined; // 由调用方丢弃
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
return value.map((v, i) => {
|
|
271
|
+
if (v === undefined)
|
|
272
|
+
throw new TypeError(`canonicalize: 数组第 ${i} 项是 undefined (JSON 会静默变 null)`);
|
|
273
|
+
return canonicalize(v);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (t === 'object') {
|
|
277
|
+
const src = value;
|
|
278
|
+
const out = {};
|
|
279
|
+
for (const k of Object.keys(src).sort()) {
|
|
280
|
+
const v = canonicalize(src[k]);
|
|
281
|
+
if (v === undefined)
|
|
282
|
+
continue;
|
|
283
|
+
out[k] = v;
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
throw new TypeError(`canonicalize: 不支持的类型 ${t}`);
|
|
288
|
+
}
|
|
289
|
+
/** canonical JSON 字符串 (键排序, 无 undefined) */
|
|
290
|
+
export function canonicalJson(value) {
|
|
291
|
+
return JSON.stringify(canonicalize(value));
|
|
292
|
+
}
|
|
293
|
+
/** 内容 → CID (dag-cbor + sha2-256 + CIDv1 codec 0x71)。与 cid-database.ts 的 contentToCid 同语义 ⇒ CID 可互通 */
|
|
294
|
+
export async function contentCid(content) {
|
|
295
|
+
const clean = canonicalize(content);
|
|
296
|
+
const bytes = dagCbor.encode(clean === undefined ? null : clean);
|
|
297
|
+
const hash = await sha256.digest(bytes);
|
|
298
|
+
return CID.createV1(0x71, hash).toString();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 外壳 → 事件 id (内容寻址)。**剔除 `id` 自己** —— 否则自指, 算不出来。
|
|
302
|
+
* 同语义事件 (字段全部相同) ⇒ 同 CID, 跨库/跨节点都对得上。
|
|
303
|
+
*/
|
|
304
|
+
export async function eventCid(shell) {
|
|
305
|
+
const copy = { ...shell };
|
|
306
|
+
delete copy.id;
|
|
307
|
+
return contentCid(copy);
|
|
308
|
+
}
|
|
309
|
+
/** 摘要: 有界 (≤maxLen), 单行。content 是对象时优先取语义字段, 否则用 canonical JSON —— 保证确定性 */
|
|
310
|
+
export function deriveSummary(content, maxLen = MAX_SUMMARY_LEN) {
|
|
311
|
+
let text = '';
|
|
312
|
+
if (content === null || content === undefined)
|
|
313
|
+
text = '';
|
|
314
|
+
else if (typeof content === 'string')
|
|
315
|
+
text = content;
|
|
316
|
+
else if (typeof content === 'number' || typeof content === 'boolean')
|
|
317
|
+
text = String(content);
|
|
318
|
+
else if (typeof content === 'object') {
|
|
319
|
+
const o = content;
|
|
320
|
+
let picked;
|
|
321
|
+
for (const k of ['summary', 'text', 'note', 'message', 'title', 'name']) {
|
|
322
|
+
if (typeof o[k] === 'string' && o[k].length) {
|
|
323
|
+
picked = o[k];
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
text = typeof picked === 'string' ? picked : canonicalJson(content);
|
|
328
|
+
}
|
|
329
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
330
|
+
return oneLine.length > maxLen ? oneLine.slice(0, maxLen) : oneLine;
|
|
331
|
+
}
|
|
332
|
+
/** 造一个新外壳: 算 cid(若给了 content) + 算 id(内容寻址) + 补 summary(若没给) */
|
|
333
|
+
export async function buildEvent(input) {
|
|
334
|
+
const shell = {
|
|
335
|
+
v: input.v ?? EVENT_SHELL_VERSION,
|
|
336
|
+
type: input.type,
|
|
337
|
+
actor: input.actor,
|
|
338
|
+
actorId: input.actorId,
|
|
339
|
+
group: input.group,
|
|
340
|
+
ts: input.ts,
|
|
341
|
+
refs: input.refs ?? [],
|
|
342
|
+
summary: input.summary ?? deriveSummary(input.content),
|
|
343
|
+
...(input.topic !== undefined ? { topic: input.topic } : {}),
|
|
344
|
+
...(input.capability !== undefined ? { capability: input.capability } : {}),
|
|
345
|
+
...(input.content !== undefined ? { cid: await contentCid(input.content) } : {}),
|
|
346
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
347
|
+
};
|
|
348
|
+
shell.id = await eventCid(shell);
|
|
349
|
+
const event = validateEvent(shell);
|
|
350
|
+
return event;
|
|
351
|
+
}
|
|
352
|
+
// ─────────────────────── 向后兼容: 老记录 ───────────────────────
|
|
353
|
+
/** cid-database.ts 的老记录类型 (没有 sort 之外的语义) */
|
|
354
|
+
export const LEGACY_CID_TYPES = ['memory', 'context', 'state', 'ui', 'knowledge'];
|
|
355
|
+
/**
|
|
356
|
+
* 老类型 → 外壳类型的**映射** (只做最接近的归类, 原类型仍完整保留在 metadata.legacy 里)。
|
|
357
|
+
* 为什么要映射而不是原样留着: 外壳的 type 是**闭集** (索引/路由都按它分支),
|
|
358
|
+
* 放一个集合外的值进去就会让下游到处是 `if (type === 'memory')` 的补丁。
|
|
359
|
+
*/
|
|
360
|
+
export const LEGACY_TYPE_MAP = {
|
|
361
|
+
memory: 'observation',
|
|
362
|
+
context: 'observation',
|
|
363
|
+
state: 'result',
|
|
364
|
+
ui: 'observation',
|
|
365
|
+
knowledge: 'discovery',
|
|
366
|
+
};
|
|
367
|
+
export function isLegacyRecord(raw) {
|
|
368
|
+
if (!isPlainObject(raw))
|
|
369
|
+
return false;
|
|
370
|
+
if (isKnownEventType(raw.type) && 'actorId' in raw)
|
|
371
|
+
return false; // 已是新外壳
|
|
372
|
+
const hasId = isNonEmptyString(raw.id);
|
|
373
|
+
const looksLegacy = 'agentId' in raw || 'timestamp' in raw || 'content' in raw;
|
|
374
|
+
const legacyType = typeof raw.type === 'string' && LEGACY_CID_TYPES.includes(raw.type);
|
|
375
|
+
return hasId && (looksLegacy || legacyType);
|
|
376
|
+
}
|
|
377
|
+
/** 老记录 → 外壳。缺什么补什么, 并且**逐条报**补了什么 (不含糊其辞) */
|
|
378
|
+
export function fromLegacyRecord(raw) {
|
|
379
|
+
const warnings = [];
|
|
380
|
+
const id = isNonEmptyString(raw.id) ? raw.id : '';
|
|
381
|
+
if (!id)
|
|
382
|
+
warnings.push('老记录没有 id');
|
|
383
|
+
const legacyType = typeof raw.type === 'string' ? raw.type : '';
|
|
384
|
+
const mapped = LEGACY_CID_TYPES.includes(legacyType)
|
|
385
|
+
? LEGACY_TYPE_MAP[legacyType]
|
|
386
|
+
: 'observation';
|
|
387
|
+
if (!LEGACY_CID_TYPES.includes(legacyType)) {
|
|
388
|
+
warnings.push(`老记录 type=${JSON.stringify(legacyType)} 不在 ${LEGACY_CID_TYPES.join('/')} 里, 归为 observation`);
|
|
389
|
+
}
|
|
390
|
+
const md = isPlainObject(raw.metadata) ? raw.metadata : {};
|
|
391
|
+
const agentId = isNonEmptyString(raw.agentId) ? raw.agentId : '';
|
|
392
|
+
if (!agentId)
|
|
393
|
+
warnings.push('老记录缺 agentId, actor 置为空串');
|
|
394
|
+
const actorId = isNonEmptyString(md.actorId) ? md.actorId : agentId;
|
|
395
|
+
if (!isNonEmptyString(md.actorId))
|
|
396
|
+
warnings.push('老记录缺 actorId —— 用 agentId 顶替 (没有伪造 DID)');
|
|
397
|
+
const group = isNonEmptyString(md.group) ? md.group : '';
|
|
398
|
+
if (!group)
|
|
399
|
+
warnings.push('老记录缺 group, 置为空串');
|
|
400
|
+
const ts = typeof raw.timestamp === 'number' && Number.isFinite(raw.timestamp) ? raw.timestamp : 0;
|
|
401
|
+
if (raw.timestamp === undefined)
|
|
402
|
+
warnings.push('老记录缺 timestamp, ts 置为 0');
|
|
403
|
+
let summary = isNonEmptyString(md.summary) ? md.summary : '';
|
|
404
|
+
if (!summary) {
|
|
405
|
+
summary = deriveSummary(raw.content);
|
|
406
|
+
warnings.push('老记录缺 summary, 由 content 现推 (≤' + MAX_SUMMARY_LEN + ' 字)');
|
|
407
|
+
}
|
|
408
|
+
else if (summary.length > MAX_SUMMARY_LEN) {
|
|
409
|
+
summary = summary.slice(0, MAX_SUMMARY_LEN);
|
|
410
|
+
warnings.push(`老记录 summary 超 ${MAX_SUMMARY_LEN} 字, 截断为有界摘要`);
|
|
411
|
+
}
|
|
412
|
+
const refs = [];
|
|
413
|
+
if (isNonEmptyString(raw.parentId)) {
|
|
414
|
+
refs.push({ type: 'event', id: raw.parentId, rel: 'parent' });
|
|
415
|
+
}
|
|
416
|
+
if (raw.version !== undefined && typeof raw.version !== 'number') {
|
|
417
|
+
warnings.push(`老记录 version 不是数字 (${JSON.stringify(raw.version)}), 原样放进 metadata.legacy`);
|
|
418
|
+
}
|
|
419
|
+
const topic = isNonEmptyString(md.topic) ? md.topic : undefined;
|
|
420
|
+
const capability = isNonEmptyString(md.capability) ? md.capability : undefined;
|
|
421
|
+
const cidField = isNonEmptyString(md.cid) ? md.cid : id;
|
|
422
|
+
if (!isNonEmptyString(md.cid) && id)
|
|
423
|
+
warnings.push('老记录没有单独的正文字段, cid 用记录 id (老记录本身就是内容寻址的)');
|
|
424
|
+
const legacyMeta = {
|
|
425
|
+
legacy: { type: legacyType, version: raw.version ?? null, agentId, keys: Object.keys(raw).sort() },
|
|
426
|
+
};
|
|
427
|
+
for (const k of ['topic', 'capability', 'cid', 'group', 'actorId']) {
|
|
428
|
+
if (md[k] !== undefined)
|
|
429
|
+
legacyMeta[k] = md[k];
|
|
430
|
+
}
|
|
431
|
+
const event = {
|
|
432
|
+
v: EVENT_SHELL_VERSION,
|
|
433
|
+
id,
|
|
434
|
+
type: mapped,
|
|
435
|
+
actor: agentId,
|
|
436
|
+
actorId,
|
|
437
|
+
group,
|
|
438
|
+
ts,
|
|
439
|
+
refs,
|
|
440
|
+
summary,
|
|
441
|
+
metadata: legacyMeta,
|
|
442
|
+
};
|
|
443
|
+
if (topic)
|
|
444
|
+
event.topic = topic;
|
|
445
|
+
if (capability)
|
|
446
|
+
event.capability = capability;
|
|
447
|
+
if (cidField)
|
|
448
|
+
event.cid = cidField;
|
|
449
|
+
return { event, legacy: true, warnings };
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 读入口: 新外壳走严格校验, 老记录走兼容映射。
|
|
453
|
+
* 两种都不是 → 如实报 UNRECOGNIZED_RECORD (不硬塞一个空壳)。
|
|
454
|
+
*/
|
|
455
|
+
export function inspectReadableEvent(raw) {
|
|
456
|
+
if (isPlainObject(raw) && isKnownEventType(raw.type) && 'actorId' in raw) {
|
|
457
|
+
const r = inspectEvent(raw);
|
|
458
|
+
if (!r.ok || !r.event)
|
|
459
|
+
return { ok: false, code: 'EVENT_INVALID', message: r.issues[0]?.message ?? '外壳校验失败', issues: r.issues };
|
|
460
|
+
return { ok: true, event: r.event, legacy: false, warnings: [] };
|
|
461
|
+
}
|
|
462
|
+
if (isLegacyRecord(raw)) {
|
|
463
|
+
const r = fromLegacyRecord(raw);
|
|
464
|
+
return { ok: true, event: r.event, legacy: true, warnings: r.warnings };
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
code: 'UNRECOGNIZED_RECORD',
|
|
469
|
+
message: `既不是事件外壳 (缺 actorId/type 不在闭集) 也不是老 CIDRecord: keys=${isPlainObject(raw) ? Object.keys(raw).join(',') : typeof raw}`,
|
|
470
|
+
issues: [],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/** 读入口 (抛版) */
|
|
474
|
+
export function readEvent(raw) {
|
|
475
|
+
const r = inspectReadableEvent(raw);
|
|
476
|
+
if (!r.ok)
|
|
477
|
+
throw new EventValidationError(r.code, r.issues.length ? r.issues : [{ field: '(root)', code: r.code, message: r.message }]);
|
|
478
|
+
return r.event;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* 流级读入 + 校验。
|
|
482
|
+
* `monotonic`:
|
|
483
|
+
* 'off' — 不查时间顺序
|
|
484
|
+
* 'report' — (默认) TS_NOT_MONOTONIC 进 issues, **不算失败**
|
|
485
|
+
* 'enforce' — TS_NOT_MONOTONIC 让 ok=false (要拒绝乱序流的调用方用)
|
|
486
|
+
* `LEGACY_COERCED` 是"读老记录时补了什么"的告知, 从不影响 ok。
|
|
487
|
+
*/
|
|
488
|
+
export function inspectEventStream(raws, opts) {
|
|
489
|
+
const events = [];
|
|
490
|
+
const issues = [];
|
|
491
|
+
let legacyCount = 0;
|
|
492
|
+
raws.forEach((raw, i) => {
|
|
493
|
+
const r = inspectReadableEvent(raw);
|
|
494
|
+
if (!r.ok) {
|
|
495
|
+
issues.push({ field: `[${i}]`, code: r.code, message: r.message });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (r.legacy) {
|
|
499
|
+
legacyCount++;
|
|
500
|
+
for (const w of r.warnings)
|
|
501
|
+
issues.push({ field: `[${i}]`, code: 'LEGACY_COERCED', message: w });
|
|
502
|
+
}
|
|
503
|
+
events.push(r.event);
|
|
504
|
+
});
|
|
505
|
+
const mode = opts?.monotonic ?? 'report';
|
|
506
|
+
const monoIssues = mode === 'off' ? [] : checkMonotonicTs(events);
|
|
507
|
+
issues.push(...monoIssues);
|
|
508
|
+
const hard = issues.filter((x) => x.code !== 'LEGACY_COERCED' && !(x.code === 'TS_NOT_MONOTONIC' && mode === 'report'));
|
|
509
|
+
return { ok: hard.length === 0, events, issues, legacyCount };
|
|
510
|
+
}
|