@bolloon/bolloon-agent 0.3.0 → 0.3.3

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.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * judgeness · protocol.ts
3
+ *
4
+ * 4 新 P2P kind (扩展 judgment-protocol 的 Kind 枚举):
5
+ * - hearth_description_publish: A 告知 B "我公开了 jd <id>"
6
+ * - hearth_description_query: A 向 B 询问 jd <id> 正文
7
+ * - hearth_autoadd_invite: A 邀请 B 加入 channel <topic>
8
+ * - hearth_block: A 屏蔽 B / 某 channel
9
+ *
10
+ * 复用 src/agents/judgment-protocol.ts 的 listener 安装模式 (174-194).
11
+ * Transport 仍走 IrohTransport (不另起), 复用 sendMessage.
12
+ *
13
+ * 防御期 (现在 → 6 月):
14
+ * - 此文件已发布, 但 4 kind 仅在 enum 占位; 不会发帧.
15
+ * - 相持期开始才真正调用 sendMessage.
16
+ */
17
+ import { EventEmitter } from 'events';
18
+ import { irohTransport as defaultIrohTransport } from '../network/iroh-transport.js';
19
+ import { resolveGate2, resolveGate3 } from './visibility.js';
20
+ class HearthEventBus extends EventEmitter {
21
+ }
22
+ export const hearthEventBus = new HearthEventBus();
23
+ // ---------------------------------------------------------------------------
24
+ // 帧构造 / 解析
25
+ // ---------------------------------------------------------------------------
26
+ function encode(f) {
27
+ return new TextEncoder().encode(JSON.stringify({ kind: f.kind, payload: f.payload, ts: f.payload.ts }));
28
+ }
29
+ function decode(buf) {
30
+ try {
31
+ const obj = JSON.parse(new TextDecoder().decode(buf));
32
+ if (!obj?.kind || !obj.payload)
33
+ return null;
34
+ if (!isHearthKind(obj.kind))
35
+ return null;
36
+ return obj;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function isHearthKind(k) {
43
+ return (k === 'hearth_description_publish' ||
44
+ k === 'hearth_description_query' ||
45
+ k === 'hearth_autoadd_invite' ||
46
+ k === 'hearth_block');
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // 协议硬约束 (发送前 throw)
50
+ // ---------------------------------------------------------------------------
51
+ /** 在调用 transport.sendMessage 前必跑一次 */
52
+ export function validateFrameBeforeSend(frame) {
53
+ const p = frame.payload;
54
+ switch (frame.kind) {
55
+ case 'hearth_description_publish':
56
+ if (!p.descriptionId)
57
+ throw new Error('hearth_description_publish: descriptionId required');
58
+ if (!p.visibility)
59
+ throw new Error('hearth_description_publish: visibility required');
60
+ break;
61
+ case 'hearth_description_query':
62
+ if (!p.descriptionId)
63
+ throw new Error('hearth_description_query: descriptionId required');
64
+ break;
65
+ case 'hearth_autoadd_invite':
66
+ if (!p.channelTopic)
67
+ throw new Error('hearth_autoadd_invite: channelTopic required');
68
+ if (p.visibility === 'private')
69
+ throw new Error('hearth_autoadd_invite: visibility=private forbidden');
70
+ break;
71
+ case 'hearth_block':
72
+ if (p.targetNodeId === p.fromNodeId)
73
+ throw new Error('hearth_block: cannot block self');
74
+ break;
75
+ }
76
+ }
77
+ const states = new WeakMap();
78
+ function getState(t) {
79
+ let s = states.get(t);
80
+ if (!s) {
81
+ s = { listenersInstalled: false };
82
+ states.set(t, s);
83
+ }
84
+ return s;
85
+ }
86
+ export function ensureHearthListeners(transport = defaultIrohTransport) {
87
+ const s = getState(transport);
88
+ if (s.listenersInstalled)
89
+ return;
90
+ s.listenersInstalled = true;
91
+ transport.onMessage('hearth_description_publish', async (msg) => {
92
+ const f = decode(msg.payload);
93
+ if (!f || f.kind !== 'hearth_description_publish')
94
+ return;
95
+ const p = f.payload;
96
+ hearthEventBus.emit('event', { kind: 'publish_received', publishId: p.publishId, fromNodeId: p.fromNodeId });
97
+ await onPublishReceived(transport, p);
98
+ });
99
+ transport.onMessage('hearth_description_query', async (msg) => {
100
+ const f = decode(msg.payload);
101
+ if (!f || f.kind !== 'hearth_description_query')
102
+ return;
103
+ const p = f.payload;
104
+ hearthEventBus.emit('event', { kind: 'query_received', queryId: p.queryId, fromNodeId: p.fromNodeId, descriptionId: p.descriptionId });
105
+ await onQueryReceived(transport, p);
106
+ });
107
+ transport.onMessage('hearth_autoadd_invite', async (msg) => {
108
+ const f = decode(msg.payload);
109
+ if (!f || f.kind !== 'hearth_autoadd_invite')
110
+ return;
111
+ const p = f.payload;
112
+ hearthEventBus.emit('event', { kind: 'invite_received', inviteId: p.inviteId, fromNodeId: p.fromNodeId, channelTopic: p.channelTopic });
113
+ await onAutoaddInviteReceived(transport, p);
114
+ });
115
+ transport.onMessage('hearth_block', async (msg) => {
116
+ const f = decode(msg.payload);
117
+ if (!f || f.kind !== 'hearth_block')
118
+ return;
119
+ const p = f.payload;
120
+ hearthEventBus.emit('event', { kind: 'block_received', blockId: p.blockId, fromNodeId: p.fromNodeId, targetNodeId: p.targetNodeId });
121
+ await onBlockReceived(p);
122
+ });
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Listener handlers (实现都先 stub, 等相持期再接 store / p2p-direct)
126
+ // ---------------------------------------------------------------------------
127
+ async function onPublishReceived(_t, _p) {
128
+ // TODO(相持期): 校验 fromNodeId 在 allowlist 内, 然后 fetch cache
129
+ }
130
+ async function onQueryReceived(_t, _p) {
131
+ // TODO(相持期): 闸 3 后, 把对应 description 走 visibility scrubber 后回发
132
+ }
133
+ async function onAutoaddInviteReceived(_t, p) {
134
+ // 闸 2: 检查 fromNodeId 是否在 allowlist, 且 channel 隐私策略兼容
135
+ const ctx = { pubkey: p.fromNodeId, role: 'agent', channelTopic: p.channelTopic };
136
+ const g2 = await resolveGate2(p.fromNodeId, p.channelTopic);
137
+ if (!g2.allow) {
138
+ // 自动回一个 block
139
+ await sendBlock(_t, p.fromNodeId, p.channelTopic);
140
+ }
141
+ }
142
+ async function onBlockReceived(p) {
143
+ // TODO(相持期): 加入 inbound 黑名单, 后续入站全 reject
144
+ void p;
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // 发送接口 (相持期 / 反攻期主用)
148
+ // ---------------------------------------------------------------------------
149
+ export async function sendPublish(transport, descriptionId, toNodeId, visibility, channelTopic) {
150
+ const frame = {
151
+ kind: 'hearth_description_publish',
152
+ payload: {
153
+ publishId: `pub-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
154
+ fromNodeId: toNodeId, // 占位: 真实发送时本方 nodeId 由 transport 提供, 这里用目标
155
+ descriptionId,
156
+ visibility,
157
+ channelTopic,
158
+ ts: Date.now(),
159
+ },
160
+ };
161
+ validateFrameBeforeSend(frame);
162
+ await transport.sendMessage(toNodeId, frame.kind, encode(frame));
163
+ hearthEventBus.emit('event', { kind: 'publish_sent', publishId: frame.payload.publishId, peer: toNodeId });
164
+ }
165
+ export async function sendQuery(transport, descriptionId, toNodeId) {
166
+ const frame = {
167
+ kind: 'hearth_description_query',
168
+ payload: {
169
+ queryId: `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
170
+ fromNodeId: toNodeId,
171
+ descriptionId,
172
+ ts: Date.now(),
173
+ },
174
+ };
175
+ validateFrameBeforeSend(frame);
176
+ await transport.sendMessage(toNodeId, frame.kind, encode(frame));
177
+ }
178
+ export async function sendAutoaddInvite(transport, channelTopic, toNodeId, visibility = 'allowlist') {
179
+ const frame = {
180
+ kind: 'hearth_autoadd_invite',
181
+ payload: {
182
+ inviteId: `inv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
183
+ fromNodeId: toNodeId,
184
+ channelTopic,
185
+ visibility,
186
+ ts: Date.now(),
187
+ },
188
+ };
189
+ validateFrameBeforeSend(frame);
190
+ await transport.sendMessage(toNodeId, frame.kind, encode(frame));
191
+ hearthEventBus.emit('event', { kind: 'invite_sent', inviteId: frame.payload.inviteId, peer: toNodeId });
192
+ }
193
+ export async function sendBlock(transport, targetNodeId, channelTopic, fromNodeId = '__self__') {
194
+ const frame = {
195
+ kind: 'hearth_block',
196
+ payload: {
197
+ blockId: `blk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
198
+ fromNodeId,
199
+ targetNodeId,
200
+ channelTopic,
201
+ ts: Date.now(),
202
+ },
203
+ };
204
+ validateFrameBeforeSend(frame);
205
+ await transport.sendMessage(targetNodeId, frame.kind, encode(frame));
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // 防御期唯一可对外暴露的健康查询 (无 IO)
209
+ // ---------------------------------------------------------------------------
210
+ export function listHearthKinds() {
211
+ return ['hearth_description_publish', 'hearth_description_query', 'hearth_autoadd_invite', 'hearth_block'];
212
+ }
213
+ // 关闭 lint: resolveGate3 未在本文件直用, 给相持期 protocol listener 用
214
+ void resolveGate3;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * judgeness · rank.ts — Discover 排名算法 (反攻期 O2)
3
+ *
4
+ * 设计原则 (与 plan §DISCOVER-RANKING 一致):
5
+ * - 可解释, 不黑盒
6
+ * - 4 因子线性: rank_score = a*recency + b*breadth + c*depth + d*trust
7
+ * - 权重可调 (visibility.yaml.ranking 段; 缺省 0.4/0.2/0.2/0.2)
8
+ * - 每条 ranked 项带 why 字段解释
9
+ *
10
+ * 防御期: 此文件已写, 但 routes-hearth.ts 的 /discover 还没接它 (DEFENSE_MODE).
11
+ */
12
+ export const DEFAULT_RANK_WEIGHTS = {
13
+ recency: 0.4,
14
+ breadth: 0.2,
15
+ depth: 0.2,
16
+ trust: 0.2,
17
+ };
18
+ /** 主函数. */
19
+ export function rankDescriptions(descs, opts = {}) {
20
+ const w = opts.weights ?? DEFAULT_RANK_WEIGHTS;
21
+ const recencyWindow = opts.recencyWindow ?? 30 * 24 * 60 * 60 * 1000;
22
+ const now = opts.nowMs ?? Date.now();
23
+ const trusted = opts.trustedPks ?? new Set();
24
+ const ownerMap = opts.ownerPkMap ?? new Map();
25
+ const out = [];
26
+ for (const d of descs) {
27
+ const recency = computeRecency(d, now, recencyWindow);
28
+ const breadth = computeBreadth(d);
29
+ const depth = computeDepth(d);
30
+ const ownerPk = ownerMap.get(d.descriptionId) ?? '__unknown__';
31
+ const trust = trusted.has(ownerPk) ? 1 : 0.3; // 不是 allowlist 也有 0.3 base score (public 维度)
32
+ const score = w.recency * recency + w.breadth * breadth + w.depth * depth + w.trust * trust;
33
+ out.push({
34
+ description: d,
35
+ rankScore: clamp01(score),
36
+ why: { recency, breadth, depth, trust },
37
+ });
38
+ }
39
+ // 排序: rankScore desc, recency desc tiebreaker
40
+ out.sort((a, b) => {
41
+ if (b.rankScore !== a.rankScore)
42
+ return b.rankScore - a.rankScore;
43
+ return b.description.createdAt.localeCompare(a.description.createdAt);
44
+ });
45
+ return out;
46
+ }
47
+ // ---- 子函数 (可单测) ----
48
+ export function computeRecency(d, now, window) {
49
+ const t = Date.parse(d.createdAt);
50
+ if (!Number.isFinite(t))
51
+ return 0;
52
+ const age = Math.max(0, now - t);
53
+ if (age >= window)
54
+ return 0;
55
+ return 1 - age / window;
56
+ }
57
+ export function computeBreadth(d) {
58
+ const topics = new Set(d.scope.topics ?? []);
59
+ const domains = new Set(d.scope.domains ?? []);
60
+ const all = new Set([...topics, ...domains]);
61
+ // 3 = 满分 (覆盖广)
62
+ return clamp01(all.size / 3);
63
+ }
64
+ export function computeDepth(d) {
65
+ const facets = d.facets ?? {};
66
+ const filled = ['judgment', 'taste_aesthetic', 'novelty_score', 'imaginative_score', 'curiosity_vector']
67
+ .filter((k) => facets[k] !== undefined && facets[k] !== null).length;
68
+ // 5 维满分 1; 加 basis 文本可冲 1.2 (clamp 1)
69
+ const basis = d.basis ?? {};
70
+ const basisBonus = ['taste_basis', 'novelty_basis', 'imagination_basis']
71
+ .filter((k) => typeof basis[k] === 'string' && basis[k].length > 5).length;
72
+ return clamp01(filled / 5 + basisBonus * 0.06);
73
+ }
74
+ function clamp01(x) {
75
+ if (!Number.isFinite(x))
76
+ return 0;
77
+ return Math.max(0, Math.min(1, x));
78
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * judgeness · reflect.ts
3
+ *
4
+ * description ↔ HumanJudgment 的双向反射:
5
+ * - reflectFromJudgment: 给一个 hv-id, 生成或更新对应 jd-id (5 维默认 unset, basis 空)
6
+ * - mergeIntoJudgment: 给一个 jd-id, 反向把 facets 写到 judgment 的 metadata (附加, 不破坏 v0)
7
+ *
8
+ * 关键不变量: 不动 HumanJudgment 主表 schema. 所有 facets 通过 metadata.judgeness_* 字段挂上.
9
+ * 这样老 v0 数据可被自动视为 judgeness_v0, 零迁移.
10
+ */
11
+ import { newDescriptionId, loadDescription, saveDescription, findDescriptionByJudgmentRef } from './store.js';
12
+ /** 给定 HumanJudgment, 生成 (或更新) JudgenessDescription.
13
+ * 若已存在同 judgmentRef 的 jd, 则 facets/basis 合并 (覆盖优先). */
14
+ export async function reflectFromJudgment(opts) {
15
+ const existing = await findDescriptionByJudgmentRef(opts.judgment.id);
16
+ const now = new Date().toISOString();
17
+ if (existing) {
18
+ const merged = {
19
+ ...existing,
20
+ facets: { ...existing.facets, ...(opts.facets ?? {}) },
21
+ basis: { ...existing.basis, ...(opts.basis ?? {}) },
22
+ scope: {
23
+ domains: opts.scopeDomains ?? existing.scope.domains,
24
+ topics: opts.scopeTopics ?? existing.scope.topics,
25
+ },
26
+ visibility: opts.visibility ?? existing.visibility,
27
+ openState: opts.openState ?? existing.openState,
28
+ updatedAt: now,
29
+ lastTransitionAt: opts.visibility && opts.visibility !== existing.visibility
30
+ ? now
31
+ : opts.openState && opts.openState !== existing.openState
32
+ ? now
33
+ : existing.lastTransitionAt,
34
+ };
35
+ await saveDescription(merged);
36
+ return merged;
37
+ }
38
+ const created = {
39
+ descriptionId: newDescriptionId(),
40
+ judgmentRef: opts.judgment.id,
41
+ description_version: 1,
42
+ facets: {
43
+ judgment: opts.facets?.judgment,
44
+ taste_aesthetic: opts.facets?.taste_aesthetic,
45
+ novelty_score: opts.facets?.novelty_score,
46
+ imaginative_score: opts.facets?.imaginative_score,
47
+ curiosity_vector: opts.facets?.curiosity_vector,
48
+ },
49
+ basis: {
50
+ taste_basis: opts.basis?.taste_basis,
51
+ novelty_basis: opts.basis?.novelty_basis,
52
+ imagination_basis: opts.basis?.imagination_basis,
53
+ },
54
+ scope: { domains: opts.scopeDomains ?? [], topics: opts.scopeTopics ?? [] },
55
+ visibility: opts.visibility ?? 'private',
56
+ openState: opts.openState ?? 'locked',
57
+ by: opts.by,
58
+ byAgentId: opts.byAgentId,
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ lastTransitionAt: now,
62
+ };
63
+ await saveDescription(created);
64
+ return created;
65
+ }
66
+ /** 反向: 从 jd-id 合成一份 metadata 注入用的字段, 不动 HumanJudgment 主表. */
67
+ export function deriveJudgmentMetadataPatch(jdId, jd) {
68
+ return {
69
+ judgeness_ref: jdId,
70
+ judgeness_card_version: jd.description_version,
71
+ judgeness_visibility: jd.visibility,
72
+ judgeness_open_state: jd.openState,
73
+ };
74
+ }
75
+ /** 给一个 hv-id 拿最新 jd (用于 judgment-protocol 的 reflect 钩子).
76
+ * 供 judgment-protocol.ts:463-537 reflect() 在写完 HumanJudgment 后异步触发. */
77
+ export async function reflectAfterJudgment(judgment, by, byAgentId) {
78
+ // 默认: scope=topics 从 context.domain 推出, facets 全 unset.
79
+ return await reflectFromJudgment({
80
+ judgment,
81
+ by,
82
+ byAgentId,
83
+ scopeDomains: [judgment.context.domain],
84
+ scopeTopics: [judgment.context.domain],
85
+ visibility: 'private',
86
+ openState: 'locked',
87
+ });
88
+ }
89
+ /** 工具: 拿一个 jd, 返回它引用的 hv (供 audit) */
90
+ export async function descriptionToJudgmentRef(jdId) {
91
+ const d = await loadDescription(jdId);
92
+ return d?.judgmentRef ?? null;
93
+ }