@saber2pr/ai-agent 0.0.41 → 0.0.42

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/lib/cli-graph.js CHANGED
@@ -5,5 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const agent_graph_1 = __importDefault(require("./core/agent-graph"));
8
- const agent = new agent_graph_1.default();
8
+ const agent = new agent_graph_1.default({
9
+ stream: true,
10
+ });
9
11
  agent.start();
@@ -1,7 +1,6 @@
1
- import { BaseMessage } from '@langchain/core/messages';
1
+ import { AIMessage, BaseMessage } from '@langchain/core/messages';
2
2
  import { GraphAgentOptions } from '../types/type';
3
3
  import { AgentGraphModel } from '../model/AgentGraphModel';
4
- export declare const CONFIG_FILE: string;
5
4
  interface TokenUsage {
6
5
  total: number;
7
6
  }
@@ -28,7 +27,18 @@ export default class McpGraphAgent<T extends AgentGraphModel = any> {
28
27
  private maxTargetCount;
29
28
  private maxTokens;
30
29
  private mcpClients;
30
+ private streamEnabled;
31
+ private streamOutputCallback;
31
32
  constructor(options?: GraphAgentOptions<T>);
33
+ /**
34
+ * 设置外部流式输出回调(如 VS Code Webview)。
35
+ * 设置后,callModel 的流式输出将通过回调发送而非写入 stdout。
36
+ */
37
+ setStreamOutput(callback: (chunk: string, type: 'think' | 'text') => void): void;
38
+ /**
39
+ * 清除外部流式输出回调,恢复默认的 stdout 输出。
40
+ */
41
+ clearStreamOutput(): void;
32
42
  private printLoadedTools;
33
43
  private loadMcpConfigs;
34
44
  private initMcpTools;
@@ -41,10 +51,15 @@ export default class McpGraphAgent<T extends AgentGraphModel = any> {
41
51
  getModel(): Promise<T>;
42
52
  private askForConfig;
43
53
  chat(query?: string): Promise<void>;
54
+ /**
55
+ * 流式执行单次查询(编程式 API)。
56
+ * 无论 options.stream 是否开启,此方法始终以流式方式输出。
57
+ */
58
+ stream(query?: string): Promise<void>;
44
59
  start(): Promise<void>;
45
60
  private renderOutput;
46
61
  callModel(state: typeof AgentState.State): Promise<{
47
- messages: import("@langchain/core/messages").AIMessageChunk[];
62
+ messages: AIMessage[];
48
63
  tokenUsage: {
49
64
  total: number;
50
65
  };
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CONFIG_FILE = void 0;
7
6
  const events_1 = require("events");
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const os_1 = __importDefault(require("os"));
@@ -20,7 +19,7 @@ const builtin_1 = require("../tools/builtin");
20
19
  const convertToLangChainTool_1 = require("../utils/convertToLangChainTool");
21
20
  const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
22
21
  const formatSchema_1 = require("../utils/formatSchema");
23
- exports.CONFIG_FILE = path_1.default.join(os_1.default.homedir(), ".saber2pr-agent.json");
22
+ const config_1 = require("../config/config");
24
23
  // ✅ 全局设置:修复 AbortSignal 监听器数量警告
25
24
  // LangChain 的 HTTP 客户端会创建多个 AbortSignal,需要增加默认限制
26
25
  events_1.EventEmitter.defaultMaxListeners = 100;
@@ -40,7 +39,7 @@ const AgentState = langgraph_1.Annotation.Root({
40
39
  }),
41
40
  mode: (0, langgraph_1.Annotation)({
42
41
  reducer: (x, y) => y !== null && y !== void 0 ? y : x,
43
- default: () => "chat",
42
+ default: () => 'chat',
44
43
  }),
45
44
  // ✅ Token 累加器
46
45
  tokenUsage: (0, langgraph_1.Annotation)({
@@ -61,6 +60,7 @@ class McpGraphAgent {
61
60
  this.langchainTools = [];
62
61
  this.stopLoadingFunc = null;
63
62
  this.mcpClients = [];
63
+ this.streamOutputCallback = null;
64
64
  this.options = options;
65
65
  this.verbose = options.verbose || false;
66
66
  this.alwaysSystem = options.alwaysSystem || true;
@@ -69,6 +69,7 @@ class McpGraphAgent {
69
69
  this.apiConfig = options.apiConfig;
70
70
  this.maxTargetCount = options.maxTargetCount || 4;
71
71
  this.maxTokens = options.maxTokens || 8000;
72
+ this.streamEnabled = options.stream || false;
72
73
  process.setMaxListeners(100);
73
74
  // ✅ 修复 AbortSignal 监听器数量警告
74
75
  // LangChain 的 HTTP 客户端会创建多个 AbortSignal,需要增加默认限制
@@ -80,16 +81,29 @@ class McpGraphAgent {
80
81
  process.stdout.write('\u001B[?25h');
81
82
  process.exit(0);
82
83
  };
83
- process.on("SIGINT", cleanup);
84
- process.on("SIGTERM", cleanup);
84
+ process.on('SIGINT', cleanup);
85
+ process.on('SIGTERM', cleanup);
86
+ }
87
+ /**
88
+ * 设置外部流式输出回调(如 VS Code Webview)。
89
+ * 设置后,callModel 的流式输出将通过回调发送而非写入 stdout。
90
+ */
91
+ setStreamOutput(callback) {
92
+ this.streamOutputCallback = callback;
93
+ }
94
+ /**
95
+ * 清除外部流式输出回调,恢复默认的 stdout 输出。
96
+ */
97
+ clearStreamOutput() {
98
+ this.streamOutputCallback = null;
85
99
  }
86
100
  printLoadedTools() {
87
- console.log("\n🛠️ [Graph] 正在加载工具节点...");
101
+ console.log('\n🛠️ [Graph] 正在加载工具节点...');
88
102
  this.langchainTools.forEach((tool) => {
89
103
  // 工具名称
90
104
  console.log(`\n🧰 工具名: ${tool.name}`);
91
105
  // 提取参数结构 (LangChain Tool 的 schema 是 Zod 对象)
92
- const schema = tool.schema;
106
+ const { schema } = tool;
93
107
  if (schema && schema.shape) {
94
108
  // 如果是 ZodObject,打印其内部 key
95
109
  console.log(` 参数结构:\n${(0, formatSchema_1.formatSchema)(schema)}`);
@@ -108,12 +122,12 @@ class McpGraphAgent {
108
122
  loadMcpConfigs() {
109
123
  const combined = { mcpServers: {} };
110
124
  const paths = [
111
- path_1.default.join(os_1.default.homedir(), ".cursor", "mcp.json"),
112
- path_1.default.join(os_1.default.homedir(), ".vscode", "mcp.json"),
125
+ path_1.default.join(os_1.default.homedir(), '.cursor', 'mcp.json'),
126
+ path_1.default.join(os_1.default.homedir(), '.vscode', 'mcp.json'),
113
127
  ];
114
- paths.forEach((p) => {
128
+ paths.forEach(p => {
115
129
  if (fs_1.default.existsSync(p)) {
116
- const content = JSON.parse(fs_1.default.readFileSync(p, "utf-8"));
130
+ const content = JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
117
131
  Object.assign(combined.mcpServers, content.mcpServers);
118
132
  }
119
133
  });
@@ -130,13 +144,13 @@ class McpGraphAgent {
130
144
  args: config.args,
131
145
  env: { ...process.env, ...(config.env || {}) },
132
146
  });
133
- const client = new index_js_1.Client({ name: "mcp-graph-client", version: "1.0.0" }, { capabilities: {} });
147
+ const client = new index_js_1.Client({ name: 'mcp-graph-client', version: '1.0.0' }, { capabilities: {} });
134
148
  await client.connect(transport);
135
149
  this.mcpClients.push(client);
136
150
  const { tools } = await client.listTools();
137
- tools.forEach((tool) => {
151
+ tools.forEach(tool => {
138
152
  mcpToolInfos.push({
139
- type: "function",
153
+ type: 'function',
140
154
  function: {
141
155
  name: tool.name,
142
156
  description: tool.description,
@@ -163,12 +177,8 @@ class McpGraphAgent {
163
177
  const builtinToolInfos = (0, builtin_1.createDefaultBuiltinTools)({ options: this.options });
164
178
  const mcpToolInfos = await this.initMcpTools();
165
179
  // 合并内置、手动传入和 MCP 工具
166
- const allToolInfos = [
167
- ...builtinToolInfos,
168
- ...(this.options.tools || []),
169
- ...mcpToolInfos
170
- ];
171
- this.langchainTools = allToolInfos.map((t) => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
180
+ const allToolInfos = [...builtinToolInfos, ...(this.options.tools || []), ...mcpToolInfos];
181
+ this.langchainTools = allToolInfos.map(t => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
172
182
  this.toolNode = new prebuilt_1.ToolNode(this.langchainTools);
173
183
  }
174
184
  // ✅ 修改:初始化逻辑
@@ -228,6 +238,7 @@ class McpGraphAgent {
228
238
  modelName: config.model,
229
239
  temperature: 0,
230
240
  maxTokens: this.maxTokens,
241
+ streaming: this.streamEnabled,
231
242
  });
232
243
  }
233
244
  this.model = modelInstance.bindTools(this.langchainTools);
@@ -237,40 +248,75 @@ class McpGraphAgent {
237
248
  if (this.apiConfig)
238
249
  return this.apiConfig;
239
250
  let config = {};
240
- if (fs_1.default.existsSync(exports.CONFIG_FILE)) {
251
+ if (fs_1.default.existsSync(config_1.CONFIG_FILE)) {
241
252
  try {
242
- config = JSON.parse(fs_1.default.readFileSync(exports.CONFIG_FILE, "utf-8"));
253
+ config = JSON.parse(fs_1.default.readFileSync(config_1.CONFIG_FILE, 'utf-8'));
243
254
  }
244
255
  catch (e) { }
245
256
  }
246
257
  if (!config.baseURL || !config.apiKey) {
247
258
  const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
248
- const question = (q) => new Promise((res) => rl.question(q, res));
249
- config.baseURL = config.baseURL || await question(`? API Base URL: `);
250
- config.apiKey = config.apiKey || await question(`? API Key: `);
251
- config.model = config.model || await question(`? Model Name: `) || "gpt-4o";
252
- fs_1.default.writeFileSync(exports.CONFIG_FILE, JSON.stringify(config, null, 2));
259
+ const question = (q) => new Promise(res => rl.question(q, res));
260
+ config.baseURL = config.baseURL || (await question(`? API Base URL: `));
261
+ config.apiKey = config.apiKey || (await question(`? API Key: `));
262
+ config.model = config.model || (await question(`? Model Name: `)) || 'gpt-4o';
263
+ fs_1.default.writeFileSync(config_1.CONFIG_FILE, JSON.stringify(config, null, 2));
253
264
  rl.close();
254
265
  }
255
266
  return config;
256
267
  }
257
- async chat(query = "开始代码审计") {
268
+ async chat(query = '开始代码审计') {
269
+ try {
270
+ await this.ensureInitialized();
271
+ await this.getModel();
272
+ const app = await this.createGraph();
273
+ const graphStream = await app.stream({
274
+ messages: [new messages_1.HumanMessage(query)],
275
+ mode: 'auto',
276
+ targetCount: this.maxTargetCount,
277
+ }, {
278
+ configurable: { thread_id: 'auto_worker' },
279
+ recursionLimit: this.recursionLimit,
280
+ debug: this.verbose,
281
+ });
282
+ for await (const output of graphStream)
283
+ this.renderOutput(output, this.streamEnabled);
284
+ }
285
+ catch (error) {
286
+ console.error('\n❌ Chat 过程中发生错误:', error);
287
+ }
288
+ finally {
289
+ await this.closeMcpClients();
290
+ }
291
+ }
292
+ /**
293
+ * 流式执行单次查询(编程式 API)。
294
+ * 无论 options.stream 是否开启,此方法始终以流式方式输出。
295
+ */
296
+ async stream(query = '开始代码审计') {
297
+ const prevStream = this.streamEnabled;
298
+ this.streamEnabled = true;
258
299
  try {
259
300
  await this.ensureInitialized();
260
301
  await this.getModel();
261
302
  const app = await this.createGraph();
262
- const stream = await app.stream({
303
+ const graphStream = await app.stream({
263
304
  messages: [new messages_1.HumanMessage(query)],
264
- mode: "auto",
305
+ mode: 'auto',
265
306
  targetCount: this.maxTargetCount,
266
- }, { configurable: { thread_id: "auto_worker" }, recursionLimit: this.recursionLimit, debug: this.verbose, });
267
- for await (const output of stream)
268
- this.renderOutput(output);
307
+ }, {
308
+ configurable: { thread_id: 'stream_worker' },
309
+ recursionLimit: this.recursionLimit,
310
+ debug: this.verbose,
311
+ });
312
+ for await (const output of graphStream)
313
+ this.renderOutput(output, true);
269
314
  }
270
315
  catch (error) {
271
- console.error("\n❌ Chat 过程中发生错误:", error);
316
+ console.error('\n❌ Stream 过程中发生错误:', error);
272
317
  }
273
318
  finally {
319
+ this.streamEnabled = prevStream;
274
320
  await this.closeMcpClients();
275
321
  }
276
322
  }
@@ -279,27 +325,31 @@ class McpGraphAgent {
279
325
  await this.getModel();
280
326
  const app = await this.createGraph();
281
327
  const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
282
- rl.on("SIGINT", () => {
328
+ rl.on('SIGINT', () => {
283
329
  this.stopLoading();
284
330
  rl.close();
285
331
  process.stdout.write('\u001B[?25h');
286
332
  process.exit(0);
287
333
  });
288
334
  const ask = () => {
289
- rl.question("> ", async (input) => {
290
- if (input.toLowerCase() === "exit") {
335
+ rl.question('> ', async (input) => {
336
+ if (input.toLowerCase() === 'exit') {
291
337
  rl.close();
292
338
  return;
293
339
  }
294
- const stream = await app.stream({ messages: [new messages_1.HumanMessage(input)], mode: "chat" }, { configurable: { thread_id: "session" }, recursionLimit: this.recursionLimit, debug: this.verbose, });
295
- for await (const output of stream)
296
- this.renderOutput(output);
340
+ const graphStream = await app.stream({ messages: [new messages_1.HumanMessage(input)], mode: 'chat' }, {
341
+ configurable: { thread_id: 'session' },
342
+ recursionLimit: this.recursionLimit,
343
+ debug: this.verbose,
344
+ });
345
+ for await (const output of graphStream)
346
+ this.renderOutput(output, this.streamEnabled);
297
347
  ask();
298
348
  });
299
349
  };
300
350
  ask();
301
351
  }
302
- renderOutput(output) {
352
+ renderOutput(output, isStreaming = false) {
303
353
  var _a, _b, _c;
304
354
  this.stopLoading(); // 停止加载动画
305
355
  const agentNode = output.agent;
@@ -321,13 +371,9 @@ class McpGraphAgent {
321
371
  const toolCallId = msg.tool_call_id || msg.id;
322
372
  if (toolCallId) {
323
373
  const toolName = toolCallMap.get(toolCallId) || msg.name || 'unknown';
324
- const content = typeof msg.content === 'string'
325
- ? msg.content
326
- : JSON.stringify(msg.content);
374
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
327
375
  // 如果内容太长,截断显示
328
- const displayContent = content.length > 500
329
- ? content.substring(0, 500) + '...'
330
- : content;
376
+ const displayContent = content.length > 500 ? content.substring(0, 500) + '...' : content;
331
377
  console.log(`✅ [工具结果] ${toolName}: ${displayContent}`);
332
378
  }
333
379
  });
@@ -335,12 +381,13 @@ class McpGraphAgent {
335
381
  if (agentNode) {
336
382
  const msg = agentNode.messages[agentNode.messages.length - 1];
337
383
  // 1. 打印思考过程(如果有)
384
+ // 流式模式下思考内容可能已经随流输出,此处仅在非流式模式或有独立 reasoning 字段时打印
338
385
  const reasoning = (_b = msg.additional_kwargs) === null || _b === void 0 ? void 0 : _b.reasoning;
339
- if (reasoning) {
386
+ if (reasoning && !isStreaming) {
340
387
  console.log(`\n🧠 [思考]: ${reasoning}`);
341
388
  }
342
- // 2. 打印 AI 回复内容
343
- if (msg.content) {
389
+ // 2. 打印 AI 回复内容(流式模式下已在 callModel 中逐字输出,跳过)
390
+ if (msg.content && !isStreaming) {
344
391
  console.log(`🤖 [AI]: ${msg.content}`);
345
392
  }
346
393
  // ✅ 3. 实时打印当次统计信息
@@ -353,20 +400,20 @@ class McpGraphAgent {
353
400
  }
354
401
  // 4. 打印工具调用情况
355
402
  if ((_c = msg.tool_calls) === null || _c === void 0 ? void 0 : _c.length) {
356
- msg.tool_calls.forEach((call) => {
403
+ msg.tool_calls.forEach(call => {
357
404
  console.log(`🛠️ [调用工具]: ${call.name} 📦 参数: ${JSON.stringify(call.args)}`);
358
405
  });
359
406
  }
360
407
  }
361
408
  }
362
409
  async callModel(state) {
363
- const auditedListStr = state.auditedFiles.length > 0
364
- ? state.auditedFiles.map(f => `\n - ${f}`).join("")
365
- : "暂无";
410
+ const auditedListStr = state.auditedFiles.length > 0 ? state.auditedFiles.map(f => `\n - ${f}`).join('') : '暂无';
366
411
  const recentToolCalls = this.getRecentToolCalls(state.messages);
367
412
  const recentToolCallsStr = recentToolCalls.length > 0
368
- ? `\n\n⚠️ 最近调用的工具(避免重复调用相同工具和参数):\n${recentToolCalls.map(tc => ` - ${tc.name}(${JSON.stringify(tc.args)})`).join("\n")}`
369
- : "";
413
+ ? `\n\n⚠️ 最近调用的工具(避免重复调用相同工具和参数):\n${recentToolCalls
414
+ .map(tc => ` - ${tc.name}(${JSON.stringify(tc.args)})`)
415
+ .join('\n')}`
416
+ : '';
370
417
  // 1. 构建当前的系统提示词模板
371
418
  const systemPromptTemplate = `你是一个代码专家。工作目录:${this.targetDir}。
372
419
  当前模式:{mode}
@@ -391,28 +438,142 @@ class McpGraphAgent {
391
438
  // ✅ 检查 options 中的 alwaysSystem 参数 (默认为 true 或根据你的需求设置)
392
439
  // 如果不希望每次都携带(即只在首轮携带),则过滤掉历史消息里的 SystemMessage
393
440
  if (this.options.alwaysSystem === false) {
394
- inputMessages = state.messages.filter(msg => msg._getType() !== "system");
441
+ inputMessages = state.messages.filter(msg => msg._getType() !== 'system');
395
442
  }
396
443
  else {
397
444
  // 默认模式:保持干净,由 PromptTemplate 重新生成最新的 System 状态
398
- inputMessages = state.messages.filter(msg => msg._getType() !== "system");
445
+ inputMessages = state.messages.filter(msg => msg._getType() !== 'system');
399
446
  }
400
447
  const prompt = prompts_1.ChatPromptTemplate.fromMessages([
401
- ["system", systemPromptTemplate],
402
- new prompts_1.MessagesPlaceholder("messages"),
448
+ ['system', systemPromptTemplate],
449
+ new prompts_1.MessagesPlaceholder('messages'),
403
450
  ]);
404
- this.startLoading("AI 正在分析并思考中");
451
+ this.startLoading('AI 正在分析并思考中');
405
452
  try {
406
- const chain = prompt.pipe(this.model);
407
- const response = await chain.invoke({
408
- messages: inputMessages, // ✅ 使用处理后的消息列表
453
+ const promptParams = {
454
+ messages: inputMessages,
409
455
  mode: state.mode,
410
456
  targetCount: state.targetCount,
411
457
  doneCount: state.auditedFiles.length,
412
458
  auditedList: auditedListStr,
413
459
  recentToolCalls: recentToolCallsStr,
414
- extraPrompt: this.options.extraSystemPrompt || "",
415
- });
460
+ extraPrompt: this.options.extraSystemPrompt || '',
461
+ };
462
+ if (this.streamEnabled) {
463
+ // ✅ 流式模式:通过 AgentGraphModel.streamGenerate 进行流式输出
464
+ const formattedMessages = await prompt.formatMessages(promptParams);
465
+ this.stopLoading();
466
+ // --- 流式 <think> 标签实时过滤 + 流式打印思考内容 ---
467
+ const THINK_OPEN = '<think>';
468
+ const THINK_CLOSE = '</think>';
469
+ let inThink = false;
470
+ let textBuffer = '';
471
+ let aiHeaderPrinted = false;
472
+ let thinkHeaderPrinted = false;
473
+ const hasExternalHandler = !!this.streamOutputCallback;
474
+ const flushText = (text) => {
475
+ if (!text)
476
+ return;
477
+ if (hasExternalHandler) {
478
+ this.streamOutputCallback(text, 'text');
479
+ }
480
+ else {
481
+ if (!aiHeaderPrinted) {
482
+ process.stdout.write('🤖 [AI]: ');
483
+ aiHeaderPrinted = true;
484
+ }
485
+ process.stdout.write(text);
486
+ }
487
+ };
488
+ const flushThink = (text) => {
489
+ if (!text)
490
+ return;
491
+ if (hasExternalHandler) {
492
+ this.streamOutputCallback(text, 'think');
493
+ }
494
+ else {
495
+ if (!thinkHeaderPrinted) {
496
+ process.stdout.write('\x1b[2m🧠 [思考]: ');
497
+ thinkHeaderPrinted = true;
498
+ }
499
+ process.stdout.write(text);
500
+ }
501
+ };
502
+ const onChunk = (chunk) => {
503
+ textBuffer += chunk;
504
+ let processing = true;
505
+ while (processing) {
506
+ if (inThink) {
507
+ // 在 <think> 块内,寻找 </think>
508
+ const closeIdx = textBuffer.indexOf(THINK_CLOSE);
509
+ if (closeIdx !== -1) {
510
+ // 流式打印 </think> 之前的思考内容
511
+ flushThink(textBuffer.slice(0, closeIdx));
512
+ textBuffer = textBuffer.slice(closeIdx + THINK_CLOSE.length);
513
+ inThink = false;
514
+ // 思考块结束:stdout 模式下换行 + 重置样式
515
+ if (!hasExternalHandler && thinkHeaderPrinted) {
516
+ process.stdout.write('\x1b[0m\n');
517
+ thinkHeaderPrinted = false;
518
+ }
519
+ }
520
+ else {
521
+ // 未找到闭合标签,安全输出可确认部分(防止 </think> 跨 chunk 截断)
522
+ const safeLen = Math.max(0, textBuffer.length - (THINK_CLOSE.length - 1));
523
+ flushThink(textBuffer.slice(0, safeLen));
524
+ textBuffer = textBuffer.slice(safeLen);
525
+ processing = false;
526
+ }
527
+ }
528
+ else {
529
+ // 不在 <think> 块内,寻找 <think>
530
+ const openIdx = textBuffer.indexOf(THINK_OPEN);
531
+ if (openIdx !== -1) {
532
+ flushText(textBuffer.slice(0, openIdx));
533
+ textBuffer = textBuffer.slice(openIdx + THINK_OPEN.length);
534
+ inThink = true;
535
+ }
536
+ else {
537
+ // 未找到开启标签,安全输出可确认部分(防止 <think> 跨 chunk 截断)
538
+ const safeLen = Math.max(0, textBuffer.length - (THINK_OPEN.length - 1));
539
+ flushText(textBuffer.slice(0, safeLen));
540
+ textBuffer = textBuffer.slice(safeLen);
541
+ processing = false;
542
+ }
543
+ }
544
+ }
545
+ };
546
+ const result = await this.model.streamGenerate(formattedMessages, onChunk);
547
+ // 刷新残留缓冲
548
+ if (textBuffer) {
549
+ if (inThink) {
550
+ flushThink(textBuffer);
551
+ }
552
+ else {
553
+ flushText(textBuffer);
554
+ }
555
+ }
556
+ // 收尾:stdout 模式下关闭样式和换行
557
+ if (!hasExternalHandler) {
558
+ if (thinkHeaderPrinted) {
559
+ process.stdout.write('\x1b[0m\n');
560
+ }
561
+ if (aiHeaderPrinted)
562
+ process.stdout.write('\n');
563
+ }
564
+ const aiMsg = result.generations[0].message;
565
+ const meta = aiMsg.response_metadata || {};
566
+ const currentToken = Number(meta.token) || 0;
567
+ const currentDuration = Number(meta.duration) || 0;
568
+ return {
569
+ messages: [aiMsg],
570
+ tokenUsage: { total: currentToken },
571
+ totalDuration: currentDuration,
572
+ };
573
+ }
574
+ // 非流式模式(原有逻辑)
575
+ const chain = prompt.pipe(this.model);
576
+ const response = await chain.invoke(promptParams);
416
577
  this.stopLoading();
417
578
  const meta = response.response_metadata || {};
418
579
  const currentToken = Number(meta.token) || 0;
@@ -420,7 +581,7 @@ class McpGraphAgent {
420
581
  return {
421
582
  messages: [response],
422
583
  tokenUsage: { total: currentToken },
423
- totalDuration: currentDuration
584
+ totalDuration: currentDuration,
424
585
  };
425
586
  }
426
587
  catch (error) {
@@ -464,51 +625,51 @@ class McpGraphAgent {
464
625
  const totalTokens = ((_a = state.tokenUsage) === null || _a === void 0 ? void 0 : _a.total) || 0;
465
626
  const totalMs = state.totalDuration || 0;
466
627
  if (totalTokens > 0 || totalMs > 0) {
467
- console.log("\n" + "".repeat(50));
628
+ console.log('\n' + ''.repeat(50));
468
629
  console.log(`🏁 \x1b[32;1m[审计任务全量结算]\x1b[0m`);
469
630
  console.log(` - 累计消耗总额: \x1b[33m${totalTokens}\x1b[0m Tokens`);
470
631
  console.log(` - 累计执行耗时: \x1b[36m${(totalMs / 1000).toFixed(2)}\x1b[0m s`);
471
632
  console.log(` - 审计文件总数: ${state.auditedFiles.length} 个`);
472
- console.log("".repeat(50) + "\n");
633
+ console.log(''.repeat(50) + '\n');
473
634
  }
474
635
  }
475
636
  async createGraph() {
476
637
  const workflow = new langgraph_1.StateGraph(AgentState)
477
- .addNode("agent", (state) => this.callModel(state))
478
- .addNode("tools", this.toolNode)
479
- .addNode("progress", (state) => this.trackProgress(state))
480
- .addEdge(langgraph_1.START, "agent")
481
- .addConditionalEdges("agent", (state) => {
482
- const messages = state.messages;
638
+ .addNode('agent', state => this.callModel(state))
639
+ .addNode('tools', this.toolNode)
640
+ .addNode('progress', state => this.trackProgress(state))
641
+ .addEdge(langgraph_1.START, 'agent')
642
+ .addConditionalEdges('agent', state => {
643
+ const { messages } = state;
483
644
  const lastMsg = messages[messages.length - 1];
484
- const content = lastMsg.content || "";
645
+ const content = lastMsg.content || '';
485
646
  // 🛑 新增:全局 Token 熔断保护
486
647
  // 如果已消耗 Token 超过了 options 中设置的 maxTokens (假设是总限额)
487
648
  if (this.options.maxTokens && state.tokenUsage.total >= this.options.maxTokens) {
488
- console.warn("⚠️ [警告] 已达到最大 Token 限制,强制结束任务。");
649
+ console.warn('⚠️ [警告] 已达到最大 Token 限制,强制结束任务。');
489
650
  this.printFinalSummary(state);
490
651
  return langgraph_1.END;
491
652
  }
492
653
  // 1. 如果 AI 想要调用工具,去 tools 节点
493
654
  if (lastMsg.tool_calls && lastMsg.tool_calls.length > 0) {
494
- return "tools";
655
+ return 'tools';
495
656
  }
496
657
  // 2. 判定结束的条件:
497
658
  // - 模式是 auto 且审计完成
498
659
  // - 或者 AI 明确输出了结束语
499
660
  // - 或者 AI 输出了普通内容且没有工具调用(针对问答模式)
500
- const isAutoFinished = state.mode === "auto" && state.auditedFiles.length > state.targetCount;
501
- const isFinalAnswer = content.includes("Final Answer");
661
+ const isAutoFinished = state.mode === 'auto' && state.auditedFiles.length > state.targetCount;
662
+ const isFinalAnswer = content.includes('Final Answer');
502
663
  // ✅ 修复核心:如果 AI 只是在聊天(没有工具调用),直接结束,不要跳回 agent
503
- if (isAutoFinished || isFinalAnswer || state.mode === "chat") {
664
+ if (isAutoFinished || isFinalAnswer || state.mode === 'chat') {
504
665
  this.printFinalSummary(state);
505
666
  return langgraph_1.END;
506
667
  }
507
668
  // 兜底:如果是在 auto 模式且还没干完活,才跳回 agent(通常不会走到这里)
508
669
  return langgraph_1.END;
509
670
  })
510
- .addEdge("tools", "progress")
511
- .addEdge("progress", "agent");
671
+ .addEdge('tools', 'progress')
672
+ .addEdge('progress', 'agent');
512
673
  return workflow.compile({ checkpointer: this.checkpointer });
513
674
  }
514
675
  }
@@ -7,13 +7,29 @@ export interface AgentGraphLLMResponse {
7
7
  token?: number;
8
8
  duration?: number;
9
9
  }
10
+ /** 流式输出的回调类型 */
11
+ export type StreamChunkCallback = (chunk: string) => void;
10
12
  export declare abstract class AgentGraphModel extends BaseChatModel {
11
13
  protected boundTools?: any[];
12
14
  constructor(fields?: BaseChatModelParams);
13
15
  bindTools(tools: any[]): any;
14
16
  abstract callApi(prompt: string): Promise<AgentGraphLLMResponse>;
17
+ /**
18
+ * 流式调用 API,子类可覆盖以实现真正的 SSE 流式传输。
19
+ * 默认回退到 callApi 非流式调用。
20
+ * @param prompt 序列化后的提示词
21
+ * @param onChunk 每收到一段文本时的回调
22
+ * @returns 完整的响应结果
23
+ */
24
+ callApiStream(prompt: string, onChunk: StreamChunkCallback): Promise<AgentGraphLLMResponse>;
15
25
  _generate(messages: BaseMessage[]): Promise<ChatResult>;
16
26
  private parseToolCalls;
17
- private serializeMessages;
27
+ /**
28
+ * 流式生成:调用 callApiStream 进行流式输出,完成后构建完整的 ChatResult。
29
+ * @param messages LangChain 消息列表
30
+ * @param onChunk 每收到一段文本时的回调
31
+ */
32
+ streamGenerate(messages: BaseMessage[], onChunk: StreamChunkCallback): Promise<ChatResult>;
33
+ serializeMessages(messages: BaseMessage[]): string;
18
34
  _llmType(): string;
19
35
  }
@@ -13,6 +13,18 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
13
13
  this.boundTools = tools.map(t => (0, function_calling_1.convertToOpenAITool)(t));
14
14
  return this;
15
15
  }
16
+ /**
17
+ * 流式调用 API,子类可覆盖以实现真正的 SSE 流式传输。
18
+ * 默认回退到 callApi 非流式调用。
19
+ * @param prompt 序列化后的提示词
20
+ * @param onChunk 每收到一段文本时的回调
21
+ * @returns 完整的响应结果
22
+ */
23
+ async callApiStream(prompt, onChunk) {
24
+ const result = await this.callApi(prompt);
25
+ onChunk(result.text);
26
+ return result;
27
+ }
16
28
  async _generate(messages) {
17
29
  const fullPrompt = this.serializeMessages(messages);
18
30
  const response = await this.callApi(fullPrompt);
@@ -92,6 +104,47 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
92
104
  },
93
105
  ];
94
106
  }
107
+ /**
108
+ * 流式生成:调用 callApiStream 进行流式输出,完成后构建完整的 ChatResult。
109
+ * @param messages LangChain 消息列表
110
+ * @param onChunk 每收到一段文本时的回调
111
+ */
112
+ async streamGenerate(messages, onChunk) {
113
+ const fullPrompt = this.serializeMessages(messages);
114
+ const response = await this.callApiStream(fullPrompt, onChunk);
115
+ let { text, reasoning } = response;
116
+ // 1. 处理思考内容
117
+ if (!reasoning && text.includes('<think>')) {
118
+ const match = text.match(/<think>([\s\S]*?)<\/think>/);
119
+ if (match) {
120
+ reasoning = match[1].trim();
121
+ text = text.replace(/<think>[\s\S]*?<\/think>/, '').trim();
122
+ }
123
+ }
124
+ // 2. 解析工具调用
125
+ const toolCalls = this.parseToolCalls(text);
126
+ return {
127
+ generations: [
128
+ {
129
+ text,
130
+ message: new messages_1.AIMessage({
131
+ content: text,
132
+ tool_calls: toolCalls,
133
+ additional_kwargs: {
134
+ reasoning: reasoning || '',
135
+ token: response.token,
136
+ duration: response.duration,
137
+ },
138
+ response_metadata: {
139
+ reasoning: reasoning || '',
140
+ token: response.token,
141
+ duration: response.duration,
142
+ },
143
+ }),
144
+ },
145
+ ],
146
+ };
147
+ }
95
148
  serializeMessages(messages) {
96
149
  const systemMsg = messages.find(m => m._getType() === 'system');
97
150
  const lastMsg = messages[messages.length - 1];
@@ -39,4 +39,6 @@ export interface GraphAgentOptions<T extends AgentGraphModel = any> extends Agen
39
39
  alwaysSystem?: boolean;
40
40
  recursionLimit?: number;
41
41
  maxTargetCount?: number;
42
+ /** 是否启用流式输出,默认 false */
43
+ stream?: boolean;
42
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber2pr/ai-agent",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "description": "AI Assistant CLI.",
5
5
  "author": "saber2pr",
6
6
  "license": "ISC",