@saber2pr/ai-agent 0.0.53 → 0.0.55
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/adapters/llm.d.ts +18 -0
- package/lib/adapters/llm.js +179 -0
- package/lib/agent/createAgent.d.ts +13 -0
- package/lib/agent/createAgent.js +19 -0
- package/lib/{cli-graph.js → cli-openai.js} +2 -4
- package/lib/cli.js +101 -6
- package/lib/core/agent-graph.js +32 -25
- package/lib/core/agent.js +19 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -1
- package/lib/model/AgentGraphModel.js +6 -5
- package/lib/tools/builtin.js +2 -2
- package/lib/tools/filesystem/index.js +81 -47
- package/lib/tools/filesystem/lib.js +3 -5
- package/lib/tools/ts-lsp/index.js +14 -13
- package/lib/utils/convertToLangChainTool.js +2 -2
- package/lib/utils/createTool.js +1 -1
- package/lib/utils/formatSchema.js +4 -4
- package/lib/utils/generateToolMarkdown.d.ts +5 -0
- package/lib/utils/generateToolMarkdown.js +27 -0
- package/package.json +4 -4
- /package/lib/{cli-graph.d.ts → cli-openai.d.ts} +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AgentGraphLLMResponse, AgentGraphModel, StreamChunkCallback } from '../model/AgentGraphModel';
|
|
2
|
+
import { BaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import { CreateAgentOptions } from '../agent/createAgent';
|
|
4
|
+
export declare class LLMModel extends AgentGraphModel {
|
|
5
|
+
private chatId;
|
|
6
|
+
private options;
|
|
7
|
+
constructor(options: CreateAgentOptions);
|
|
8
|
+
resetChat(): void;
|
|
9
|
+
callApi(prompt: string): Promise<AgentGraphLLMResponse>;
|
|
10
|
+
/**
|
|
11
|
+
* 流式调用 API:发送 stream: true,自动适配多种响应格式(SSE / NDJSON / 普通 JSON)。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 流式调用 API:发送 stream: true
|
|
15
|
+
* 适配 SSE 格式,解析内容增量、思考过程以及最终的 Token 统计
|
|
16
|
+
*/
|
|
17
|
+
callApiStream(prompt: string, lastMsg: BaseMessage, onChunk: StreamChunkCallback): Promise<AgentGraphLLMResponse>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LLMModel = void 0;
|
|
4
|
+
const AgentGraphModel_1 = require("../model/AgentGraphModel");
|
|
5
|
+
class LLMModel extends AgentGraphModel_1.AgentGraphModel {
|
|
6
|
+
chatId;
|
|
7
|
+
options;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super();
|
|
10
|
+
this.chatId = '';
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
resetChat() {
|
|
14
|
+
this.chatId = '';
|
|
15
|
+
}
|
|
16
|
+
async callApi(prompt) {
|
|
17
|
+
const response = await fetch(this.options.apiUrl, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
query: prompt,
|
|
21
|
+
chatId: this.chatId,
|
|
22
|
+
stream: false,
|
|
23
|
+
}),
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`LLM API 响应异常: ${response.status} ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
this.chatId = data.chat_id || data.chatId;
|
|
34
|
+
return {
|
|
35
|
+
text: data.text || '',
|
|
36
|
+
reasoning: data.reason || data.thought || '', // 适配后端可能的思考字段名
|
|
37
|
+
token: data.token, // ✅ 确保这里取到了 API 返回的 token
|
|
38
|
+
duration: data.duration, // ✅ 确保这里取到了 API 返回的 duration
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 流式调用 API:发送 stream: true,自动适配多种响应格式(SSE / NDJSON / 普通 JSON)。
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* 流式调用 API:发送 stream: true
|
|
46
|
+
* 适配 SSE 格式,解析内容增量、思考过程以及最终的 Token 统计
|
|
47
|
+
*/
|
|
48
|
+
async callApiStream(prompt, lastMsg, onChunk) {
|
|
49
|
+
const files = lastMsg?.additional_kwargs?.files || [];
|
|
50
|
+
const response = await fetch(this.options.apiUrl, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
query: prompt,
|
|
54
|
+
chatId: this.chatId,
|
|
55
|
+
stream: true,
|
|
56
|
+
files
|
|
57
|
+
}),
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`LLM API 响应异常: ${response.status} ${response.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
const contentType = response.headers.get('content-type') || '';
|
|
67
|
+
// ✅ 情况1: 兜底处理非流式响应
|
|
68
|
+
if (contentType.includes('application/json')) {
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
this.chatId = data.chat_id || data.chatId;
|
|
71
|
+
const text = data.text || data.content || '';
|
|
72
|
+
if (text)
|
|
73
|
+
onChunk(text);
|
|
74
|
+
return {
|
|
75
|
+
text,
|
|
76
|
+
reasoning: data.reason || data.thought || data.reasoning_content || '',
|
|
77
|
+
token: data.total_tokens || data.token || 0,
|
|
78
|
+
duration: data.duration || 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// ✅ 情况2: 流式响应处理
|
|
82
|
+
const reader = response.body?.getReader();
|
|
83
|
+
if (!reader) {
|
|
84
|
+
throw new Error('响应体不支持流式读取');
|
|
85
|
+
}
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
let fullText = '';
|
|
88
|
+
let reasoning = '';
|
|
89
|
+
let token = 0;
|
|
90
|
+
let duration = 0;
|
|
91
|
+
let buffer = '';
|
|
92
|
+
let currentEvent = ''; // 记录当前的 SSE 事件类型
|
|
93
|
+
/**
|
|
94
|
+
* 内部解析函数:处理每一行 SSE 数据
|
|
95
|
+
*/
|
|
96
|
+
const processLine = (line) => {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed)
|
|
99
|
+
return;
|
|
100
|
+
// 1. 识别事件类型 (如 event: answer)
|
|
101
|
+
if (trimmed.startsWith('event:')) {
|
|
102
|
+
currentEvent = trimmed.slice(6).trim();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// 2. 提取 Data 字符串
|
|
106
|
+
let dataStr = trimmed;
|
|
107
|
+
if (trimmed.startsWith('data:')) {
|
|
108
|
+
dataStr = trimmed.slice(5).trim();
|
|
109
|
+
}
|
|
110
|
+
// 跳过结束标志
|
|
111
|
+
if (dataStr === '[DONE]')
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
const data = JSON.parse(dataStr);
|
|
115
|
+
// A. 提取对话文本 (仅在 answer 事件中)
|
|
116
|
+
if (currentEvent === 'answer' || data.event === 'answer') {
|
|
117
|
+
const chunkText = data.delta?.content || data.text || '';
|
|
118
|
+
if (chunkText) {
|
|
119
|
+
onChunk(chunkText);
|
|
120
|
+
fullText += chunkText;
|
|
121
|
+
}
|
|
122
|
+
// 提取流式思考内容 (如果有)
|
|
123
|
+
const rChunk = data.delta?.reasoning_content || '';
|
|
124
|
+
if (rChunk)
|
|
125
|
+
reasoning += rChunk;
|
|
126
|
+
}
|
|
127
|
+
// B. 提取统计信息 (在 done 或 flowNodeStatus 事件中)
|
|
128
|
+
// 根据你的日志,统计数据可能在 data.data 下或根部
|
|
129
|
+
const nestedData = data.data || data;
|
|
130
|
+
// 关键:优先匹配 total_tokens
|
|
131
|
+
const foundToken = nestedData.total_tokens || nestedData.totalTokens || nestedData.token || 0;
|
|
132
|
+
const foundDuration = nestedData.duration || 0;
|
|
133
|
+
if (foundToken > 0)
|
|
134
|
+
token = foundToken;
|
|
135
|
+
if (foundDuration > 0)
|
|
136
|
+
duration = foundDuration;
|
|
137
|
+
// C. 更新会话 ID
|
|
138
|
+
if (data.chat_id || data.chatId) {
|
|
139
|
+
this.chatId = data.chat_id || data.chatId;
|
|
140
|
+
}
|
|
141
|
+
// D. 提取非流式的完整思考内容
|
|
142
|
+
const fullReasoning = nestedData.reasoning_content || nestedData.reason || nestedData.thought;
|
|
143
|
+
if (fullReasoning && currentEvent !== 'answer') {
|
|
144
|
+
reasoning = fullReasoning;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
// 非 JSON 格式行,且当前处于回答状态时,作为纯文本 fallback
|
|
149
|
+
if (currentEvent === 'answer') {
|
|
150
|
+
onChunk(trimmed);
|
|
151
|
+
fullText += trimmed;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
// 主循环:读取流
|
|
156
|
+
while (true) {
|
|
157
|
+
const { done, value } = await reader.read();
|
|
158
|
+
if (done)
|
|
159
|
+
break;
|
|
160
|
+
buffer += decoder.decode(value, { stream: true });
|
|
161
|
+
const lines = buffer.split('\n');
|
|
162
|
+
buffer = lines.pop() || ''; // 留下不完整的一行在下个循环处理
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
processLine(line);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 处理流结束后的残留 buffer
|
|
168
|
+
if (buffer.trim()) {
|
|
169
|
+
processLine(buffer);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
text: fullText,
|
|
173
|
+
reasoning,
|
|
174
|
+
token,
|
|
175
|
+
duration
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.LLMModel = LLMModel;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import McpGraphAgent from '../core/agent-graph';
|
|
2
|
+
import { GraphAgentOptions } from '../types/type';
|
|
3
|
+
import { LLMModel } from '../adapters/llm';
|
|
4
|
+
export interface CreateAgentOptions {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
targetDir?: string;
|
|
8
|
+
/** 是否启用流式输出,默认 false */
|
|
9
|
+
stream?: boolean;
|
|
10
|
+
config?: GraphAgentOptions;
|
|
11
|
+
}
|
|
12
|
+
export declare const createAgent: (options: CreateAgentOptions) => McpGraphAgent<LLMModel>;
|
|
13
|
+
export type Agent = ReturnType<typeof createAgent>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAgent = void 0;
|
|
7
|
+
const agent_graph_1 = __importDefault(require("../core/agent-graph"));
|
|
8
|
+
const llm_1 = require("../adapters/llm");
|
|
9
|
+
const createAgent = (options) => {
|
|
10
|
+
const agent = new agent_graph_1.default({
|
|
11
|
+
alwaysSystem: false,
|
|
12
|
+
apiModel: new llm_1.LLMModel(options),
|
|
13
|
+
targetDir: options.targetDir,
|
|
14
|
+
stream: options.stream, // ✅ 将 stream 选项透传到 McpGraphAgent
|
|
15
|
+
...(options?.config || {}),
|
|
16
|
+
});
|
|
17
|
+
return agent;
|
|
18
|
+
};
|
|
19
|
+
exports.createAgent = createAgent;
|
|
@@ -4,8 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const
|
|
8
|
-
const agent = new
|
|
9
|
-
stream: true,
|
|
10
|
-
});
|
|
7
|
+
const agent_1 = __importDefault(require("./core/agent"));
|
|
8
|
+
const agent = new agent_1.default();
|
|
11
9
|
agent.start();
|
package/lib/cli.js
CHANGED
|
@@ -1,9 +1,104 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const config_1 = require("./config/config");
|
|
6
|
+
const readline_1 = require("readline");
|
|
7
|
+
const createAgent_1 = require("./agent/createAgent");
|
|
8
|
+
// 读取配置文件
|
|
9
|
+
function loadConfig() {
|
|
10
|
+
if (!(0, fs_1.existsSync)(config_1.CONFIG_FILE)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const content = (0, fs_1.readFileSync)(config_1.CONFIG_FILE, 'utf-8');
|
|
15
|
+
const config = JSON.parse(content);
|
|
16
|
+
// 验证配置完整性
|
|
17
|
+
if (!config.apiKey || !config.apiUrl) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error(`读取配置文件失败: ${config_1.CONFIG_FILE}`, error);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// 保存配置文件
|
|
28
|
+
function saveConfig(config) {
|
|
29
|
+
try {
|
|
30
|
+
(0, fs_1.writeFileSync)(config_1.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
31
|
+
console.log(`配置已保存到: ${config_1.CONFIG_FILE}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(`保存配置文件失败: ${config_1.CONFIG_FILE}`, error);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// CLI 交互式输入配置
|
|
39
|
+
async function promptConfig() {
|
|
40
|
+
const rl = (0, readline_1.createInterface)({
|
|
41
|
+
input: process.stdin,
|
|
42
|
+
output: process.stdout,
|
|
43
|
+
});
|
|
44
|
+
const question = (query) => {
|
|
45
|
+
return new Promise(resolve => {
|
|
46
|
+
rl.question(query, resolve);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
console.log('\n欢迎使用 AI Agent CLI!');
|
|
51
|
+
console.log('首次使用需要配置 API 信息。\n');
|
|
52
|
+
const apiUrlInput = await question(`请输入 API URL: `);
|
|
53
|
+
if (!apiUrlInput.trim()) {
|
|
54
|
+
console.error('API URL 不能为空!');
|
|
55
|
+
rl.close();
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const apiKey = await question('请输入 API Key: ');
|
|
59
|
+
if (!apiKey.trim()) {
|
|
60
|
+
console.error('API Key 不能为空!');
|
|
61
|
+
rl.close();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const apiUrl = apiUrlInput.trim();
|
|
65
|
+
rl.close();
|
|
66
|
+
const config = {
|
|
67
|
+
apiKey: apiKey.trim(),
|
|
68
|
+
apiUrl: apiUrl.trim(),
|
|
69
|
+
};
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
rl.close();
|
|
74
|
+
console.error('输入配置时出错:', error);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 主函数
|
|
79
|
+
async function main() {
|
|
80
|
+
let config = loadConfig();
|
|
81
|
+
// 如果配置不存在或不完整,提示用户输入
|
|
82
|
+
if (!config) {
|
|
83
|
+
console.log(`未找到配置文件或配置不完整。${config_1.CONFIG_FILE}`);
|
|
84
|
+
config = await promptConfig();
|
|
85
|
+
saveConfig(config);
|
|
86
|
+
}
|
|
87
|
+
// 创建并启动 agent(默认开启流式输出)
|
|
88
|
+
const agent = (0, createAgent_1.createAgent)({
|
|
89
|
+
apiKey: config.apiKey,
|
|
90
|
+
apiUrl: config.apiUrl,
|
|
91
|
+
stream: true,
|
|
92
|
+
targetDir: process.cwd()
|
|
93
|
+
});
|
|
94
|
+
const model = await agent.ensureInitialized();
|
|
95
|
+
if (process.env.AI_AGENT_CLI_NO_MCP) {
|
|
96
|
+
model.setMcpEnabled(false);
|
|
97
|
+
}
|
|
98
|
+
console.log('欢迎使用 AI Agent CLI!请输入问题,按 Ctrl+C 退出。\n当前目录:', process.cwd());
|
|
99
|
+
await agent.start();
|
|
100
|
+
}
|
|
101
|
+
main().catch(error => {
|
|
102
|
+
console.error('启动失败:', error);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
package/lib/core/agent-graph.js
CHANGED
|
@@ -32,7 +32,7 @@ const AgentState = langgraph_1.Annotation.Root({
|
|
|
32
32
|
// ✅ Token 累加器
|
|
33
33
|
tokenUsage: (0, langgraph_1.Annotation)({
|
|
34
34
|
reducer: (x, y) => ({
|
|
35
|
-
total: (
|
|
35
|
+
total: (x?.total || 0) + (y?.total || 0),
|
|
36
36
|
}),
|
|
37
37
|
default: () => ({ total: 0 }),
|
|
38
38
|
}),
|
|
@@ -43,12 +43,22 @@ const AgentState = langgraph_1.Annotation.Root({
|
|
|
43
43
|
}),
|
|
44
44
|
});
|
|
45
45
|
class McpGraphAgent {
|
|
46
|
+
model;
|
|
47
|
+
toolNode;
|
|
48
|
+
targetDir;
|
|
49
|
+
options;
|
|
50
|
+
checkpointer = new langgraph_1.MemorySaver();
|
|
51
|
+
langchainTools = [];
|
|
52
|
+
stopLoadingFunc = null;
|
|
53
|
+
verbose;
|
|
54
|
+
alwaysSystem;
|
|
55
|
+
recursionLimit;
|
|
56
|
+
apiConfig;
|
|
57
|
+
maxTokens;
|
|
58
|
+
mcpClients = [];
|
|
59
|
+
streamEnabled;
|
|
60
|
+
streamOutputCallback = null;
|
|
46
61
|
constructor(options = {}) {
|
|
47
|
-
this.checkpointer = new langgraph_1.MemorySaver();
|
|
48
|
-
this.langchainTools = [];
|
|
49
|
-
this.stopLoadingFunc = null;
|
|
50
|
-
this.mcpClients = [];
|
|
51
|
-
this.streamOutputCallback = null;
|
|
52
62
|
this.options = options;
|
|
53
63
|
this.verbose = options.verbose || false;
|
|
54
64
|
this.alwaysSystem = options.alwaysSystem || true;
|
|
@@ -342,7 +352,6 @@ class McpGraphAgent {
|
|
|
342
352
|
ask();
|
|
343
353
|
}
|
|
344
354
|
renderOutput(output, isStreaming = false) {
|
|
345
|
-
var _a, _b, _c;
|
|
346
355
|
this.stopLoading(); // 停止加载动画
|
|
347
356
|
const agentNode = output.agent;
|
|
348
357
|
// ✅ 打印工具执行结果(tools 节点的输出)
|
|
@@ -350,9 +359,9 @@ class McpGraphAgent {
|
|
|
350
359
|
if (toolsNode && toolsNode.messages) {
|
|
351
360
|
const toolMessages = Array.isArray(toolsNode.messages) ? toolsNode.messages : [];
|
|
352
361
|
// 获取最近的 AI 消息以匹配 tool_call_id
|
|
353
|
-
const lastAiMsg =
|
|
362
|
+
const lastAiMsg = agentNode?.messages?.[agentNode.messages.length - 1];
|
|
354
363
|
const toolCallMap = new Map();
|
|
355
|
-
if (lastAiMsg
|
|
364
|
+
if (lastAiMsg?.tool_calls) {
|
|
356
365
|
lastAiMsg.tool_calls.forEach((tc) => {
|
|
357
366
|
if (tc.id)
|
|
358
367
|
toolCallMap.set(tc.id, tc.name);
|
|
@@ -374,7 +383,7 @@ class McpGraphAgent {
|
|
|
374
383
|
const msg = agentNode.messages[agentNode.messages.length - 1];
|
|
375
384
|
// 1. 打印思考过程(如果有)
|
|
376
385
|
// 流式模式下思考内容可能已经随流输出,此处仅在非流式模式或有独立 reasoning 字段时打印
|
|
377
|
-
const reasoning =
|
|
386
|
+
const reasoning = msg.additional_kwargs?.reasoning;
|
|
378
387
|
if (reasoning && !isStreaming) {
|
|
379
388
|
console.log(`\n🧠 [思考]: ${reasoning}`);
|
|
380
389
|
}
|
|
@@ -391,7 +400,7 @@ class McpGraphAgent {
|
|
|
391
400
|
process.stdout.write(`📊 \x1b[2m[实时统计] 消耗: ${token} tokens | 耗时: ${duration}ms\x1b[0m\n`);
|
|
392
401
|
}
|
|
393
402
|
// 4. 打印工具调用情况
|
|
394
|
-
if (
|
|
403
|
+
if (msg.tool_calls?.length) {
|
|
395
404
|
msg.tool_calls.forEach(call => {
|
|
396
405
|
console.log(`🛠️ [调用工具]: ${call.name} 📦 参数: ${JSON.stringify(call.args)}`);
|
|
397
406
|
});
|
|
@@ -406,19 +415,19 @@ class McpGraphAgent {
|
|
|
406
415
|
.join('\n')}`
|
|
407
416
|
: '';
|
|
408
417
|
// 1. 构建当前的系统提示词模板
|
|
409
|
-
const systemPromptTemplate =
|
|
418
|
+
const systemPromptTemplate = `You are an expert software engineer. Working directory: ${this.targetDir}.
|
|
410
419
|
|
|
411
|
-
# 🧠
|
|
412
|
-
|
|
420
|
+
# 🧠 Mandatory Thinking Process
|
|
421
|
+
Before providing any output or calling a tool, you **MUST** conduct a deep logical analysis. Wrap your thought process within <think> tags.
|
|
413
422
|
|
|
414
|
-
# 🛠️
|
|
415
|
-
1. Arguments
|
|
416
|
-
2.
|
|
417
|
-
3.
|
|
423
|
+
# 🛠️ Tool Call Specifications
|
|
424
|
+
1. **Pure JSON Arguments**: Arguments must be a valid JSON object. NEVER wrap the entire JSON object in a string or quotes.
|
|
425
|
+
2. **No Double Escaping**: Do not double-escape characters within the JSON.
|
|
426
|
+
3. **No Idle Operations**: If the task is complete or no tool is needed, DO NOT output any "Action" structure. Never use "None", "null", or empty strings as a tool name.
|
|
418
427
|
|
|
419
|
-
# 🎯
|
|
420
|
-
1.
|
|
421
|
-
2.
|
|
428
|
+
# 🎯 Core Instructions
|
|
429
|
+
1. **Termination Criterion**: Once you have read the requested files, answered the questions, or completed the code implementation, you must provide the final response immediately.
|
|
430
|
+
2. **Response Format**: Upon task completion, start your summary with "Final Answer:". No further tool calls should be made after this point.
|
|
422
431
|
|
|
423
432
|
{extraPrompt}`;
|
|
424
433
|
// 2. 核心逻辑:处理消息上下文
|
|
@@ -574,12 +583,11 @@ class McpGraphAgent {
|
|
|
574
583
|
}
|
|
575
584
|
}
|
|
576
585
|
getRecentToolCalls(messages, limit = 5) {
|
|
577
|
-
var _a;
|
|
578
586
|
const toolCalls = [];
|
|
579
587
|
// 从后往前遍历消息,收集最近的工具调用
|
|
580
588
|
for (let i = messages.length - 1; i >= 0 && toolCalls.length < limit; i--) {
|
|
581
589
|
const msg = messages[i];
|
|
582
|
-
if (
|
|
590
|
+
if (msg.tool_calls?.length) {
|
|
583
591
|
for (const tc of msg.tool_calls) {
|
|
584
592
|
toolCalls.push({ name: tc.name, args: tc.args });
|
|
585
593
|
if (toolCalls.length >= limit)
|
|
@@ -590,8 +598,7 @@ class McpGraphAgent {
|
|
|
590
598
|
return toolCalls;
|
|
591
599
|
}
|
|
592
600
|
printFinalSummary(state) {
|
|
593
|
-
|
|
594
|
-
const totalTokens = ((_a = state.tokenUsage) === null || _a === void 0 ? void 0 : _a.total) || 0;
|
|
601
|
+
const totalTokens = state.tokenUsage?.total || 0;
|
|
595
602
|
const totalMs = state.totalDuration || 0;
|
|
596
603
|
if (totalTokens > 0 || totalMs > 0) {
|
|
597
604
|
console.log('\n' + '═'.repeat(50));
|
package/lib/core/agent.js
CHANGED
|
@@ -49,16 +49,21 @@ const config_1 = require("../config/config");
|
|
|
49
49
|
const builtin_1 = require("../tools/builtin");
|
|
50
50
|
const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
|
|
51
51
|
class McpAgent {
|
|
52
|
+
openai;
|
|
53
|
+
modelName = '';
|
|
54
|
+
allTools = [];
|
|
55
|
+
messages = [];
|
|
56
|
+
encoder;
|
|
57
|
+
extraTools = [];
|
|
58
|
+
maxTokens;
|
|
59
|
+
apiConfig;
|
|
60
|
+
targetDir;
|
|
61
|
+
mcpClients = [];
|
|
52
62
|
constructor(options) {
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.mcpClients = [];
|
|
58
|
-
this.targetDir = (options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd();
|
|
59
|
-
this.extraTools = (options === null || options === void 0 ? void 0 : options.tools) || []; // 接收外部传入的工具
|
|
60
|
-
this.maxTokens = (options === null || options === void 0 ? void 0 : options.maxTokens) || 100000; // 默认 100k
|
|
61
|
-
this.apiConfig = options === null || options === void 0 ? void 0 : options.apiConfig;
|
|
63
|
+
this.targetDir = options?.targetDir || process.cwd();
|
|
64
|
+
this.extraTools = options?.tools || []; // 接收外部传入的工具
|
|
65
|
+
this.maxTokens = options?.maxTokens || 100000; // 默认 100k
|
|
66
|
+
this.apiConfig = options?.apiConfig;
|
|
62
67
|
let baseSystemPrompt = `你是一个专业的代码架构师。
|
|
63
68
|
你的目标是理解并分析用户项目,请务必遵循以下工作流:
|
|
64
69
|
|
|
@@ -74,7 +79,7 @@ class McpAgent {
|
|
|
74
79
|
- 优先查看 Skeleton(骨架),只有需要修复逻辑时才读取完整 Text(全文)。
|
|
75
80
|
- 始终以中文回答思考过程。`;
|
|
76
81
|
// 2. 拼接额外指令
|
|
77
|
-
if (options
|
|
82
|
+
if (options?.extraSystemPrompt) {
|
|
78
83
|
const extra = typeof options.extraSystemPrompt === 'string'
|
|
79
84
|
? options.extraSystemPrompt
|
|
80
85
|
: JSON.stringify(options.extraSystemPrompt, null, 2);
|
|
@@ -152,7 +157,7 @@ class McpAgent {
|
|
|
152
157
|
}),
|
|
153
158
|
...this.extraTools,
|
|
154
159
|
];
|
|
155
|
-
if (allTools
|
|
160
|
+
if (allTools?.length) {
|
|
156
161
|
this.allTools.push(...allTools);
|
|
157
162
|
}
|
|
158
163
|
}
|
|
@@ -251,7 +256,6 @@ class McpAgent {
|
|
|
251
256
|
});
|
|
252
257
|
}
|
|
253
258
|
async processChat(userInput) {
|
|
254
|
-
var _a;
|
|
255
259
|
this.messages.push({ role: 'user', content: userInput });
|
|
256
260
|
while (true) {
|
|
257
261
|
// --- 新增:发送请求前先检查并裁剪 ---
|
|
@@ -282,7 +286,7 @@ class McpAgent {
|
|
|
282
286
|
const { message } = response.choices[0];
|
|
283
287
|
this.messages.push(message);
|
|
284
288
|
// 计算本次 AI 回复生成的 Token
|
|
285
|
-
const completionTokens =
|
|
289
|
+
const completionTokens = response.usage?.completion_tokens ||
|
|
286
290
|
(message.content ? this.encoder.encode(message.content).length : 0);
|
|
287
291
|
console.log(`✨ AI 回复消耗: ${completionTokens} tokens`);
|
|
288
292
|
if (!message.tool_calls) {
|
|
@@ -301,10 +305,10 @@ class McpAgent {
|
|
|
301
305
|
console.log(`\n 🛠️ 执行工具: \x1b[36m${call.function.name}\x1b[0m`);
|
|
302
306
|
console.log(` 📦 传入参数: \x1b[2m${JSON.stringify(args)}\x1b[0m`);
|
|
303
307
|
let result;
|
|
304
|
-
if (tool
|
|
308
|
+
if (tool?._handler) {
|
|
305
309
|
result = await tool._handler(args);
|
|
306
310
|
}
|
|
307
|
-
else if (
|
|
311
|
+
else if (tool?._client && tool._originalName) {
|
|
308
312
|
const mcpRes = await tool._client.callTool({
|
|
309
313
|
name: tool._originalName,
|
|
310
314
|
arguments: args,
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.McpGraphAgent = exports.createTool = exports.default = void 0;
|
|
20
|
+
exports.createAgent = exports.McpGraphAgent = exports.createTool = exports.default = void 0;
|
|
21
21
|
__exportStar(require("./core/agent"), exports);
|
|
22
22
|
var agent_1 = require("./core/agent");
|
|
23
23
|
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(agent_1).default; } });
|
|
@@ -27,3 +27,5 @@ __exportStar(require("./model/AgentGraphModel"), exports);
|
|
|
27
27
|
var agent_graph_1 = require("./core/agent-graph");
|
|
28
28
|
Object.defineProperty(exports, "McpGraphAgent", { enumerable: true, get: function () { return __importDefault(agent_graph_1).default; } });
|
|
29
29
|
__exportStar(require("./types/type"), exports);
|
|
30
|
+
var createAgent_1 = require("./agent/createAgent");
|
|
31
|
+
Object.defineProperty(exports, "createAgent", { enumerable: true, get: function () { return createAgent_1.createAgent; } });
|
|
@@ -4,9 +4,12 @@ exports.AgentGraphModel = void 0;
|
|
|
4
4
|
const chat_models_1 = require("@langchain/core/language_models/chat_models");
|
|
5
5
|
const messages_1 = require("@langchain/core/messages");
|
|
6
6
|
const function_calling_1 = require("@langchain/core/utils/function_calling");
|
|
7
|
-
const
|
|
7
|
+
const generateToolMarkdown_1 = require("../utils/generateToolMarkdown");
|
|
8
8
|
const kit_1 = require("../utils/kit");
|
|
9
9
|
class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
10
|
+
boundTools;
|
|
11
|
+
mcpEnabled = true;
|
|
12
|
+
mcpTools = [];
|
|
10
13
|
setMcpTools(tools) {
|
|
11
14
|
this.mcpTools = tools;
|
|
12
15
|
}
|
|
@@ -21,12 +24,10 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
|
21
24
|
}
|
|
22
25
|
isMcpTool(tool) {
|
|
23
26
|
const mcpTools = (0, kit_1.getArray)(this.mcpTools);
|
|
24
|
-
return mcpTools.some(t =>
|
|
27
|
+
return mcpTools.some(t => t?.function?.name === tool?.function?.name);
|
|
25
28
|
}
|
|
26
29
|
constructor(fields) {
|
|
27
30
|
super(fields || {});
|
|
28
|
-
this.mcpEnabled = true;
|
|
29
|
-
this.mcpTools = [];
|
|
30
31
|
}
|
|
31
32
|
bindTools(tools) {
|
|
32
33
|
this.boundTools = tools.map(t => (0, function_calling_1.convertToOpenAITool)(t));
|
|
@@ -175,7 +176,7 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
|
175
176
|
};
|
|
176
177
|
const tools = this.getMcpEnabled() ? (0, kit_1.getArray)(this.boundTools) : (0, kit_1.getArray)(this.boundTools).filter(tool => !this.isMcpTool(tool));
|
|
177
178
|
const toolsContext = tools.length
|
|
178
|
-
?
|
|
179
|
+
? `${(0, generateToolMarkdown_1.generateToolMarkdown)(tools)}`
|
|
179
180
|
: '';
|
|
180
181
|
return `
|
|
181
182
|
${format(systemMsg)}
|
package/lib/tools/builtin.js
CHANGED
|
@@ -6,7 +6,7 @@ const ts_lsp_1 = require("./ts-lsp");
|
|
|
6
6
|
function createDefaultBuiltinTools(context) {
|
|
7
7
|
const { options } = context;
|
|
8
8
|
return [
|
|
9
|
-
...(0, ts_lsp_1.getTsLspTools)(
|
|
10
|
-
...(0, filesystem_1.getFilesystemTools)(
|
|
9
|
+
...(0, ts_lsp_1.getTsLspTools)(options?.targetDir || process.cwd()),
|
|
10
|
+
...(0, filesystem_1.getFilesystemTools)(options?.targetDir || process.cwd()),
|
|
11
11
|
];
|
|
12
12
|
}
|
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.getFilesystemTools = void 0;
|
|
7
40
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
-
const
|
|
41
|
+
const minimatchLib = __importStar(require("minimatch"));
|
|
42
|
+
const minimatch = typeof minimatchLib === 'function' ? minimatchLib : minimatchLib.minimatch;
|
|
9
43
|
const path_1 = __importDefault(require("path"));
|
|
10
44
|
const zod_1 = require("zod");
|
|
11
45
|
const createTool_1 = require("../../utils/createTool");
|
|
@@ -50,7 +84,7 @@ const ListDirectoryWithSizesArgsSchema = zod_1.z.object({
|
|
|
50
84
|
const DirectoryTreeArgsSchema = zod_1.z.object({
|
|
51
85
|
path: zod_1.z.string(),
|
|
52
86
|
excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([]),
|
|
53
|
-
depth: zod_1.z.number().optional().default(2).describe('
|
|
87
|
+
depth: zod_1.z.number().optional().default(2).describe('Recursive depth, default 2 layers. Increasing depth will consume more tokens'),
|
|
54
88
|
});
|
|
55
89
|
const MoveFileArgsSchema = zod_1.z.object({
|
|
56
90
|
source: zod_1.z.string(),
|
|
@@ -65,19 +99,19 @@ const GetFileInfoArgsSchema = zod_1.z.object({
|
|
|
65
99
|
path: zod_1.z.string(),
|
|
66
100
|
});
|
|
67
101
|
const GrepSearchArgsSchema = zod_1.z.object({
|
|
68
|
-
path: zod_1.z.string().describe('
|
|
69
|
-
query: zod_1.z.string().describe('
|
|
70
|
-
includePattern: zod_1.z.string().optional().default('**/*').describe('
|
|
71
|
-
maxFiles: zod_1.z.number().optional().default(100).describe('
|
|
102
|
+
path: zod_1.z.string().describe('The starting directory path to search'),
|
|
103
|
+
query: zod_1.z.string().describe('The text keyword to search'),
|
|
104
|
+
includePattern: zod_1.z.string().optional().default('**/*').describe('The matching pattern, for example "**/*.ts"'),
|
|
105
|
+
maxFiles: zod_1.z.number().optional().default(100).describe('The maximum number of files to scan, to prevent large projects from timing out'),
|
|
72
106
|
});
|
|
73
107
|
const PatchEditArgsSchema = zod_1.z.object({
|
|
74
|
-
path: zod_1.z.string().describe('
|
|
108
|
+
path: zod_1.z.string().describe('The file path'),
|
|
75
109
|
patches: zod_1.z.array(zod_1.z.object({
|
|
76
|
-
startLine: zod_1.z.number().describe('
|
|
77
|
-
endLine: zod_1.z.number().describe('
|
|
78
|
-
replacement: zod_1.z.string().describe('
|
|
79
|
-
originalSnippet: zod_1.z.string().optional().describe('
|
|
80
|
-
})).describe('
|
|
110
|
+
startLine: zod_1.z.number().describe('The start line number (inclusive)'),
|
|
111
|
+
endLine: zod_1.z.number().describe('The end line number (inclusive)'),
|
|
112
|
+
replacement: zod_1.z.string().describe('The new code content to insert'),
|
|
113
|
+
originalSnippet: zod_1.z.string().optional().describe('Optional: The original code snippet within the line range, used to verify and prevent line number offset'),
|
|
114
|
+
})).describe('The patch list. Note: If there are multiple patches, it is recommended to execute from the end of the file to the beginning, or ensure that the line numbers do not overlap'),
|
|
81
115
|
});
|
|
82
116
|
const getFilesystemTools = (targetDir) => {
|
|
83
117
|
(0, lib_1.setAllowedDirectories)([targetDir]);
|
|
@@ -101,15 +135,15 @@ const getFilesystemTools = (targetDir) => {
|
|
|
101
135
|
};
|
|
102
136
|
const readTextFileTool = (0, createTool_1.createTool)({
|
|
103
137
|
name: 'read_text_file',
|
|
104
|
-
description: '
|
|
138
|
+
description: 'Read the full content of the file. If it exceeds 100 lines, it is forbidden to use, must use read_file_range. Supports head/tail parameters.',
|
|
105
139
|
parameters: ReadTextFileArgsSchema,
|
|
106
140
|
handler: readTextFileHandler,
|
|
107
141
|
});
|
|
108
142
|
const readMultipleFilesTool = (0, createTool_1.createTool)({
|
|
109
143
|
name: 'read_multiple_files',
|
|
110
|
-
description: '
|
|
111
|
-
'
|
|
112
|
-
'
|
|
144
|
+
description: 'Read the content of multiple files at the same time. Use when you need to compare multiple files or analyze cross-file relationships.' +
|
|
145
|
+
'Note: To prevent token overflow, this tool can read up to 10 files at a time, and each file only displays the first 6000 characters.' +
|
|
146
|
+
'If you need to view the complete large file or specific logic, please use read_file_range instead.',
|
|
113
147
|
parameters: ReadMultipleFilesArgsSchema,
|
|
114
148
|
handler: async (args) => {
|
|
115
149
|
// 保护 1:文件数量限制 (防止 AI 一次传入几十个文件)
|
|
@@ -124,25 +158,25 @@ const getFilesystemTools = (targetDir) => {
|
|
|
124
158
|
const validPath = await (0, lib_1.validatePath)(targetDir, filePath);
|
|
125
159
|
const content = await (0, lib_1.readFileContent)(validPath);
|
|
126
160
|
if (content.length > MAX_CHARS_PER_FILE) {
|
|
127
|
-
return `${filePath} (
|
|
161
|
+
return `${filePath} (Content truncated):\n${content.substring(0, MAX_CHARS_PER_FILE)}\n\n[... Content too long, only showing the first ${MAX_CHARS_PER_FILE} characters. If you need to view the subsequent content, please use read_file_range to specify the line number to read ...]`;
|
|
128
162
|
}
|
|
129
163
|
return `${filePath}:\n${content}\n`;
|
|
130
164
|
}
|
|
131
165
|
catch (error) {
|
|
132
166
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
133
|
-
return `${filePath}:
|
|
167
|
+
return `${filePath}: Read failed - ${errorMessage}`;
|
|
134
168
|
}
|
|
135
169
|
}));
|
|
136
170
|
let text = results.join('\n---\n');
|
|
137
171
|
if (isTruncatedByCount) {
|
|
138
|
-
text += `\n\n⚠️
|
|
172
|
+
text += `\n\n⚠️ Note: The maximum number of files processed in one request is ${MAX_FILES}. ${args.paths.length - MAX_FILES} files remain unread, please request in batches.`;
|
|
139
173
|
}
|
|
140
174
|
return text;
|
|
141
175
|
},
|
|
142
176
|
});
|
|
143
177
|
const writeFileTool = (0, createTool_1.createTool)({
|
|
144
178
|
name: 'write_file',
|
|
145
|
-
description: '
|
|
179
|
+
description: 'Only used to create new files. It is forbidden to use it to modify existing source code.',
|
|
146
180
|
parameters: WriteFileArgsSchema,
|
|
147
181
|
handler: async (args) => {
|
|
148
182
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -153,7 +187,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
153
187
|
});
|
|
154
188
|
const createDirectoryTool = (0, createTool_1.createTool)({
|
|
155
189
|
name: 'create_directory',
|
|
156
|
-
description: '
|
|
190
|
+
description: 'Recursively create a directory. Supports nested directories. If the directory already exists, it will be successful silently. Only allowed directories.',
|
|
157
191
|
parameters: CreateDirectoryArgsSchema,
|
|
158
192
|
handler: async (args) => {
|
|
159
193
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -164,7 +198,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
164
198
|
});
|
|
165
199
|
const listDirectoryWithSizesTool = (0, createTool_1.createTool)({
|
|
166
200
|
name: 'list_directory',
|
|
167
|
-
description: '
|
|
201
|
+
description: 'List the contents of the directory. Return entries with [FILE]/[DIR] prefix, size, and summary. Supports sorting by name or size.',
|
|
168
202
|
parameters: ListDirectoryWithSizesArgsSchema,
|
|
169
203
|
handler: async (args) => {
|
|
170
204
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -215,8 +249,8 @@ const getFilesystemTools = (targetDir) => {
|
|
|
215
249
|
});
|
|
216
250
|
const directoryTreeTool = (0, createTool_1.createTool)({
|
|
217
251
|
name: 'directory_tree',
|
|
218
|
-
description: '
|
|
219
|
-
'
|
|
252
|
+
description: 'Get the recursive tree JSON structure of the directory.' +
|
|
253
|
+
'Default only shows 2 layers of depth to save tokens. If you need to see deeper levels, please increase the depth parameter.',
|
|
220
254
|
parameters: DirectoryTreeArgsSchema,
|
|
221
255
|
handler: async (args) => {
|
|
222
256
|
// 在 directory_tree 的 handler 内部
|
|
@@ -230,7 +264,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
230
264
|
const result = [];
|
|
231
265
|
for (const entry of entries) {
|
|
232
266
|
const relativePath = path_1.default.relative(rootPath, path_1.default.join(currentPath, entry.name));
|
|
233
|
-
const shouldExclude = excludePatterns.some(pattern =>
|
|
267
|
+
const shouldExclude = excludePatterns.some(pattern => minimatch(relativePath, pattern, { dot: true }));
|
|
234
268
|
if (shouldExclude)
|
|
235
269
|
continue;
|
|
236
270
|
const entryData = {
|
|
@@ -251,7 +285,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
251
285
|
});
|
|
252
286
|
const moveFileTool = (0, createTool_1.createTool)({
|
|
253
287
|
name: 'move_file',
|
|
254
|
-
description: '
|
|
288
|
+
description: 'Move or rename a file/directory. If the target path already exists, it will fail. The source and target must be within the allowed directories.',
|
|
255
289
|
parameters: MoveFileArgsSchema,
|
|
256
290
|
handler: async (args) => {
|
|
257
291
|
const validSourcePath = await (0, lib_1.validatePath)(targetDir, args.source);
|
|
@@ -263,7 +297,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
263
297
|
});
|
|
264
298
|
const searchFilesTool = (0, createTool_1.createTool)({
|
|
265
299
|
name: 'search_files',
|
|
266
|
-
description: '
|
|
300
|
+
description: 'Search for file names matching a pattern in the specified path. Return a list of matching relative paths.',
|
|
267
301
|
parameters: SearchFilesArgsSchema,
|
|
268
302
|
handler: async (args) => {
|
|
269
303
|
const combinedExcludes = [...DEFAULT_IGNORE, ...(args.excludePatterns || [])];
|
|
@@ -277,7 +311,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
277
311
|
});
|
|
278
312
|
const getFileInfoTool = (0, createTool_1.createTool)({
|
|
279
313
|
name: 'get_file_info',
|
|
280
|
-
description: '
|
|
314
|
+
description: 'View file metadata (size, line count, modification time). Must call this tool before reading large files.',
|
|
281
315
|
parameters: GetFileInfoArgsSchema,
|
|
282
316
|
handler: async (args) => {
|
|
283
317
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -299,9 +333,9 @@ const getFilesystemTools = (targetDir) => {
|
|
|
299
333
|
});
|
|
300
334
|
const grepSearchTool = (0, createTool_1.createTool)({
|
|
301
335
|
name: 'grep_search',
|
|
302
|
-
description: '
|
|
303
|
-
'
|
|
304
|
-
'
|
|
336
|
+
description: 'Search for keywords in the content of files in the specified directory.' +
|
|
337
|
+
'This tool will return the file paths and preview of matching lines containing the keywords.' +
|
|
338
|
+
'Please narrow down the search scope as much as possible using includePattern.',
|
|
305
339
|
parameters: GrepSearchArgsSchema,
|
|
306
340
|
handler: async (args) => {
|
|
307
341
|
const startPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -333,21 +367,21 @@ const getFilesystemTools = (targetDir) => {
|
|
|
333
367
|
}));
|
|
334
368
|
}
|
|
335
369
|
let response = matches.length > 0
|
|
336
|
-
?
|
|
337
|
-
:
|
|
370
|
+
? `The location of the keyword "${args.query}" is as follows:\n${matches.join('\n')}`
|
|
371
|
+
: `The content containing "${args.query}" was not found.`;
|
|
338
372
|
if (allFiles.length > args.maxFiles) {
|
|
339
|
-
response += `\n\
|
|
373
|
+
response += `\n\nNote: The search has reached the limit, only scanned the first ${args.maxFiles} files. If no results are found, please provide a more precise path or includePattern.`;
|
|
340
374
|
}
|
|
341
375
|
return response;
|
|
342
376
|
},
|
|
343
377
|
});
|
|
344
378
|
const readFileRangeTool = (0, createTool_1.createTool)({
|
|
345
379
|
name: 'read_file_range',
|
|
346
|
-
description: '
|
|
380
|
+
description: 'Precisely read the specified line range (includes line number prefix). Must use this tool before modifying code or locating errors based on error messages.',
|
|
347
381
|
parameters: zod_1.z.object({
|
|
348
|
-
path: zod_1.z.string().describe('
|
|
349
|
-
startLine: zod_1.z.number().describe('
|
|
350
|
-
endLine: zod_1.z.number().describe('
|
|
382
|
+
path: zod_1.z.string().describe('The file path relative to the target directory'),
|
|
383
|
+
startLine: zod_1.z.number().describe('The starting line number (starts from 1)'),
|
|
384
|
+
endLine: zod_1.z.number().describe('The ending line number'),
|
|
351
385
|
}),
|
|
352
386
|
handler: async (args) => {
|
|
353
387
|
// 1. 验证路径安全(沿用你代码中的 validatePath 逻辑)
|
|
@@ -360,26 +394,26 @@ const getFilesystemTools = (targetDir) => {
|
|
|
360
394
|
const start = Math.max(1, args.startLine);
|
|
361
395
|
const end = Math.min(totalLines, args.endLine);
|
|
362
396
|
if (start > totalLines) {
|
|
363
|
-
return
|
|
397
|
+
return `Error: The file only has ${totalLines} lines, the starting line number ${start} is out of range.`;
|
|
364
398
|
}
|
|
365
399
|
if (start > end) {
|
|
366
|
-
return
|
|
400
|
+
return `Error: The starting line number ${start} cannot be greater than the ending line number ${end}.`;
|
|
367
401
|
}
|
|
368
402
|
// 3. 截取并添加行号索引(核心:增强 AI 的位置感)
|
|
369
403
|
const selectedLines = lines.slice(start - 1, end);
|
|
370
404
|
const formattedContent = selectedLines
|
|
371
405
|
.map((line, index) => `${start + index}| ${line}`)
|
|
372
406
|
.join('\n');
|
|
373
|
-
return `[
|
|
407
|
+
return `[File: ${args.path} | Lines ${start} to ${end} / Total ${totalLines} lines]\n${formattedContent}`;
|
|
374
408
|
}
|
|
375
409
|
catch (error) {
|
|
376
|
-
return
|
|
410
|
+
return `Failed to read the file range: ${error.message}`;
|
|
377
411
|
}
|
|
378
412
|
},
|
|
379
413
|
});
|
|
380
414
|
const editFileTool = (0, createTool_1.createTool)({
|
|
381
415
|
name: 'edit_file',
|
|
382
|
-
description: '
|
|
416
|
+
description: 'Replace code based on line number range. The only tool for modifying logic. Must call read_file_range to get the latest line number before calling. Supports deletion (empty content) or single line replacement.',
|
|
383
417
|
parameters: PatchEditArgsSchema,
|
|
384
418
|
handler: async (args) => {
|
|
385
419
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -391,14 +425,14 @@ const getFilesystemTools = (targetDir) => {
|
|
|
391
425
|
for (const patch of sortedPatches) {
|
|
392
426
|
// 校验行号合法性
|
|
393
427
|
if (patch.startLine < 1 || patch.endLine > lines.length || patch.startLine > patch.endLine) {
|
|
394
|
-
return
|
|
428
|
+
return `Error: The line number range ${patch.startLine}-${patch.endLine} is out of the actual range of the file (1-${lines.length})`;
|
|
395
429
|
}
|
|
396
430
|
// 可选:二次校验(防止 AI 记忆了错误的行号)
|
|
397
431
|
if (patch.originalSnippet) {
|
|
398
432
|
const currentText = lines.slice(patch.startLine - 1, patch.endLine).join('\n');
|
|
399
433
|
// 模糊对比,如果差异太大则报错
|
|
400
434
|
if (!currentText.includes(patch.originalSnippet.trim()) && currentText.trim().length > 0) {
|
|
401
|
-
return
|
|
435
|
+
return `Warning: The content of the ${patch.startLine} line has changed, which does not match your expected code. Please read the file again to get the latest line number.`;
|
|
402
436
|
}
|
|
403
437
|
}
|
|
404
438
|
// 执行替换:splice(开始索引, 删除数量, 替换内容)
|
|
@@ -406,10 +440,10 @@ const getFilesystemTools = (targetDir) => {
|
|
|
406
440
|
lines.splice(patch.startLine - 1, (patch.endLine - patch.startLine) + 1, patch.replacement);
|
|
407
441
|
}
|
|
408
442
|
await promises_1.default.writeFile(validPath, lines.join('\n'), 'utf-8');
|
|
409
|
-
return
|
|
443
|
+
return `Successfully updated ${args.path} with ${args.patches.length} code changes.`;
|
|
410
444
|
}
|
|
411
445
|
catch (error) {
|
|
412
|
-
return `Patch
|
|
446
|
+
return `Patch failed: ${error.message}`;
|
|
413
447
|
}
|
|
414
448
|
},
|
|
415
449
|
});
|
|
@@ -142,7 +142,6 @@ async function writeFileContent(filePath, content) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
async function applyFileEdits(filePath, edits, dryRun = false) {
|
|
145
|
-
var _a;
|
|
146
145
|
// Read file content and normalize line endings
|
|
147
146
|
const content = normalizeLineEndings(await promises_1.default.readFile(filePath, 'utf-8'));
|
|
148
147
|
// Apply edits sequentially
|
|
@@ -168,14 +167,13 @@ async function applyFileEdits(filePath, edits, dryRun = false) {
|
|
|
168
167
|
});
|
|
169
168
|
if (isMatch) {
|
|
170
169
|
// Preserve original indentation of first line
|
|
171
|
-
const originalIndent =
|
|
170
|
+
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
|
|
172
171
|
const newLines = normalizedNew.split('\n').map((line, j) => {
|
|
173
|
-
var _a, _b, _c;
|
|
174
172
|
if (j === 0)
|
|
175
173
|
return originalIndent + line.trimStart();
|
|
176
174
|
// For subsequent lines, try to preserve relative indentation
|
|
177
|
-
const oldIndent =
|
|
178
|
-
const newIndent =
|
|
175
|
+
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
|
|
176
|
+
const newIndent = line.match(/^\s*/)?.[0] || '';
|
|
179
177
|
if (oldIndent && newIndent) {
|
|
180
178
|
const relativeIndent = newIndent.length - oldIndent.length;
|
|
181
179
|
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
|
|
@@ -9,18 +9,19 @@ const getTsLspTools = (targetDir) => {
|
|
|
9
9
|
return [
|
|
10
10
|
(0, createTool_1.createTool)({
|
|
11
11
|
name: 'get_method_body',
|
|
12
|
-
description: '
|
|
12
|
+
description: '[Only for TS/JS] Extract code blocks by method name. More resistant to interference than line number reading, preferred when referencing logic.',
|
|
13
13
|
parameters: zod_1.z.object({
|
|
14
|
-
filePath: zod_1.z.string().describe('
|
|
15
|
-
methodName: zod_1.z.string().describe('
|
|
14
|
+
filePath: zod_1.z.string().describe('The relative file path'),
|
|
15
|
+
methodName: zod_1.z.string().describe('The method name or function name'),
|
|
16
16
|
}),
|
|
17
17
|
handler: async ({ filePath, methodName }) => {
|
|
18
|
-
|
|
18
|
+
const res = engine.getMethodImplementation(filePath, methodName);
|
|
19
|
+
return typeof res === 'string' ? res : JSON.stringify(res);
|
|
19
20
|
},
|
|
20
21
|
}),
|
|
21
22
|
(0, createTool_1.createTool)({
|
|
22
23
|
name: 'get_repo_map',
|
|
23
|
-
description: '
|
|
24
|
+
description: 'Get the global file structure and export list of the project, used for quick code location',
|
|
24
25
|
parameters: zod_1.z.object({}),
|
|
25
26
|
handler: async () => {
|
|
26
27
|
engine.refresh();
|
|
@@ -29,26 +30,26 @@ const getTsLspTools = (targetDir) => {
|
|
|
29
30
|
}),
|
|
30
31
|
(0, createTool_1.createTool)({
|
|
31
32
|
name: 'analyze_deps',
|
|
32
|
-
description: '
|
|
33
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('
|
|
33
|
+
description: 'Analyze the dependencies of the specified file, support tsconfig path alias parsing',
|
|
34
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('The relative file path') }),
|
|
34
35
|
handler: async ({ filePath }) => engine.getDeps(filePath),
|
|
35
36
|
}),
|
|
36
37
|
(0, createTool_1.createTool)({
|
|
37
38
|
name: 'read_skeleton',
|
|
38
|
-
description: '
|
|
39
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('
|
|
39
|
+
description: 'Extract the structure definition of the file (interface, class, method signature), ignoring the specific implementation to save tokens',
|
|
40
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('The relative file path') }),
|
|
40
41
|
handler: async (args) => {
|
|
41
|
-
const pathArg = args
|
|
42
|
+
const pathArg = args?.filePath;
|
|
42
43
|
if (typeof pathArg !== 'string' || pathArg.trim() === '') {
|
|
43
|
-
return `Error:
|
|
44
|
+
return `Error: The parameter 'filePath' is invalid. Received: ${JSON.stringify(pathArg)}`;
|
|
44
45
|
}
|
|
45
46
|
try {
|
|
46
47
|
engine.refresh();
|
|
47
48
|
const result = engine.getSkeleton(pathArg);
|
|
48
|
-
return result || `// Warning:
|
|
49
|
+
return result || `// Warning: The file ${pathArg} exists but no structure can be extracted.`;
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
|
-
return `Error:
|
|
52
|
+
return `Error: An internal error occurred while parsing the file ${pathArg}: ${error.message}`;
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
}),
|
|
@@ -5,7 +5,7 @@ const tools_1 = require("@langchain/core/tools");
|
|
|
5
5
|
function convertToLangChainTool(info) {
|
|
6
6
|
return new tools_1.DynamicStructuredTool({
|
|
7
7
|
name: info.function.name,
|
|
8
|
-
description: info.function.description ||
|
|
8
|
+
description: info.function.description || '',
|
|
9
9
|
schema: info.function.parameters,
|
|
10
10
|
func: async (args) => {
|
|
11
11
|
if (info._handler)
|
|
@@ -17,7 +17,7 @@ function convertToLangChainTool(info) {
|
|
|
17
17
|
});
|
|
18
18
|
return JSON.stringify(result);
|
|
19
19
|
}
|
|
20
|
-
return
|
|
20
|
+
return 'Error: No tool execution handler found.';
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
23
|
}
|
package/lib/utils/createTool.js
CHANGED
|
@@ -11,13 +11,13 @@ function formatSchema(schema) {
|
|
|
11
11
|
const requiredKeys = res.required || [];
|
|
12
12
|
const lines = [];
|
|
13
13
|
for (const key in res.properties) {
|
|
14
|
-
lines.push(` - ${key}: ${res.properties[key].type}${requiredKeys.includes(key) ?
|
|
14
|
+
lines.push(` - ${key}: ${res.properties[key].type}${requiredKeys.includes(key) ? ' (required)' : ''}`);
|
|
15
15
|
}
|
|
16
16
|
if (lines.length > 0) {
|
|
17
|
-
return lines.join(
|
|
17
|
+
return lines.join('\n');
|
|
18
18
|
}
|
|
19
|
-
return
|
|
19
|
+
return 'No parameters';
|
|
20
20
|
}
|
|
21
21
|
const keys = Object.keys(schema.shape);
|
|
22
|
-
return keys.join(
|
|
22
|
+
return keys.join(', ');
|
|
23
23
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateToolMarkdown = generateToolMarkdown;
|
|
4
|
+
const cleanToolDefinition_1 = require("./cleanToolDefinition");
|
|
5
|
+
const kit_1 = require("./kit");
|
|
6
|
+
/**
|
|
7
|
+
* 将工具定义从 Zod Schema 转换为极简 Markdown 格式
|
|
8
|
+
* 目的:显著节省 System Prompt 的 Token,同时保持 LLM 理解力
|
|
9
|
+
*/
|
|
10
|
+
function generateToolMarkdown(tools) {
|
|
11
|
+
let markdown = "## Tool Definitions\n\n";
|
|
12
|
+
(0, kit_1.getArray)(tools).forEach((tool) => {
|
|
13
|
+
const { name, description, parameters } = (0, cleanToolDefinition_1.cleanToolDefinition)(tool);
|
|
14
|
+
markdown += `- **${name}**: ${description}\n`;
|
|
15
|
+
// 提取 Zod 参数
|
|
16
|
+
const shape = parameters.properties || {};
|
|
17
|
+
const requiredFields = parameters.required || [];
|
|
18
|
+
Object.entries(shape).forEach(([paramName, schema]) => {
|
|
19
|
+
const isRequired = requiredFields.includes(paramName);
|
|
20
|
+
const type = schema.type.replace('Zod', '').toLowerCase();
|
|
21
|
+
const desc = schema.description || schema.title || '';
|
|
22
|
+
markdown += ` - \`${paramName}\` (${type}${isRequired ? ', required' : ''})${desc ? `: ${desc}` : ''}\n`;
|
|
23
|
+
});
|
|
24
|
+
markdown += "\n";
|
|
25
|
+
});
|
|
26
|
+
return markdown;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber2pr/ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"description": "AI Assistant CLI.",
|
|
5
5
|
"author": "saber2pr",
|
|
6
6
|
"license": "ISC",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
],
|
|
10
10
|
"bin": {
|
|
11
11
|
"sagent": "./lib/cli.js",
|
|
12
|
-
"sagent-
|
|
12
|
+
"sagent-openai": "./lib/cli-openai.js"
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@langchain/langgraph": "^0.2.74",
|
|
28
28
|
"@langchain/openai": "0.4.9",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
30
|
-
"@saber2pr/ts-context-mcp": "^0.0.
|
|
30
|
+
"@saber2pr/ts-context-mcp": "^0.0.10",
|
|
31
31
|
"diff": "^8.0.3",
|
|
32
32
|
"glob": "^10.5.0",
|
|
33
33
|
"minimatch": "^10.0.1",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@langchain/core": "0.3.40"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@types/node": "^
|
|
45
|
+
"@types/node": "^25.2.3",
|
|
46
46
|
"prettier": "^2.4.1"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
File without changes
|