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 +1 -1
- package/plugins/executors/extension/index.js +1 -1
- package/src/agent/chat.js +67 -5
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui.js +20 -16
- package/src/common/queue.js +1 -1
- package/src/framework/framework.js +33 -0
- package/src/session/chat.js +1 -1
- package/src/tool/executor.js +1 -1
package/package.json
CHANGED
|
@@ -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
|
|
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({
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/src/cli/ui/chat-ui.js
CHANGED
|
@@ -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
|
-
|
|
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(`${
|
|
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(`${
|
|
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('[
|
|
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.
|
|
676
|
-
|
|
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
|
-
|
|
682
|
+
|
|
683
|
+
process.on('SIGINT', () => {
|
|
684
|
+
// 防止默认 SIGINT 退出(由 tui 的 ctrl+c 处理)
|
|
685
|
+
});
|
|
682
686
|
}
|
|
683
687
|
|
|
684
688
|
|
package/src/common/queue.js
CHANGED
|
@@ -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('
|
|
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); }
|
package/src/session/chat.js
CHANGED
|
@@ -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
|
}
|
package/src/tool/executor.js
CHANGED
|
@@ -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) {
|