-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathAgent.ts
More file actions
2524 lines (2230 loc) · 86.2 KB
/
Agent.ts
File metadata and controls
2524 lines (2230 loc) · 86.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Agent核心类 - 无状态设计
*
* 设计原则:
* 1. Agent 本身不保存任何会话状态(sessionId, messages 等)
* 2. 所有状态通过 context 参数传入
* 3. Agent 实例可以每次命令创建,用完即弃
* 4. 历史连续性由外部 SessionContext 保证
*
* 负责:LLM 交互、工具执行、循环检测
*/
import { nanoid } from 'nanoid';
import * as os from 'os';
import * as path from 'path';
import {
type BladeConfig,
ConfigManager,
type PermissionConfig,
PermissionMode,
} from '../config/index.js';
import type { ModelConfig } from '../config/types.js';
import { CompactionService } from '../context/CompactionService.js';
import { ContextManager } from '../context/ContextManager.js';
import { HookManager } from '../hooks/HookManager.js';
import { createLogger, LogCategory } from '../logging/Logger.js';
import { streamDebug } from '../logging/StreamDebugLogger.js';
import { loadMcpConfigFromCli } from '../mcp/loadMcpConfig.js';
import { McpRegistry } from '../mcp/McpRegistry.js';
import { buildSystemPrompt, createPlanModeReminder } from '../prompts/index.js';
import { AttachmentCollector } from '../prompts/processors/AttachmentCollector.js';
import type { Attachment } from '../prompts/processors/types.js';
import { buildSpecModePrompt, createSpecModeReminder } from '../prompts/spec.js';
import {
type ChatResponse,
type ContentPart,
createChatServiceAsync,
type IChatService,
type Message,
type StreamToolCall,
} from '../services/ChatServiceInterface.js';
import type { JsonValue } from '../store/types.js';
function toJsonValue(value: string | object): JsonValue {
if (typeof value === 'string') return value;
try {
return JSON.parse(JSON.stringify(value)) as JsonValue;
} catch {
return String(value);
}
}
import { discoverSkills, injectSkillsMetadata } from '../skills/index.js';
import { SpecManager } from '../spec/SpecManager.js';
import {
appActions,
configActions,
ensureStoreInitialized,
getAllModels,
getConfig,
getCurrentModel,
getMcpServers,
getModelById,
getThinkingModeEnabled,
} from '../store/vanilla.js';
import { getBuiltinTools } from '../tools/builtin/index.js';
import { ExecutionPipeline } from '../tools/execution/ExecutionPipeline.js';
import { ToolRegistry } from '../tools/registry/ToolRegistry.js';
import { type Tool, ToolErrorType, type ToolResult } from '../tools/types/index.js';
import { getEnvironmentContext } from '../utils/environment.js';
import { isThinkingModel } from '../utils/modelDetection.js';
import { ExecutionEngine } from './ExecutionEngine.js';
import { SessionRuntime } from './runtime/SessionRuntime.js';
import { subagentRegistry } from './subagents/SubagentRegistry.js';
import type {
AgentOptions,
AgentResponse,
AgentTask,
ChatContext,
LoopOptions,
LoopResult,
UserMessageContent,
} from './types.js';
// 创建 Agent 专用 Logger
const logger = createLogger(LogCategory.AGENT);
/**
* Skill 执行上下文
* 用于跟踪当前活动的 Skill 及其工具限制
*/
interface SkillExecutionContext {
skillName: string;
allowedTools?: string[];
basePath: string;
}
export class Agent {
private config: BladeConfig;
private runtimeOptions: AgentOptions;
private isInitialized = false;
private activeTask?: AgentTask;
private executionPipeline: ExecutionPipeline;
// systemPrompt 已移除 - 改为从 context 参数传入(无状态设计)
// sessionId 已移除 - 改为从 context 参数传入(无状态设计)
// 核心组件
private chatService!: IChatService;
private executionEngine!: ExecutionEngine;
private attachmentCollector?: AttachmentCollector;
// Skill 执行上下文(用于 allowed-tools 限制)
private activeSkillContext?: SkillExecutionContext;
// 当前模型的上下文窗口大小(用于 tokenUsage 上报)
private currentModelMaxContextTokens!: number;
private currentModelId?: string;
private sessionRuntime?: SessionRuntime;
constructor(
config: BladeConfig,
runtimeOptions: AgentOptions = {},
executionPipeline?: ExecutionPipeline,
sessionRuntime?: SessionRuntime
) {
this.config = config;
this.runtimeOptions = runtimeOptions;
this.executionPipeline = executionPipeline || this.createDefaultPipeline();
this.sessionRuntime = sessionRuntime;
// sessionId 不再存储在 Agent 内部,改为从 context 传入
}
/**
* 创建默认的 ExecutionPipeline
*/
private createDefaultPipeline(): ExecutionPipeline {
const registry = new ToolRegistry();
// 合并基础权限配置和运行时覆盖
const permissions: PermissionConfig = {
...this.config.permissions,
...this.runtimeOptions.permissions,
};
const permissionMode =
this.runtimeOptions.permissionMode ??
this.config.permissionMode ??
PermissionMode.DEFAULT;
return new ExecutionPipeline(registry, {
permissionConfig: permissions,
permissionMode,
maxHistorySize: 1000,
});
}
private resolveModelConfig(requestedModelId?: string): ModelConfig {
const modelId = requestedModelId && requestedModelId !== 'inherit' ? requestedModelId : undefined;
const modelConfig = modelId ? getModelById(modelId) : getCurrentModel();
if (!modelConfig) {
throw new Error(`❌ 模型配置未找到: ${modelId ?? 'current'}`);
}
return modelConfig;
}
private async applyModelConfig(modelConfig: ModelConfig, label: string): Promise<void> {
this.log(`${label} ${modelConfig.name} (${modelConfig.model})`);
const modelSupportsThinking = isThinkingModel(modelConfig);
const thinkingModeEnabled = getThinkingModeEnabled();
const supportsThinking = modelSupportsThinking && thinkingModeEnabled;
if (modelSupportsThinking && !thinkingModeEnabled) {
this.log(`🧠 模型支持 Thinking,但用户未开启(按 Tab 开启)`);
} else if (supportsThinking) {
this.log(`🧠 Thinking 模式已启用,启用 reasoning_content 支持`);
}
this.currentModelMaxContextTokens =
modelConfig.maxContextTokens ?? this.config.maxContextTokens;
this.chatService = await createChatServiceAsync({
provider: modelConfig.provider,
apiKey: modelConfig.apiKey,
model: modelConfig.model,
baseUrl: modelConfig.baseUrl,
temperature: modelConfig.temperature ?? this.config.temperature,
maxContextTokens: this.currentModelMaxContextTokens,
maxOutputTokens: modelConfig.maxOutputTokens ?? this.config.maxOutputTokens,
timeout: this.config.timeout,
supportsThinking,
});
const contextManager = this.executionEngine?.getContextManager();
this.executionEngine = new ExecutionEngine(this.chatService, contextManager);
this.currentModelId = modelConfig.id;
}
private async switchModelIfNeeded(modelId: string): Promise<void> {
if (this.sessionRuntime) {
await this.sessionRuntime.refresh({ modelId });
this.syncRuntimeState();
return;
}
if (!modelId || modelId === this.currentModelId) return;
const modelConfig = getModelById(modelId);
if (!modelConfig) {
this.log(`⚠️ 模型配置未找到: ${modelId}`);
return;
}
await this.applyModelConfig(modelConfig, '🔁 切换模型');
}
/**
* 快速创建并初始化 Agent 实例(静态工厂方法)
* 使用 Store 获取配置
*/
static async create(options: AgentOptions = {}): Promise<Agent> {
if (options.sessionId) {
throw new Error(
'Agent.create() does not accept sessionId. Create a SessionRuntime explicitly and use Agent.createWithRuntime().'
);
}
// 0. 确保 store 已初始化(防御性检查)
await ensureStoreInitialized();
// 1. 检查是否有可用的模型配置
const models = getAllModels();
if (models.length === 0) {
throw new Error(
'❌ 没有可用的模型配置\n\n' +
'请先使用以下命令添加模型:\n' +
' /model add\n\n' +
'或运行初始化向导:\n' +
' /init'
);
}
// 2. 获取 BladeConfig(从 Store)
const config = getConfig();
if (!config) {
throw new Error('❌ 配置未初始化,请确保应用已正确启动');
}
// 3. 验证配置
const configManager = ConfigManager.getInstance();
configManager.validateConfig(config);
// 4. 创建并初始化 Agent
// 将 options 作为运行时参数传递
const agent = new Agent(config, options);
await agent.initialize();
// 5. 应用工具白名单(如果指定)
if (options.toolWhitelist && options.toolWhitelist.length > 0) {
agent.applyToolWhitelist(options.toolWhitelist);
}
return agent;
}
static async createWithRuntime(
runtime: SessionRuntime,
options: AgentOptions = {}
): Promise<Agent> {
const agent = new Agent(
runtime.getConfig(),
options,
runtime.createExecutionPipeline(options),
runtime
);
await agent.initialize();
return agent;
}
/**
* 初始化Agent
*/
public async initialize(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
this.log('初始化Agent...');
if (this.sessionRuntime) {
await this.initializeSystemPrompt();
await this.sessionRuntime.refresh(this.runtimeOptions);
this.syncRuntimeState();
this.isInitialized = true;
this.log(
`Agent初始化完成,已加载 ${this.executionPipeline.getRegistry().getAll().length} 个工具`
);
return;
}
// 1. 初始化系统提示
await this.initializeSystemPrompt();
// 2. 注册内置工具
await this.registerBuiltinTools();
// 3. 加载 subagent 配置
await this.loadSubagents();
// 4. 发现并注册 Skills
await this.discoverSkills();
// 5. 初始化核心组件
const modelConfig = this.resolveModelConfig(this.runtimeOptions.modelId);
await this.applyModelConfig(modelConfig, '🚀 使用模型:');
// 5. 初始化附件收集器(@ 文件提及)
this.attachmentCollector = new AttachmentCollector({
cwd: process.cwd(),
maxFileSize: 1024 * 1024, // 1MB
maxLines: 2000,
maxTokens: 32000,
});
this.isInitialized = true;
this.log(
`Agent初始化完成,已加载 ${this.executionPipeline.getRegistry().getAll().length} 个工具`
);
} catch (error) {
this.error('Agent初始化失败', error);
throw error;
}
}
/**
* 执行任务
*/
public async executeTask(task: AgentTask): Promise<AgentResponse> {
if (!this.isInitialized) {
throw new Error('Agent未初始化');
}
this.activeTask = task;
try {
this.log(`开始执行任务: ${task.id}`);
const response = await this.executionEngine.executeTask(task);
this.activeTask = undefined;
this.log(`任务执行完成: ${task.id}`);
return response;
} catch (error) {
this.activeTask = undefined;
this.error(`任务执行失败: ${task.id}`, error);
throw error;
}
}
/**
* 简单聊天接口
* @param message - 用户消息内容(支持纯文本或多模态)
*/
public async chat(
message: UserMessageContent,
context?: ChatContext,
options?: LoopOptions
): Promise<string> {
if (!this.isInitialized) {
throw new Error('Agent未初始化');
}
// ✨ 处理 @ 文件提及(在发送前预处理)
// 支持纯文本和多模态消息
const enhancedMessage = await this.processAtMentionsForContent(message);
// 如果提供了 context,使用增强的工具调用流程
if (context) {
// 合并 signal 和 options
const loopOptions: LoopOptions = {
signal: context.signal,
...options,
};
// Plan/Spec 模式使用专门的 runLoop 方法
let result: LoopResult;
if (context.permissionMode === 'plan') {
result = await this.runPlanLoop(enhancedMessage, context, loopOptions);
} else if (context.permissionMode === 'spec') {
result = await this.runSpecLoop(enhancedMessage, context, loopOptions);
} else {
result = await this.runLoop(enhancedMessage, context, loopOptions);
}
if (!result.success) {
// 如果是用户中止或用户拒绝,返回空字符串(不抛出异常)
if (result.error?.type === 'aborted' || result.metadata?.shouldExitLoop) {
return ''; // 返回空字符串,让调用方自行处理
}
// 其他错误则抛出异常
throw new Error(result.error?.message || '执行失败');
}
// 🆕 检查是否需要切换模式并重新执行(Plan 模式批准后)
if (result.metadata?.targetMode && context.permissionMode === 'plan') {
const targetMode = result.metadata.targetMode as PermissionMode;
const planContent = result.metadata.planContent as string | undefined;
logger.debug(`🔄 Plan 模式已批准,切换到 ${targetMode} 模式并重新执行`);
// 更新内存中的权限模式(运行时状态,不持久化)
await configActions().setPermissionMode(targetMode);
logger.debug(`✅ 权限模式已更新: ${targetMode}`);
// 创建新的 context,使用批准的目标模式
const newContext: ChatContext = {
...context,
permissionMode: targetMode,
};
// 🆕 将 plan 内容注入到消息中,确保 AI 按照 plan 执行
let messageWithPlan: UserMessageContent = enhancedMessage;
if (planContent) {
const planSuffix = `
<approved-plan>
${planContent}
</approved-plan>
IMPORTANT: Execute according to the approved plan above. Follow the steps exactly as specified.`;
// 处理多模态消息:将 plan 内容追加到文本部分
if (typeof enhancedMessage === 'string') {
messageWithPlan = enhancedMessage + planSuffix;
} else {
// 多模态消息:在最后添加一个文本部分
messageWithPlan = [...enhancedMessage, { type: 'text', text: planSuffix }];
}
logger.debug(`📋 已将 plan 内容注入到消息中 (${planContent.length} 字符)`);
}
return this.runLoop(messageWithPlan, newContext, loopOptions).then(
(newResult) => {
if (!newResult.success) {
throw new Error(newResult.error?.message || '执行失败');
}
return newResult.finalMessage || '';
}
);
}
return result.finalMessage || '';
}
// 否则使用原有的简单流程(仅支持纯文本消息)
// 多模态消息在简单流程中不支持,提取纯文本部分
const textPrompt =
typeof enhancedMessage === 'string'
? enhancedMessage
: enhancedMessage
.filter((p) => p.type === 'text')
.map((p) => (p as { text: string }).text)
.join('\n');
const task: AgentTask = {
id: this.generateTaskId(),
type: 'simple',
prompt: textPrompt,
};
const response = await this.executeTask(task);
return response.content;
}
/**
* 运行 Plan 模式循环 - 专门处理 Plan 模式的逻辑
* Plan 模式特点:只读调研、系统化研究方法论、最终输出实现计划
*/
/**
* Plan 模式入口 - 准备 Plan 专用配置后调用通用循环
*/
private async runPlanLoop(
message: UserMessageContent,
context: ChatContext,
options?: LoopOptions
): Promise<LoopResult> {
logger.debug('🔵 Processing Plan mode message...');
// Plan 模式差异 1: 使用统一入口构建 Plan 模式系统提示词
const { prompt: systemPrompt } = await buildSystemPrompt({
projectPath: process.cwd(),
mode: PermissionMode.PLAN,
includeEnvironment: true,
language: this.config.language,
});
// Plan 模式差异 2: 在用户消息中注入 system-reminder
// 处理多模态消息:提取文本部分添加 reminder
let messageWithReminder: UserMessageContent;
if (typeof message === 'string') {
messageWithReminder = createPlanModeReminder(message);
} else {
// 多模态消息:在第一个文本部分前添加 reminder,或创建新的文本部分
const textParts = message.filter((p) => p.type === 'text');
if (textParts.length > 0) {
const firstTextPart = textParts[0] as { type: 'text'; text: string };
messageWithReminder = message.map((p) =>
p === firstTextPart
? {
type: 'text' as const,
text: createPlanModeReminder(firstTextPart.text),
}
: p
);
} else {
// 仅图片,添加空的 reminder
messageWithReminder = [
{ type: 'text', text: createPlanModeReminder('') },
...message,
];
}
}
// 调用通用循环,传入 Plan 模式专用配置
// 注意:不再传递 isPlanMode 参数,executeLoop 会从 context.permissionMode 读取
return this.executeLoop(messageWithReminder, context, options, systemPrompt);
}
/**
* Spec 模式入口 - 准备 Spec 专用配置后调用通用循环
* Spec 模式特点:结构化 4 阶段工作流(Requirements → Design → Tasks → Implementation)
*/
private async runSpecLoop(
message: UserMessageContent,
context: ChatContext,
options?: LoopOptions
): Promise<LoopResult> {
logger.debug('🔷 Processing Spec mode message...');
// 1. 确保 SpecManager 已初始化
const specManager = SpecManager.getInstance();
const workspaceRoot = context.workspaceRoot || process.cwd();
try {
// 尝试初始化(如果已初始化会安全返回)
await specManager.initialize(workspaceRoot);
} catch (error) {
logger.warn('SpecManager initialization warning:', error);
// 继续执行,可能是首次进入 Spec 模式
}
// 2. 获取当前 Spec 上下文
const currentSpec = specManager.getCurrentSpec();
const steeringContextString = await specManager.getSteeringContextString();
// 2. 构建 Spec 模式系统提示词
const systemPrompt = buildSpecModePrompt(currentSpec, steeringContextString);
// 3. 在用户消息中注入 spec-mode-reminder
let messageWithReminder: UserMessageContent;
const phase = currentSpec?.phase || 'init';
if (typeof message === 'string') {
messageWithReminder = `${createSpecModeReminder(phase)}\n\n${message}`;
} else {
// 多模态消息:在第一个文本部分前添加 reminder
const textParts = message.filter((p) => p.type === 'text');
if (textParts.length > 0) {
const firstTextPart = textParts[0] as { type: 'text'; text: string };
messageWithReminder = message.map((p) =>
p === firstTextPart
? {
type: 'text' as const,
text: `${createSpecModeReminder(phase)}\n\n${firstTextPart.text}`,
}
: p
);
} else {
// 仅图片,添加 reminder
messageWithReminder = [
{ type: 'text', text: createSpecModeReminder(phase) },
...message,
];
}
}
// 4. 调用通用循环,传入 Spec 模式专用配置
return this.executeLoop(messageWithReminder, context, options, systemPrompt);
}
/**
* 普通模式入口 - 准备普通模式配置后调用通用循环
* 无状态设计:systemPrompt 从 context 传入,或按需动态构建
*/
private async runLoop(
message: UserMessageContent,
context: ChatContext,
options?: LoopOptions
): Promise<LoopResult> {
logger.debug('💬 Processing enhanced chat message...');
// 无状态设计:优先使用 context.systemPrompt,否则按需构建
const basePrompt =
context.systemPrompt ?? (await this.buildSystemPromptOnDemand());
const envContext = getEnvironmentContext();
const systemPrompt = basePrompt
? `${envContext}\n\n---\n\n${basePrompt}`
: envContext;
// 调用通用循环
return this.executeLoop(message, context, options, systemPrompt);
}
/**
* 按需构建系统提示词(用于未传入 context.systemPrompt 的场景)
*/
private async buildSystemPromptOnDemand(): Promise<string> {
const replacePrompt = this.runtimeOptions.systemPrompt;
const appendPrompt = this.runtimeOptions.appendSystemPrompt;
const result = await buildSystemPrompt({
projectPath: process.cwd(),
replaceDefault: replacePrompt,
append: appendPrompt,
includeEnvironment: false,
language: this.config.language,
});
return result.prompt;
}
/**
* 核心执行循环 - 所有模式共享的通用循环逻辑
* 持续执行 LLM → 工具 → 结果注入 直到任务完成或达到限制
*
* @param message - 用户消息(可能已被 Plan 模式注入 system-reminder)
* @param context - 聊天上下文(包含 permissionMode,用于决定工具暴露策略)
* @param options - 循环选项
* @param systemPrompt - 系统提示词(Plan 模式和普通模式使用不同的提示词)
*/
private async executeLoop(
message: UserMessageContent,
context: ChatContext,
options?: LoopOptions,
systemPrompt?: string
): Promise<LoopResult> {
if (!this.isInitialized) {
throw new Error('Agent未初始化');
}
const startTime = Date.now();
try {
// 1. 获取可用工具定义
// 根据 permissionMode 决定工具暴露策略(单一信息源:ToolRegistry.getFunctionDeclarationsByMode)
const registry = this.executionPipeline.getRegistry();
const permissionMode = context.permissionMode as PermissionMode | undefined;
let rawTools = registry.getFunctionDeclarationsByMode(permissionMode);
// 注入 Skills 元数据到 Skill 工具的 <available_skills> 占位符
rawTools = injectSkillsMetadata(rawTools);
// 应用 Skill 的 allowed-tools 限制(如果有活动的 Skill)
const tools = this.applySkillToolRestrictions(rawTools);
const isPlanMode = permissionMode === PermissionMode.PLAN;
if (isPlanMode) {
const readOnlyTools = registry.getReadOnlyTools();
logger.debug(
`🔒 Plan mode: 使用只读工具 (${readOnlyTools.length} 个): ${readOnlyTools.map((t) => t.name).join(', ')}`
);
}
// 2. 构建消息历史
const needsSystemPrompt =
context.messages.length === 0 ||
!context.messages.some((msg) => msg.role === 'system');
const messages: Message[] = [];
// 注入系统提示词(由调用方决定使用哪个提示词)
// 🆕 为 Anthropic 模型启用 Prompt Caching(成本降低 90%,延迟降低 85%)
if (needsSystemPrompt && systemPrompt) {
messages.push({
role: 'system',
content: [
{
type: 'text',
text: systemPrompt,
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
},
],
});
}
// 添加历史消息和当前用户消息
messages.push(...context.messages, { role: 'user', content: message });
// === 保存用户消息到 JSONL ===
let lastMessageUuid: string | null = null; // 追踪上一条消息的 UUID,用于建立消息链
try {
const contextMgr = this.executionEngine?.getContextManager();
// 提取纯文本内容用于保存(多模态消息只保存文本部分)
const textContent =
typeof message === 'string'
? message
: message
.filter((p) => p.type === 'text')
.map((p) => (p as { text: string }).text)
.join('\n');
// 🔧 修复:过滤空用户消息(与助手消息保持一致)
if (contextMgr && context.sessionId && textContent.trim() !== '') {
lastMessageUuid = await contextMgr.saveMessage(
context.sessionId,
'user',
textContent,
null,
undefined,
context.subagentInfo
);
} else if (textContent.trim() === '') {
logger.debug('[Agent] 跳过保存空用户消息');
}
} catch (error) {
logger.warn('[Agent] 保存用户消息失败:', error);
// 不阻塞主流程
}
// === Agentic Loop: 循环调用直到任务完成 ===
const SAFETY_LIMIT = 100; // 安全上限(100 轮后询问用户)
const isYoloMode = context.permissionMode === PermissionMode.YOLO; // YOLO 模式不限制
// 优先级: runtimeOptions (CLI参数) > options (chat调用参数) > config (配置文件) > 默认值(-1)
const configuredMaxTurns =
this.runtimeOptions.maxTurns ?? options?.maxTurns ?? this.config.maxTurns ?? -1;
// 特殊值处理:maxTurns = 0 完全禁用对话功能
if (configuredMaxTurns === 0) {
return {
success: false,
error: {
type: 'chat_disabled',
message:
'对话功能已被禁用 (maxTurns=0)。如需启用,请调整配置:\n' +
' • CLI 参数: blade --max-turns -1\n' +
' • 配置文件: ~/.blade/config.json 中设置 "maxTurns": -1\n' +
' • 环境变量: export BLADE_MAX_TURNS=-1',
},
metadata: {
turnsCount: 0,
toolCallsCount: 0,
duration: 0,
},
};
}
// 应用安全上限:-1 表示无限制,但仍受 SAFETY_LIMIT 保护(YOLO 模式除外)
const maxTurns =
configuredMaxTurns === -1
? SAFETY_LIMIT
: Math.min(configuredMaxTurns, SAFETY_LIMIT);
// 调试日志
if (this.config.debug) {
logger.debug(
`[MaxTurns] runtimeOptions: ${this.runtimeOptions.maxTurns}, options: ${options?.maxTurns}, config: ${this.config.maxTurns}, 最终: ${configuredMaxTurns} → ${maxTurns}, YOLO: ${isYoloMode}`
);
}
let turnsCount = 0;
const allToolResults: ToolResult[] = [];
let totalTokens = 0; //- 累计 token 使用量
let lastPromptTokens: number | undefined; // 上一轮 LLM 返回的真实 prompt tokens
// Agentic Loop: 循环调用直到任务完成
// eslint-disable-next-line no-constant-condition
while (true) {
// === 1. 检查中断信号 ===
if (options?.signal?.aborted) {
return {
success: false,
error: {
type: 'aborted',
message: '任务已被用户中止',
},
metadata: {
turnsCount,
toolCallsCount: allToolResults.length,
duration: Date.now() - startTime,
},
};
}
// === 2. 每轮循环前检查并压缩上下文 ===
// 📊 记录压缩前的状态,用于判断是否需要重建 messages
const preCompactLength = context.messages.length;
// 传递实际要发送给 LLM 的 messages 数组(包含 system prompt)
// checkAndCompactInLoop 返回是否发生了压缩
// 🆕 传入上一轮 LLM 返回的真实 prompt tokens(比估算更准确)
const didCompact = await this.checkAndCompactInLoop(
context,
turnsCount,
lastPromptTokens, // 首轮为 undefined,使用估算;后续轮次使用真实值
options?.onCompacting
);
// 🔧 关键修复:如果发生了压缩,必须重建 messages 数组
// 即使长度相同但内容不同的压缩场景也能正确处理
if (didCompact) {
logger.debug(
`[Agent] [轮次 ${turnsCount}] 检测到压缩发生,重建 messages 数组 (${preCompactLength} → ${context.messages.length} 条历史消息)`
);
// 找到 messages 中非历史部分的起始位置
// messages 结构: [system?, ...context.messages(旧), user当前消息?, assistant?, tool?...]
const systemMsgCount = needsSystemPrompt && systemPrompt ? 1 : 0;
const historyEndIdx = systemMsgCount + preCompactLength;
// 保留非历史部分(当前轮次新增的消息)
const systemMessages = messages.slice(0, systemMsgCount);
const newMessages = messages.slice(historyEndIdx); // 当前轮次新增的 user/assistant/tool
// 重建:system + 压缩后的历史 + 当前轮次新增
messages.length = 0; // 清空原数组
messages.push(...systemMessages, ...context.messages, ...newMessages);
logger.debug(
`[Agent] [轮次 ${turnsCount}] messages 重建完成: ${systemMessages.length} system + ${context.messages.length} 历史 + ${newMessages.length} 新增 = ${messages.length} 总计`
);
}
// === 3. 轮次计数 ===
turnsCount++;
logger.debug(`🔄 [轮次 ${turnsCount}/${maxTurns}] 调用 LLM...`);
// 再次检查 abort 信号(在调用 LLM 前)
if (options?.signal?.aborted) {
return {
success: false,
error: {
type: 'aborted',
message: '任务已被用户中止',
},
metadata: {
turnsCount: turnsCount - 1, // 这一轮还没开始
toolCallsCount: allToolResults.length,
duration: Date.now() - startTime,
},
};
}
// 触发轮次开始事件 (供 UI 显示进度)
options?.onTurnStart?.({ turn: turnsCount, maxTurns });
// 🔍 调试:打印发送给 LLM 的消息
logger.debug('\n========== 发送给 LLM ==========');
logger.debug('轮次:', turnsCount + 1);
logger.debug('消息数量:', messages.length);
logger.debug('最后 3 条消息:');
messages.slice(-3).forEach((msg, idx) => {
logger.debug(
` [${idx}] ${msg.role}:`,
typeof msg.content === 'string'
? msg.content.substring(0, 100) + (msg.content.length > 100 ? '...' : '')
: JSON.stringify(msg.content).substring(0, 100)
);
if (msg.tool_calls) {
logger.debug(
' tool_calls:',
msg.tool_calls
.map((tc) => ('function' in tc ? tc.function.name : tc.type))
.join(', ')
);
}
});
logger.debug('可用工具数量:', tools.length);
logger.debug('================================\n');
// 3. 调用 ChatService(流式或非流式)
// 默认启用流式,除非显式设置 stream: false
const isStreamEnabled = options?.stream !== false;
const turnResult = isStreamEnabled
? await this.processStreamResponse(messages, tools, options)
: await this.chatService.chat(messages, tools, options?.signal);
streamDebug('executeLoop', 'after processStreamResponse/chat', {
isStreamEnabled,
turnResultContentLen: turnResult.content?.length ?? 0,
turnResultToolCallsLen: turnResult.toolCalls?.length ?? 0,
hasReasoningContent: !!turnResult.reasoningContent,
});
// 累加 token 使用量,并保存真实的 prompt tokens 用于下一轮压缩检查
if (turnResult.usage) {
if (turnResult.usage.totalTokens) {
totalTokens += turnResult.usage.totalTokens;
}
// 保存真实的 prompt tokens,用于下一轮循环的压缩检查(比估算更准确)
lastPromptTokens = turnResult.usage.promptTokens;
logger.debug(
`[Agent] LLM usage: prompt=${lastPromptTokens}, completion=${turnResult.usage.completionTokens}, total=${turnResult.usage.totalTokens}`
);
// 通知 UI 更新 token 使用量
if (options?.onTokenUsage) {
options.onTokenUsage({
inputTokens: turnResult.usage.promptTokens ?? 0,
outputTokens: turnResult.usage.completionTokens ?? 0,
totalTokens,
maxContextTokens: this.currentModelMaxContextTokens,
});
}
}
// 检查 abort 信号(LLM 调用后)
if (options?.signal?.aborted) {
return {
success: false,
error: {
type: 'aborted',
message: '任务已被用户中止',
},
metadata: {
turnsCount: turnsCount - 1,
toolCallsCount: allToolResults.length,
duration: Date.now() - startTime,
},
};
}
// 🔍 调试:打印模型返回
logger.debug('\n========== LLM 返回 ==========');
logger.debug('Content:', turnResult.content);
logger.debug('Tool Calls:', JSON.stringify(turnResult.toolCalls, null, 2));
logger.debug('当前权限模式:', context.permissionMode);
logger.debug('================================\n');
// 🆕 如果 LLM 返回了 thinking 内容(DeepSeek R1 等),通知 UI
// 流式模式下,增量已通过 onThinkingDelta 发送,这里发送完整内容用于兼容
// 非流式模式下,这是唯一的通知途径
// 注意:检查 abort 状态,避免取消后仍然触发回调
if (
turnResult.reasoningContent &&
options?.onThinking &&
!options.signal?.aborted
) {
options.onThinking(turnResult.reasoningContent);
}
// 🆕 如果 LLM 返回了 content,通知 UI
// 流式模式下:增量已通过 onContentDelta 发送,调用 onStreamEnd 标记结束
// 非流式模式下:调用 onContent 发送完整内容
// 注意:检查 abort 状态,避免取消后仍然触发回调
if (
turnResult.content &&
turnResult.content.trim() &&
!options?.signal?.aborted
) {
if (isStreamEnabled) {
streamDebug('executeLoop', 'calling onStreamEnd (stream mode)', {
contentLen: turnResult.content.length,
});
options?.onStreamEnd?.();
} else if (options?.onContent) {
streamDebug('executeLoop', 'calling onContent (non-stream mode)', {
contentLen: turnResult.content.length,
});
options.onContent(turnResult.content);
}
}
// 4. 检查是否需要工具调用(任务完成条件)
if (!turnResult.toolCalls || turnResult.toolCalls.length === 0) {
// === 检测"意图未完成"模式 ===
// 某些模型(如 qwen)会说"让我来..."但不实际调用工具
const INCOMPLETE_INTENT_PATTERNS = [
/:\s*$/, // 中文冒号结尾
/:\s*$/, // 英文冒号结尾
/\.\.\.\s*$/, // 省略号结尾
/让我(先|来|开始|查看|检查|修复)/, // 中文意图词
/Let me (first|start|check|look|fix)/i, // 英文意图词
];
const content = turnResult.content || '';
const isIncompleteIntent = INCOMPLETE_INTENT_PATTERNS.some((p) =>
p.test(content)
);
// 统计最近的重试消息数量(避免无限循环)
const RETRY_PROMPT = '请执行你提到的操作,不要只是描述。';
const recentRetries = messages
.slice(-10)
.filter((m) => m.role === 'user' && m.content === RETRY_PROMPT).length;
if (isIncompleteIntent && recentRetries < 2) {
logger.debug(
`⚠️ 检测到意图未完成(重试 ${recentRetries + 1}/2): "${content.slice(-50)}"`
);
// 追加提示消息,要求 LLM 执行操作
messages.push({
role: 'user',
content: RETRY_PROMPT,
});