@saber2pr/ai-agent 0.0.14 → 0.0.15

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.
@@ -1,7 +1,6 @@
1
1
  import { AgentOptions } from '../types/type';
2
2
  export default class McpChainAgent {
3
3
  private allTools;
4
- private messages;
5
4
  private encoder;
6
5
  private extraTools;
7
6
  private maxTokens;
@@ -9,17 +8,12 @@ export default class McpChainAgent {
9
8
  private apiConfig;
10
9
  private maxIterations;
11
10
  private apiModel?;
12
- private targetDir;
13
- private verbose;
11
+ private memory?;
12
+ private systemPrompt;
14
13
  private runningTokenCounter;
15
14
  constructor(options?: AgentOptions);
16
- /**
17
- * 工具处理器包装逻辑:增加日志打印和 Token 监控
18
- */
19
- private wrapHandler;
20
15
  private initTools;
21
- private calculateTokens;
22
- private pruneMessages;
16
+ private wrapHandler;
23
17
  init(): Promise<void>;
24
18
  chat(input: string): Promise<string>;
25
19
  private showLoading;
@@ -39,149 +39,106 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const fs_1 = __importDefault(require("fs"));
40
40
  const js_tiktoken_1 = require("js-tiktoken");
41
41
  const agents_1 = require("langchain/agents");
42
- const readline = __importStar(require("readline"));
43
42
  const prompts_1 = require("@langchain/core/prompts");
44
43
  const tools_1 = require("@langchain/core/tools");
45
44
  const openai_1 = require("@langchain/openai");
45
+ const memory_1 = require("langchain/memory");
46
+ const readline = __importStar(require("readline"));
46
47
  const config_1 = require("../config/config");
47
48
  const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
48
49
  const builtin_1 = require("../tools/builtin");
49
50
  class McpChainAgent {
50
51
  constructor(options) {
51
52
  this.allTools = [];
52
- this.messages = [];
53
53
  this.encoder = (0, js_tiktoken_1.getEncoding)("cl100k_base");
54
54
  this.extraTools = [];
55
- this.runningTokenCounter = 0; // 新增:用于追踪当前 Task 的动态消耗
55
+ this.runningTokenCounter = 0;
56
56
  this.extraTools = (options === null || options === void 0 ? void 0 : options.tools) || [];
57
57
  this.maxTokens = (options === null || options === void 0 ? void 0 : options.maxTokens) || 100000;
58
58
  this.apiConfig = options === null || options === void 0 ? void 0 : options.apiConfig;
59
59
  this.maxIterations = (options === null || options === void 0 ? void 0 : options.maxIterations) || 20;
60
60
  this.apiModel = options === null || options === void 0 ? void 0 : options.apiModel;
61
- this.targetDir = (options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd();
62
- this.verbose = (options === null || options === void 0 ? void 0 : options.verbose) || false;
63
- const baseSystemPrompt = `你是一个专业的 AI 代码架构师,具备深度的源码分析与工程化处理能力。
64
-
65
- ### 核心操作规范:
66
- 1. **全局扫描(强制首选)**:在开始任何分析任务前,你【必须】首先调用 'get_repo_map'。这是理解项目结构、技术栈及模块关系的唯一来源。
67
- 2. **循序渐进的分析路径**:
68
- - 优先使用 'read_skeleton' 提取接口和函数签名。
69
- - 仅在需要分析具体逻辑或准备修复代码时,才使用 'read_text_file'
70
- 3. **真实性原则**:所有的代码分析必须基于工具返回的真实内容,严禁虚假猜测。`;
71
- this.messages.push({
72
- role: "system",
73
- content: (options === null || options === void 0 ? void 0 : options.extraSystemPrompt)
74
- ? `${baseSystemPrompt}\n\n[额外指令]:\n${JSON.stringify(options.extraSystemPrompt)}`
75
- : baseSystemPrompt,
76
- });
61
+ const baseSystemPrompt = `你是一个专业的代码架构师。
62
+ 你的目标是理解并分析用户项目,请务必遵循以下工作流:
63
+
64
+ ### 第一阶段:全景感知 (The "Where" Phase)
65
+ 1. **必须首先调用 'get_directory_tree'**:获取项目完整文件列表,包括样式文件 (.less, .css) 和资源文件。
66
+ 2. 结合目录结构,观察项目架构(如 Monorepo 结构或 src 布局)。
67
+
68
+ ### 第二阶段:逻辑映射 (The "What" Phase)
69
+ 1. **调用 'get_repo_map'**:针对代码文件提取导出定义,理解模块间的调用关系。
70
+ 2. 如果需要查看具体的样式定义,直接使用 'read_text_file' 读取 .less 或 .css 文件。
71
+
72
+ ### 核心原则:
73
+ - 不要猜测文件是否存在,先看目录树。
74
+ - 优先查看 Skeleton(骨架),只有需要修复逻辑时才读取完整 Text(全文)。
75
+ - 始终以中文回答思考过程。`;
76
+ this.systemPrompt = (options === null || options === void 0 ? void 0 : options.extraSystemPrompt)
77
+ ? `${baseSystemPrompt}\n\n[额外指令]:\n${JSON.stringify(options.extraSystemPrompt)}`
78
+ : baseSystemPrompt;
77
79
  this.initTools(options);
78
80
  }
79
- /**
80
- * 工具处理器包装逻辑:增加日志打印和 Token 监控
81
- */
81
+ initTools(options) {
82
+ const allTools = [...(0, builtin_1.createDefaultBuiltinTools)({ options: { ...options, ...this } }), ...this.extraTools];
83
+ this.allTools = allTools.map((t) => ({
84
+ ...t,
85
+ _handler: this.wrapHandler(t.function.name, t._handler),
86
+ }));
87
+ }
82
88
  wrapHandler(name, handler) {
83
89
  return async (args) => {
84
- // 1. 打印工具执行日志
85
90
  console.log(`\n [工具调用]: ${name}`);
86
- if (args === null || args === void 0 ? void 0 : args.filePath) {
87
- console.log(` [目标文件]: ${args.filePath}`);
88
- }
89
- // 2. 执行逻辑
90
91
  const result = await handler(args);
91
92
  const content = typeof result === "string" ? result : JSON.stringify(result);
92
- // 3. 统计 Token 消耗
93
- const outputTokens = this.encoder.encode(content).length;
94
- console.log(` [输出长度]: ${outputTokens} tokens`);
95
- // 2. 更新动态计数器:
96
- // 这里的逻辑是:当前基础 Context + 本次工具调用的输出
97
- // 随着迭代增加,这个值会一直累加,直到任务结束存入 messages
98
- const baseTokens = this.calculateTokens();
99
- this.runningTokenCounter = baseTokens + outputTokens;
100
- // 3. 打印正确且递增的状态
101
- console.log(` 📊 状态: Context ${this.runningTokenCounter} / Limit ${this.maxTokens} tokens`);
93
+ this.runningTokenCounter += this.encoder.encode(content).length;
102
94
  return content;
103
95
  };
104
96
  }
105
- initTools(options) {
106
- const allTools = [
107
- // 注册内置工具
108
- ...(0, builtin_1.createDefaultBuiltinTools)({
109
- options: {
110
- ...options,
111
- ...this
112
- }
113
- }),
114
- ...this.extraTools
115
- ];
116
- if (allTools === null || allTools === void 0 ? void 0 : allTools.length) {
117
- this.allTools.push(...allTools.map((t) => ({
118
- type: t.type,
119
- function: t.function,
120
- _handler: this.wrapHandler(t.function.name, t._handler),
121
- })));
122
- }
123
- }
124
- calculateTokens() {
125
- return this.messages.reduce((acc, msg) => acc + this.encoder.encode(String(msg.content || "")).length, 0);
126
- }
127
- pruneMessages() {
128
- const current = this.calculateTokens();
129
- if (current > this.maxTokens) {
130
- console.log(`\n⚠️ 上下文达到限制 (${current} tokens),正在裁剪旧消息...`);
131
- // 保留 system prompt (index 0),移除后续消息
132
- while (this.calculateTokens() > this.maxTokens * 0.8 && this.messages.length > 2) {
133
- this.messages.splice(1, 1);
134
- }
135
- console.log(`✅ 裁剪完成,当前: ${this.calculateTokens()} tokens`);
136
- }
137
- }
138
97
  async init() {
139
98
  if (this.executor)
140
99
  return;
141
100
  let model;
142
101
  if (this.apiModel) {
143
- console.log("ℹ️ 使用自定义 API Model 实例");
144
102
  model = this.apiModel;
145
103
  }
146
104
  else {
147
- // 降级方案:使用配置创建默认的 ChatOpenAI
148
105
  const apiConfig = await this.ensureApiConfig();
149
- console.log(`ℹ️ 使用默认 ChatOpenAI (${apiConfig.model})`);
150
106
  model = new openai_1.ChatOpenAI({
151
107
  configuration: { baseURL: apiConfig.baseURL, apiKey: apiConfig.apiKey },
152
108
  modelName: apiConfig.model,
153
109
  temperature: 0,
154
- streaming: false
155
110
  });
156
111
  }
157
- const langchainTools = this.allTools.map(t => {
158
- return new tools_1.DynamicStructuredTool({
159
- name: t.function.name,
160
- description: t.function.description || "",
161
- // 定义 schema 告诉 LangChain 这是一个对象输入
162
- // passthrough() 允许接收未在 schema 中显式定义的其他字段
163
- schema: (0, jsonSchemaToZod_1.jsonSchemaToZod)(t.function.parameters),
164
- func: async (args) => {
165
- // 这里的 args 已经被 LangChain 自动解析为对象
166
- return await t._handler(args);
167
- },
168
- });
112
+ // 1. 初始化 SummaryBufferMemory
113
+ // maxTokenLimit 决定了当对话历史超过多少 Token 时触发“自动总结”
114
+ this.memory = new memory_1.ConversationSummaryBufferMemory({
115
+ llm: model,
116
+ maxTokenLimit: 2000,
117
+ memoryKey: "chat_history",
118
+ returnMessages: true,
119
+ // 必须添加下面这两行显式声明:
120
+ inputKey: "input", // 对应 invoke 里的 input 字段
121
+ outputKey: "output", // 对应 Agent 输出的字段
169
122
  });
170
- // src/core/agent-chain.ts 中的 prompt 修改
171
- const prompt = prompts_1.PromptTemplate.fromTemplate(`
172
- {system_prompt}
123
+ const langchainTools = this.allTools.map(t => new tools_1.DynamicStructuredTool({
124
+ name: t.function.name,
125
+ description: t.function.description || "",
126
+ schema: (0, jsonSchemaToZod_1.jsonSchemaToZod)(t.function.parameters),
127
+ func: async (args) => await t._handler(args),
128
+ }));
129
+ // 2. 构造支持 Memory 的 Prompt
130
+ const prompt = prompts_1.PromptTemplate.fromTemplate(`{system_prompt}
131
+
132
+ ### 历史记录摘要及近期对话:
133
+ {chat_history}
173
134
 
174
- ### 🛠 可用工具列表 (TOOLS)
175
- --------------------
135
+ ### 可用工具:
176
136
  {tools}
177
137
 
178
138
  工具名称列表: [{tool_names}]
179
139
 
180
- ### 📝 交互协议格式 (PROTOCOL)
181
- --------------------
182
- 你必须严格遵守以下回复格式:
183
-
184
- Thought: 首先,我会[此处用中文简述你的分析思路和下一步目标]。
140
+ ### 交互协议:
141
+ Thought: [你的中文分析思路]
185
142
  \`\`\`json
186
143
  {{
187
144
  "action": "工具名称",
@@ -189,10 +146,6 @@ Thought: 首先,我会[此处用中文简述你的分析思路和下一步目
189
146
  }}
190
147
  \`\`\`
191
148
 
192
- 注意:
193
- - 严禁直接输出 JSON,必须先写 Thought。
194
- - Thought 必须包含具体的分析意图,不少于 10 个字。
195
-
196
149
  Begin!
197
150
  Question: {input}
198
151
  Thought: {agent_scratchpad}`);
@@ -200,61 +153,35 @@ Thought: {agent_scratchpad}`);
200
153
  this.executor = new agents_1.AgentExecutor({
201
154
  agent,
202
155
  tools: langchainTools,
203
- verbose: this.verbose, // 我们已经有了 wrapHandler 日志,关闭原生 verbose 以保持整洁
156
+ memory: this.memory, // 挂载内存模块
157
+ verbose: false,
204
158
  handleParsingErrors: true,
205
159
  maxIterations: this.maxIterations
206
160
  });
207
161
  }
208
162
  async chat(input) {
209
- var _a;
210
163
  if (!this.executor)
211
164
  await this.init();
212
- this.messages.push({ role: "user", content: input });
213
- this.pruneMessages();
214
- console.log(`\n📊 状态: Context ${this.calculateTokens()} / Limit ${this.maxTokens} tokens`);
215
- const stopLoading = this.showLoading("🤖 Agent 正在思考并执行工具...");
165
+ this.runningTokenCounter = this.encoder.encode(input).length;
166
+ const stopLoading = this.showLoading("🤖 Agent 正在思考并管理上下文...");
216
167
  try {
168
+ // 执行请求,AgentExecutor 会自动:
169
+ // 1. 从 memory 加载历史 (chat_history)
170
+ // 2. 将这次对话的结果 saveContext 到 memory
217
171
  const response = await this.executor.invoke({
218
172
  input: input,
219
- system_prompt: this.messages[0].content,
173
+ system_prompt: this.systemPrompt,
220
174
  }, {
221
- // --- 新增:使用回调函数捕获 Thought ---
222
175
  callbacks: [{
223
- handleAgentAction: (action, runId) => {
224
- if (action.log) {
225
- const log = action.log.trim();
226
- // 1. 提取 Thought 部分:取 Thought: 之后,直到遇到 ```json 或 { 之前的内容
227
- const thoughtMatch = log.match(/Thought:\s*([\s\S]*?)(?=(?:```json|\{|Action:|$))/i);
228
- let thought = "";
229
- if (thoughtMatch && thoughtMatch[1]) {
230
- thought = thoughtMatch[1].trim();
231
- }
232
- else {
233
- // 备选方案:如果没有 Thought 标签,直接截取 JSON 之前的文本
234
- thought = log.split(/```json|\{/)[0].replace(/Thought:/i, "").trim();
235
- }
236
- // 2. 只有当 thought 真的有文字内容(且不是 JSON)时才打印
237
- if (thought && thought.length > 0 && !thought.startsWith('{')) {
238
- // 进一步清洗:如果 thought 包含多行,只取非空的第一行,避免打印太长
239
- const displayThought = thought.split('\n').find(line => line.trim().length > 0);
240
- if (displayThought) {
241
- console.log(`\n💭 [思考]: ${displayThought}`);
242
- }
243
- }
176
+ handleAgentAction: (action) => {
177
+ const thought = action.log.split(/```json|\{/)[0].replace(/Thought:/i, "").trim();
178
+ if (thought && !thought.startsWith('{')) {
179
+ console.log(`\n💭 [思考]: ${thought.split('\n')[0]}`);
244
180
  }
245
181
  }
246
182
  }]
247
183
  });
248
- // 修复点:确保 output 是字符串
249
- let output = typeof response.output === 'string'
250
- ? response.output
251
- : JSON.stringify(response.output);
252
- // 清洗 ReAct 冗余标签
253
- if (output.includes("Final Answer:")) {
254
- output = ((_a = output.split("Final Answer:").pop()) === null || _a === void 0 ? void 0 : _a.trim()) || output;
255
- }
256
- this.messages.push({ role: "assistant", content: output });
257
- return output;
184
+ return typeof response.output === 'string' ? response.output : JSON.stringify(response.output);
258
185
  }
259
186
  finally {
260
187
  stopLoading();
@@ -267,16 +194,12 @@ Thought: {agent_scratchpad}`);
267
194
  process.stdout.write(`\r${chars[i]} ${text}`);
268
195
  i = (i + 1) % chars.length;
269
196
  }, 80);
270
- return () => {
271
- clearInterval(timer);
272
- process.stdout.write('\r\x1b[K');
273
- };
197
+ return () => { clearInterval(timer); process.stdout.write('\r\x1b[K'); };
274
198
  }
275
199
  async start() {
276
200
  await this.init();
277
201
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
278
- console.log(`\n🚀 AI 助手启动 (LangChain 核心)`);
279
- console.log(`📂 目标目录: ${this.targetDir}`);
202
+ console.log(`\n🚀 AI 助手 (Summary+Window 模式) 已启动`);
280
203
  const chatLoop = () => {
281
204
  rl.question("\n👤 你: ", async (input) => {
282
205
  if (!input.trim())
@@ -284,8 +207,8 @@ Thought: {agent_scratchpad}`);
284
207
  if (input.toLowerCase() === "exit")
285
208
  process.exit(0);
286
209
  try {
287
- const result = await this.chat(input);
288
- console.log(`\n🤖 Agent: ${result}`);
210
+ const res = await this.chat(input);
211
+ console.log(`\n🤖 Agent: ${res}`);
289
212
  }
290
213
  catch (err) {
291
214
  console.error("\n❌ 系统错误:", err.message);
@@ -298,20 +221,11 @@ Thought: {agent_scratchpad}`);
298
221
  async ensureApiConfig() {
299
222
  if (this.apiConfig)
300
223
  return this.apiConfig;
301
- if (fs_1.default.existsSync(config_1.CONFIG_FILE)) {
224
+ if (fs_1.default.existsSync(config_1.CONFIG_FILE))
302
225
  return JSON.parse(fs_1.default.readFileSync(config_1.CONFIG_FILE, "utf-8"));
303
- }
304
- const rl = readline.createInterface({
305
- input: process.stdin,
306
- output: process.stdout,
307
- });
226
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
308
227
  const question = (q) => new Promise((res) => rl.question(q, res));
309
- console.log("\n🔑 配置 API 凭据:");
310
- const config = {
311
- baseURL: await question("? API Base URL (如 https://api.openai.com/v1): "),
312
- apiKey: await question("? API Key: "),
313
- model: await question("? Model Name (如 gpt-4o): "),
314
- };
228
+ const config = { baseURL: await question("? API Base URL: "), apiKey: await question("? API Key: "), model: await question("? Model Name: ") };
315
229
  fs_1.default.writeFileSync(config_1.CONFIG_FILE, JSON.stringify(config, null, 2));
316
230
  rl.close();
317
231
  return config;
package/lib/core/agent.js CHANGED
@@ -57,24 +57,21 @@ class McpAgent {
57
57
  this.extraTools = (options === null || options === void 0 ? void 0 : options.tools) || []; // 接收外部传入的工具
58
58
  this.maxTokens = (options === null || options === void 0 ? void 0 : options.maxTokens) || 100000; // 默认 100k
59
59
  this.apiConfig = options === null || options === void 0 ? void 0 : options.apiConfig;
60
- let baseSystemPrompt = `你是一个专业的 AI 代码架构师,具备深度的源码分析与工程化处理能力。
60
+ let baseSystemPrompt = `你是一个专业的代码架构师。
61
+ 你的目标是理解并分析用户项目,请务必遵循以下工作流:
61
62
 
62
- ### 核心操作规范:
63
- 1. **全局扫描(强制首选)**:在开始任何分析任务前,你【必须】首先调用 'get_repo_map'。这是你理解项目目录结构、技术栈及模块关系的唯一权威来源。
64
- 2. **循序渐进的分析路径**:
65
- - 优先使用 'read_skeleton' 提取接口、类和函数签名,以最低的 Token 成本建立代码逻辑视图。
66
- - 仅在需要深入分析具体业务逻辑、提取精准代码块或进行代码修改建议时,才使用 'read_text_file' 'get_method_body'。
67
- 3. **真实性原则**:
68
- - 所有的代码分析、行号定位和逻辑推断必须基于工具返回的真实内容,严禁基于文件名进行虚假猜测。
69
- - 如果工具返回结果为空或报错,应尝试调整路径或更换工具。
70
-
71
- ### 技术能力:
72
- - 精通 TypeScript/JavaScript 及其 AST 结构,能准确识别各种复杂的声明与调用关系。
73
- - 能够理解代码间的依赖链路,并结合项目上下文给出合理的架构建议。
74
-
75
- ### 执行准则:
76
- - **任务导向**:直接通过工具链解决问题,减少不必要的中间对话。
77
- - **自主决策**:根据任务需求自主选择最合适的工具组合,无需每一步都向用户请示。`;
63
+ ### 第一阶段:全景感知 (The "Where" Phase)
64
+ 1. **必须首先调用 'get_directory_tree'**:获取项目完整文件列表,包括样式文件 (.less, .css) 和资源文件。
65
+ 2. 结合目录结构,观察项目架构(如 Monorepo 结构或 src 布局)。
66
+
67
+ ### 第二阶段:逻辑映射 (The "What" Phase)
68
+ 1. **调用 'get_repo_map'**:针对代码文件提取导出定义,理解模块间的调用关系。
69
+ 2. 如果需要查看具体的样式定义,直接使用 'read_text_file' 读取 .less 或 .css 文件。
70
+
71
+ ### 核心原则:
72
+ - 不要猜测文件是否存在,先看目录树。
73
+ - 优先查看 Skeleton(骨架),只有需要修复逻辑时才读取完整 Text(全文)。
74
+ - 始终以中文回答思考过程。`;
78
75
  // 2. 拼接额外指令
79
76
  if (options === null || options === void 0 ? void 0 : options.extraSystemPrompt) {
80
77
  const extra = typeof options.extraSystemPrompt === 'string'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber2pr/ai-agent",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "AI Assistant CLI.",
5
5
  "author": "saber2pr",
6
6
  "license": "ISC",
@@ -25,13 +25,13 @@
25
25
  "@langchain/core": "^1.1.18",
26
26
  "@langchain/openai": "^1.2.4",
27
27
  "@modelcontextprotocol/sdk": "^1.25.3",
28
- "@saber2pr/ts-context-mcp": "^0.0.6",
29
- "js-tiktoken": "^1.0.21",
30
- "langchain": "~0.3",
31
- "openai": "^6.16.0",
28
+ "@saber2pr/ts-context-mcp": "^0.0.8",
32
29
  "diff": "^8.0.3",
33
30
  "glob": "^10.5.0",
34
- "minimatch": "^10.0.1"
31
+ "js-tiktoken": "^1.0.21",
32
+ "langchain": "~0.3",
33
+ "minimatch": "^10.0.1",
34
+ "openai": "^6.16.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^16.3.3",