foliko 2.0.3 → 2.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -302,7 +302,7 @@ class ExtensionExecutorPlugin extends Plugin {
302
302
  ext.tools.push(toolEntry);
303
303
  }
304
304
 
305
- log.debug(` Registered tool '${toolDef.name}' for extension '${pluginName}'`);
305
+ //log.debug(` Registered tool '${toolDef.name}' for extension '${pluginName}'`);
306
306
 
307
307
  if (this._framework && this._framework._ready) {
308
308
  this._invalidatePromptCaches(this._framework);
package/src/agent/chat.js CHANGED
@@ -473,7 +473,7 @@ class AgentChatHandler extends EventEmitter {
473
473
  const { messageStore, messages } = await this._prepareSession(message, sessionId);
474
474
 
475
475
  if (!this._aiClient) {
476
- throw new Error('AI client not configured.');
476
+ throw new Error('AI 客户端未配置');
477
477
  }
478
478
  this._validateToolCalls(messages);
479
479
  const systemPrompt = framework.getSystemPrompt();
@@ -497,7 +497,12 @@ class AgentChatHandler extends EventEmitter {
497
497
  let reasoningContent = '';
498
498
 
499
499
  const result = await framework.runInSession(sessionId, {}, async () => {
500
- return agent.stream({ messages, ...this.providerOptions, onError: handleSDKError });
500
+ return agent.stream({
501
+ messages,
502
+ ...this.providerOptions,
503
+ onError: handleSDKError,
504
+ abortSignal: framework.getAbortSignal?.(),
505
+ });
501
506
  });
502
507
 
503
508
  const stream = result.fullStream;
@@ -595,7 +600,10 @@ class AgentChatHandler extends EventEmitter {
595
600
  // 统一错误处理:提取简洁消息并通过 yield 传递
596
601
  const friendlyMessage = err?.message?.split('\n')[0] || '未知错误';
597
602
 
598
- this.emit('message:error', { sessionId, error: friendlyMessage, originalError: err });
603
+ // 用户主动中断(framework.stop())不上报 message:error,避免与上游重复
604
+ if (err.name !== 'AbortError') {
605
+ this.emit('message:error', { sessionId, error: friendlyMessage, originalError: err });
606
+ }
599
607
  yield { type: 'error', error: friendlyMessage };
600
608
  } finally {
601
609
  const messageStore = this._getSessionMessageStore(sessionId);
@@ -648,7 +656,7 @@ class AgentChatHandler extends EventEmitter {
648
656
  const { messageStore, messages } = await this._prepareSession(message, sessionId);
649
657
 
650
658
  if (!this._aiClient) {
651
- throw new Error('AI client not configured.');
659
+ throw new Error('AI 客户端未配置');
652
660
  }
653
661
  this._validateToolCalls(messages);
654
662
  const systemPrompt = framework.getSystemPrompt();
@@ -669,7 +677,7 @@ class AgentChatHandler extends EventEmitter {
669
677
  });
670
678
 
671
679
  const result = await framework.runInSession(sessionId, {}, async () => {
672
- return agent.generate({ messages, ...apiOptions });
680
+ return agent.generate({ messages, ...apiOptions, abortSignal: framework.getAbortSignal?.() });
673
681
  });
674
682
 
675
683
  if (result.usage && (result.usage.promptTokens > 0 || result.usage.completionTokens > 0)) {
@@ -780,6 +788,9 @@ class AgentChatHandler extends EventEmitter {
780
788
  messages.push(userMessage);
781
789
  this._validateToolCalls(messages);
782
790
 
791
+ // 最终兜底:暴力清理所有 orphaned tool 消息,确保 AI SDK 不报错
792
+ this._stripOrphanedToolMessages(messages);
793
+
783
794
  return { messageStore, messages };
784
795
  }
785
796
 
@@ -832,6 +843,57 @@ class AgentChatHandler extends EventEmitter {
832
843
  messageStore._cacheMessageCount = messageStore.messages.length;
833
844
  }
834
845
 
846
+ /**
847
+ * 最终兜底:暴力清理所有 orphaned tool 消息
848
+ * 遍历所有 tool 角色消息,删除没有对应 tool-call 的
849
+ * 处理数组格式(AI SDK)、字符串格式(OpenAI 兼容)、tool_call_id 格式
850
+ */
851
+ _stripOrphanedToolMessages(messages) {
852
+ const validIds = new Set();
853
+ for (const msg of messages) {
854
+ if (msg.role !== 'assistant') continue;
855
+ if (Array.isArray(msg.content)) {
856
+ for (const item of msg.content) {
857
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) validIds.add(item.toolCallId);
858
+ }
859
+ }
860
+ if (Array.isArray(msg.tool_calls)) {
861
+ for (const tc of msg.tool_calls) { if (tc.id) validIds.add(tc.id); }
862
+ }
863
+ }
864
+
865
+ let removed = 0;
866
+ for (let i = messages.length - 1; i >= 0; i--) {
867
+ const msg = messages[i];
868
+ if (msg.role !== 'tool') continue;
869
+
870
+ let isOrphaned = false;
871
+ if (Array.isArray(msg.content)) {
872
+ msg.content = msg.content.filter(item => {
873
+ if (item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId && !validIds.has(item.toolCallId)) {
874
+ return false;
875
+ }
876
+ return true;
877
+ });
878
+ if (msg.content.length === 0) isOrphaned = true;
879
+ } else if (msg.tool_call_id && !validIds.has(msg.tool_call_id)) {
880
+ isOrphaned = true;
881
+ } else if (!msg.tool_call_id && validIds.size === 0) {
882
+ // 没有任何 tool-call,所有 tool 消息都是 orphaned
883
+ isOrphaned = true;
884
+ }
885
+
886
+ if (isOrphaned) {
887
+ messages.splice(i, 1);
888
+ removed++;
889
+ }
890
+ }
891
+
892
+ if (removed > 0) {
893
+ logger.debug(`[_stripOrphanedToolMessages] removed ${removed} orphaned tool message(s)`);
894
+ }
895
+ }
896
+
835
897
  /**
836
898
  * 计算工具列表缓存 key
837
899
  * @private
package/src/agent/sub.js CHANGED
@@ -157,7 +157,7 @@ class SubAgent extends BaseAgent {
157
157
  } catch (err) {
158
158
  lastError = err;
159
159
  if (isNetworkError(err) && attempt < maxRetries) {
160
- logger.warn(`[SubAgent:${this.name}] AI unavailable, retrying (${attempt + 1}/${maxRetries})`);
160
+ logger.warn(`[SubAgent:${this.name}] AI 服务不可用,重试中 (${attempt + 1}/${maxRetries})`);
161
161
  await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
162
162
  continue;
163
163
  }
@@ -287,9 +287,11 @@ class ChatUI {
287
287
  const self=this;
288
288
  queue.add(function(){
289
289
  var next = this.next;
290
+ self.interrupted = false; // 新消息重置中断状态,确保 stream chunks 正常处理
290
291
  self.agent.sendMessage(message, { sessionId:self.sessionId }).then(next).catch(function(e){
291
- self.create_message(chalk.red(`[错误] ${e?.message || e || '发送消息失败'}\n`), colored('● ', RED))
292
+ // 错误由 sessionScope.on('queue:failed') 统一显示,此处只需清理状态
292
293
  self.clear_message_done();
294
+ next(); // 必须调 next() 推进队列,否则后续消息卡死
293
295
  });
294
296
  })
295
297
 
@@ -592,11 +594,11 @@ class ChatUI {
592
594
  }
593
595
  } else if (chunk.type === 'tool-call') {
594
596
  const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 30) : '';
595
- this.statusBar.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
597
+ this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(args+'...')}`)
596
598
  } else if(chunk.type==='tool-result'){
597
599
  const result = chunk.result;
598
600
  const shortResult = typeof result === 'string' ? result.slice(0, 30) : JSON.stringify(result).slice(0, 30);
599
- this.statusBar.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
601
+ this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(shortResult+'...')}`)
600
602
 
601
603
  // 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
602
604
  if (chunk.toolName === 'edit' || chunk.toolName === 'edit_file') {
@@ -610,7 +612,7 @@ class ChatUI {
610
612
  const bgColor = chalk.bgHex('#1a1a2e');
611
613
  this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
612
614
  // this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
613
- this.statusBar.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
615
+ this.statusBar.tooler.show(`${chalk.green('[差异]')} ${folikoGold(resultObj.filePath)}`);
614
616
  }
615
617
  }
616
618
  // setTimeout(() => this.tooler.hide(), 3000);
@@ -662,23 +664,25 @@ class ChatUI {
662
664
 
663
665
  this.interrupted = false;
664
666
 
665
- const interruptHandler = () => {
666
- if (!this.interrupted) {
667
- this.interrupted = true;
668
- console.log(`\n${colored('[中断]', RED)} 输出已中断`);
669
- }
670
- };
671
-
672
-
673
667
  this.tui.addInputListener((data) => {
674
668
  if (matchesKey(data, 'ctrl+c')) {
675
- this.tui.stop();
676
- process.exit(0);
669
+ if (this.isResponding) {
670
+ // 正在响应中:中断对话但不退出
671
+ this.isResponding = false;
672
+ this.interrupted = true;
673
+ this.agent?.framework?.stop();
674
+ } else {
675
+ // 空闲状态:退出
676
+ this.tui.stop();
677
+ process.exit(0);
678
+ }
677
679
  }
678
680
  });
679
681
  this.tui.start();
680
-
681
- //process.on('SIGINT', interruptHandler);
682
+
683
+ process.on('SIGINT', () => {
684
+ // 防止默认 SIGINT 退出(由 tui 的 ctrl+c 处理)
685
+ });
682
686
  }
683
687
 
684
688
 
@@ -260,7 +260,7 @@ class ChatQueueManager extends EventEmitter {
260
260
  this.activeCount = 0;
261
261
 
262
262
  pendingItems.forEach((item) => {
263
- item.reject(new Error('Queue cleared by user'));
263
+ item.reject(new Error('队列已清空'));
264
264
  });
265
265
 
266
266
  this.emit('queue:cleared', { count: pendingItems.length });
@@ -117,6 +117,9 @@ class Framework extends EventEmitter {
117
117
  // Coordinator
118
118
  this._coordinatorManager = null;
119
119
 
120
+ // Global abort controller for framework.stop()
121
+ this._abortController = null;
122
+
120
123
  // Session TTL
121
124
  this._sessionTTL = new SessionTTL({
122
125
  getSessionContexts: () => this._sessionContexts,
@@ -336,6 +339,36 @@ class Framework extends EventEmitter {
336
339
 
337
340
  getCwd() { return this._cwd; }
338
341
 
342
+ /**
343
+ * 获取全局 abort signal,用于中断正在运行的对话
344
+ */
345
+ getAbortSignal() {
346
+ if (!this._abortController || this._abortController.signal.aborted) {
347
+ this._abortController = new AbortController();
348
+ }
349
+ return this._abortController.signal;
350
+ }
351
+
352
+ /**
353
+ * 立即中断所有正在运行的 Agent 对话
354
+ */
355
+ stop() {
356
+ if (this._abortController) {
357
+ this._abortController.abort(new Error('用户中断'));
358
+ this._abortController = null;
359
+ }
360
+ // 清空消息队列
361
+ for (const agent of this._agents) {
362
+ if (agent._chatHandler?.queueManager) {
363
+ agent._chatHandler.queueManager.clear();
364
+ }
365
+ if (typeof agent.cancelSession === 'function') {
366
+ agent.cancelSession('default');
367
+ }
368
+ }
369
+ this.emit('framework:stopped');
370
+ }
371
+
339
372
  registerEventDescription(eventType, description, schema = null) { this.pluginManager.registerEventDescription(eventType, description, schema); return this; }
340
373
  getEventDescriptions() { return this.pluginManager.getEventDescriptions(); }
341
374
  getEventDescription(eventType) { return this.pluginManager.getEventDescription(eventType); }
@@ -225,7 +225,7 @@ class ChatSession extends EventEmitter {
225
225
  messageStore.messages = messages;
226
226
  messageStore.historyLoaded = true;
227
227
  messageStore._hasMoreHistory = allMessages.length > MAX_INITIAL_LOAD;
228
- logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
228
+ //logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
229
229
  }
230
230
  }
231
231
  }
@@ -35,7 +35,7 @@ class ToolExecutor extends EventEmitter {
35
35
  return;
36
36
  }
37
37
  this._tools.set(tool.name, tool);
38
- logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
38
+ //logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
39
39
  }
40
40
 
41
41
  unregisterTool(name) {