@jiangyuan1209/yuan-claw 0.1.0
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/.env.example +28 -0
- package/README.md +441 -0
- package/dist/agent/build-system-prompt.js +46 -0
- package/dist/agent/message-types.js +1 -0
- package/dist/agent/message-utils.js +239 -0
- package/dist/agent/parse-agent-response.js +48 -0
- package/dist/agent/prompts.js +27 -0
- package/dist/agent/protocol.js +1 -0
- package/dist/agent/read-approval.js +45 -0
- package/dist/agent/read-confirmation.js +17 -0
- package/dist/agent/run-local-agent-loop.js +272 -0
- package/dist/cli/help.js +24 -0
- package/dist/cli/main.js +108 -0
- package/dist/cli/parse-args.js +75 -0
- package/dist/cli/repl.js +188 -0
- package/dist/config/config-path.js +9 -0
- package/dist/config/init-user-config.js +39 -0
- package/dist/config/load-config.js +46 -0
- package/dist/config.js +29 -0
- package/dist/events/event-bus.js +62 -0
- package/dist/lib/initGlobalProxy.js +52 -0
- package/dist/memory/session-store.js +43 -0
- package/dist/memory/types.js +1 -0
- package/dist/model/client.js +7 -0
- package/dist/model/providers/openai-compatible.js +42 -0
- package/dist/scripts/test-brave.js +16 -0
- package/dist/security/network-policy.js +19 -0
- package/dist/security/path-guards.js +18 -0
- package/dist/security/shell-policy.js +40 -0
- package/dist/shared/utils.js +1 -0
- package/dist/tools/file/grep-text.js +85 -0
- package/dist/tools/file/list-files.js +52 -0
- package/dist/tools/file/read-file.js +39 -0
- package/dist/tools/file/write-file.js +37 -0
- package/dist/tools/git/git-diff.js +54 -0
- package/dist/tools/git/git-status.js +44 -0
- package/dist/tools/registry.js +41 -0
- package/dist/tools/shell/shell-exec.js +41 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/web/extract-readable-text.js +54 -0
- package/dist/tools/web/http-fetch.js +55 -0
- package/dist/tools/web/index.js +4 -0
- package/dist/tools/web/search-providers/brave.js +31 -0
- package/dist/tools/web/web-search.js +33 -0
- package/package.json +26 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const DEFAULT_TEXT_MAX_CHARS = 4_000;
|
|
2
|
+
const DEFAULT_TEXT_HEAD_CHARS = 2_500;
|
|
3
|
+
const DEFAULT_TEXT_TAIL_CHARS = 1_000;
|
|
4
|
+
const DEFAULT_TEXT_MARKER = "\n...[truncated]...\n";
|
|
5
|
+
const DEFAULT_VALUE_MAX_STRING_CHARS = 4_000;
|
|
6
|
+
const DEFAULT_VALUE_MAX_ARRAY_ITEMS = 20;
|
|
7
|
+
const DEFAULT_VALUE_MAX_OBJECT_KEYS = 40;
|
|
8
|
+
const DEFAULT_VALUE_MAX_DEPTH = 6;
|
|
9
|
+
const DEFAULT_MAX_MESSAGES = 24;
|
|
10
|
+
const DEFAULT_MAX_TOTAL_CHARS = 24_000;
|
|
11
|
+
const DEFAULT_PRESERVE_RECENT_MESSAGES = 8;
|
|
12
|
+
export function truncateText(text, options = {}) {
|
|
13
|
+
const maxChars = options.maxChars ?? DEFAULT_TEXT_MAX_CHARS;
|
|
14
|
+
const headChars = options.headChars ?? DEFAULT_TEXT_HEAD_CHARS;
|
|
15
|
+
const tailChars = options.tailChars ?? DEFAULT_TEXT_TAIL_CHARS;
|
|
16
|
+
const marker = options.marker ?? DEFAULT_TEXT_MARKER;
|
|
17
|
+
const totalChars = text.length;
|
|
18
|
+
if (totalChars <= maxChars) {
|
|
19
|
+
return {
|
|
20
|
+
text,
|
|
21
|
+
truncated: false,
|
|
22
|
+
totalChars,
|
|
23
|
+
returnedChars: totalChars,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const safeHead = Math.max(0, headChars);
|
|
27
|
+
const safeTail = Math.max(0, tailChars);
|
|
28
|
+
let next = text.slice(0, safeHead) + marker;
|
|
29
|
+
if (safeTail > 0) {
|
|
30
|
+
next += text.slice(-safeTail);
|
|
31
|
+
}
|
|
32
|
+
if (next.length > maxChars) {
|
|
33
|
+
next = next.slice(0, maxChars);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
text: next,
|
|
37
|
+
truncated: true,
|
|
38
|
+
totalChars,
|
|
39
|
+
returnedChars: next.length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function compactValueInternal(value, options, depth) {
|
|
46
|
+
if (value == null) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "boolean" ||
|
|
50
|
+
typeof value === "number" ||
|
|
51
|
+
typeof value === "string") {
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
const truncated = truncateText(value, {
|
|
54
|
+
maxChars: options.maxStringChars,
|
|
55
|
+
});
|
|
56
|
+
return truncated.text;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (depth >= options.maxDepth) {
|
|
61
|
+
return "[MaxDepthExceeded]";
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
const items = value
|
|
65
|
+
.slice(0, options.maxArrayItems)
|
|
66
|
+
.map((item) => compactValueInternal(item, options, depth + 1));
|
|
67
|
+
if (value.length > options.maxArrayItems) {
|
|
68
|
+
items.push(`[... ${value.length - options.maxArrayItems} more items omitted]`);
|
|
69
|
+
}
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
72
|
+
if (isPlainObject(value)) {
|
|
73
|
+
const entries = Object.entries(value);
|
|
74
|
+
const limitedEntries = entries.slice(0, options.maxObjectKeys);
|
|
75
|
+
const result = {};
|
|
76
|
+
for (const [key, val] of limitedEntries) {
|
|
77
|
+
result[key] = compactValueInternal(val, options, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
if (entries.length > options.maxObjectKeys) {
|
|
80
|
+
result.__omittedKeys = entries.length - options.maxObjectKeys;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
return String(value);
|
|
85
|
+
}
|
|
86
|
+
export function compactValue(value, options = {}) {
|
|
87
|
+
return compactValueInternal(value, {
|
|
88
|
+
maxStringChars: options.maxStringChars ?? DEFAULT_VALUE_MAX_STRING_CHARS,
|
|
89
|
+
maxArrayItems: options.maxArrayItems ?? DEFAULT_VALUE_MAX_ARRAY_ITEMS,
|
|
90
|
+
maxObjectKeys: options.maxObjectKeys ?? DEFAULT_VALUE_MAX_OBJECT_KEYS,
|
|
91
|
+
maxDepth: options.maxDepth ?? DEFAULT_VALUE_MAX_DEPTH,
|
|
92
|
+
}, 0);
|
|
93
|
+
}
|
|
94
|
+
export function compactToolResult(result, options = {}) {
|
|
95
|
+
return compactValue(result, options);
|
|
96
|
+
}
|
|
97
|
+
export function compactAssistantToolCall(content) {
|
|
98
|
+
const truncated = truncateText(content, {
|
|
99
|
+
maxChars: 800,
|
|
100
|
+
headChars: 600,
|
|
101
|
+
tailChars: 120,
|
|
102
|
+
});
|
|
103
|
+
return truncated.truncated
|
|
104
|
+
? `[assistant tool call summary]\n${truncated.text}`
|
|
105
|
+
: content;
|
|
106
|
+
}
|
|
107
|
+
function estimateMessageChars(message) {
|
|
108
|
+
const content = typeof message.content === "string"
|
|
109
|
+
? message.content
|
|
110
|
+
: JSON.stringify(message.content ?? "");
|
|
111
|
+
return content.length + message.role.length;
|
|
112
|
+
}
|
|
113
|
+
function compactMessage(message) {
|
|
114
|
+
const content = typeof message.content === "string"
|
|
115
|
+
? message.content
|
|
116
|
+
: JSON.stringify(message.content ?? "");
|
|
117
|
+
if (message.role === "tool") {
|
|
118
|
+
return {
|
|
119
|
+
...message,
|
|
120
|
+
content: JSON.stringify(compactToolResult(safeJsonParse(content) ?? content)),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (message.role === "assistant") {
|
|
124
|
+
return {
|
|
125
|
+
...message,
|
|
126
|
+
content: compactAssistantToolCall(content),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (message.role === "user") {
|
|
130
|
+
const truncated = truncateText(content, {
|
|
131
|
+
maxChars: 2_000,
|
|
132
|
+
headChars: 1_400,
|
|
133
|
+
tailChars: 300,
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
...message,
|
|
137
|
+
content: truncated.text,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (message.role === "system") {
|
|
141
|
+
const truncated = truncateText(content, {
|
|
142
|
+
maxChars: 3_000,
|
|
143
|
+
headChars: 2_400,
|
|
144
|
+
tailChars: 300,
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
...message,
|
|
148
|
+
content: truncated.text,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return message;
|
|
152
|
+
}
|
|
153
|
+
function safeJsonParse(text) {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(text);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function trimMessages(messages, options = {}) {
|
|
162
|
+
const maxMessages = options.maxMessages ?? DEFAULT_MAX_MESSAGES;
|
|
163
|
+
const maxTotalChars = options.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
|
|
164
|
+
const preserveSystemMessages = options.preserveSystemMessages ?? true;
|
|
165
|
+
const preserveRecentMessages = options.preserveRecentMessages ?? DEFAULT_PRESERVE_RECENT_MESSAGES;
|
|
166
|
+
const compactToolMessages = options.compactToolMessages ?? true;
|
|
167
|
+
if (messages.length === 0) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const systemMessages = preserveSystemMessages
|
|
171
|
+
? messages.filter((m) => m.role === "system")
|
|
172
|
+
: [];
|
|
173
|
+
const nonSystemMessages = preserveSystemMessages
|
|
174
|
+
? messages.filter((m) => m.role !== "system")
|
|
175
|
+
: [...messages];
|
|
176
|
+
const recent = nonSystemMessages.slice(-preserveRecentMessages);
|
|
177
|
+
const older = nonSystemMessages.slice(0, Math.max(0, nonSystemMessages.length - preserveRecentMessages));
|
|
178
|
+
const compactedOlder = older.map((message) => {
|
|
179
|
+
if (compactToolMessages || message.role !== "tool") {
|
|
180
|
+
return compactMessage(message);
|
|
181
|
+
}
|
|
182
|
+
return message;
|
|
183
|
+
});
|
|
184
|
+
let result = [...systemMessages, ...compactedOlder, ...recent];
|
|
185
|
+
if (result.length > maxMessages) {
|
|
186
|
+
const preservedSystem = result.filter((m) => m.role === "system");
|
|
187
|
+
const others = result.filter((m) => m.role !== "system");
|
|
188
|
+
result = [...preservedSystem, ...others.slice(-(maxMessages - preservedSystem.length))];
|
|
189
|
+
}
|
|
190
|
+
let totalChars = result.reduce((sum, message) => sum + estimateMessageChars(message), 0);
|
|
191
|
+
if (totalChars <= maxTotalChars) {
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
const finalMessages = [];
|
|
195
|
+
const systems = result.filter((m) => m.role === "system");
|
|
196
|
+
const others = result.filter((m) => m.role !== "system");
|
|
197
|
+
for (const msg of systems) {
|
|
198
|
+
finalMessages.push(compactMessage(msg));
|
|
199
|
+
}
|
|
200
|
+
for (const msg of others.slice(-preserveRecentMessages)) {
|
|
201
|
+
finalMessages.push(compactMessage(msg));
|
|
202
|
+
}
|
|
203
|
+
totalChars = finalMessages.reduce((sum, message) => sum + estimateMessageChars(message), 0);
|
|
204
|
+
if (totalChars <= maxTotalChars) {
|
|
205
|
+
return finalMessages;
|
|
206
|
+
}
|
|
207
|
+
const trimmed = [];
|
|
208
|
+
let running = 0;
|
|
209
|
+
for (const msg of [...systems, ...others].reverse()) {
|
|
210
|
+
const compacted = compactMessage(msg);
|
|
211
|
+
const size = estimateMessageChars(compacted);
|
|
212
|
+
if (running + size > maxTotalChars) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
trimmed.push(compacted);
|
|
216
|
+
running += size;
|
|
217
|
+
}
|
|
218
|
+
return trimmed.reverse();
|
|
219
|
+
}
|
|
220
|
+
export function prepareMessagesForModel(messages, options = {}) {
|
|
221
|
+
return trimMessages(messages, options);
|
|
222
|
+
}
|
|
223
|
+
export function serializeToolMessage(result) {
|
|
224
|
+
const compacted = compactToolResult(result);
|
|
225
|
+
if (compacted && typeof compacted === "object" && !Array.isArray(compacted)) {
|
|
226
|
+
return JSON.stringify({
|
|
227
|
+
...compacted,
|
|
228
|
+
_meta: {
|
|
229
|
+
note: "Tool output may be compacted or truncated. If visibility is partial, say so explicitly.",
|
|
230
|
+
},
|
|
231
|
+
}, null, 2);
|
|
232
|
+
}
|
|
233
|
+
return JSON.stringify({
|
|
234
|
+
value: compacted,
|
|
235
|
+
_meta: {
|
|
236
|
+
note: "Tool output may be compacted or truncated. If visibility is partial, say so explicitly.",
|
|
237
|
+
},
|
|
238
|
+
}, null, 2);
|
|
239
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function parseAgentResponse(raw) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error("Agent response is not valid JSON.");
|
|
11
|
+
}
|
|
12
|
+
if (!isPlainObject(parsed)) {
|
|
13
|
+
throw new Error("Agent response must be a JSON object.");
|
|
14
|
+
}
|
|
15
|
+
const type = parsed.type;
|
|
16
|
+
if (type === "tool_call") {
|
|
17
|
+
if (typeof parsed.toolName !== "string" || parsed.toolName.trim() === "") {
|
|
18
|
+
throw new Error('tool_call response must include a non-empty string field "toolName".');
|
|
19
|
+
}
|
|
20
|
+
if (!isPlainObject(parsed.args)) {
|
|
21
|
+
throw new Error('tool_call response must include an object field "args".');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
type: "tool_call",
|
|
25
|
+
toolName: parsed.toolName,
|
|
26
|
+
args: parsed.args,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (type === "final") {
|
|
30
|
+
if (typeof parsed.message !== "string") {
|
|
31
|
+
throw new Error('final response must include a string field "message".');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
type: "final",
|
|
35
|
+
message: parsed.message,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (type === "ask_confirmation") {
|
|
39
|
+
if (typeof parsed.message !== "string" || parsed.message.trim() === "") {
|
|
40
|
+
throw new Error('ask_confirmation response must include a non-empty string field "message".');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
type: "ask_confirmation",
|
|
44
|
+
message: parsed.message,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Unsupported agent response type: ${String(type)}`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function buildToolsPrompt(tools) {
|
|
2
|
+
return Array.from(tools.values())
|
|
3
|
+
.map((tool) => `- ${tool.name}: ${tool.description}`)
|
|
4
|
+
.join("\n");
|
|
5
|
+
}
|
|
6
|
+
export function makeSystemPrompt(tools) {
|
|
7
|
+
return [
|
|
8
|
+
"You are a local coding agent.",
|
|
9
|
+
"You may use tools to inspect files, run commands, inspect git state, fetch web content, and complete tasks.",
|
|
10
|
+
"You must respond with exactly one JSON object and nothing else.",
|
|
11
|
+
'Use {"type":"final","message":"..."} for final answers.',
|
|
12
|
+
'Use {"type":"tool_call","toolName":"...","args":{}} for tool calls.',
|
|
13
|
+
"Call only one tool at a time.",
|
|
14
|
+
"Prefer using tools when the task requires checking files, git state, commands, or web content.",
|
|
15
|
+
"If you need repository status, use git_status.",
|
|
16
|
+
"If you need concrete code changes, use git_diff.",
|
|
17
|
+
"If you need file contents, use read_file.",
|
|
18
|
+
"Do not infer exact filenames, renames, code edits, or configuration details unless they appear in tool output or prior messages.",
|
|
19
|
+
"When tool output is incomplete or truncated, explicitly say the information may be incomplete.",
|
|
20
|
+
"Distinguish observed facts from tentative inferences.",
|
|
21
|
+
"Only make claims supported by tool outputs or prior messages.",
|
|
22
|
+
"Final answers must be in Simplified Chinese unless the user explicitly asks for another language.",
|
|
23
|
+
"",
|
|
24
|
+
"Available tools:",
|
|
25
|
+
buildToolsPrompt(tools),
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
function printApprovalMenu(message) {
|
|
4
|
+
console.log(message);
|
|
5
|
+
console.log("");
|
|
6
|
+
console.log("请选择:");
|
|
7
|
+
console.log(" 1) 不允许");
|
|
8
|
+
console.log(" 2) 允许");
|
|
9
|
+
console.log(" 3) 总是允许");
|
|
10
|
+
console.log("");
|
|
11
|
+
}
|
|
12
|
+
function normalizeAnswer(answer) {
|
|
13
|
+
const value = answer.trim().toLowerCase();
|
|
14
|
+
if (value === "1" || value === "n" || value === "no") {
|
|
15
|
+
return "deny";
|
|
16
|
+
}
|
|
17
|
+
if (value === "2" || value === "y" || value === "yes") {
|
|
18
|
+
return "allow-once";
|
|
19
|
+
}
|
|
20
|
+
if (value === "3" ||
|
|
21
|
+
value === "a" ||
|
|
22
|
+
value === "always" ||
|
|
23
|
+
value === "always-allow") {
|
|
24
|
+
return "allow-always";
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export async function readApproval(message) {
|
|
29
|
+
const rl = readline.createInterface({ input, output });
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
printApprovalMenu(message);
|
|
33
|
+
const answer = await rl.question("输入 1/2/3: ");
|
|
34
|
+
const decision = normalizeAnswer(answer);
|
|
35
|
+
if (decision) {
|
|
36
|
+
console.log("");
|
|
37
|
+
return decision;
|
|
38
|
+
}
|
|
39
|
+
console.log("无效输入,请输入 1、2 或 3。\n");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
rl.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
export async function readConfirmation(message) {
|
|
3
|
+
const rl = readline.createInterface({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stdout,
|
|
6
|
+
});
|
|
7
|
+
try {
|
|
8
|
+
const answer = await new Promise((resolve) => {
|
|
9
|
+
rl.question(`${message} [y/N] `, resolve);
|
|
10
|
+
});
|
|
11
|
+
const normalized = answer.trim().toLowerCase();
|
|
12
|
+
return normalized === "y" || normalized === "yes";
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
rl.close();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { buildSystemPrompt } from "./build-system-prompt.js";
|
|
2
|
+
import { parseAgentResponse } from "./parse-agent-response.js";
|
|
3
|
+
function stringifyForModel(value) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function trimText(text, maxChars) {
|
|
12
|
+
if (text.length <= maxChars) {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
return `${text.slice(0, maxChars)}\n...[truncated]`;
|
|
16
|
+
}
|
|
17
|
+
function formatToolResultForModel(result, maxChars = 12000) {
|
|
18
|
+
return trimText(stringifyForModel(result), maxChars);
|
|
19
|
+
}
|
|
20
|
+
async function persistMessages(messages, onMessagesUpdated) {
|
|
21
|
+
if (onMessagesUpdated) {
|
|
22
|
+
await onMessagesUpdated(messages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function runLocalAgentLoop(params) {
|
|
26
|
+
const { userInput, modelClient, tools, eventBus, maxSteps = 8, previousMessages = [], onMessagesUpdated, requestApproval, } = params;
|
|
27
|
+
let approvalMode = params.approvalMode ?? "ask";
|
|
28
|
+
let allowOneHighRiskToolCall = approvalMode === "always-allow";
|
|
29
|
+
eventBus.emit({
|
|
30
|
+
type: "run_start",
|
|
31
|
+
input: userInput,
|
|
32
|
+
});
|
|
33
|
+
const historyMessages = previousMessages.filter((message) => message.role !== "system");
|
|
34
|
+
const messages = [
|
|
35
|
+
{
|
|
36
|
+
role: "system",
|
|
37
|
+
content: buildSystemPrompt(Array.from(tools.values())),
|
|
38
|
+
},
|
|
39
|
+
...historyMessages,
|
|
40
|
+
{
|
|
41
|
+
role: "user",
|
|
42
|
+
content: userInput,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
46
|
+
for (let step = 1; step <= maxSteps; step += 1) {
|
|
47
|
+
let rawOutput;
|
|
48
|
+
try {
|
|
49
|
+
rawOutput = await modelClient.generate(messages);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
53
|
+
eventBus.emit({
|
|
54
|
+
type: "run_error",
|
|
55
|
+
step,
|
|
56
|
+
stage: "model_generate",
|
|
57
|
+
error: errorMessage,
|
|
58
|
+
});
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
eventBus.emit({
|
|
62
|
+
type: "model_raw",
|
|
63
|
+
text: rawOutput,
|
|
64
|
+
step,
|
|
65
|
+
});
|
|
66
|
+
messages.push({
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: rawOutput,
|
|
69
|
+
});
|
|
70
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = parseAgentResponse(rawOutput);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
77
|
+
eventBus.emit({
|
|
78
|
+
type: "run_error",
|
|
79
|
+
step,
|
|
80
|
+
stage: "parse_agent_response",
|
|
81
|
+
error: errorMessage,
|
|
82
|
+
});
|
|
83
|
+
messages.push({
|
|
84
|
+
role: "user",
|
|
85
|
+
content: [
|
|
86
|
+
"Your previous response could not be parsed.",
|
|
87
|
+
`Parse error: ${errorMessage}`,
|
|
88
|
+
"You must respond with exactly one valid JSON object.",
|
|
89
|
+
'Allowed formats:',
|
|
90
|
+
'- {"type":"tool_call","toolName":"<tool>","args":{}}',
|
|
91
|
+
'- {"type":"final","message":"<answer>"}',
|
|
92
|
+
'- {"type":"ask_confirmation","message":"<question>"}',
|
|
93
|
+
].join("\n"),
|
|
94
|
+
});
|
|
95
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (response.type === "final") {
|
|
99
|
+
eventBus.emit({
|
|
100
|
+
type: "assistant",
|
|
101
|
+
message: response.message,
|
|
102
|
+
});
|
|
103
|
+
eventBus.emit({
|
|
104
|
+
type: "run_end",
|
|
105
|
+
reason: "final",
|
|
106
|
+
step,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
finalMessage: response.message,
|
|
110
|
+
approvalMode,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (response.type === "ask_confirmation") {
|
|
114
|
+
let decision;
|
|
115
|
+
try {
|
|
116
|
+
if (approvalMode === "always-allow") {
|
|
117
|
+
decision = "allow-once";
|
|
118
|
+
}
|
|
119
|
+
else if (requestApproval) {
|
|
120
|
+
decision = await requestApproval(response.message);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
decision = "deny";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
128
|
+
eventBus.emit({
|
|
129
|
+
type: "run_error",
|
|
130
|
+
step,
|
|
131
|
+
stage: "confirmation",
|
|
132
|
+
error: errorMessage,
|
|
133
|
+
});
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
if (decision === "allow-always") {
|
|
137
|
+
approvalMode = "always-allow";
|
|
138
|
+
allowOneHighRiskToolCall = true;
|
|
139
|
+
console.log("已启用“总是允许”模式,本会话后续 confirm / dangerous 操作将自动执行。输入 /reset 可恢复逐次确认。\n");
|
|
140
|
+
}
|
|
141
|
+
else if (decision === "allow-once") {
|
|
142
|
+
allowOneHighRiskToolCall = true;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
allowOneHighRiskToolCall = false;
|
|
146
|
+
}
|
|
147
|
+
messages.push({
|
|
148
|
+
role: "user",
|
|
149
|
+
content: decision === "deny"
|
|
150
|
+
? `The user denied your request: ${response.message}. Do not perform that action. Offer a safer alternative or stop.`
|
|
151
|
+
: decision === "allow-always"
|
|
152
|
+
? `The user approved your request and enabled always-allow mode for this session: ${response.message}`
|
|
153
|
+
: `The user approved your request: ${response.message}`,
|
|
154
|
+
});
|
|
155
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (response.type === "tool_call") {
|
|
159
|
+
const tool = tools.get(response.toolName);
|
|
160
|
+
if (!tool) {
|
|
161
|
+
const errorMessage = `Unknown tool "${response.toolName}".`;
|
|
162
|
+
eventBus.emit({
|
|
163
|
+
type: "tool_error",
|
|
164
|
+
toolName: response.toolName,
|
|
165
|
+
error: errorMessage,
|
|
166
|
+
step,
|
|
167
|
+
});
|
|
168
|
+
messages.push({
|
|
169
|
+
role: "user",
|
|
170
|
+
content: [
|
|
171
|
+
`Tool execution failed: ${errorMessage}`,
|
|
172
|
+
"Use only the available tools listed in the system prompt.",
|
|
173
|
+
].join("\n"),
|
|
174
|
+
});
|
|
175
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const needsConfirmation = tool.riskLevel === "confirm" || tool.riskLevel === "dangerous";
|
|
179
|
+
if (needsConfirmation &&
|
|
180
|
+
approvalMode !== "always-allow" &&
|
|
181
|
+
!allowOneHighRiskToolCall) {
|
|
182
|
+
const errorMessage = `Tool "${tool.name}" requires confirmation before execution. Ask for confirmation first.`;
|
|
183
|
+
eventBus.emit({
|
|
184
|
+
type: "tool_error",
|
|
185
|
+
toolName: response.toolName,
|
|
186
|
+
error: errorMessage,
|
|
187
|
+
step,
|
|
188
|
+
});
|
|
189
|
+
messages.push({
|
|
190
|
+
role: "user",
|
|
191
|
+
content: [
|
|
192
|
+
errorMessage,
|
|
193
|
+
"Do not execute this tool yet.",
|
|
194
|
+
"Ask the user for confirmation first.",
|
|
195
|
+
].join("\n"),
|
|
196
|
+
});
|
|
197
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
eventBus.emit({
|
|
201
|
+
type: "tool_start",
|
|
202
|
+
toolName: response.toolName,
|
|
203
|
+
args: response.args,
|
|
204
|
+
step,
|
|
205
|
+
});
|
|
206
|
+
let result;
|
|
207
|
+
try {
|
|
208
|
+
result = await tool.execute(response.args);
|
|
209
|
+
if (needsConfirmation && approvalMode !== "always-allow") {
|
|
210
|
+
allowOneHighRiskToolCall = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (needsConfirmation && approvalMode !== "always-allow") {
|
|
215
|
+
allowOneHighRiskToolCall = false;
|
|
216
|
+
}
|
|
217
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
218
|
+
eventBus.emit({
|
|
219
|
+
type: "tool_error",
|
|
220
|
+
toolName: response.toolName,
|
|
221
|
+
error: errorMessage,
|
|
222
|
+
step,
|
|
223
|
+
});
|
|
224
|
+
messages.push({
|
|
225
|
+
role: "user",
|
|
226
|
+
content: [
|
|
227
|
+
`Tool "${response.toolName}" crashed.`,
|
|
228
|
+
`Error: ${errorMessage}`,
|
|
229
|
+
"Revise your plan. You may call another tool or return a final response.",
|
|
230
|
+
].join("\n"),
|
|
231
|
+
});
|
|
232
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
eventBus.emit({
|
|
236
|
+
type: "tool_end",
|
|
237
|
+
toolName: response.toolName,
|
|
238
|
+
success: result.success,
|
|
239
|
+
result: result.success ? result.output : result.error,
|
|
240
|
+
step,
|
|
241
|
+
});
|
|
242
|
+
if (result.success) {
|
|
243
|
+
messages.push({
|
|
244
|
+
role: "user",
|
|
245
|
+
content: [
|
|
246
|
+
`Tool "${response.toolName}" completed successfully.`,
|
|
247
|
+
`Result: ${formatToolResultForModel(result.output)}`,
|
|
248
|
+
"Continue the task. If it is complete, return a final response.",
|
|
249
|
+
].join("\n"),
|
|
250
|
+
});
|
|
251
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
messages.push({
|
|
255
|
+
role: "user",
|
|
256
|
+
content: [
|
|
257
|
+
`Tool "${response.toolName}" failed.`,
|
|
258
|
+
`Error: ${result.error}`,
|
|
259
|
+
"Revise your plan. You may call another tool or return a final response.",
|
|
260
|
+
].join("\n"),
|
|
261
|
+
});
|
|
262
|
+
await persistMessages(messages, onMessagesUpdated);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
eventBus.emit({
|
|
267
|
+
type: "run_end",
|
|
268
|
+
reason: "max_steps_exceeded",
|
|
269
|
+
step: maxSteps,
|
|
270
|
+
});
|
|
271
|
+
throw new Error(`Agent loop exceeded maximum number of steps (${maxSteps}).`);
|
|
272
|
+
}
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function getHelpText() {
|
|
2
|
+
return `
|
|
3
|
+
my-agent - local CLI coding agent
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
npm run dev -- [options] "your task"
|
|
7
|
+
npm run start -- [options] "your task"
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--session <id> Use a persistent session id
|
|
11
|
+
--workspace <path> Set workspace root
|
|
12
|
+
--max-steps <n> Limit agent loop steps
|
|
13
|
+
--model <name> Override model name
|
|
14
|
+
--json Output events as JSON lines
|
|
15
|
+
--quiet Reduce console output
|
|
16
|
+
--help, -h Show help
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
npm run dev -- "read package.json"
|
|
20
|
+
npm run dev -- --session demo "what do you know about this project?"
|
|
21
|
+
npm run dev -- --max-steps 4 "show git diff and summarize changes"
|
|
22
|
+
npm run dev -- --json "read tsconfig.json"
|
|
23
|
+
`.trim();
|
|
24
|
+
}
|