@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.
- package/README.md +39 -0
- package/dist/agents/agent-lang.js +101 -0
- package/dist/agents/agent-manifest-protocol.js +4 -0
- package/dist/agents/gateway-group.js +44 -3
- package/dist/agents/pi-sdk.js +101 -8
- package/dist/agents/workflow-pivot-loop.js +3 -1
- package/dist/efficode/compress.js +78 -0
- package/dist/efficode/crc32.js +38 -0
- package/dist/efficode/did.js +66 -0
- package/dist/efficode/index.js +24 -0
- package/dist/efficode/lz77.js +170 -0
- package/dist/efficode/negotiate.js +187 -0
- package/dist/efficode/ops.js +87 -0
- package/dist/efficode/packet.js +271 -0
- package/dist/efficode/types.js +45 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +102 -8
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +3 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +685 -48
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/llm/pi-ai.js +666 -47
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/mobile.js +64 -0
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode/packet - 包封装 (帧结构 + CRC32 尾段 + 严格解析)
|
|
3
|
+
*
|
|
4
|
+
* 布局 (照规范):
|
|
5
|
+
* [Header 2B] | [DID 验证段 32B] | [指令段 动态] | [数据块 动态] | [CRC32 4B]
|
|
6
|
+
*
|
|
7
|
+
* Header 2B:
|
|
8
|
+
* byte0: version(高 4 位) | mode(低 4 位) mode: 0=compact · 1=text
|
|
9
|
+
* byte1: flags bit0=有 DID 段 · bit1=有指令段 · bit2=有数据块
|
|
10
|
+
* bit3=数据块已压缩 · bit4..7=保留 (必须 0)
|
|
11
|
+
*
|
|
12
|
+
* 指令段: [varint opLen][opBytes] opBytes 见 ops.ts 的逐条形状
|
|
13
|
+
* 数据块: [varint dataLen][dataBytes] dataBytes[0] = 压缩算法 id (见 compress.ts)
|
|
14
|
+
*
|
|
15
|
+
* 两条硬纪律 (变异 M1/M2 就是拆这两条, 门必须变红):
|
|
16
|
+
* ① 解析必须**逐段自描述**: 长度对不上就报截断, 不许"能解多少解多少";
|
|
17
|
+
* ② 尾段 CRC32 覆盖 CRC 之前的**全部**字节, 对不上就报 CRC_MISMATCH,
|
|
18
|
+
* 绝不放行"内容看着像就收下"。
|
|
19
|
+
*
|
|
20
|
+
* 诚实边界:
|
|
21
|
+
* - "DID 段 32B" 在规范里是固定段; 参考实现允许省略 (省 32B), 省略时 flags 如实写 0,
|
|
22
|
+
* 解码端按 flags 解, **不假设**段存在;
|
|
23
|
+
* - CRC32 只防传输损坏, **不是**防篡改 (无密钥)。防篡改要真签名 —— 规范中, 见 did.ts 注释。
|
|
24
|
+
*/
|
|
25
|
+
import { crc32Bytes, readCrc32Bytes, crc32 } from './crc32.js';
|
|
26
|
+
import { DID_SEGMENT_BYTES, EfficodeError, EFFICODE_VERSION, CRC_BYTES, MIN_PACKET_BYTES } from './types.js';
|
|
27
|
+
import { compressBlock, decompressBlock, pickAlgo } from './compress.js';
|
|
28
|
+
import { decodeDidSegment, didDigestString, encodeDidSegment } from './did.js';
|
|
29
|
+
import { instructionsToSymbolic, opByCode, opBySymbol, symbolicToInstructions } from './ops.js';
|
|
30
|
+
const MODE_TO_NUM = { compact: 0, text: 1 };
|
|
31
|
+
const NUM_TO_MODE = { 0: 'compact', 1: 'text' };
|
|
32
|
+
const F_HAS_DID = 1 << 0;
|
|
33
|
+
const F_HAS_OP = 1 << 1;
|
|
34
|
+
const F_HAS_DATA = 1 << 2;
|
|
35
|
+
const F_COMPRESSED = 1 << 3;
|
|
36
|
+
const FLAG_RESERVED = 0xf0;
|
|
37
|
+
function writeVarint(out, n) {
|
|
38
|
+
let v = n >>> 0;
|
|
39
|
+
while (v >= 0x80) {
|
|
40
|
+
out.push((v & 0x7f) | 0x80);
|
|
41
|
+
v >>>= 7;
|
|
42
|
+
}
|
|
43
|
+
out.push(v & 0xff);
|
|
44
|
+
}
|
|
45
|
+
function readVarint(buf, pos) {
|
|
46
|
+
let value = 0;
|
|
47
|
+
let shift = 0;
|
|
48
|
+
let i = pos;
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (i >= buf.length)
|
|
51
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', 'varint 未读完就到底', i);
|
|
52
|
+
const b = buf[i++];
|
|
53
|
+
value |= (b & 0x7f) << shift;
|
|
54
|
+
if ((b & 0x80) === 0)
|
|
55
|
+
break;
|
|
56
|
+
shift += 7;
|
|
57
|
+
if (shift > 28)
|
|
58
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', 'varint 超过 32 位上限', i);
|
|
59
|
+
}
|
|
60
|
+
return { value: value >>> 0, next: i };
|
|
61
|
+
}
|
|
62
|
+
/** 指令序列 → 指令段字节 (不含外层 varint 长度) */
|
|
63
|
+
function encodeInstructions(instructions) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const ins of instructions) {
|
|
66
|
+
const def = opBySymbol(ins.op);
|
|
67
|
+
out.push(def.code);
|
|
68
|
+
const payload = def.hasPayload ? Buffer.from(String(ins.value ?? ''), 'utf-8') : Buffer.alloc(0);
|
|
69
|
+
if (!def.hasPayload && (ins.value ?? '') !== '') {
|
|
70
|
+
throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `控制指令 ${def.symbol} 不接受载荷`);
|
|
71
|
+
}
|
|
72
|
+
writeVarint(out, payload.length);
|
|
73
|
+
for (const b of payload)
|
|
74
|
+
out.push(b);
|
|
75
|
+
}
|
|
76
|
+
return Uint8Array.from(out);
|
|
77
|
+
}
|
|
78
|
+
/** 指令段字节 → 指令序列 (严格: 未知 opcode / 截断都抛) */
|
|
79
|
+
function decodeInstructions(buf) {
|
|
80
|
+
const out = [];
|
|
81
|
+
let pos = 0;
|
|
82
|
+
while (pos < buf.length) {
|
|
83
|
+
const code = buf[pos];
|
|
84
|
+
const def = opByCode(code, pos); // 未知 opcode → 抛 (变异 M1 拆掉这里 → 门变红)
|
|
85
|
+
pos += 1;
|
|
86
|
+
const len = readVarint(buf, pos);
|
|
87
|
+
pos = len.next;
|
|
88
|
+
if (def.hasPayload) {
|
|
89
|
+
if (pos + len.value > buf.length) {
|
|
90
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `指令 ${def.symbol} 声明 ${len.value}B 载荷但只剩 ${buf.length - pos}B`, pos);
|
|
91
|
+
}
|
|
92
|
+
out.push({ op: def.symbol, value: Buffer.from(buf.subarray(pos, pos + len.value)).toString('utf-8') });
|
|
93
|
+
pos += len.value;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
if (len.value !== 0) {
|
|
97
|
+
throw new EfficodeError('EFFICODE_UNKNOWN_OPCODE', `控制指令 ${def.symbol} 声明了 ${len.value}B 载荷`, pos);
|
|
98
|
+
}
|
|
99
|
+
out.push({ op: def.symbol });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** 打包 (总是产出二进制; mode 记在 header 里) */
|
|
105
|
+
export function buildPacket(msg) {
|
|
106
|
+
const mode = msg.mode ?? 'compact';
|
|
107
|
+
if (!(mode in MODE_TO_NUM)) {
|
|
108
|
+
// 语音/声波模式等未实现模式 = 显式拒绝, 不静默降级
|
|
109
|
+
throw new EfficodeError('EFFICODE_MODE_NOT_IMPLEMENTED', `模式 ${String(mode)} 未实现 (只有 compact/text)`);
|
|
110
|
+
}
|
|
111
|
+
const instructions = msg.instructions ?? [];
|
|
112
|
+
if (!instructions.length)
|
|
113
|
+
throw new EfficodeError('EFFICODE_BAD_FLAGS', '指令段不能为空 (至少一条指令)');
|
|
114
|
+
const hasDid = typeof msg.from === 'string' && msg.from.length > 0;
|
|
115
|
+
const didSeg = hasDid ? encodeDidSegment(msg.from) : null;
|
|
116
|
+
if (didSeg && didSeg.length !== DID_SEGMENT_BYTES) {
|
|
117
|
+
throw new EfficodeError('EFFICODE_DID_SEGMENT_BAD', `DID 段长度异常 ${didSeg.length}B`);
|
|
118
|
+
}
|
|
119
|
+
const opBytes = encodeInstructions(instructions);
|
|
120
|
+
const rawData = Buffer.from(msg.data ?? '', 'utf-8');
|
|
121
|
+
const algo = msg.compression ?? pickAlgo(new Uint8Array(rawData));
|
|
122
|
+
const dataBlock = rawData.length > 0 ? compressBlock(new Uint8Array(rawData), algo) : new Uint8Array(0);
|
|
123
|
+
let flags = F_HAS_OP;
|
|
124
|
+
if (didSeg)
|
|
125
|
+
flags |= F_HAS_DID;
|
|
126
|
+
if (dataBlock.length > 0)
|
|
127
|
+
flags |= F_HAS_DATA;
|
|
128
|
+
if (dataBlock.length > 0 && algo !== 'none')
|
|
129
|
+
flags |= F_COMPRESSED;
|
|
130
|
+
const out = [];
|
|
131
|
+
out.push(((EFFICODE_VERSION & 0xf) << 4) | (MODE_TO_NUM[mode] & 0xf));
|
|
132
|
+
out.push(flags);
|
|
133
|
+
if (didSeg)
|
|
134
|
+
for (const b of didSeg)
|
|
135
|
+
out.push(b);
|
|
136
|
+
writeVarint(out, opBytes.length);
|
|
137
|
+
for (const b of opBytes)
|
|
138
|
+
out.push(b);
|
|
139
|
+
if (dataBlock.length > 0) {
|
|
140
|
+
writeVarint(out, dataBlock.length);
|
|
141
|
+
for (const b of dataBlock)
|
|
142
|
+
out.push(b);
|
|
143
|
+
}
|
|
144
|
+
const head = Uint8Array.from(out);
|
|
145
|
+
const crc = crc32Bytes(head);
|
|
146
|
+
const packet = new Uint8Array(head.length + CRC_BYTES);
|
|
147
|
+
packet.set(head, 0);
|
|
148
|
+
packet.set(crc, head.length);
|
|
149
|
+
return packet;
|
|
150
|
+
}
|
|
151
|
+
/** 编码成要上网的形态: text 模式 → Base64 串; compact 模式 → 二进制 */
|
|
152
|
+
export function encodePacket(msg) {
|
|
153
|
+
const packet = buildPacket(msg);
|
|
154
|
+
const mode = msg.mode ?? 'compact';
|
|
155
|
+
return mode === 'text' ? Buffer.from(packet).toString('base64') : packet;
|
|
156
|
+
}
|
|
157
|
+
/** 解析 (严格)。入参可以是二进制包或 text 模式的 Base64 串 */
|
|
158
|
+
export function parsePacket(input) {
|
|
159
|
+
let mode;
|
|
160
|
+
let buf;
|
|
161
|
+
if (typeof input === 'string') {
|
|
162
|
+
mode = 'text';
|
|
163
|
+
const cleaned = input.trim();
|
|
164
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(cleaned) || cleaned.length % 4 !== 0) {
|
|
165
|
+
throw new EfficodeError('EFFICODE_BAD_BASE64', '文本模式载荷不是合法 Base64');
|
|
166
|
+
}
|
|
167
|
+
buf = new Uint8Array(Buffer.from(cleaned, 'base64'));
|
|
168
|
+
if (buf.length === 0)
|
|
169
|
+
throw new EfficodeError('EFFICODE_BAD_BASE64', 'Base64 解出 0 字节');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
mode = 'compact';
|
|
173
|
+
buf = input;
|
|
174
|
+
}
|
|
175
|
+
if (buf.length < MIN_PACKET_BYTES) {
|
|
176
|
+
throw new EfficodeError('EFFICODE_TOO_SHORT', `包 ${buf.length}B < 最短合法 ${MIN_PACKET_BYTES}B`);
|
|
177
|
+
}
|
|
178
|
+
const version = (buf[0] >> 4) & 0xf;
|
|
179
|
+
if (version !== EFFICODE_VERSION) {
|
|
180
|
+
throw new EfficodeError('EFFICODE_BAD_VERSION', `包版本 ${version} 不被支持 (本实现只认 ${EFFICODE_VERSION})`, 0);
|
|
181
|
+
}
|
|
182
|
+
const modeNum = buf[0] & 0xf;
|
|
183
|
+
const declaredMode = NUM_TO_MODE[modeNum];
|
|
184
|
+
if (!declaredMode)
|
|
185
|
+
throw new EfficodeError('EFFICODE_BAD_MODE', `未知模式 id=${modeNum}`, 0);
|
|
186
|
+
if (declaredMode !== mode) {
|
|
187
|
+
throw new EfficodeError('EFFICODE_BAD_MODE', `传输形态(${mode})与包头声明(${declaredMode})不一致`, 0);
|
|
188
|
+
}
|
|
189
|
+
const flags = buf[1];
|
|
190
|
+
if ((flags & FLAG_RESERVED) !== 0) {
|
|
191
|
+
throw new EfficodeError('EFFICODE_BAD_FLAGS', `flags 保留位被置位: 0x${flags.toString(16)}`, 1);
|
|
192
|
+
}
|
|
193
|
+
let pos = 2;
|
|
194
|
+
let didDigest = null;
|
|
195
|
+
let didSegmentHex = null;
|
|
196
|
+
if (flags & F_HAS_DID) {
|
|
197
|
+
if (pos + DID_SEGMENT_BYTES > buf.length) {
|
|
198
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `声明有 DID 段但只剩 ${buf.length - pos}B`, pos);
|
|
199
|
+
}
|
|
200
|
+
const seg = buf.subarray(pos, pos + DID_SEGMENT_BYTES);
|
|
201
|
+
const parsed = decodeDidSegment(seg); // algo 未知 → 抛
|
|
202
|
+
didDigest = didDigestString(parsed);
|
|
203
|
+
didSegmentHex = Buffer.from(seg).toString('hex');
|
|
204
|
+
pos += DID_SEGMENT_BYTES;
|
|
205
|
+
}
|
|
206
|
+
let instructions = [];
|
|
207
|
+
if (flags & F_HAS_OP) {
|
|
208
|
+
const opLen = readVarint(buf, pos);
|
|
209
|
+
pos = opLen.next;
|
|
210
|
+
if (pos + opLen.value > buf.length) {
|
|
211
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `指令段声明 ${opLen.value}B 但只剩 ${buf.length - pos}B`, pos);
|
|
212
|
+
}
|
|
213
|
+
instructions = decodeInstructions(buf.subarray(pos, pos + opLen.value));
|
|
214
|
+
pos += opLen.value;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
throw new EfficodeError('EFFICODE_BAD_FLAGS', 'flags 声明无指令段 —— 本实现不接受无指令的包', 1);
|
|
218
|
+
}
|
|
219
|
+
let data = '';
|
|
220
|
+
let compression = 'none';
|
|
221
|
+
let rawDataBytes = 0;
|
|
222
|
+
if (flags & F_HAS_DATA) {
|
|
223
|
+
const dataLen = readVarint(buf, pos);
|
|
224
|
+
pos = dataLen.next;
|
|
225
|
+
if (pos + dataLen.value > buf.length) {
|
|
226
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `数据块声明 ${dataLen.value}B 但只剩 ${buf.length - pos}B`, pos);
|
|
227
|
+
}
|
|
228
|
+
const block = buf.subarray(pos, pos + dataLen.value);
|
|
229
|
+
const un = decompressBlock(block);
|
|
230
|
+
compression = un.algo;
|
|
231
|
+
rawDataBytes = un.data.length;
|
|
232
|
+
data = Buffer.from(un.data).toString('utf-8');
|
|
233
|
+
pos += dataLen.value;
|
|
234
|
+
}
|
|
235
|
+
const expectCrcAt = buf.length - CRC_BYTES;
|
|
236
|
+
if (pos !== expectCrcAt) {
|
|
237
|
+
throw new EfficodeError('EFFICODE_TRAILING_BYTES', `解析在 ${pos}B 结束, 但 CRC 段在 ${expectCrcAt}B —— 中间有 ${expectCrcAt - pos}B 垃圾`, pos);
|
|
238
|
+
}
|
|
239
|
+
const head = buf.subarray(0, expectCrcAt);
|
|
240
|
+
const want = readCrc32Bytes(buf, expectCrcAt);
|
|
241
|
+
const got = crc32(head);
|
|
242
|
+
if (want !== got) {
|
|
243
|
+
throw new EfficodeError('EFFICODE_CRC_MISMATCH', `CRC32 不符: 包内 0x${want.toString(16)}, 实算 0x${got.toString(16)}`, expectCrcAt);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
mode: declaredMode,
|
|
247
|
+
didDigest,
|
|
248
|
+
didSegmentHex,
|
|
249
|
+
instructions,
|
|
250
|
+
data,
|
|
251
|
+
compression,
|
|
252
|
+
bytes: buf.length,
|
|
253
|
+
rawDataBytes,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** 便捷: 包 → 语义消息 (保留符号化形态, 供文本模式/调试面读取) */
|
|
257
|
+
export function packetToMessage(input) {
|
|
258
|
+
const p = parsePacket(input);
|
|
259
|
+
return {
|
|
260
|
+
instructions: p.instructions,
|
|
261
|
+
data: p.data || undefined,
|
|
262
|
+
mode: p.mode,
|
|
263
|
+
compression: p.compression,
|
|
264
|
+
bytes: p.bytes,
|
|
265
|
+
symbolic: instructionsToSymbolic(p.instructions),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** 从符号化文本造一条消息 (调试/文本模式入口) */
|
|
269
|
+
export function messageFromSymbolic(text, data) {
|
|
270
|
+
return { instructions: symbolicToInstructions(text), data, mode: 'compact' };
|
|
271
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode/types - Efficode 参考实现的公共类型
|
|
3
|
+
*
|
|
4
|
+
* 设计口径 (照 leo 给的规范, 不自行发明新协议):
|
|
5
|
+
* - 符号化指令集 + 前缀表达式: @DID: 身份 · #DATA: 数据 · #REQ: 请求 · !ACK/!SEND/!END 控制
|
|
6
|
+
* - 两种模式: 文本模式 (调试/低带宽, 可 Base64) 与 二进制模式 (默认, 高密度)
|
|
7
|
+
* - 自解压数据块: 数据块自带轻量解码规则 (压缩算法 id + 原始长度), 接收方按规则直接展开
|
|
8
|
+
* - 包结构: [Header 2B] | [DID 验证段 32B] | [指令段 动态] | [数据块 动态] | [CRC32 4B]
|
|
9
|
+
*
|
|
10
|
+
* 诚实标注 (都写进 docs/wiki/efficode.md, 不在这里当已验证结论):
|
|
11
|
+
* - 本文件是**参考实现**: 能真编真解、逐字节回环、畸形包报错;
|
|
12
|
+
* - 语音/声波模式 (ggwave 类) **未实现** —— 只留 `mode` 枚举里的保留位, 编码时显式拒绝;
|
|
13
|
+
* - 链上签名验证 / ECC 临时会话密钥 **规范中** —— 本实现只做 DID 摘要段, 不做签名;
|
|
14
|
+
* - 压缩率/信息密度这类宣传数字一律按**实测**写, 见 scripts/verify-efficode.ts 的真测输出。
|
|
15
|
+
*/
|
|
16
|
+
export const COMPRESSION_IDS = {
|
|
17
|
+
none: 0,
|
|
18
|
+
lz77: 1,
|
|
19
|
+
deflate: 2,
|
|
20
|
+
};
|
|
21
|
+
export const COMPRESSION_BY_ID = {
|
|
22
|
+
0: 'none',
|
|
23
|
+
1: 'lz77',
|
|
24
|
+
2: 'deflate',
|
|
25
|
+
};
|
|
26
|
+
/** 包版本 (Header 高 4 位) */
|
|
27
|
+
export const EFFICODE_VERSION = 1;
|
|
28
|
+
/** DID 段固定长度 (字节) —— 照规范: 32B */
|
|
29
|
+
export const DID_SEGMENT_BYTES = 32;
|
|
30
|
+
/** CRC32 尾段固定长度 (字节) */
|
|
31
|
+
export const CRC_BYTES = 4;
|
|
32
|
+
/** 最短合法包 = Header 2 + 无 DID 段 + 指令段最少 (varint 长度 1 + 一条控制指令 2) + 无数据块 + CRC 4 = 9B
|
|
33
|
+
* (DID 段 32B 是**可选**省略的, 所以不放进来; 带 DID 的包应 ≥ 41B, 由 flags 驱动逐段校验而不是靠这个下限) */
|
|
34
|
+
export const MIN_PACKET_BYTES = 2 + 1 + 2 + CRC_BYTES;
|
|
35
|
+
export class EfficodeError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
/** 出错位置 (字节偏移); 拿不到时为 -1 */
|
|
38
|
+
offset;
|
|
39
|
+
constructor(code, message, offset = -1) {
|
|
40
|
+
super(`${code}: ${message}${offset >= 0 ? ` (offset=${offset})` : ''}`);
|
|
41
|
+
this.name = 'EfficodeError';
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.offset = offset;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -104,6 +104,10 @@ function setLocalManifest(m) {
|
|
|
104
104
|
localManifest.exportments = [];
|
|
105
105
|
if (!('sciences' in m))
|
|
106
106
|
localManifest.sciences = [];
|
|
107
|
+
// 2026-09-28: 交流语言声明同规矩 —— 不给就显式清空, 免得旧的 efficode 声明在对端已经不支持时还留着.
|
|
108
|
+
// (老 manifest 语义不变: 没有 supportedLangs = 没声明 = 自然语言)
|
|
109
|
+
if (!('supportedLangs' in m))
|
|
110
|
+
localManifest.supportedLangs = [];
|
|
107
111
|
// 2026-09-18: 网络脉冲 (只记公开统计; 不落 DID/正文; 统计失败绝不影响主路径)
|
|
108
112
|
try {
|
|
109
113
|
const did = String(localManifest.ownerPublicKey || '');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-manifest-protocol.js","sourceRoot":"","sources":["../../../src/agents/agent-manifest-protocol.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"agent-manifest-protocol.js","sourceRoot":"","sources":["../../../src/agents/agent-manifest-protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,6 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
47
|
exports.PiAgentSession = exports.runSelfImproveLoop = exports.resetAgentSession = exports.getAgentSession = exports.createAgentSession = exports.PiSessionManager = exports.TOOL_DEFINITIONS = void 0;
|
|
48
48
|
exports.decideUnreported = decideUnreported;
|
|
49
|
+
exports.writeBackCurrentTurnInto = writeBackCurrentTurnInto;
|
|
49
50
|
const fsSync = __importStar(require("fs"));
|
|
50
51
|
const os = __importStar(require("os"));
|
|
51
52
|
const path = __importStar(require("path"));
|
|
@@ -70,6 +71,8 @@ const goal_store_js_1 = require("./goal-store.js");
|
|
|
70
71
|
const model_selection_js_1 = require("../llm/model-selection.js");
|
|
71
72
|
// 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
|
|
72
73
|
const tool_name_js_1 = require("../llm/tool-name.js");
|
|
74
|
+
// 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
|
|
75
|
+
const pi_ai_js_1 = require("../llm/pi-ai.js");
|
|
73
76
|
const pi_harness_js_1 = require("./pi-harness.js");
|
|
74
77
|
const heartbeat_js_1 = require("../social/heartbeat.js");
|
|
75
78
|
const constraint_runtime_2 = require("@bolloon/constraint-runtime");
|
|
@@ -122,6 +125,55 @@ const loop_review_js_1 = require("./loop-review.js");
|
|
|
122
125
|
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
123
126
|
*/
|
|
124
127
|
const MAX_SAME_TOOL_FAILURES = 3;
|
|
128
|
+
/**
|
|
129
|
+
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的「当前轮已注入」内容写回调用方自己的 history.
|
|
130
|
+
*
|
|
131
|
+
* 谁是调用方: ReAct 循环每轮用 `buildMessages()` **重建全新对象** —— 注入只活在那一份里.
|
|
132
|
+
* 不写回的话下一轮那条 user 就退回注入前, 前缀从它起分叉:
|
|
133
|
+
* 第 1 次 `S H [D1 U1]` 第 2 次 `S H U1 U2` ← U1 丢了 D1
|
|
134
|
+
* 正确形态: `S H [D1 U1] U2` (D1 成了 U1 的一部分, 于是成为后续轮的稳定前缀).
|
|
135
|
+
*
|
|
136
|
+
* 为什么是纯函数 (不塞进类里): 门 `scripts/verify-kv-prefix.ts` 要能**直接驱它**断言,
|
|
137
|
+
* 而不是绕一整条 ReAct 循环才验到"写回"这一下.
|
|
138
|
+
*
|
|
139
|
+
* 匹配判据 (只做加法, 绝不乱改历史):
|
|
140
|
+
* 1. wire 最后一条 content 必须以 CURRENT TURN 标记开头 (没注入过 → 0, 不动);
|
|
141
|
+
* 2. 从 history **末尾往前**找第一条 `content` 是它后缀的条目 ——
|
|
142
|
+
* 去掉后缀剩下的那截必须仍以标记开头 (证明"注入是前部加了一段");
|
|
143
|
+
* 3. 命中就整段写回 (tool 条目拿到的是含 `[工具结果]\n` 前缀 + 注入的整段),
|
|
144
|
+
* 写回后该条 `content` 已等于 wire → 第二次调用前缀为空, 自然返回 0 (幂等).
|
|
145
|
+
*
|
|
146
|
+
* @returns 真写回的条数 (0 = 没注入过 / 匹配不上 / 已经写回过)
|
|
147
|
+
*/
|
|
148
|
+
function writeBackCurrentTurnInto(history, wire) {
|
|
149
|
+
try {
|
|
150
|
+
if (!Array.isArray(history) || history.length === 0)
|
|
151
|
+
return 0;
|
|
152
|
+
if (!Array.isArray(wire) || wire.length === 0)
|
|
153
|
+
return 0;
|
|
154
|
+
const last = wire[wire.length - 1];
|
|
155
|
+
const wireContent = typeof last?.content === 'string' ? last.content : '';
|
|
156
|
+
if (!wireContent.startsWith(pi_ai_js_1.CURRENT_TURN_MARKER))
|
|
157
|
+
return 0; // 没注入过 → 不动
|
|
158
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
159
|
+
const entry = history[i];
|
|
160
|
+
const inner = typeof entry?.content === 'string' ? entry.content : '';
|
|
161
|
+
if (!inner)
|
|
162
|
+
continue;
|
|
163
|
+
if (!wireContent.endsWith(inner))
|
|
164
|
+
continue;
|
|
165
|
+
const prefix = wireContent.slice(0, wireContent.length - inner.length);
|
|
166
|
+
if (!prefix || !prefix.startsWith(pi_ai_js_1.CURRENT_TURN_MARKER))
|
|
167
|
+
continue;
|
|
168
|
+
entry.content = wireContent;
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return 0; // 写回失败绝不影响对话本身
|
|
175
|
+
}
|
|
176
|
+
}
|
|
125
177
|
class PiAgentSession {
|
|
126
178
|
cwd;
|
|
127
179
|
peerId;
|
|
@@ -1818,12 +1870,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1818
1870
|
.join('\n');
|
|
1819
1871
|
loopProgressSection = `\n【本轮循环进度】你已完成以下 ${loopActionLog.length} 个动作, 这是连续执行的同一轮任务:\n${actionLines}\n请基于已有结果继续推进, 不要重复执行上面已成功的动作. 全部完成后用 <final gen> 结束.\n`;
|
|
1820
1872
|
}
|
|
1873
|
+
// 2026-09-28 (前缀 KV 可命中): `refineContext` / `loopProgressSection` 是**每轮都在变**的段
|
|
1874
|
+
// (质量分 / 连续错误数 / 本轮已完成动作). 留在 system 里 = 每轮把 IMMUTABLE PREFIX 打碎 =
|
|
1875
|
+
// 服务端前缀 KV 永远 miss. 移到 CURRENT TURN 区 (注入到最后一条 user 前部) —— 文本一字未改, 只换位置.
|
|
1876
|
+
const currentTurnContext = `${refineContext}${loopProgressSection}`;
|
|
1821
1877
|
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
|
|
1822
1878
|
当前工作目录: ${this.cwd}
|
|
1823
1879
|
当前身份: ${this.identity.name} (${this.identity.did})
|
|
1824
|
-
${refineContext}
|
|
1825
1880
|
${this.currentIntentHint}
|
|
1826
|
-
${loopProgressSection}
|
|
1827
1881
|
|
|
1828
1882
|
${toolDefs}
|
|
1829
1883
|
|
|
@@ -1873,7 +1927,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1873
1927
|
// 2026-09-16 (Milestone 1-B): 模型调用前后走唯一门面 (扩展点 + 计数留痕; 默认不加新 hook 事件, 避免改变现有触发次数)
|
|
1874
1928
|
this.piHarness().beforeModelCall(this.harnessCtx());
|
|
1875
1929
|
const _modelCallT0 = Date.now();
|
|
1876
|
-
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, openaiFormattedTools);
|
|
1930
|
+
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, openaiFormattedTools, currentTurnContext);
|
|
1931
|
+
// 2026-09-28 (前缀 KV 可命中): 把注入了 CURRENT TURN 的当前轮**原样写回自己的 messageHistory** ——
|
|
1932
|
+
// buildMessages() 每轮重建全新对象, 不写回的话下一轮那条 user 就退回注入前, 前缀从那里分叉.
|
|
1933
|
+
// 用 chat() 回带的 wire (真正发出去的那一份), 而不是把 messages 再拼一遍.
|
|
1934
|
+
this.writeBackCurrentTurn(response.messages);
|
|
1877
1935
|
this.piHarness().afterModelCall(this.harnessCtx(), { ms: Date.now() - _modelCallT0 });
|
|
1878
1936
|
const reply = (response.reply || '').trim();
|
|
1879
1937
|
// 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
|
|
@@ -2614,6 +2672,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2614
2672
|
const source = this.projectedHistory ?? this.messageHistory;
|
|
2615
2673
|
const WINDOW = 15;
|
|
2616
2674
|
const out = [];
|
|
2675
|
+
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
2676
|
+
// 两个理由: (1) 注入块是"当前轮的前缀", 重渲染 (重加 `[工具结果]` 前缀 / 再 slice 截断)
|
|
2677
|
+
// 会改字节 → 下一轮前缀从这条起分叉; (2) 已注入的 tool 条目在 wire 上已经是 user 形状,
|
|
2678
|
+
// 再套一次前缀就是双前缀, 连内容都变了.
|
|
2679
|
+
const isInjectedEntry = (m) => typeof m.content === 'string' && m.content.includes(pi_ai_js_1.CURRENT_TURN_MARKER);
|
|
2617
2680
|
// 早期历史压缩: 超过窗口时, 不直接丢弃 — 提取前段用户意图摘要注入 (同步, 无 LLM).
|
|
2618
2681
|
// 结构对齐 Context OS: System Prompt(persona) + 压缩摘要 + 最近消息.
|
|
2619
2682
|
if (source.length > WINDOW) {
|
|
@@ -2629,6 +2692,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2629
2692
|
});
|
|
2630
2693
|
for (const m of slice) {
|
|
2631
2694
|
const r = m.role;
|
|
2695
|
+
if (isInjectedEntry(m)) {
|
|
2696
|
+
out.push({ role: 'user', content: String(m.content) });
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2632
2699
|
if (r === 'tool') {
|
|
2633
2700
|
out.push({ role: 'user', content: `[工具结果]\n${(m.content || '').slice(0, 2000)}` });
|
|
2634
2701
|
continue;
|
|
@@ -2651,6 +2718,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2651
2718
|
const slice = source.slice(-WINDOW);
|
|
2652
2719
|
for (const m of slice) {
|
|
2653
2720
|
const r = m.role;
|
|
2721
|
+
if (isInjectedEntry(m)) {
|
|
2722
|
+
out.push({ role: 'user', content: String(m.content) });
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2654
2725
|
if (r === 'tool') {
|
|
2655
2726
|
out.push({ role: 'user', content: `[工具结果]\n${m.content || ''}` });
|
|
2656
2727
|
continue;
|
|
@@ -2674,6 +2745,20 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2674
2745
|
return [{ role: 'user', content: this.buildContext() }];
|
|
2675
2746
|
}
|
|
2676
2747
|
}
|
|
2748
|
+
/**
|
|
2749
|
+
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2750
|
+
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2751
|
+
* 这里只负责接到 `this.messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2752
|
+
*/
|
|
2753
|
+
writeBackCurrentTurn(wire) {
|
|
2754
|
+
try {
|
|
2755
|
+
return writeBackCurrentTurnInto(this.messageHistory, wire);
|
|
2756
|
+
}
|
|
2757
|
+
catch (err) {
|
|
2758
|
+
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
2759
|
+
return 0;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2677
2762
|
/**
|
|
2678
2763
|
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
2679
2764
|
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
@@ -2695,7 +2780,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2695
2780
|
*
|
|
2696
2781
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
2697
2782
|
*/
|
|
2698
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools
|
|
2783
|
+
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools,
|
|
2784
|
+
/** 2026-09-28: 当前轮易变段 (循环进度/改进提示) —— 进 CURRENT TURN 区, 不进 system */
|
|
2785
|
+
currentTurnContext) {
|
|
2699
2786
|
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2700
2787
|
const estimated = this.estimateHistoryTokens();
|
|
2701
2788
|
if (estimated > this.maxContextTokens() * 0.8) {
|
|
@@ -2749,9 +2836,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2749
2836
|
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2750
2837
|
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2751
2838
|
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
2752
|
-
|
|
2839
|
+
// 2026-09-28 (前缀 KV 可命中): 显式 purpose/source —— pi-sdk 的 ReAct 循环**就是主对话**
|
|
2840
|
+
// (完整 IMMUTABLE PREFIX + 工具全集 + CURRENT TURN, 也只有它带 cache_prompt);
|
|
2841
|
+
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2842
|
+
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2843
|
+
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2753
2844
|
// 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2754
|
-
return { reply: response.reply || '', toolCalls: response.toolCalls };
|
|
2845
|
+
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2755
2846
|
}
|
|
2756
2847
|
catch (err) {
|
|
2757
2848
|
// 用户主动 abort: 不重试, 立即抛
|
|
@@ -2859,7 +2950,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2859
2950
|
// 给 Context Collapse (虚拟投影) 和 Auto-Compact (摘要) 共用
|
|
2860
2951
|
const llm = (0, index_js_1.getMinimax)();
|
|
2861
2952
|
const llmChat = async (systemPrompt, userPrompt) => {
|
|
2862
|
-
|
|
2953
|
+
// 2026-09-28 (前缀 KV 可命中): 压缩摘要是**非主对话的一次性调用** → 轻量前缀
|
|
2954
|
+
// (它不该拖满 IMMUTABLE PREFIX, 更不该把主对话的 KV slot 顶掉).
|
|
2955
|
+
const r = await llm.chat(userPrompt, systemPrompt, signal, undefined, 'auto-compact', 'pi-sdk-compact');
|
|
2863
2956
|
return r.reply;
|
|
2864
2957
|
};
|
|
2865
2958
|
// 2026-08-06: 预算 = ContextManager 配置 (1M * 55% ≈ 550K), 不再写死 8000.
|
|
@@ -3125,7 +3218,8 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3125
3218
|
const conversation = messages.map(m => `${m.type === 'user' ? '用户' : '助手'}: ${m.content}`).join('\n');
|
|
3126
3219
|
const llm = (0, index_js_1.getMinimax)();
|
|
3127
3220
|
try {
|
|
3128
|
-
const response = await llm.chat(`根据以下对话内容,为这个对话生成一个简短的名称(不超过20个字):\n\n${conversation}\n\n直接输出名称,不要其他解释。`, '命名建议'
|
|
3221
|
+
const response = await llm.chat(`根据以下对话内容,为这个对话生成一个简短的名称(不超过20个字):\n\n${conversation}\n\n直接输出名称,不要其他解释。`, '命名建议', undefined, undefined, 'chat', 'session-namer' // 非主对话: 轻量前缀, 不抢主对话的 KV slot
|
|
3222
|
+
);
|
|
3129
3223
|
const name = response.reply.trim();
|
|
3130
3224
|
// 拒绝错误回退串 (LLM 不可用时返回的占位文本)
|
|
3131
3225
|
if (!name)
|