foliko 2.0.11 → 2.0.12

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.
@@ -114,17 +114,21 @@ function validateToolCalls(messages, options = {}) {
114
114
  if (typeof input !== 'string') continue;
115
115
 
116
116
  const trimmed = input.trim();
117
- if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
118
- if (item.toolCallId) {
119
- invalidatedToolCallIds.add(item.toolCallId);
120
- }
117
+ // 只有真正无法解析为 JSON 的才标记为 invalid
118
+ let invalid = false;
119
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
120
+ try { JSON.parse(trimmed); } catch { invalid = true; }
121
+ } else if (trimmed !== '') {
122
+ invalid = true;
123
+ }
124
+
125
+ if (invalid) {
126
+ if (item.toolCallId) invalidatedToolCallIds.add(item.toolCallId);
121
127
  if (shouldLog) {
122
128
  logger.warn(
123
- `[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId ||
124
- '?'}, converting to text`
129
+ `[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId || '?'}, converting to text`
125
130
  );
126
131
  }
127
- // 把无效的 tool-call 转换成 text 文本
128
132
  item.type = 'text';
129
133
  item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
130
134
  delete item.toolCallId;
@@ -155,24 +159,143 @@ function validateToolCalls(messages, options = {}) {
155
159
  }
156
160
  }
157
161
 
158
- if (shouldLog && fixedCount > 0) {
159
- // logger.info(`[validateToolCalls] Fixed ${fixedCount} incomplete tool calls/results`);
160
- }
162
+ if (shouldLog && fixedCount > 0) {}
161
163
 
162
164
  return fixedCount;
163
165
  }
164
166
 
165
167
  /**
166
- * 一次遍历完成配对验证 + tool-call 格式验证
167
- * 相比分别调用减少消息数组迭代次数
168
+ * 单次遍历完成所有验证和规范化:
169
+ * 1. 修复 tool-call 格式(不完整 JSON → text)
170
+ * 2. 收集有效 toolCallId(content[] 和 tool_calls[])
171
+ * 3. 规范化 error-text/error-json → text/json
172
+ * 4. 移除 orphaned tool-result(没有对应 tool-call 的)
173
+ * 5. 移除整个 orphaned tool 消息(content 被清空的)
174
+ *
175
+ * 比分别调用减少 3-4 次消息数组遍历
168
176
  * @param {Array} messages - 消息数组(会被修改)
169
177
  * @returns {Array} 过滤后的消息数组
170
178
  */
171
179
  function validateAll(messages) {
172
- // 1. 先修复 tool-call 格式(避免无效 ID 干扰后续配对验证)
173
- validateToolCalls(messages);
174
- // 2. 再移除 orphaned tool-result
175
- return validateMessagesPairing(messages);
180
+ const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
181
+ const validToolCallIds = new Set();
182
+ const invalidatedIds = new Set();
183
+
184
+ // 第一遍:收集有效 ID + 修复格式 + 规范化 error 类型
185
+ for (const msg of messages) {
186
+ if (msg.role === 'assistant') {
187
+ const content = msg.content;
188
+ if (Array.isArray(content)) {
189
+ for (const item of content) {
190
+ if (item && (item.type === 'tool-call' || item.type === 'tool-use')) {
191
+ const input = item.input;
192
+ // 检查 input 是否为不完整的 JSON:空字符串、或只有 { 或 }
193
+ // 有效 input 应能被 JSON.parse(即使是 {})
194
+ let invalid = false;
195
+ if (typeof input === 'string') {
196
+ const trimmed = input.trim();
197
+ // 有效 input:能解析为 JSON 的字符串(包括 '{}')
198
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
199
+ try { JSON.parse(trimmed); } catch { invalid = true; }
200
+ } else if (trimmed !== '') {
201
+ // 非空且不是 {} 开头:无效
202
+ invalid = true;
203
+ }
204
+ // 空字符串 '' 单独放过(无参数工具是合法的)
205
+ } else if (input === undefined || input === null) {
206
+ // input 为 null/undefined 时:只有当工具 schema 要求必填时才应该报错
207
+ // 但我们无法在此判断工具是否必填,统一放行让后续验证处理
208
+ // invalid = true;
209
+ }
210
+ if (invalid) {
211
+ logger.debug(`[validateAll] INVALID detected: toolName=${item.toolName}, input=${JSON.stringify(input)}, typeof=${typeof input}`);
212
+ // 无效输入 → 转 text,标记为 invalid
213
+ item.type = 'text';
214
+ item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
215
+ if (item.toolCallId) invalidatedIds.add(item.toolCallId);
216
+ delete item.toolCallId;
217
+ delete item.toolName;
218
+ delete item.input;
219
+ } else if (item.toolCallId) {
220
+ validToolCallIds.add(item.toolCallId);
221
+ }
222
+ } else if (item && item.type === 'tool-result') {
223
+ // 规范化 tool-result output
224
+ const out = item.output;
225
+ if (out === undefined || out === null || typeof out !== 'object') {
226
+ item.output = { type: 'text', value: String(out ?? 'undefined') };
227
+ } else if (!VALID_OUTPUT_TYPES.has(out.type)) {
228
+ if (out.type === 'error-text' || out.type === 'error_text') {
229
+ item.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
230
+ } else if (out.type === 'error-json' || out.type === 'error_json') {
231
+ item.output = { type: 'json', value: out.value ?? null };
232
+ } else {
233
+ try { item.output = { type: 'text', value: JSON.stringify(out) }; }
234
+ catch { item.output = { type: 'text', value: String(out) }; }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ // tool_calls[] 格式(OpenAI 兼容)
241
+ if (Array.isArray(msg.tool_calls)) {
242
+ for (const tc of msg.tool_calls) {
243
+ if (tc.id && !invalidatedIds.has(tc.id)) validToolCallIds.add(tc.id);
244
+ }
245
+ }
246
+ } else if (msg.role === 'tool') {
247
+ // 规范化 tool 消息 output
248
+ if (Array.isArray(msg.content)) {
249
+ for (const item of msg.content) {
250
+ if (item && item.type === 'tool-result') {
251
+ const out = item.output;
252
+ if (out === undefined || out === null || typeof out !== 'object') {
253
+ item.output = { type: 'text', value: String(out ?? 'undefined') };
254
+ } else if (!VALID_OUTPUT_TYPES.has(out.type)) {
255
+ if (out.type === 'error-text' || out.type === 'error_text') {
256
+ item.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
257
+ } else if (out.type === 'error-json' || out.type === 'error_json') {
258
+ item.output = { type: 'json', value: out.value ?? null };
259
+ } else {
260
+ try { item.output = { type: 'text', value: JSON.stringify(out) }; }
261
+ catch { item.output = { type: 'text', value: String(out) }; }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ // 第二遍:过滤 orphaned tool-result + orphaned tool messages
271
+ const toolMessageIndicesToRemove = new Set();
272
+ for (let i = 0; i < messages.length; i++) {
273
+ const msg = messages[i];
274
+ if (msg.role !== 'tool') continue;
275
+
276
+ let hasValidResult = false;
277
+ if (Array.isArray(msg.content)) {
278
+ const originalLen = msg.content.length;
279
+ msg.content = msg.content.filter((item) => {
280
+ if (!item || (item.type !== 'tool-result' && item.type !== 'tool_result')) return true;
281
+ const id = item.toolCallId;
282
+ if (!id || (!validToolCallIds.has(id) && !invalidatedIds.has(id))) return false;
283
+ hasValidResult = true;
284
+ return true;
285
+ });
286
+ if (msg.content.length === 0 && originalLen > 0) {
287
+ toolMessageIndicesToRemove.add(i);
288
+ }
289
+ } else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
290
+ toolMessageIndicesToRemove.add(i);
291
+ }
292
+ }
293
+
294
+ if (toolMessageIndicesToRemove.size > 0) {
295
+ messages = messages.filter((_, i) => !toolMessageIndicesToRemove.has(i));
296
+ }
297
+
298
+ return messages;
176
299
  }
177
300
 
178
301
  // AI SDK v6 ToolResultOutput 允许的类型(用于 ModelMessage[] 输入 schema)