@bolloon/bolloon-agent 0.5.1 → 0.5.2

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,170 @@
1
+ /**
2
+ * efficode/lz77 - 自解压数据块用的轻量 LZ77 (真实实现, 不是占位)
3
+ *
4
+ * 数据块布局 (自解压头 + 码流):
5
+ * [varint origLen][varint compLen][token stream]
6
+ *
7
+ * token stream = 控制字节 + 载荷, 每控制字节管 8 个 token, MSB 在前:
8
+ * 控制位 0 → 后接 1 字节字面量
9
+ * 控制位 1 → 后接 2 字节匹配 (12 位 offset, 4 位 length-3)
10
+ *
11
+ * origLen 在头里 ⇒ 解码器**不需要**猜结束位置 (靠输出长度收敛), 尾随多余码流一律报错。
12
+ *
13
+ * 匹配查找用 deflate 那套**哈希链**: 3 字节前缀哈希 → 链上回溯 (最多 MAX_CHAIN 个候选).
14
+ * 第一版是"从 pos-1 逐个往前比"的朴素写法, 在 `...\n` 这种周期 > 16 的重复文本上**找不到匹配**
15
+ * (300 行重复文本压完反而膨胀 1538B —— 实测出来的, 已换掉)。
16
+ *
17
+ * 诚实边界 (写进 wiki 时按实测数字):
18
+ * - 参考实现: 窗口 4095B / 最短匹配 3B / 最长匹配 18B, **没有**哈夫曼那一层;
19
+ * - 对重复度高的结构化文本有效, 对**随机字节**会膨胀 (控制位占 1/8);
20
+ * - 真实压缩率对比见 scripts/verify-efficode.ts 的真测输出, 不许在文档里编。
21
+ */
22
+ import { EfficodeError } from './types.js';
23
+ const WINDOW = 4095; // 12 位 offset (0 保留为非法)
24
+ const MIN_MATCH = 3; // length-3 存 4 位 ⇒ 3..18
25
+ const MAX_MATCH = 18;
26
+ const MAX_CHAIN = 16; // 哈希链回溯上限 (控制最坏耗时)
27
+ const HASH_SIZE = 1 << 16; // 3 字节前缀哈希表大小
28
+ function writeVarint(out, n) {
29
+ let v = n >>> 0;
30
+ while (v >= 0x80) {
31
+ out.push((v & 0x7f) | 0x80);
32
+ v >>>= 7;
33
+ }
34
+ out.push(v & 0xff);
35
+ }
36
+ function readVarint(buf, pos) {
37
+ let value = 0;
38
+ let shift = 0;
39
+ let i = pos;
40
+ for (;;) {
41
+ if (i >= buf.length)
42
+ throw new EfficodeError('EFFICODE_TRUNCATED', 'varint 未读完就到底', i);
43
+ const b = buf[i++];
44
+ value |= (b & 0x7f) << shift;
45
+ if ((b & 0x80) === 0)
46
+ break;
47
+ shift += 7;
48
+ if (shift > 28)
49
+ throw new EfficodeError('EFFICODE_TRUNCATED', 'varint 超过 32 位上限', i);
50
+ }
51
+ return { value: value >>> 0, next: i };
52
+ }
53
+ /** 压缩。返回 [varint origLen][varint compLen][tokens] */
54
+ export function lz77Compress(input) {
55
+ const out = [];
56
+ writeVarint(out, input.length);
57
+ const bodyStart = out.length;
58
+ out.push(0); // 占位 (1B, varint(0)); 最后整体重拼, 不依赖占位宽度
59
+ const head = new Int32Array(HASH_SIZE).fill(-1);
60
+ const prev = new Int32Array(input.length).fill(-1);
61
+ const insertAt = (pos) => {
62
+ if (pos + MIN_MATCH > input.length)
63
+ return;
64
+ const h = hash3(input, pos);
65
+ prev[pos] = head[h];
66
+ head[h] = pos;
67
+ };
68
+ const findMatch = (pos) => {
69
+ if (pos + MIN_MATCH > input.length)
70
+ return null;
71
+ const h = hash3(input, pos);
72
+ const maxLen = Math.min(MAX_MATCH, input.length - pos);
73
+ let cand = head[h];
74
+ let chain = 0;
75
+ let bestLen = 0;
76
+ let bestOff = 0;
77
+ while (cand >= 0 && chain < MAX_CHAIN) {
78
+ const distance = pos - cand;
79
+ if (distance > WINDOW)
80
+ break;
81
+ let len = 0;
82
+ while (len < maxLen && input[cand + len] === input[pos + len])
83
+ len++;
84
+ if (len > bestLen) {
85
+ bestLen = len;
86
+ bestOff = distance;
87
+ if (len === maxLen)
88
+ break;
89
+ }
90
+ cand = prev[cand];
91
+ chain++;
92
+ }
93
+ return bestLen >= MIN_MATCH ? { offset: bestOff, length: bestLen } : null;
94
+ };
95
+ let i = 0;
96
+ while (i < input.length) {
97
+ const controlIdx = out.length;
98
+ let control = 0;
99
+ out.push(0);
100
+ for (let bit = 7; bit >= 0 && i < input.length; bit--) {
101
+ const match = findMatch(i);
102
+ if (match) {
103
+ control |= 1 << bit;
104
+ const v = ((match.offset & 0xfff) << 4) | ((match.length - MIN_MATCH) & 0xf);
105
+ out.push((v >>> 8) & 0xff, v & 0xff);
106
+ for (let k = 0; k < match.length; k++)
107
+ insertAt(i + k);
108
+ i += match.length;
109
+ }
110
+ else {
111
+ out.push(input[i]);
112
+ insertAt(i);
113
+ i += 1;
114
+ }
115
+ }
116
+ out[controlIdx] = control;
117
+ }
118
+ // compLen = 码流字节数 (= 占位字节之后的所有字节); 之前这里把占位字节也算进去了 ⇒ 声明比实际多 1 ⇒ 解码必报截断
119
+ const body = out.slice(bodyStart + 1);
120
+ const filled = [];
121
+ writeVarint(filled, input.length);
122
+ writeVarint(filled, body.length);
123
+ filled.push(...body);
124
+ return Uint8Array.from(filled);
125
+ }
126
+ function hash3(b, i) {
127
+ return (((b[i] << 10) ^ (b[i + 1] << 5) ^ b[i + 2]) & (HASH_SIZE - 1)) >>> 0;
128
+ }
129
+ /** 解压 (严格)。码流不足 / 长度对不上 / 超长一律抛, 不做任何"尽量解" */
130
+ export function lz77Decompress(block) {
131
+ const head = readVarint(block, 0);
132
+ const origLen = head.value;
133
+ const head2 = readVarint(block, head.next);
134
+ const compLen = head2.value;
135
+ const bodyEnd = head2.next + compLen;
136
+ if (bodyEnd > block.length) {
137
+ throw new EfficodeError('EFFICODE_TRUNCATED', `LZ77 码流声明 ${compLen}B 但只剩 ${block.length - head2.next}B`, head2.next);
138
+ }
139
+ const out = [];
140
+ let pos = head2.next;
141
+ while (out.length < origLen) {
142
+ if (pos >= bodyEnd) {
143
+ throw new EfficodeError('EFFICODE_TRUNCATED', `LZ77 码流提前结束 (期望 ${origLen}B, 实得 ${out.length}B)`, pos);
144
+ }
145
+ const control = block[pos++];
146
+ for (let bit = 7; bit >= 0 && out.length < origLen; bit--) {
147
+ if (control & (1 << bit)) {
148
+ if (pos + 2 > bodyEnd) {
149
+ throw new EfficodeError('EFFICODE_TRUNCATED', 'LZ77 匹配 token 缺字节', pos);
150
+ }
151
+ const v = (block[pos] << 8) | block[pos + 1];
152
+ pos += 2;
153
+ const offset = (v >>> 4) & 0xfff;
154
+ const length = (v & 0xf) + MIN_MATCH;
155
+ if (offset === 0 || offset > out.length) {
156
+ throw new EfficodeError('EFFICODE_TRUNCATED', `LZ77 匹配 offset=${offset} 越界 (已解 ${out.length}B)`, pos);
157
+ }
158
+ for (let k = 0; k < length; k++)
159
+ out.push(out[out.length - offset]);
160
+ }
161
+ else {
162
+ if (pos >= bodyEnd) {
163
+ throw new EfficodeError('EFFICODE_TRUNCATED', 'LZ77 字面量 token 缺字节', pos);
164
+ }
165
+ out.push(block[pos++]);
166
+ }
167
+ }
168
+ }
169
+ return Uint8Array.from(out);
170
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * efficode/negotiate - 智能体"用哪种语言交流"的声明与协商
3
+ *
4
+ * 三条硬规则 (照 leo 的要求):
5
+ * ① 只有**双方都声明** efficode 才用 Efficode;
6
+ * ② 单方声明 / 对端不支持 / 对端声明了本机不认识的字符串 ⇒ **明确回落自然语言**, 并记一笔;
7
+ * ③ 声明语言**不是** efficode 的载荷, **绝不去解析它** (不猜、不试解) ——
8
+ * `decodeFromPeer` 里那条 `EFFICODE_LANG_UNSUPPORTED` 就是这条的落地处。
9
+ *
10
+ * 记录: 每次裁决写 ~/.bolloon/efficode-lang.jsonl (只记语言名与原因, **不记正文/DID**)。
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as os from 'node:os';
14
+ import * as path from 'node:path';
15
+ import { EfficodeError } from './types.js';
16
+ import { buildPacket, parsePacket } from './packet.js';
17
+ import { instructionsToSymbolic } from './ops.js';
18
+ export const AGENT_LANGS = ['natural', 'efficode'];
19
+ /** 把任意外来声明归一成两个已知值之一; 其他一律 null (= 未知语言, **不是** natural) */
20
+ export function normalizeLang(v) {
21
+ if (typeof v === 'string') {
22
+ const s = v.trim();
23
+ if (s === 'natural' || s === 'efficode')
24
+ return s;
25
+ return null;
26
+ }
27
+ if (Array.isArray(v)) {
28
+ // manifest 里的 supportedLangs 形态: 优先 efficode (更专的声明优先)
29
+ if (v.some((x) => x === 'efficode'))
30
+ return 'efficode';
31
+ if (v.some((x) => x === 'natural'))
32
+ return 'natural';
33
+ return null;
34
+ }
35
+ return null;
36
+ }
37
+ /** 声明里出现了哪些**本机不认识**的语言字符串 (用于如实标注, 不当成 natural) */
38
+ export function unknownLangNames(v) {
39
+ const arr = Array.isArray(v) ? v : v == null ? [] : [v];
40
+ return arr
41
+ .map((x) => (typeof x === 'string' ? x.trim() : ''))
42
+ .filter((s) => s && !AGENT_LANGS.includes(s));
43
+ }
44
+ /**
45
+ * 协商。`mine`/`theirs` 可以是 'natural' | 'efficode' | 字符串数组 | undefined | 任意外来值。
46
+ * 任何"不是恰好双方 efficode"的情形都落到 natural。
47
+ */
48
+ export function negotiateLang(mine, theirs) {
49
+ const m = normalizeLang(mine);
50
+ const t = normalizeLang(theirs);
51
+ const mUnknown = unknownLangNames(mine);
52
+ const tUnknown = unknownLangNames(theirs);
53
+ const declared = {
54
+ mine: Array.isArray(mine) ? mine.join('|') : mine == null ? null : String(mine),
55
+ theirs: Array.isArray(theirs) ? theirs.join('|') : theirs == null ? null : String(theirs),
56
+ };
57
+ const unknownSide = mUnknown.length && tUnknown.length ? 'both' : mUnknown.length ? 'mine' : tUnknown.length ? 'theirs' : null;
58
+ if (m === 'efficode' && t === 'efficode') {
59
+ return { lang: 'efficode', fallback: false, reason: '双方都声明 efficode', declared, unknownSide };
60
+ }
61
+ const wanted = m === 'efficode' || t === 'efficode';
62
+ let reason;
63
+ if (!m && !t) {
64
+ reason = unknownSide ? '双方都没有可识别的语言声明' : '双方都没声明语言';
65
+ }
66
+ else if (m === 'efficode') {
67
+ reason = unknownSide
68
+ ? `对端声明了本机不认识的语言 (${tUnknown.join(',')}) → 回落自然语言`
69
+ : '对端没声明 efficode → 回落自然语言';
70
+ }
71
+ else if (t === 'efficode') {
72
+ reason = '本机没声明 efficode → 回落自然语言';
73
+ }
74
+ else {
75
+ reason = unknownSide ? '声明里只有本机不认识的语言 → 回落自然语言' : '双方都只声明 natural';
76
+ }
77
+ return { lang: 'natural', fallback: wanted, reason, declared, unknownSide };
78
+ }
79
+ /**
80
+ * 出站: **双方都声明** efficode 才编码; 否则原文直发 (声明 natural)。
81
+ * 走的是文本模式 (整个包 Base64), 所以线上仍是可打印文本, 任何文本通道都能带。
82
+ *
83
+ * 指令构造 (照规范的前缀表达式): `@DID:<身份> <op> !SEND`
84
+ * —— 正文进**数据块**, 指令段只放"这是什么"。
85
+ */
86
+ export function encodeForPeer(opts) {
87
+ const decision = negotiateLang(opts.mine, opts.theirs);
88
+ if (decision.lang !== 'efficode') {
89
+ return { text: opts.text, lang: 'natural', decision, bytes: 0, note: decision.reason, symbolic: '' };
90
+ }
91
+ const instructions = [];
92
+ if (opts.from)
93
+ instructions.push({ op: '@DID:', value: opts.from });
94
+ instructions.push({ op: opts.op ?? '#DATA:', value: '' });
95
+ instructions.push({ op: '!SEND' });
96
+ const packed = buildPacket({
97
+ from: opts.from,
98
+ instructions,
99
+ data: opts.text,
100
+ mode: 'text',
101
+ compression: 'deflate',
102
+ });
103
+ return {
104
+ text: Buffer.from(packed).toString('base64'),
105
+ lang: 'efficode',
106
+ decision,
107
+ bytes: packed.length,
108
+ note: '',
109
+ symbolic: instructionsToSymbolic(instructions),
110
+ };
111
+ }
112
+ /**
113
+ * 入站:**严格**按对端声明的语言分派。
114
+ * - 声明 efficode 且本机也声明支持 → 真解包 (畸形/CRC 不符 → 抛, 不许"解一半");
115
+ * - 声明不是 efficode (含未知字符串) → 原样返回, **不进解码器**。
116
+ */
117
+ export function decodeFromPeer(opts) {
118
+ const raw = Array.isArray(opts.declaredLang)
119
+ ? opts.declaredLang.join('|')
120
+ : opts.declaredLang == null
121
+ ? null
122
+ : String(opts.declaredLang);
123
+ const declared = normalizeLang(opts.declaredLang);
124
+ const mineOk = normalizeLang(opts.mine) === 'efficode';
125
+ if (declared !== 'efficode') {
126
+ return {
127
+ text: opts.payload,
128
+ declaredLang: declared,
129
+ declaredRaw: raw,
130
+ decoded: false,
131
+ note: raw && declared === null
132
+ ? `对端声明的语言「${raw}」本机不认识 → 不解析, 原样呈现`
133
+ : '对端未声明 efficode → 原样呈现',
134
+ bytes: 0,
135
+ };
136
+ }
137
+ if (!mineOk) {
138
+ throw new EfficodeError('EFFICODE_LANG_UNSUPPORTED', '对端声明 efficode, 但本机未声明支持 → 拒绝解码 (不许硬解)');
139
+ }
140
+ const p = parsePacket(opts.payload);
141
+ return { text: p.data, declaredLang: declared, declaredRaw: raw, decoded: true, note: '', bytes: p.bytes };
142
+ }
143
+ /** 本机默认声明哪种语言 (没给 = natural; 声明不变量绝不会"猜"成 efficode) */
144
+ export function defaultDeclaredLang(env = process.env) {
145
+ const v = String(env.BOLLOON_EFFICODE_LANG ?? '').trim();
146
+ return normalizeLang(v) ?? 'natural';
147
+ }
148
+ // ============== 裁决记录 (只记语言与原因, 不记正文) ==============
149
+ export function langDecisionLogPath(env = process.env) {
150
+ const home = env.BOLLOON_HOME || path.join(os.homedir() || '/tmp', '.bolloon');
151
+ return path.join(home, 'efficode-lang.jsonl');
152
+ }
153
+ /** 记一笔 (失败绝不打断主路径 —— 记录是旁路) */
154
+ export function recordLangDecision(decision, meta) {
155
+ try {
156
+ const file = langDecisionLogPath();
157
+ fs.mkdirSync(path.dirname(file), { recursive: true });
158
+ const line = JSON.stringify({
159
+ ts: new Date().toISOString(),
160
+ channel: meta.channel,
161
+ peer: meta.peer ? String(meta.peer).slice(0, 64) : null,
162
+ lang: decision.lang,
163
+ fallback: decision.fallback,
164
+ reason: decision.reason,
165
+ unknownSide: decision.unknownSide,
166
+ declared: decision.declared,
167
+ });
168
+ fs.appendFileSync(file, line + '\n', 'utf-8');
169
+ return true;
170
+ }
171
+ catch {
172
+ return false;
173
+ }
174
+ }
175
+ /** 读回记录 (门用来断言"真记了一笔") */
176
+ export function readLangDecisionLog(env = process.env) {
177
+ try {
178
+ return fs
179
+ .readFileSync(langDecisionLogPath(env), 'utf-8')
180
+ .split('\n')
181
+ .filter(Boolean)
182
+ .map((l) => JSON.parse(l));
183
+ }
184
+ catch {
185
+ return [];
186
+ }
187
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * efficode/ops - 指令集 / 词典 (符号 ↔ 二进制操作码)
3
+ *
4
+ * 照 leo 的规范: 符号化指令集 + 前缀表达式。
5
+ * @DID: 身份 1 载荷 = 身份串
6
+ * #DATA: 数据 2 载荷 = 数据正文
7
+ * #REQ: 请求 3 载荷 = 请求内容
8
+ * !ACK 确认 4 无载荷
9
+ * !SEND 发送 5 无载荷
10
+ * !END 结束 6 无载荷
11
+ *
12
+ * 指令段二进制形状 (逐条): [1B opcode][varint payloadLen][payload UTF-8]
13
+ * 末尾**没有**隐式终止符 —— 靠 payloadLen 自描述; 少一个字节就报截断。
14
+ *
15
+ * 严格性: 任何不在表里的 opcode 一律抛 EFFICODE_UNKNOWN_OPCODE。
16
+ * (变异 M1 会把这里改成"宽容跳过", 门必须变红 —— 这就是那条断言存在的意义。)
17
+ */
18
+ import { EfficodeError } from './types.js';
19
+ export const OP_TABLE = [
20
+ { symbol: '@DID:', code: 1, hasPayload: true, prefix: '@DID:' },
21
+ { symbol: '#DATA:', code: 2, hasPayload: true, prefix: '#DATA:' },
22
+ { symbol: '#REQ:', code: 3, hasPayload: true, prefix: '#REQ:' },
23
+ { symbol: '!ACK', code: 4, hasPayload: false, prefix: '!ACK' },
24
+ { symbol: '!SEND', code: 5, hasPayload: false, prefix: '!SEND' },
25
+ { symbol: '!END', code: 6, hasPayload: false, prefix: '!END' },
26
+ ];
27
+ const BY_SYMBOL = new Map(OP_TABLE.map((d) => [d.symbol, d]));
28
+ const BY_CODE = new Map(OP_TABLE.map((d) => [d.code, d]));
29
+ export function opBySymbol(symbol) {
30
+ const d = BY_SYMBOL.get(symbol);
31
+ if (!d)
32
+ throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `未知符号 ${String(symbol)}`);
33
+ return d;
34
+ }
35
+ /** 按 opcode 取定义; 未知一律抛 (不返回 undefined 让上层"随便处理") */
36
+ export function opByCode(code, offset = -1) {
37
+ const d = BY_CODE.get(code);
38
+ if (!d)
39
+ throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `操作码 ${code} 不在指令表内`, offset);
40
+ return d;
41
+ }
42
+ export function isKnownOpCode(code) {
43
+ return BY_CODE.has(code);
44
+ }
45
+ /** 指令序列 → 前缀表达式文本 (调试/文本模式可读形态) */
46
+ export function instructionsToSymbolic(instructions) {
47
+ return instructions
48
+ .map((ins) => {
49
+ const def = opBySymbol(ins.op);
50
+ return def.hasPayload ? `${def.prefix}${ins.value ?? ''}` : def.prefix;
51
+ })
52
+ .join(' ');
53
+ }
54
+ /**
55
+ * 前缀表达式文本 → 指令序列。
56
+ *
57
+ * 解析规则 (写死, 免得被当成"随便切"):
58
+ * ① 按空白切词;
59
+ * ② 词首是 sigil 字符 (@ / # / !) → 必须匹配一条**已知**前缀, 否则抛 UNKNOWN_OPCODE;
60
+ * ③ 不带 sigil 的词 → 追加到**上一条**指令的载荷后面 (没有上一条 → 抛);
61
+ * ④ 控制指令 (!ACK/!SEND/!END) 后面不许跟载荷。
62
+ */
63
+ export function symbolicToInstructions(text) {
64
+ const out = [];
65
+ const words = String(text || '').trim().split(/\s+/).filter(Boolean);
66
+ for (const token of words) {
67
+ const isSigil = /^[@#!]/.test(token);
68
+ if (!isSigil) {
69
+ const last = out[out.length - 1];
70
+ if (!last || !opBySymbol(last.op).hasPayload) {
71
+ throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `前缀表达式含未知指令: ${token.slice(0, 24)}`);
72
+ }
73
+ last.value = `${last.value ?? ''} ${token}`.trim();
74
+ continue;
75
+ }
76
+ const def = OP_TABLE.find((d) => token.startsWith(d.prefix));
77
+ if (!def)
78
+ throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `前缀表达式含未知指令: ${token.slice(0, 24)}`);
79
+ if (def.hasPayload)
80
+ out.push({ op: def.symbol, value: token.slice(def.prefix.length) });
81
+ else if (token === def.prefix)
82
+ out.push({ op: def.symbol });
83
+ else
84
+ throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `控制指令 ${def.prefix} 不接受载荷: ${token.slice(0, 24)}`);
85
+ }
86
+ return out;
87
+ }