foliko 2.0.9 → 2.0.10
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/package.json +1 -1
- package/src/agent/chat.js +47 -0
- package/src/agent/main.js +2 -0
- package/src/session/chat.js +8 -0
- package/src/utils/message-validator.js +58 -0
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl +303 -0
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl.bak.1783160606497 +303 -0
package/package.json
CHANGED
package/src/agent/chat.js
CHANGED
|
@@ -555,6 +555,9 @@ class AgentChatHandler extends EventEmitter {
|
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
|
+
// 写入端 sanitize:AI SDK v6 响应里允许 error-text/error-json(工具失败标记),
|
|
559
|
+
// 但 ModelMessage 输入 schema 不接受;持久化前必须规范化
|
|
560
|
+
this._normalizeToolOutputs(finishMessages);
|
|
558
561
|
messages.push(...finishMessages);
|
|
559
562
|
|
|
560
563
|
// 获取或估算 token 用量
|
|
@@ -795,12 +798,56 @@ class AgentChatHandler extends EventEmitter {
|
|
|
795
798
|
messages.push(userMessage);
|
|
796
799
|
this._validateToolCalls(messages);
|
|
797
800
|
|
|
801
|
+
// 读取端 sanitize:修复历史 chat log 中可能存在的 error-text/error-json
|
|
802
|
+
this._normalizeToolOutputs(messages);
|
|
803
|
+
|
|
798
804
|
// 最终兜底:暴力清理所有 orphaned tool 消息,确保 AI SDK 不报错
|
|
799
805
|
this._stripOrphanedToolMessages(messages);
|
|
800
806
|
|
|
801
807
|
return { messageStore, messages };
|
|
802
808
|
}
|
|
803
809
|
|
|
810
|
+
/**
|
|
811
|
+
* 规范化 tool-result 的 output 字段。
|
|
812
|
+
* AI SDK v6 响应中允许 'error-text' / 'error-json'(表示工具执行失败),
|
|
813
|
+
* 但 ModelMessage 输入 schema(AI SDK 发送 prompt 时使用)不接受这两类。
|
|
814
|
+
* 必须在写入磁盘和读取发送前都做转换。
|
|
815
|
+
* @private
|
|
816
|
+
*/
|
|
817
|
+
_normalizeToolOutputs(messages) {
|
|
818
|
+
const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
|
|
819
|
+
let fixed = 0;
|
|
820
|
+
for (const msg of messages) {
|
|
821
|
+
if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
|
|
822
|
+
for (const part of msg.content) {
|
|
823
|
+
if (!part || part.type !== 'tool-result') continue;
|
|
824
|
+
const out = part.output;
|
|
825
|
+
if (out === undefined || out === null || typeof out !== 'object') {
|
|
826
|
+
const v = out === undefined ? 'undefined' : (typeof out === 'string' ? out : JSON.stringify(out));
|
|
827
|
+
part.output = { type: 'text', value: String(v) };
|
|
828
|
+
fixed++;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (VALID_OUTPUT_TYPES.has(out.type)) continue;
|
|
832
|
+
if (out.type === 'error-text' || out.type === 'error_text') {
|
|
833
|
+
part.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
|
|
834
|
+
fixed++;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (out.type === 'error-json' || out.type === 'error_json') {
|
|
838
|
+
part.output = { type: 'json', value: out.value ?? null };
|
|
839
|
+
fixed++;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
// 未知 type 降级为 text
|
|
843
|
+
try { part.output = { type: 'text', value: JSON.stringify(out) }; }
|
|
844
|
+
catch { part.output = { type: 'text', value: String(out) }; }
|
|
845
|
+
fixed++;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return fixed;
|
|
849
|
+
}
|
|
850
|
+
|
|
804
851
|
/**
|
|
805
852
|
* 更新消息存储的使用量
|
|
806
853
|
* @private
|
package/src/agent/main.js
CHANGED
|
@@ -401,6 +401,8 @@ class MainAgent extends BaseAgent {
|
|
|
401
401
|
const sessionCtx = this.framework?.getSessionContext(sessionId);
|
|
402
402
|
if (!messages?.length && sessionCtx) {
|
|
403
403
|
messages = sessionCtx.getMessages();
|
|
404
|
+
// 同样规范化:避免压缩器处理到坏数据
|
|
405
|
+
require('../utils/message-validator').normalizeToolOutputs(messages);
|
|
404
406
|
messageStore.messages = messages;
|
|
405
407
|
}
|
|
406
408
|
if (!messages?.length) throw new Error('No messages to compress');
|
package/src/session/chat.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const { EventEmitter } = require('../common/events');
|
|
12
12
|
const { logger } = require('../common/logger');
|
|
13
|
+
const { normalizeToolOutputs } = require('../utils/message-validator');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Session 作用域的事件监听器
|
|
@@ -222,6 +223,13 @@ class ChatSession extends EventEmitter {
|
|
|
222
223
|
const messages = allMessages.length > MAX_INITIAL_LOAD
|
|
223
224
|
? allMessages.slice(-MAX_INITIAL_LOAD)
|
|
224
225
|
: allMessages;
|
|
226
|
+
// 聊天前过滤:AI SDK v6 响应中允许 error-text/error-json(工具失败),
|
|
227
|
+
// 但 ModelMessage 输入 schema 不接受。加载到内存时立即规范化,
|
|
228
|
+
// 这样显示/统计/压缩/持久化回写都看到干净数据。
|
|
229
|
+
const fixed = normalizeToolOutputs(messages);
|
|
230
|
+
if (fixed > 0) {
|
|
231
|
+
logger.info(`[loadHistory] 规范化 ${fixed} 条 tool-result(error-text/error-json → text/json)`);
|
|
232
|
+
}
|
|
225
233
|
messageStore.messages = messages;
|
|
226
234
|
messageStore.historyLoaded = true;
|
|
227
235
|
messageStore._hasMoreHistory = allMessages.length > MAX_INITIAL_LOAD;
|
|
@@ -175,6 +175,63 @@ function validateAll(messages) {
|
|
|
175
175
|
return validateMessagesPairing(messages);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// AI SDK v6 ToolResultOutput 允许的类型(用于 ModelMessage[] 输入 schema)
|
|
179
|
+
const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 规范化 tool-result 的 output 字段。
|
|
183
|
+
* AI SDK v6 响应中允许 'error-text' / 'error-json'(表示工具执行失败),
|
|
184
|
+
* 但 ModelMessage 输入 schema 不接受这两类。需要在发送前 / 持久化前转换。
|
|
185
|
+
*
|
|
186
|
+
* 转换规则:
|
|
187
|
+
* - error-text → text
|
|
188
|
+
* - error-json → json
|
|
189
|
+
* - 任何未知 type → text(保留 value 字段)
|
|
190
|
+
* - output 缺失或非对象 → 包装为 text 'null'
|
|
191
|
+
*
|
|
192
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
193
|
+
* @returns {number} 规范化次数
|
|
194
|
+
*/
|
|
195
|
+
function normalizeToolOutputs(messages) {
|
|
196
|
+
let fixed = 0;
|
|
197
|
+
for (const msg of messages) {
|
|
198
|
+
if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
|
|
199
|
+
for (const part of msg.content) {
|
|
200
|
+
if (!part || part.type !== 'tool-result') continue;
|
|
201
|
+
const out = part.output;
|
|
202
|
+
// 缺 output 或 output 非对象:包装为 text
|
|
203
|
+
if (out === undefined || out === null || typeof out !== 'object') {
|
|
204
|
+
const v = out === undefined ? 'undefined' : (typeof out === 'string' ? out : JSON.stringify(out));
|
|
205
|
+
part.output = { type: 'text', value: String(v) };
|
|
206
|
+
fixed++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// 已知合法 type:跳过
|
|
210
|
+
if (VALID_OUTPUT_TYPES.has(out.type)) continue;
|
|
211
|
+
// error-text → text(保留原 value 字符串)
|
|
212
|
+
if (out.type === 'error-text' || out.type === 'error_text') {
|
|
213
|
+
part.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
|
|
214
|
+
fixed++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// error-json → json
|
|
218
|
+
if (out.type === 'error-json' || out.type === 'error_json') {
|
|
219
|
+
part.output = { type: 'json', value: out.value ?? null };
|
|
220
|
+
fixed++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// 其它未知 type:降级为 text(保留 value 字符串或 JSON 序列化)
|
|
224
|
+
try {
|
|
225
|
+
part.output = { type: 'text', value: JSON.stringify(out) };
|
|
226
|
+
} catch {
|
|
227
|
+
part.output = { type: 'text', value: String(out) };
|
|
228
|
+
}
|
|
229
|
+
fixed++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return fixed;
|
|
233
|
+
}
|
|
234
|
+
|
|
178
235
|
/**
|
|
179
236
|
* 过滤消息,保留配对的 tool-call → tool-result 链条
|
|
180
237
|
* 用于上下文压缩后保留完整的工具调用链路
|
|
@@ -283,5 +340,6 @@ module.exports = {
|
|
|
283
340
|
validateMessagesPairing,
|
|
284
341
|
validateToolCalls,
|
|
285
342
|
validateAll,
|
|
343
|
+
normalizeToolOutputs,
|
|
286
344
|
filterPairedMessages,
|
|
287
345
|
};
|