@yachiyo-5i/xlyra-agent 0.2.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/LICENSE +661 -0
- package/README.md +306 -0
- package/dist/chunk-QH6SEOO6.js +3590 -0
- package/dist/chunk-QH6SEOO6.js.map +1 -0
- package/dist/cli.cjs +3833 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +287 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +3703 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1171 -0
- package/dist/index.d.ts +1171 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,3590 @@
|
|
|
1
|
+
// src/llm/models.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var toolCallSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
name: z.string(),
|
|
6
|
+
/** 参数 JSON 原文(流式归并后的完整串;可能不是合法 JSON,由校验阶段兜底) */
|
|
7
|
+
raw_arguments: z.string()
|
|
8
|
+
});
|
|
9
|
+
var chatMessageSchema = z.object({
|
|
10
|
+
role: z.enum(["system", "user", "assistant", "tool"]),
|
|
11
|
+
content: z.string().default(""),
|
|
12
|
+
tool_calls: z.array(toolCallSchema).optional(),
|
|
13
|
+
tool_call_id: z.string().optional(),
|
|
14
|
+
name: z.string().optional(),
|
|
15
|
+
is_error: z.boolean().optional(),
|
|
16
|
+
thinking: z.string().optional(),
|
|
17
|
+
thinking_signature: z.string().optional()
|
|
18
|
+
});
|
|
19
|
+
function messageText(message) {
|
|
20
|
+
return message.content ?? "";
|
|
21
|
+
}
|
|
22
|
+
var tokenUsageSchema = z.object({
|
|
23
|
+
prompt_tokens: z.number().default(0),
|
|
24
|
+
completion_tokens: z.number().default(0),
|
|
25
|
+
total_tokens: z.number().default(0),
|
|
26
|
+
cache_read_tokens: z.number().default(0)
|
|
27
|
+
});
|
|
28
|
+
function emptyUsage() {
|
|
29
|
+
return { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cache_read_tokens: 0 };
|
|
30
|
+
}
|
|
31
|
+
function addUsage(total, step) {
|
|
32
|
+
return {
|
|
33
|
+
prompt_tokens: total.prompt_tokens + step.prompt_tokens,
|
|
34
|
+
completion_tokens: total.completion_tokens + step.completion_tokens,
|
|
35
|
+
total_tokens: total.total_tokens + step.total_tokens,
|
|
36
|
+
cache_read_tokens: total.cache_read_tokens + step.cache_read_tokens
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var modelSettingsSchema = z.object({
|
|
40
|
+
temperature: z.number().optional(),
|
|
41
|
+
max_tokens: z.number().optional(),
|
|
42
|
+
/** 思考强度:映射到 Anthropic thinking budget / OpenAI reasoning effort */
|
|
43
|
+
reasoning_effort: z.enum(["low", "medium", "high"]).optional()
|
|
44
|
+
});
|
|
45
|
+
function responseToMessage(response) {
|
|
46
|
+
const message = { role: "assistant", content: response.content };
|
|
47
|
+
if (response.tool_calls.length > 0) message.tool_calls = response.tool_calls;
|
|
48
|
+
if (response.thinking) message.thinking = response.thinking;
|
|
49
|
+
if (response.thinking_signature) message.thinking_signature = response.thinking_signature;
|
|
50
|
+
return message;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/llm/errors.ts
|
|
54
|
+
var LlmError = class extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "LlmError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/llm/sse.ts
|
|
62
|
+
async function* parseSse(body) {
|
|
63
|
+
const reader = body.getReader();
|
|
64
|
+
const decoder = new TextDecoder();
|
|
65
|
+
let buffer = "";
|
|
66
|
+
let eventName = "";
|
|
67
|
+
let dataLines = [];
|
|
68
|
+
const flush = () => {
|
|
69
|
+
if (dataLines.length === 0) {
|
|
70
|
+
eventName = "";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const evt = { event: eventName, data: dataLines.join("\n") };
|
|
74
|
+
eventName = "";
|
|
75
|
+
dataLines = [];
|
|
76
|
+
return evt;
|
|
77
|
+
};
|
|
78
|
+
const handleLine = function* (line) {
|
|
79
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
80
|
+
if (line === "") {
|
|
81
|
+
const evt = flush();
|
|
82
|
+
if (evt) yield evt;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (line.startsWith(":")) return;
|
|
86
|
+
const colon = line.indexOf(":");
|
|
87
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
88
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
89
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
90
|
+
if (field === "event") eventName = value;
|
|
91
|
+
else if (field === "data") dataLines.push(value);
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
for (; ; ) {
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (done) break;
|
|
97
|
+
buffer += decoder.decode(value, { stream: true });
|
|
98
|
+
let nl;
|
|
99
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
100
|
+
const line = buffer.slice(0, nl);
|
|
101
|
+
buffer = buffer.slice(nl + 1);
|
|
102
|
+
yield* handleLine(line);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (buffer.length > 0) yield* handleLine(buffer);
|
|
106
|
+
const tail = flush();
|
|
107
|
+
if (tail) yield tail;
|
|
108
|
+
} finally {
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/llm/stream-assembler.ts
|
|
114
|
+
var ToolCallBuffer = class {
|
|
115
|
+
buffers = /* @__PURE__ */ new Map();
|
|
116
|
+
/** 名称确定的第一刻登记;返回仅含 id/name 的 ToolCall(供 toolcall_start 事件) */
|
|
117
|
+
start(key, id, name) {
|
|
118
|
+
this.buffers.set(key, { id, name, raw: "" });
|
|
119
|
+
return { id, name, raw_arguments: "" };
|
|
120
|
+
}
|
|
121
|
+
/** 追加参数分片;返回当前快照(供 toolcall_delta 事件携带归属信息) */
|
|
122
|
+
append(key, delta) {
|
|
123
|
+
const entry = this.buffers.get(key);
|
|
124
|
+
if (!entry) return null;
|
|
125
|
+
entry.raw += delta;
|
|
126
|
+
return { id: entry.id, name: entry.name, raw_arguments: entry.raw };
|
|
127
|
+
}
|
|
128
|
+
/** 结束归并;fullRaw 提供时以全量为准(Responses 的 .done 事件自带全量串) */
|
|
129
|
+
finish(key, fullRaw) {
|
|
130
|
+
const entry = this.buffers.get(key);
|
|
131
|
+
if (!entry) return null;
|
|
132
|
+
this.buffers.delete(key);
|
|
133
|
+
return { id: entry.id, name: entry.name, raw_arguments: fullRaw ?? entry.raw };
|
|
134
|
+
}
|
|
135
|
+
/** 流异常终止时丢弃全部未完成归并 */
|
|
136
|
+
clear() {
|
|
137
|
+
this.buffers.clear();
|
|
138
|
+
}
|
|
139
|
+
get pendingCount() {
|
|
140
|
+
return this.buffers.size;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/llm/protocols/messages.ts
|
|
145
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
146
|
+
var THINKING_BUDGET = { low: 2048, medium: 8192, high: 24576 };
|
|
147
|
+
function toAnthropicMessages(messages) {
|
|
148
|
+
const systemParts = [];
|
|
149
|
+
const out = [];
|
|
150
|
+
const flushToolResults = (pending) => {
|
|
151
|
+
if (pending.length > 0) {
|
|
152
|
+
out.push({ role: "user", content: pending.splice(0, pending.length) });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const pendingToolResults = [];
|
|
156
|
+
for (const m of messages) {
|
|
157
|
+
if (m.role === "system") {
|
|
158
|
+
if (m.content) systemParts.push(m.content);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (m.role === "tool") {
|
|
162
|
+
const block = {
|
|
163
|
+
type: "tool_result",
|
|
164
|
+
tool_use_id: m.tool_call_id ?? "",
|
|
165
|
+
content: m.content
|
|
166
|
+
};
|
|
167
|
+
if (m.is_error) block.is_error = true;
|
|
168
|
+
pendingToolResults.push(block);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
flushToolResults(pendingToolResults);
|
|
172
|
+
if (m.role === "user") {
|
|
173
|
+
out.push({ role: "user", content: m.content });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const blocks = [];
|
|
177
|
+
if (m.thinking && m.thinking_signature) {
|
|
178
|
+
blocks.push({ type: "thinking", thinking: m.thinking, signature: m.thinking_signature });
|
|
179
|
+
}
|
|
180
|
+
if (m.content) blocks.push({ type: "text", text: m.content });
|
|
181
|
+
for (const tc of m.tool_calls ?? []) {
|
|
182
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: parseArgs(tc.raw_arguments) });
|
|
183
|
+
}
|
|
184
|
+
out.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] });
|
|
185
|
+
}
|
|
186
|
+
flushToolResults(pendingToolResults);
|
|
187
|
+
return { system: systemParts.length ? systemParts.join("\n\n") : void 0, messages: out };
|
|
188
|
+
}
|
|
189
|
+
function parseArgs(raw) {
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(raw);
|
|
192
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
193
|
+
} catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function toAnthropicTools(tools) {
|
|
198
|
+
return tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
|
|
199
|
+
}
|
|
200
|
+
var AnthropicMessagesProtocol = class {
|
|
201
|
+
name = "anthropic-messages";
|
|
202
|
+
baseUrl;
|
|
203
|
+
apiKey;
|
|
204
|
+
provider;
|
|
205
|
+
apiVersion;
|
|
206
|
+
fetchImpl;
|
|
207
|
+
constructor(opts) {
|
|
208
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
209
|
+
this.apiKey = opts.apiKey;
|
|
210
|
+
this.provider = opts.providerName ?? "anthropic";
|
|
211
|
+
this.apiVersion = opts.apiVersion ?? "2023-06-01";
|
|
212
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
213
|
+
}
|
|
214
|
+
async *chatStream(req) {
|
|
215
|
+
const { system, messages } = toAnthropicMessages(req.messages);
|
|
216
|
+
const maxTokens = req.settings.max_tokens ?? DEFAULT_MAX_TOKENS;
|
|
217
|
+
const body = { model: req.model, max_tokens: maxTokens, messages, stream: true };
|
|
218
|
+
if (system) body.system = system;
|
|
219
|
+
if (req.tools?.length) body.tools = toAnthropicTools(req.tools);
|
|
220
|
+
if (req.settings.temperature !== void 0) body.temperature = req.settings.temperature;
|
|
221
|
+
if (req.settings.reasoning_effort) {
|
|
222
|
+
const budget = Math.min(THINKING_BUDGET[req.settings.reasoning_effort], maxTokens - 1);
|
|
223
|
+
if (budget >= 1024) body.thinking = { type: "enabled", budget_tokens: budget };
|
|
224
|
+
}
|
|
225
|
+
let res;
|
|
226
|
+
try {
|
|
227
|
+
res = await this.fetchImpl(`${this.baseUrl}/v1/messages`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"content-type": "application/json",
|
|
231
|
+
"x-api-key": this.apiKey,
|
|
232
|
+
"anthropic-version": this.apiVersion
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(body),
|
|
235
|
+
signal: req.signal ?? null
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (req.signal?.aborted) return;
|
|
239
|
+
yield { type: "error", error: `\u8BF7\u6C42\u5931\u8D25\uFF08\u7F51\u7EDC\u9519\u8BEF\uFF09\uFF1A${errMsg(err)}` };
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!res.ok || !res.body) {
|
|
243
|
+
const detail = await res.text().catch(() => "");
|
|
244
|
+
yield {
|
|
245
|
+
type: "error",
|
|
246
|
+
error: `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A${detail.slice(0, 500) || res.statusText}`
|
|
247
|
+
};
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const blockKinds = /* @__PURE__ */ new Map();
|
|
251
|
+
const calls = new ToolCallBuffer();
|
|
252
|
+
const finishedCalls = [];
|
|
253
|
+
let text = "";
|
|
254
|
+
let thinking = "";
|
|
255
|
+
let thinkingSignature = null;
|
|
256
|
+
let stopReason = null;
|
|
257
|
+
const usage = { input: 0, output: 0, cacheRead: 0 };
|
|
258
|
+
let model = "";
|
|
259
|
+
let sawTerminal = false;
|
|
260
|
+
try {
|
|
261
|
+
for await (const sse of parseSse(res.body)) {
|
|
262
|
+
const evt = safeParse(sse.data);
|
|
263
|
+
if (!evt) continue;
|
|
264
|
+
const type = String(evt.type ?? "");
|
|
265
|
+
if (type === "message_start") {
|
|
266
|
+
const msg = evt.message ?? {};
|
|
267
|
+
model = String(msg.model ?? "");
|
|
268
|
+
const u = msg.usage ?? {};
|
|
269
|
+
usage.input = Number(u.input_tokens ?? 0);
|
|
270
|
+
usage.cacheRead = Number(u.cache_read_input_tokens ?? 0);
|
|
271
|
+
} else if (type === "content_block_start") {
|
|
272
|
+
const index = Number(evt.index ?? 0);
|
|
273
|
+
const block = evt.content_block ?? {};
|
|
274
|
+
const kind = String(block.type ?? "");
|
|
275
|
+
blockKinds.set(index, kind);
|
|
276
|
+
if (kind === "tool_use") {
|
|
277
|
+
const tc = calls.start(String(index), String(block.id ?? ""), String(block.name ?? ""));
|
|
278
|
+
yield { type: "toolcall_start", tool_call: tc };
|
|
279
|
+
}
|
|
280
|
+
} else if (type === "content_block_delta") {
|
|
281
|
+
const index = Number(evt.index ?? 0);
|
|
282
|
+
const delta = evt.delta ?? {};
|
|
283
|
+
const kind = blockKinds.get(index);
|
|
284
|
+
const deltaType = String(delta.type ?? "");
|
|
285
|
+
if (deltaType === "text_delta" && kind === "text") {
|
|
286
|
+
const d = String(delta.text ?? "");
|
|
287
|
+
text += d;
|
|
288
|
+
yield { type: "text_delta", delta: d };
|
|
289
|
+
} else if (deltaType === "thinking_delta" && kind === "thinking") {
|
|
290
|
+
const d = String(delta.thinking ?? "");
|
|
291
|
+
thinking += d;
|
|
292
|
+
yield { type: "thinking_delta", delta: d };
|
|
293
|
+
} else if (deltaType === "signature_delta" && kind === "thinking") {
|
|
294
|
+
thinkingSignature = String(delta.signature ?? "") || thinkingSignature;
|
|
295
|
+
} else if (deltaType === "input_json_delta" && kind === "tool_use") {
|
|
296
|
+
const d = String(delta.partial_json ?? "");
|
|
297
|
+
const tc = calls.append(String(index), d);
|
|
298
|
+
if (tc) yield { type: "toolcall_delta", tool_call: tc, delta: d };
|
|
299
|
+
} else if (deltaType) {
|
|
300
|
+
yield { type: "error", error: `\u6D41\u635F\u574F\uFF1Ablock ${index}\uFF08${kind}\uFF09\u6536\u5230 ${deltaType}` };
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
} else if (type === "content_block_stop") {
|
|
304
|
+
const index = Number(evt.index ?? 0);
|
|
305
|
+
if (blockKinds.get(index) === "tool_use") {
|
|
306
|
+
const tc = calls.finish(String(index));
|
|
307
|
+
if (tc) {
|
|
308
|
+
finishedCalls.push(tc);
|
|
309
|
+
yield { type: "toolcall_end", tool_call: tc };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
blockKinds.delete(index);
|
|
313
|
+
} else if (type === "message_delta") {
|
|
314
|
+
stopReason = String(evt.delta?.stop_reason ?? "") || stopReason;
|
|
315
|
+
usage.output = Number(evt.usage?.output_tokens ?? usage.output);
|
|
316
|
+
} else if (type === "message_stop") {
|
|
317
|
+
sawTerminal = true;
|
|
318
|
+
yield {
|
|
319
|
+
type: "done",
|
|
320
|
+
response: {
|
|
321
|
+
content: text,
|
|
322
|
+
thinking: thinking || null,
|
|
323
|
+
thinking_signature: thinkingSignature,
|
|
324
|
+
tool_calls: finishedCalls,
|
|
325
|
+
usage: {
|
|
326
|
+
prompt_tokens: usage.input,
|
|
327
|
+
completion_tokens: usage.output,
|
|
328
|
+
total_tokens: usage.input + usage.output,
|
|
329
|
+
cache_read_tokens: usage.cacheRead
|
|
330
|
+
},
|
|
331
|
+
finish_reason: stopReason,
|
|
332
|
+
model,
|
|
333
|
+
provider: this.provider
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
return;
|
|
337
|
+
} else if (type === "error") {
|
|
338
|
+
const err = evt.error?.message;
|
|
339
|
+
yield { type: "error", error: `\u6A21\u578B\u8C03\u7528\u5931\u8D25\uFF1A${String(err ?? "\u539F\u56E0\u672A\u77E5")}` };
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (req.signal?.aborted) return;
|
|
345
|
+
yield { type: "error", error: `\u6D41\u8BFB\u53D6\u5931\u8D25\uFF1A${errMsg(err)}` };
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!sawTerminal) yield { type: "error", error: "\u6A21\u578B\u6D41\u5F02\u5E38\u7EC8\u6B62\uFF0C\u672A\u8FD4\u56DE\u7ED3\u679C" };
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
function safeParse(data) {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(data);
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function errMsg(err) {
|
|
359
|
+
return err instanceof Error ? err.message : String(err);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/llm/protocols/responses.ts
|
|
363
|
+
function toResponsesInput(messages) {
|
|
364
|
+
const instructions = [];
|
|
365
|
+
const input = [];
|
|
366
|
+
for (const m of messages) {
|
|
367
|
+
if (m.role === "system") {
|
|
368
|
+
if (m.content) instructions.push(m.content);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (m.role === "user") {
|
|
372
|
+
input.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (m.role === "assistant") {
|
|
376
|
+
if (m.content) {
|
|
377
|
+
input.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: m.content }] });
|
|
378
|
+
}
|
|
379
|
+
for (const tc of m.tool_calls ?? []) {
|
|
380
|
+
input.push({ type: "function_call", call_id: tc.id, name: tc.name, arguments: tc.raw_arguments });
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
input.push({ type: "function_call_output", call_id: m.tool_call_id ?? "", output: m.content });
|
|
385
|
+
}
|
|
386
|
+
return { instructions: instructions.length ? instructions.join("\n\n") : void 0, input };
|
|
387
|
+
}
|
|
388
|
+
function toResponsesTools(tools) {
|
|
389
|
+
return tools.map((t) => ({
|
|
390
|
+
type: "function",
|
|
391
|
+
name: t.name,
|
|
392
|
+
description: t.description,
|
|
393
|
+
parameters: t.parameters
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
function responseFromCompleted(response, provider) {
|
|
397
|
+
let content = "";
|
|
398
|
+
let thinking = "";
|
|
399
|
+
const toolCalls = [];
|
|
400
|
+
for (const item of response.output ?? []) {
|
|
401
|
+
if (item.type === "message") {
|
|
402
|
+
for (const part of item.content ?? []) {
|
|
403
|
+
if (part.type === "output_text" && typeof part.text === "string") content += part.text;
|
|
404
|
+
}
|
|
405
|
+
} else if (item.type === "reasoning") {
|
|
406
|
+
for (const part of item.summary ?? []) {
|
|
407
|
+
if (part.type === "summary_text" && typeof part.text === "string") {
|
|
408
|
+
thinking += (thinking ? "\n" : "") + part.text;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} else if (item.type === "function_call") {
|
|
412
|
+
toolCalls.push({
|
|
413
|
+
id: String(item.call_id ?? item.id ?? ""),
|
|
414
|
+
name: String(item.name ?? ""),
|
|
415
|
+
raw_arguments: String(item.arguments ?? "")
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const usage = response.usage ?? {};
|
|
420
|
+
const cached = usage.input_tokens_details?.cached_tokens;
|
|
421
|
+
return {
|
|
422
|
+
content,
|
|
423
|
+
thinking: thinking || null,
|
|
424
|
+
thinking_signature: null,
|
|
425
|
+
// Responses 的思考摘要无签名概念
|
|
426
|
+
tool_calls: toolCalls,
|
|
427
|
+
usage: {
|
|
428
|
+
prompt_tokens: Number(usage.input_tokens ?? 0),
|
|
429
|
+
completion_tokens: Number(usage.output_tokens ?? 0),
|
|
430
|
+
total_tokens: Number(usage.total_tokens ?? 0),
|
|
431
|
+
cache_read_tokens: Number(cached ?? 0)
|
|
432
|
+
},
|
|
433
|
+
finish_reason: response.status ?? "completed",
|
|
434
|
+
model: String(response.model ?? ""),
|
|
435
|
+
provider
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
var OpenAIResponsesProtocol = class {
|
|
439
|
+
name = "openai-responses";
|
|
440
|
+
baseUrl;
|
|
441
|
+
apiKey;
|
|
442
|
+
provider;
|
|
443
|
+
fetchImpl;
|
|
444
|
+
constructor(opts) {
|
|
445
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
446
|
+
this.apiKey = opts.apiKey;
|
|
447
|
+
this.provider = opts.providerName ?? "openai";
|
|
448
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
449
|
+
}
|
|
450
|
+
async *chatStream(req) {
|
|
451
|
+
const { instructions, input } = toResponsesInput(req.messages);
|
|
452
|
+
const body = { model: req.model, input, stream: true };
|
|
453
|
+
if (instructions) body.instructions = instructions;
|
|
454
|
+
if (req.tools?.length) body.tools = toResponsesTools(req.tools);
|
|
455
|
+
if (req.settings.max_tokens) body.max_output_tokens = req.settings.max_tokens;
|
|
456
|
+
if (req.settings.temperature !== void 0) body.temperature = req.settings.temperature;
|
|
457
|
+
if (req.settings.reasoning_effort) {
|
|
458
|
+
body.reasoning = { effort: req.settings.reasoning_effort, summary: "auto" };
|
|
459
|
+
body.include = ["reasoning.encrypted_content"];
|
|
460
|
+
}
|
|
461
|
+
let res;
|
|
462
|
+
try {
|
|
463
|
+
res = await this.fetchImpl(`${this.baseUrl}/responses`, {
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers: {
|
|
466
|
+
"content-type": "application/json",
|
|
467
|
+
authorization: `Bearer ${this.apiKey}`
|
|
468
|
+
},
|
|
469
|
+
body: JSON.stringify(body),
|
|
470
|
+
signal: req.signal ?? null
|
|
471
|
+
});
|
|
472
|
+
} catch (err) {
|
|
473
|
+
if (req.signal?.aborted) return;
|
|
474
|
+
yield { type: "error", error: `\u8BF7\u6C42\u5931\u8D25\uFF08\u7F51\u7EDC\u9519\u8BEF\uFF09\uFF1A${errMsg2(err)}` };
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!res.ok || !res.body) {
|
|
478
|
+
const detail = await res.text().catch(() => "");
|
|
479
|
+
yield {
|
|
480
|
+
type: "error",
|
|
481
|
+
error: `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A${detail.slice(0, 500) || res.statusText}`
|
|
482
|
+
};
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const calls = new ToolCallBuffer();
|
|
486
|
+
const callIdByItem = /* @__PURE__ */ new Map();
|
|
487
|
+
let streamedText = "";
|
|
488
|
+
let streamedThinking = "";
|
|
489
|
+
let doneCalls = [];
|
|
490
|
+
try {
|
|
491
|
+
for await (const sse of parseSse(res.body)) {
|
|
492
|
+
if (sse.data === "[DONE]") break;
|
|
493
|
+
const evt = safeParse2(sse.data);
|
|
494
|
+
if (!evt) continue;
|
|
495
|
+
const type = String(evt.type ?? "");
|
|
496
|
+
if (type === "response.output_item.added") {
|
|
497
|
+
const item = evt.item;
|
|
498
|
+
if (item?.type === "function_call") {
|
|
499
|
+
const itemId = String(item.id ?? "");
|
|
500
|
+
const callId = String(item.call_id ?? itemId);
|
|
501
|
+
callIdByItem.set(itemId, callId);
|
|
502
|
+
const tc = calls.start(itemId, callId, String(item.name ?? ""));
|
|
503
|
+
yield { type: "toolcall_start", tool_call: tc };
|
|
504
|
+
}
|
|
505
|
+
} else if (type === "response.function_call_arguments.delta") {
|
|
506
|
+
const itemId = String(evt.item_id ?? "");
|
|
507
|
+
const tc = calls.append(itemId, String(evt.delta ?? ""));
|
|
508
|
+
if (tc) yield { type: "toolcall_delta", tool_call: tc, delta: String(evt.delta ?? "") };
|
|
509
|
+
} else if (type === "response.function_call_arguments.done") {
|
|
510
|
+
const itemId = String(evt.item_id ?? "");
|
|
511
|
+
const tc = calls.finish(itemId, String(evt.arguments ?? ""));
|
|
512
|
+
if (tc) {
|
|
513
|
+
doneCalls.push(tc);
|
|
514
|
+
yield { type: "toolcall_end", tool_call: tc };
|
|
515
|
+
}
|
|
516
|
+
} else if (type === "response.output_item.done") {
|
|
517
|
+
const item = evt.item;
|
|
518
|
+
const itemId = String(item?.id ?? "");
|
|
519
|
+
if (item?.type === "function_call" && itemId && callIdByItem.has(itemId)) {
|
|
520
|
+
const tc = calls.finish(itemId, String(item.arguments ?? ""));
|
|
521
|
+
if (tc) {
|
|
522
|
+
doneCalls.push(tc);
|
|
523
|
+
yield { type: "toolcall_end", tool_call: tc };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} else if (type === "response.output_text.delta") {
|
|
527
|
+
const delta = String(evt.delta ?? "");
|
|
528
|
+
streamedText += delta;
|
|
529
|
+
yield { type: "text_delta", delta };
|
|
530
|
+
} else if (type === "response.reasoning_summary_text.delta") {
|
|
531
|
+
const delta = String(evt.delta ?? "");
|
|
532
|
+
streamedThinking += delta;
|
|
533
|
+
yield { type: "thinking_delta", delta };
|
|
534
|
+
} else if (type === "response.completed") {
|
|
535
|
+
const response = responseFromCompleted(evt.response ?? {}, this.provider);
|
|
536
|
+
yield { type: "done", response };
|
|
537
|
+
return;
|
|
538
|
+
} else if (type === "response.incomplete") {
|
|
539
|
+
if (doneCalls.length > 0) {
|
|
540
|
+
const response = responseFromCompleted(evt.response ?? {}, this.provider);
|
|
541
|
+
yield {
|
|
542
|
+
type: "done",
|
|
543
|
+
response: {
|
|
544
|
+
...response,
|
|
545
|
+
content: response.content || streamedText,
|
|
546
|
+
thinking: response.thinking ?? (streamedThinking || null),
|
|
547
|
+
tool_calls: doneCalls
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
} else {
|
|
551
|
+
const detail = evt.response?.incomplete_details?.reason;
|
|
552
|
+
yield { type: "error", error: `\u54CD\u5E94\u4E0D\u5B8C\u6574${detail ? `\uFF08${String(detail)}\uFF09` : ""}\uFF0C\u8BF7\u91CD\u8BD5` };
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
} else if (type === "response.failed") {
|
|
556
|
+
const err = evt.response?.error?.message;
|
|
557
|
+
yield { type: "error", error: `\u6A21\u578B\u8C03\u7528\u5931\u8D25\uFF1A${String(err ?? "\u539F\u56E0\u672A\u77E5")}` };
|
|
558
|
+
return;
|
|
559
|
+
} else if (type === "error") {
|
|
560
|
+
yield { type: "error", error: `\u6A21\u578B\u8C03\u7528\u5931\u8D25\uFF1A${String(evt.message ?? "\u539F\u56E0\u672A\u77E5")}` };
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} catch (err) {
|
|
565
|
+
if (req.signal?.aborted) return;
|
|
566
|
+
yield { type: "error", error: `\u6D41\u8BFB\u53D6\u5931\u8D25\uFF1A${errMsg2(err)}` };
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
yield { type: "error", error: "\u6A21\u578B\u6D41\u5F02\u5E38\u7EC8\u6B62\uFF0C\u672A\u8FD4\u56DE\u7ED3\u679C" };
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
function safeParse2(data) {
|
|
573
|
+
try {
|
|
574
|
+
return JSON.parse(data);
|
|
575
|
+
} catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function errMsg2(err) {
|
|
580
|
+
return err instanceof Error ? err.message : String(err);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/llm/router.ts
|
|
584
|
+
var BUILTIN_CONTEXT_WINDOWS = [
|
|
585
|
+
["claude-", 2e5],
|
|
586
|
+
["gpt-4o", 128e3],
|
|
587
|
+
["gpt-4.1", 1047576],
|
|
588
|
+
["gpt-5", 4e5],
|
|
589
|
+
["o1", 2e5],
|
|
590
|
+
["o3", 2e5],
|
|
591
|
+
["o4", 2e5]
|
|
592
|
+
];
|
|
593
|
+
function builtinContextWindow(modelId) {
|
|
594
|
+
for (const [prefix, win] of BUILTIN_CONTEXT_WINDOWS) {
|
|
595
|
+
if (modelId.startsWith(prefix)) return win;
|
|
596
|
+
}
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
var EndpointResolver = class {
|
|
600
|
+
endpoints;
|
|
601
|
+
protocols = /* @__PURE__ */ new Map();
|
|
602
|
+
constructor(endpoints) {
|
|
603
|
+
if (endpoints.length === 0) throw new LlmError("\u672A\u914D\u7F6E\u4EFB\u4F55\u6A21\u578B\u7AEF\u70B9");
|
|
604
|
+
this.endpoints = endpoints;
|
|
605
|
+
}
|
|
606
|
+
resolve(model) {
|
|
607
|
+
let endpoint;
|
|
608
|
+
let modelId;
|
|
609
|
+
if (!model) {
|
|
610
|
+
endpoint = this.endpoints[0];
|
|
611
|
+
modelId = endpoint.default_model ?? "";
|
|
612
|
+
if (!modelId) throw new LlmError(`\u7AEF\u70B9 ${endpoint.name} \u672A\u914D\u7F6E\u9ED8\u8BA4\u6A21\u578B`);
|
|
613
|
+
} else if (model.includes("/")) {
|
|
614
|
+
const [name, ...rest] = model.split("/");
|
|
615
|
+
modelId = rest.join("/");
|
|
616
|
+
endpoint = this.endpoints.find((e) => e.name === name);
|
|
617
|
+
if (!endpoint) throw new LlmError(`\u6A21\u578B\u7AEF\u70B9\u4E0D\u5B58\u5728\uFF1A${name}`);
|
|
618
|
+
} else {
|
|
619
|
+
modelId = model;
|
|
620
|
+
const hits = this.endpoints.filter((e) => e.models && model in e.models);
|
|
621
|
+
if (hits.length > 1) {
|
|
622
|
+
throw new LlmError(`\u6A21\u578B ${model} \u5728\u591A\u4E2A\u7AEF\u70B9\u4E2D\u5747\u6709\u914D\u7F6E\uFF0C\u8BF7\u7528\u300C\u7AEF\u70B9\u540D/\u6A21\u578Bid\u300D\u663E\u5F0F\u6307\u5B9A`);
|
|
623
|
+
}
|
|
624
|
+
endpoint = hits[0] ?? this.endpoints[0];
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
endpoint,
|
|
628
|
+
protocol: this.protocolFor(endpoint),
|
|
629
|
+
modelId,
|
|
630
|
+
contextWindow: endpoint.models?.[modelId]?.context_window ?? builtinContextWindow(modelId)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
protocolFor(endpoint) {
|
|
634
|
+
let protocol = this.protocols.get(endpoint.name);
|
|
635
|
+
if (!protocol) {
|
|
636
|
+
const opts = {
|
|
637
|
+
baseUrl: endpoint.base_url,
|
|
638
|
+
apiKey: endpoint.api_key,
|
|
639
|
+
providerName: endpoint.name
|
|
640
|
+
};
|
|
641
|
+
protocol = endpoint.protocol === "anthropic-messages" ? new AnthropicMessagesProtocol(opts) : new OpenAIResponsesProtocol(opts);
|
|
642
|
+
this.protocols.set(endpoint.name, protocol);
|
|
643
|
+
}
|
|
644
|
+
return protocol;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// src/llm/validate.ts
|
|
649
|
+
function validateToolCall(definitions, call) {
|
|
650
|
+
const def = definitions.find((d) => d.name === call.name);
|
|
651
|
+
if (!def) {
|
|
652
|
+
return { args: null, error: `\u5DE5\u5177\u4E0D\u5B58\u5728\uFF1A${call.name}\uFF1B\u53EF\u7528\u5DE5\u5177\uFF1A${definitions.map((d) => d.name).join("\u3001")}` };
|
|
653
|
+
}
|
|
654
|
+
let parsed;
|
|
655
|
+
try {
|
|
656
|
+
parsed = JSON.parse(call.raw_arguments || "{}");
|
|
657
|
+
} catch {
|
|
658
|
+
return {
|
|
659
|
+
args: null,
|
|
660
|
+
error: `\u5DE5\u5177 ${call.name} \u7684\u53C2\u6570\u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u8BF7\u4FEE\u6B63\u540E\u91CD\u8BD5\u3002\u539F\u6587\uFF1A${call.raw_arguments.slice(0, 200)}`
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
664
|
+
return { args: null, error: `\u5DE5\u5177 ${call.name} \u7684\u53C2\u6570\u5FC5\u987B\u662F JSON object` };
|
|
665
|
+
}
|
|
666
|
+
const err = validateAgainstSchema(parsed, def.parameters);
|
|
667
|
+
if (err) return { args: null, error: `\u5DE5\u5177 ${call.name} \u53C2\u6570\u6821\u9A8C\u5931\u8D25\uFF1A${err}` };
|
|
668
|
+
return { args: parsed, error: null };
|
|
669
|
+
}
|
|
670
|
+
function validateAgainstSchema(args, schema) {
|
|
671
|
+
const properties = schema.properties ?? {};
|
|
672
|
+
const required = (schema.required ?? []).filter(
|
|
673
|
+
(key) => properties[key]?.default === void 0
|
|
674
|
+
);
|
|
675
|
+
for (const key of required) {
|
|
676
|
+
if (!(key in args) || args[key] === void 0 || args[key] === null) {
|
|
677
|
+
return `\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570 ${key}`;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
for (const [key, value] of Object.entries(args)) {
|
|
681
|
+
const prop = properties[key];
|
|
682
|
+
if (!prop) continue;
|
|
683
|
+
const expected = prop.type;
|
|
684
|
+
if (value === void 0 || !expected) continue;
|
|
685
|
+
const ok2 = expected === "string" && typeof value === "string" || expected === "number" && typeof value === "number" || expected === "integer" && typeof value === "number" && Number.isInteger(value) || expected === "boolean" && typeof value === "boolean" || expected === "array" && Array.isArray(value) || expected === "object" && typeof value === "object" && !Array.isArray(value);
|
|
686
|
+
if (!ok2) return `\u53C2\u6570 ${key} \u7C7B\u578B\u5E94\u4E3A ${expected}`;
|
|
687
|
+
const enumValues = prop.enum;
|
|
688
|
+
if (enumValues && !enumValues.includes(value)) {
|
|
689
|
+
return `\u53C2\u6570 ${key} \u53D6\u503C\u987B\u4E3A ${enumValues.join(" / ")}`;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/agent/events.ts
|
|
696
|
+
import { z as z2 } from "zod";
|
|
697
|
+
var agentStartParamsSchema = z2.object({
|
|
698
|
+
/** 用户的任务描述(作为本轮 user 消息) */
|
|
699
|
+
input: z2.string(),
|
|
700
|
+
/** 是否把 input 追加为新的 user 消息;提权续跑沿用 history 中的原始输入 */
|
|
701
|
+
include_input: z2.boolean().default(true),
|
|
702
|
+
/** 多轮历史(可选):追加在 system 之后、本轮 input 之前 */
|
|
703
|
+
history: z2.array(chatMessageSchema).default([]),
|
|
704
|
+
/** 模型引用("端点名/模型id" 或裸模型 id);空串 = 第一个端点的默认模型 */
|
|
705
|
+
model: z2.string().default(""),
|
|
706
|
+
/** 覆盖默认系统提示词(缺省用 prompts.buildSystemPrompt()) */
|
|
707
|
+
system_prompt: z2.string().nullish(),
|
|
708
|
+
settings: modelSettingsSchema.default({})
|
|
709
|
+
});
|
|
710
|
+
var agentToolResultSchema = z2.object({
|
|
711
|
+
tool_call_id: z2.string(),
|
|
712
|
+
name: z2.string(),
|
|
713
|
+
/** 喂回模型的文本(事件里截断到 2000 字符,完整版进对话上下文) */
|
|
714
|
+
output: z2.string(),
|
|
715
|
+
is_error: z2.boolean().default(false),
|
|
716
|
+
elapsed_ms: z2.number().default(0)
|
|
717
|
+
});
|
|
718
|
+
var agentCompactionSchema = z2.object({
|
|
719
|
+
summary: z2.string(),
|
|
720
|
+
tokens_before: z2.number(),
|
|
721
|
+
tokens_after: z2.number()
|
|
722
|
+
});
|
|
723
|
+
var agentEscalationSchema = z2.object({
|
|
724
|
+
/** 本次提权请求的唯一 id(确认接口的定位参数) */
|
|
725
|
+
escalation_id: z2.string(),
|
|
726
|
+
/** 用户/模型给出的原始路径(展示用) */
|
|
727
|
+
requested_path: z2.string(),
|
|
728
|
+
/** 沙箱解析后的绝对路径(确认后登记的授权键) */
|
|
729
|
+
resolved_path: z2.string(),
|
|
730
|
+
/** 触发提权的工具名(如 list/read) */
|
|
731
|
+
tool_name: z2.string(),
|
|
732
|
+
resource_type: z2.enum(["path", "command"]).default("path"),
|
|
733
|
+
requested_command: z2.array(z2.string()).nullish(),
|
|
734
|
+
/** 当前工作区根(展示给用户看边界在哪) */
|
|
735
|
+
workdir: z2.string()
|
|
736
|
+
});
|
|
737
|
+
var agentDoneSchema = z2.object({
|
|
738
|
+
/** 最后一步的产出(loop 的最终答复) */
|
|
739
|
+
text: z2.string().nullish(),
|
|
740
|
+
thinking: z2.string().nullish(),
|
|
741
|
+
finish_reason: z2.string().nullish(),
|
|
742
|
+
/** 全部步骤的累计用量 */
|
|
743
|
+
usage: tokenUsageSchema.default({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cache_read_tokens: 0 }),
|
|
744
|
+
/** 模型调用次数 */
|
|
745
|
+
steps: z2.number().default(1),
|
|
746
|
+
model: z2.string().default(""),
|
|
747
|
+
provider: z2.string().default(""),
|
|
748
|
+
elapsed_ms: z2.number().default(0)
|
|
749
|
+
});
|
|
750
|
+
var agentEventSchema = z2.object({
|
|
751
|
+
type: z2.enum([
|
|
752
|
+
"agent_start",
|
|
753
|
+
"thinking_delta",
|
|
754
|
+
"text_delta",
|
|
755
|
+
"tool_call_start",
|
|
756
|
+
"tool_call_delta",
|
|
757
|
+
"tool_call",
|
|
758
|
+
"tool_result",
|
|
759
|
+
"context_compacted",
|
|
760
|
+
"escalation_request",
|
|
761
|
+
"agent_done",
|
|
762
|
+
"agent_error",
|
|
763
|
+
"agent_cancelled"
|
|
764
|
+
]),
|
|
765
|
+
/** 本次运行的唯一 id,一次 start 产生的所有事件共享同一个值 */
|
|
766
|
+
run_id: z2.string(),
|
|
767
|
+
delta: z2.string().nullish(),
|
|
768
|
+
tool_call: toolCallSchema.nullish(),
|
|
769
|
+
tool_call_id: z2.string().nullish(),
|
|
770
|
+
tool_result: agentToolResultSchema.nullish(),
|
|
771
|
+
compaction: agentCompactionSchema.nullish(),
|
|
772
|
+
/** escalation_request 事件:路径提权请求 */
|
|
773
|
+
escalation: agentEscalationSchema.nullish(),
|
|
774
|
+
/** agent_start:实际路由到的端点名与模型 id */
|
|
775
|
+
provider: z2.string().nullish(),
|
|
776
|
+
model: z2.string().nullish(),
|
|
777
|
+
/** agent_done 的终态载荷 */
|
|
778
|
+
result: agentDoneSchema.nullish(),
|
|
779
|
+
/** agent_error 的中文错误说明 */
|
|
780
|
+
error: z2.string().nullish()
|
|
781
|
+
});
|
|
782
|
+
var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
783
|
+
"agent_done",
|
|
784
|
+
"agent_error",
|
|
785
|
+
"agent_cancelled"
|
|
786
|
+
]);
|
|
787
|
+
|
|
788
|
+
// src/agent/prompts.ts
|
|
789
|
+
var SUMMARY_PREFIX = "\u3010\u4E0A\u4E0B\u6587\u538B\u7F29\u6458\u8981\u3011\u6B64\u524D\u7684\u5BF9\u8BDD\u5DF2\u88AB\u538B\u7F29\uFF0C\u4EE5\u4E0B\u662F\u4EA4\u63A5\u6458\u8981\uFF1A";
|
|
790
|
+
var COMPACT_PROMPT = `\u4F60\u6B63\u5728\u6267\u884C\u4E00\u6B21\u4E0A\u4E0B\u6587\u538B\u7F29\u3002\u8BF7\u4E3A\u5C06\u8981\u63A5\u624B\u8FD9\u6BB5\u5BF9\u8BDD\u7684\u53E6\u4E00\u4E2A LLM \u5199\u4E00\u4EFD\u4EA4\u63A5\u6458\u8981\u3002
|
|
791
|
+
|
|
792
|
+
\u6458\u8981\u5FC5\u987B\u5305\u542B\uFF1A
|
|
793
|
+
- \u4EFB\u52A1\u76EE\u6807\u4E0E\u5F53\u524D\u8FDB\u5EA6\u3001\u5DF2\u505A\u51FA\u7684\u5173\u952E\u51B3\u5B9A
|
|
794
|
+
- \u7528\u6237\u63D0\u51FA\u7684\u7EA6\u675F\u3001\u504F\u597D\u548C\u91CD\u8981\u80CC\u666F
|
|
795
|
+
- \u5C1A\u672A\u5B8C\u6210\u7684\u4E8B\u9879\uFF08\u660E\u786E\u7684\u4E0B\u4E00\u6B65\uFF09
|
|
796
|
+
- \u7EE7\u7EED\u5DE5\u4F5C\u5FC5\u987B\u4FDD\u7559\u7684\u5173\u952E\u6570\u636E\uFF08\u8DEF\u5F84\u3001ID\u3001\u7F16\u53F7\u3001\u67E5\u8BE2\u7ED3\u679C\u8981\u70B9\u7B49\uFF09
|
|
797
|
+
|
|
798
|
+
\u4FDD\u6301\u7B80\u6D01\u3001\u7ED3\u6784\u5316\uFF0C\u4EE5\u5E2E\u52A9\u4E0B\u4E00\u4E2A LLM \u65E0\u7F1D\u63A5\u7EED\u5DE5\u4F5C\u4E3A\u552F\u4E00\u76EE\u6807\u3002\u53EA\u8F93\u51FA\u6458\u8981\u6B63\u6587\u3002
|
|
799
|
+
`;
|
|
800
|
+
var SYSTEM_PROMPT_TEMPLATE = `\u4F60\u662F {agentName}\uFF0C\u4E00\u4E2A\u53CB\u597D\u7684\u667A\u80FD\u52A9\u7406\u3002{persona}
|
|
801
|
+
|
|
802
|
+
# \u8FD0\u884C\u65B9\u5F0F
|
|
803
|
+
- \u4F60\u7684\u6587\u5B57\u8F93\u51FA\u4EE5 Markdown \u6E32\u67D3\u5728\u4F1A\u8BDD\u9875\uFF0C\u5DE5\u5177\u8C03\u7528\u8FC7\u7A0B\u7528\u6237\u5168\u7A0B\u53EF\u89C1\u3002\u6267\u884C\u4E2D\u4E0D\u8981\u65C1\u767D\u300C\u6211\u73B0\u5728\u8981\u53BB\u505A\u4EC0\u4E48\u300D\u2014\u2014\u5DE5\u5177\u8C03\u7528\u672C\u8EAB\u5C31\u662F\u8FC7\u7A0B\u5C55\u793A\u3002
|
|
804
|
+
- \u9700\u8981\u4E8B\u5B9E\u65F6\u6C38\u8FDC\u5148\u8C03\u7528\u5DE5\u5177\u67E5\u8BC1\uFF0C\u4E00\u5F8B\u4EE5\u5DE5\u5177\u8FD4\u56DE\u4E3A\u51C6\uFF0C\u7EDD\u4E0D\u51ED\u5370\u8C61\u81C6\u65AD\u3002
|
|
805
|
+
- \u6587\u4EF6\u4E0E\u5DE5\u4F5C\u72B6\u6001\u90FD\u5177\u6709\u65F6\u6548\u6027\uFF1B\u6BCF\u6B21\u7528\u6237\u8BE2\u95EE\u6B64\u7C7B\u4FE1\u606F\u65F6\uFF0C\u5FC5\u987B\u5728\u5F53\u524D\u8F6E\u6B21\u91CD\u65B0\u8C03\u7528\u5DE5\u5177\u67E5\u8BE2\u6700\u65B0\u6570\u636E\uFF0C\u4E0D\u8981\u76F8\u4FE1\u6216\u590D\u7528\u5386\u53F2\u5BF9\u8BDD\u4E2D\u5DF2\u7ECF\u83B7\u53D6\u7684\u7ED3\u679C\u3002
|
|
806
|
+
- \u5DE5\u5177\u62A5\u9519\u6216\u88AB\u62D2\u7EDD\uFF0C\u4E0D\u8981\u539F\u6837\u91CD\u53D1\uFF1A\u8BFB\u61C2\u539F\u56E0\uFF0C\u4FEE\u6B63\u53C2\u6570\u6216\u6362\u4E00\u6761\u8DEF\u3002
|
|
807
|
+
|
|
808
|
+
# \u5DE5\u5177\u4F7F\u7528\u89C4\u5219
|
|
809
|
+
- \u5DE5\u5177\u5217\u8868\u4F1A\u968F\u5F53\u524D\u4F1A\u8BDD\u914D\u7F6E\u52A8\u6001\u6CE8\u5165\uFF1B\u53EA\u80FD\u8C03\u7528\u5217\u8868\u4E2D\u5B9E\u9645\u5B58\u5728\u7684\u5DE5\u5177\uFF0C\u4E0D\u8981\u81C6\u9020\u5DE5\u5177\u540D\u6216\u53C2\u6570\u3002
|
|
810
|
+
- \u5148\u7528 search \u5B9A\u4F4D\u4EE3\u7801\u6216\u6587\u672C\uFF0C\u518D\u7528 read \u67E5\u770B\u5FC5\u8981\u4E0A\u4E0B\u6587\uFF1B\u4E0D\u8981\u4E3A\u5BFB\u627E\u5185\u5BB9\u9012\u5F52\u8BFB\u53D6\u5927\u91CF\u6587\u4EF6\u3002
|
|
811
|
+
- \u4FEE\u6539\u5DF2\u6709\u6587\u4EF6\u4F18\u5148\u4F7F\u7528 edit\uFF1B\u9700\u8981\u540C\u65F6\u4FEE\u6539\u591A\u4E2A\u6587\u4EF6\u65F6\u4F7F\u7528 apply_patch\uFF1B\u53EA\u6709\u660E\u786E\u9700\u8981\u6574\u4F53\u66FF\u6362\u65F6\u624D\u4F7F\u7528 write\u3002
|
|
812
|
+
- \u4FEE\u6539\u540E\u5982\u679C\u5F53\u524D\u4F1A\u8BDD\u63D0\u4F9B exec_command\uFF0C\u5E94\u8FD0\u884C\u6700\u76F8\u5173\u7684\u683C\u5F0F\u5316\u3001\u7C7B\u578B\u68C0\u67E5\u6216\u6D4B\u8BD5\uFF0C\u5E76\u6839\u636E\u7ED3\u679C\u7EE7\u7EED\u4FEE\u6B63\u3002
|
|
813
|
+
- exec_command \u4F7F\u7528 argv \u6570\u7EC4\u800C\u4E0D\u662F shell \u5B57\u7B26\u4E32\uFF1B\u547D\u4EE4\u4F1A\u8BDD\u8FD4\u56DE session_id \u65F6\uFF0C\u7528 write_stdin \u7EE7\u7EED\u8BFB\u53D6\uFF0C\u4E0D\u8981\u91CD\u590D\u542F\u52A8\u540C\u4E00\u4E2A\u547D\u4EE4\u3002
|
|
814
|
+
- exec_command \u4E0E write_stdin \u5373\u4F7F\u9700\u8981\u6388\u6743\u4E5F\u4F1A\u51FA\u73B0\u5728\u5DE5\u5177\u76EE\u5F55\u4E2D\uFF1B\u8C03\u7528\u540E\u82E5\u6536\u5230\u63D0\u6743\u8BF7\u6C42\uFF0C\u5E94\u7B49\u5F85\u7528\u6237\u786E\u8BA4\uFF0C\u4E0D\u8981\u58F0\u79F0\u5F53\u524D\u6CA1\u6709\u547D\u4EE4\u5DE5\u5177\u3002
|
|
815
|
+
- \u4EFB\u4F55\u5DE5\u5177\u88AB\u62D2\u7EDD\u3001\u8D8A\u754C\u6216\u53D6\u6D88\u65F6\uFF0C\u5148\u7406\u89E3\u8FD4\u56DE\u539F\u56E0\uFF1B\u4E0D\u8981\u901A\u8FC7\u6362\u5199\u6CD5\u7ED5\u8FC7\u6743\u9650\u8FB9\u754C\u3002
|
|
816
|
+
|
|
817
|
+
# \u5E76\u884C\u5DE5\u5177\u8C03\u7528
|
|
818
|
+
\u9700\u8981\u591A\u9879\u4E92\u4E0D\u4F9D\u8D56\u7684\u4FE1\u606F\u65F6\uFF0C\u5728\u540C\u4E00\u8F6E\u91CC\u4E00\u6B21\u6027\u53D1\u8D77\u5168\u90E8\u8C03\u7528\uFF0C\u800C\u4E0D\u662F\u4E00\u8F6E\u4E00\u4E2A\u3002\u72EC\u7ACB\u7684\u68C0\u7D22\u3001\u67E5\u8BE2\u3001\u72B6\u6001\u8BFB\u53D6\u90FD\u5E94\u5408\u5E76\u5230\u540C\u4E00\u6B21\u56DE\u590D\u4E2D\u2014\u2014\u8FD0\u884C\u65F6\u4F1A\u5B89\u5168\u5E76\u53D1\u6267\u884C\u8FDE\u7EED\u7684\u53EA\u8BFB\u8C03\u7528\u5E76\u6309\u539F\u987A\u5E8F\u8FD4\u56DE\u7ED3\u679C\uFF0C\u5408\u5E76\u8C03\u7528\u907F\u514D\u4E86\u6BCF\u591A\u4E00\u8F6E\u5C31\u628A\u6574\u4E2A\u5BF9\u8BDD\u91CD\u65B0\u53D1\u9001\u4E00\u904D\u7684\u5F00\u9500\u3002
|
|
819
|
+
\u53EA\u6709\u5F53\u540E\u4E00\u4E2A\u8C03\u7528\u786E\u5B9E\u4F9D\u8D56\u524D\u4E00\u4E2A\u7684\u7ED3\u679C\u65F6\u624D\u5206\u8F6E\u4E32\u884C\u3002\u62FF\u4E0D\u51C6\u4E14\u8C03\u7528\u4E4B\u95F4\u76F8\u4E92\u72EC\u7ACB\u65F6\uFF0C\u4E00\u5F8B\u5408\u5E76\u3002
|
|
820
|
+
|
|
821
|
+
# \u5DE5\u4F5C\u5FAA\u73AF
|
|
822
|
+
\u4EE5\u300C\u601D\u8003 \u2192 \u884C\u52A8 \u2192 \u89C2\u5BDF\u300D\u6301\u7EED\u5FAA\u73AF\uFF0C\u76F4\u5230\u4EFB\u52A1\u5B8C\u6210\uFF1A
|
|
823
|
+
- \u6BCF\u6B21\u89C2\u5BDF\u7ED3\u679C\u540E\u91CD\u65B0\u8BC4\u4F30\uFF1A\u63A8\u8FDB\u4E86\u5C31\u8D70\u4E0B\u4E00\u6B65\uFF1B\u6CA1\u63A8\u8FDB\u5C31\u6362\u4E00\u79CD\u65B9\u5F0F\u518D\u8BD5\uFF0C\u800C\u4E0D\u662F\u539F\u6837\u91CD\u8BD5\u3002
|
|
824
|
+
- \u575A\u6301\u5230\u5E95\uFF0C\u4E0D\u628A\u505A\u4E86\u4E00\u534A\u7684\u4EFB\u52A1\u4EA4\u56DE\u7528\u6237\u3002\u540C\u4E00\u969C\u788D\u8FDE\u7EED\u4E24\u6B21\u7ED5\u4E0D\u8FC7\u53BB\uFF0C\u624D\u505C\u4E0B\u5411\u7528\u6237\u8BF4\u660E\u5361\u70B9\u3002
|
|
825
|
+
- \u5141\u8BB8\u505C\u4E0B\u63D0\u95EE\u7684\u53EA\u6709\u4E24\u79CD\u60C5\u51B5\uFF1A\u7F3A\u5C11\u53EA\u6709\u7528\u6237\u80FD\u505A\u7684\u51B3\u5B9A\uFF0C\u6216\u52A8\u4F5C\u8D85\u51FA\u4E86\u672C\u6B21\u7684\u660E\u786E\u6388\u6743\u3002
|
|
826
|
+
|
|
827
|
+
# \u56DE\u590D\u98CE\u683C
|
|
828
|
+
- \u5168\u7A0B\u4E2D\u6587\u3002\u6700\u7EC8\u7B54\u590D\u5148\u7ED3\u8BBA\u3001\u540E\u5173\u952E\u4F9D\u636E\uFF0C\u4E0D\u590D\u8FF0\u6267\u884C\u8FC7\u7A0B\u3002
|
|
829
|
+
- \u627E\u4E0D\u5230\u5C31\u8BF4\u627E\u4E0D\u5230\uFF0C\u7EDD\u4E0D\u865A\u6784\u4EFB\u4F55\u6570\u636E\u3002
|
|
830
|
+
- \u7B80\u77ED\u4F18\u5148\uFF1A\u4E00\u53E5\u80FD\u8BF4\u6E05\u7684\u4E0D\u7528\u4E09\u53E5\u3002
|
|
831
|
+
- \u53CB\u597D\u3001\u6709\u6E29\u5EA6\uFF1A\u50CF\u4E00\u4E2A\u957F\u671F\u966A\u4F34\u7528\u6237\u7684\u771F\u4EBA\u4F19\u4F34\u90A3\u6837\u4EA4\u6D41\uFF0C\u5728\u6070\u5F53\u7684\u65F6\u5019\u7ED9\u7528\u6237\u60C5\u7EEA\u4EF7\u503C\u3002\u53EF\u4EE5\u9002\u5EA6\u4F7F\u7528\u8868\u60C5\u7B26\u53F7\uFF0C\u4F46\u522B\u6CDB\u6EE5\u3002
|
|
832
|
+
`;
|
|
833
|
+
function buildAvailableToolsPrompt(availableTools) {
|
|
834
|
+
if (availableTools.length === 0) {
|
|
835
|
+
return "# \u5F53\u524D\u4F1A\u8BDD\u53EF\u7528\u5DE5\u5177\n- \u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u53EF\u7528\u5DE5\u5177\uFF1B\u4E0D\u8981\u5C1D\u8BD5\u8C03\u7528\u5DE5\u5177\u3002";
|
|
836
|
+
}
|
|
837
|
+
return [
|
|
838
|
+
"# \u5F53\u524D\u4F1A\u8BDD\u53EF\u7528\u5DE5\u5177",
|
|
839
|
+
...availableTools.map((tool) => `- ${tool.name}\uFF1A${tool.description.replace(/\s+/g, " ").trim()}`)
|
|
840
|
+
].join("\n");
|
|
841
|
+
}
|
|
842
|
+
function buildSystemPrompt(opts = {}) {
|
|
843
|
+
const body = SYSTEM_PROMPT_TEMPLATE.replace("{agentName}", opts.agentName ?? "xlyra \u52A9\u7406").replace(
|
|
844
|
+
"{persona}",
|
|
845
|
+
opts.persona ? ` ${opts.persona}` : ""
|
|
846
|
+
);
|
|
847
|
+
const lines = [body, "# \u73AF\u5883", `- \u5F53\u524D\u65E5\u671F\uFF1A${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`];
|
|
848
|
+
if (opts.extraEnvironment) lines.push(opts.extraEnvironment);
|
|
849
|
+
lines.push(buildAvailableToolsPrompt(opts.availableTools ?? []));
|
|
850
|
+
return lines.join("\n");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/agent/compaction.ts
|
|
854
|
+
var COMPACT_TRIGGER_RATIO = 0.9;
|
|
855
|
+
var RETAINED_USER_TOKEN_BUDGET = 2e4;
|
|
856
|
+
var APPROX_BYTES_PER_TOKEN = 4;
|
|
857
|
+
function estimateTokens(target) {
|
|
858
|
+
if (typeof target === "string") {
|
|
859
|
+
return Math.floor(Buffer.byteLength(target, "utf-8") / APPROX_BYTES_PER_TOKEN);
|
|
860
|
+
}
|
|
861
|
+
if (Array.isArray(target)) {
|
|
862
|
+
return target.reduce((sum, m) => sum + estimateTokens(m), 0);
|
|
863
|
+
}
|
|
864
|
+
let total = Buffer.byteLength(target.content ?? "", "utf-8");
|
|
865
|
+
for (const tc of target.tool_calls ?? []) {
|
|
866
|
+
total += Buffer.byteLength(tc.name, "utf-8");
|
|
867
|
+
total += Buffer.byteLength(tc.raw_arguments, "utf-8");
|
|
868
|
+
}
|
|
869
|
+
return Math.floor(total / APPROX_BYTES_PER_TOKEN);
|
|
870
|
+
}
|
|
871
|
+
function shouldCompact(contextTokens, contextWindow) {
|
|
872
|
+
if (!contextWindow) return false;
|
|
873
|
+
return contextTokens >= Math.floor(contextWindow * COMPACT_TRIGGER_RATIO);
|
|
874
|
+
}
|
|
875
|
+
function isSummaryMessage(message) {
|
|
876
|
+
return message.role === "user" && (message.content ?? "").startsWith(SUMMARY_PREFIX);
|
|
877
|
+
}
|
|
878
|
+
function truncateMiddle(text, maxTokens) {
|
|
879
|
+
const budgetBytes = maxTokens * APPROX_BYTES_PER_TOKEN;
|
|
880
|
+
const data = Buffer.from(text, "utf-8");
|
|
881
|
+
if (data.length <= budgetBytes) return text;
|
|
882
|
+
const half = Math.floor(budgetBytes / 2);
|
|
883
|
+
const head = data.subarray(0, half).toString("utf-8");
|
|
884
|
+
const tail = data.subarray(data.length - half).toString("utf-8");
|
|
885
|
+
const omitted = Math.floor((data.length - budgetBytes) / APPROX_BYTES_PER_TOKEN);
|
|
886
|
+
return `${head}
|
|
887
|
+
\u2026\uFF08\u4E2D\u95F4\u7EA6 ${omitted} token \u5DF2\u622A\u65AD\uFF09\u2026
|
|
888
|
+
${tail}`;
|
|
889
|
+
}
|
|
890
|
+
function buildReplacementHistory(messages, summary) {
|
|
891
|
+
const retained = [];
|
|
892
|
+
let remaining = RETAINED_USER_TOKEN_BUDGET;
|
|
893
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
894
|
+
const message = messages[i];
|
|
895
|
+
if (message.role !== "user" || isSummaryMessage(message)) continue;
|
|
896
|
+
if (remaining <= 0) break;
|
|
897
|
+
const tokens = estimateTokens(message);
|
|
898
|
+
if (tokens <= remaining) {
|
|
899
|
+
retained.push(message);
|
|
900
|
+
remaining -= tokens;
|
|
901
|
+
} else {
|
|
902
|
+
retained.push({ role: "user", content: truncateMiddle(message.content ?? "", remaining) });
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
retained.reverse();
|
|
907
|
+
return [...retained, { role: "user", content: `${SUMMARY_PREFIX}
|
|
908
|
+
${summary}` }];
|
|
909
|
+
}
|
|
910
|
+
async function compact(protocol, model, messages, settings) {
|
|
911
|
+
let summary = null;
|
|
912
|
+
try {
|
|
913
|
+
for await (const event of protocol.chatStream({
|
|
914
|
+
model,
|
|
915
|
+
// 摘要指令是普通 user 消息;不带工具定义,模型只能输出文本
|
|
916
|
+
messages: [...messages, { role: "user", content: COMPACT_PROMPT }],
|
|
917
|
+
settings
|
|
918
|
+
})) {
|
|
919
|
+
if (event.type === "error") {
|
|
920
|
+
console.warn(`\u4E0A\u4E0B\u6587\u538B\u7F29\u5931\u8D25\uFF08\u6A21\u578B\u8C03\u7528\u51FA\u9519\uFF09\uFF1A${event.error}`);
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
if (event.type === "done") summary = (event.response.content ?? "").trim();
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
console.warn(`\u4E0A\u4E0B\u6587\u538B\u7F29\u5931\u8D25\uFF08\u8BF7\u6C42\u5F02\u5E38\uFF09\uFF1A${err instanceof Error ? err.message : err}`);
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
if (!summary) {
|
|
930
|
+
console.warn("\u4E0A\u4E0B\u6587\u538B\u7F29\u5931\u8D25\uFF1A\u6A21\u578B\u8FD4\u56DE\u4E86\u7A7A\u6458\u8981");
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
const replacement = buildReplacementHistory(messages, summary);
|
|
934
|
+
const system = messages.slice(0, 1).filter((m) => m.role === "system");
|
|
935
|
+
return {
|
|
936
|
+
summary,
|
|
937
|
+
replacement_history: replacement,
|
|
938
|
+
tokens_before: estimateTokens(messages),
|
|
939
|
+
tokens_after: estimateTokens([...system, ...replacement])
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/agent/sessions.ts
|
|
944
|
+
import crypto from "crypto";
|
|
945
|
+
import fs from "fs";
|
|
946
|
+
import os from "os";
|
|
947
|
+
import path from "path";
|
|
948
|
+
import { z as z3 } from "zod";
|
|
949
|
+
var SESSION_FORMAT_VERSION = 3;
|
|
950
|
+
var PREVIEW_MAX_CHARS = 80;
|
|
951
|
+
function defaultDataDir() {
|
|
952
|
+
return path.join(os.homedir(), ".xlyra-agent");
|
|
953
|
+
}
|
|
954
|
+
function defaultSessionsDir() {
|
|
955
|
+
return path.join(defaultDataDir(), "sessions");
|
|
956
|
+
}
|
|
957
|
+
var sessionHeaderSchema = z3.object({
|
|
958
|
+
type: z3.literal("session").default("session"),
|
|
959
|
+
version: z3.number().default(SESSION_FORMAT_VERSION),
|
|
960
|
+
session_id: z3.string(),
|
|
961
|
+
created_at: z3.string()
|
|
962
|
+
});
|
|
963
|
+
var sessionMessageEntrySchema = z3.object({
|
|
964
|
+
type: z3.literal("message").default("message"),
|
|
965
|
+
uuid: z3.string(),
|
|
966
|
+
parent_uuid: z3.string().nullish(),
|
|
967
|
+
timestamp: z3.string(),
|
|
968
|
+
message: chatMessageSchema,
|
|
969
|
+
/** 以下仅 assistant 消息携带(运行元数据,不属于 message 本身,放信封层) */
|
|
970
|
+
model: z3.string().nullish(),
|
|
971
|
+
usage: tokenUsageSchema.nullish(),
|
|
972
|
+
finish_reason: z3.string().nullish()
|
|
973
|
+
});
|
|
974
|
+
var sessionCompactionEntrySchema = z3.object({
|
|
975
|
+
type: z3.literal("compaction").default("compaction"),
|
|
976
|
+
uuid: z3.string(),
|
|
977
|
+
parent_uuid: z3.string().nullish(),
|
|
978
|
+
timestamp: z3.string(),
|
|
979
|
+
summary: z3.string(),
|
|
980
|
+
replacement_history: z3.array(chatMessageSchema),
|
|
981
|
+
tokens_before: z3.number().nullish(),
|
|
982
|
+
tokens_after: z3.number().nullish()
|
|
983
|
+
});
|
|
984
|
+
var sessionEscalationEntrySchema = z3.object({
|
|
985
|
+
type: z3.literal("escalation").default("escalation"),
|
|
986
|
+
uuid: z3.string(),
|
|
987
|
+
parent_uuid: z3.string().nullish(),
|
|
988
|
+
timestamp: z3.string(),
|
|
989
|
+
requested_path: z3.string(),
|
|
990
|
+
resolved_path: z3.string(),
|
|
991
|
+
tool_name: z3.string(),
|
|
992
|
+
resource_type: z3.enum(["path", "command"]).default("path"),
|
|
993
|
+
requested_command: z3.array(z3.string()).nullish(),
|
|
994
|
+
/** 旧记录可能省略待确认状态;读取时统一规范化为 null。 */
|
|
995
|
+
granted: z3.boolean().nullish().default(null)
|
|
996
|
+
});
|
|
997
|
+
function nowIso() {
|
|
998
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
999
|
+
}
|
|
1000
|
+
function entryUuid() {
|
|
1001
|
+
return crypto.randomUUID().replaceAll("-", "").slice(0, 12);
|
|
1002
|
+
}
|
|
1003
|
+
function isMessageEntry(e) {
|
|
1004
|
+
return e.type === "message";
|
|
1005
|
+
}
|
|
1006
|
+
function isCompactionEntry(e) {
|
|
1007
|
+
return e.type === "compaction";
|
|
1008
|
+
}
|
|
1009
|
+
function isEscalationEntry(e) {
|
|
1010
|
+
return e.type === "escalation";
|
|
1011
|
+
}
|
|
1012
|
+
function lastCompactionIndex(entries) {
|
|
1013
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1014
|
+
if (isCompactionEntry(entries[i])) return i;
|
|
1015
|
+
}
|
|
1016
|
+
return -1;
|
|
1017
|
+
}
|
|
1018
|
+
function messagesAfterLastCompaction(entries) {
|
|
1019
|
+
const last = lastCompactionIndex(entries);
|
|
1020
|
+
return entries.slice(last + 1).filter(isMessageEntry).map((e) => e.message);
|
|
1021
|
+
}
|
|
1022
|
+
var AgentSessionStore = class {
|
|
1023
|
+
root;
|
|
1024
|
+
/** session_id → 最后一条 entry 的 uuid(避免每次 append 都重读文件) */
|
|
1025
|
+
leafCache = /* @__PURE__ */ new Map();
|
|
1026
|
+
constructor(root) {
|
|
1027
|
+
this.root = path.resolve(root ?? defaultSessionsDir());
|
|
1028
|
+
}
|
|
1029
|
+
get rootDir() {
|
|
1030
|
+
return this.root;
|
|
1031
|
+
}
|
|
1032
|
+
pathOf(sessionId) {
|
|
1033
|
+
return path.join(this.root, `${sessionId}.jsonl`);
|
|
1034
|
+
}
|
|
1035
|
+
exists(sessionId) {
|
|
1036
|
+
return fs.existsSync(this.pathOf(sessionId));
|
|
1037
|
+
}
|
|
1038
|
+
// ------------------------------------------------------------------
|
|
1039
|
+
// 写入
|
|
1040
|
+
// ------------------------------------------------------------------
|
|
1041
|
+
/** 新建会话文件并写入头行,返回头信息 */
|
|
1042
|
+
create(sessionId) {
|
|
1043
|
+
const header = {
|
|
1044
|
+
type: "session",
|
|
1045
|
+
version: SESSION_FORMAT_VERSION,
|
|
1046
|
+
session_id: sessionId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
1047
|
+
created_at: nowIso()
|
|
1048
|
+
};
|
|
1049
|
+
fs.mkdirSync(this.root, { recursive: true });
|
|
1050
|
+
fs.writeFileSync(this.pathOf(header.session_id), JSON.stringify(header) + "\n", { flag: "wx" });
|
|
1051
|
+
this.leafCache.set(header.session_id, null);
|
|
1052
|
+
return header;
|
|
1053
|
+
}
|
|
1054
|
+
/** 追加一条定稿消息,自动接到当前链尾,返回写入的 entry */
|
|
1055
|
+
append(sessionId, message, meta) {
|
|
1056
|
+
const parent = this.currentLeaf(sessionId);
|
|
1057
|
+
const entry = {
|
|
1058
|
+
type: "message",
|
|
1059
|
+
uuid: entryUuid(),
|
|
1060
|
+
parent_uuid: parent,
|
|
1061
|
+
timestamp: nowIso(),
|
|
1062
|
+
message,
|
|
1063
|
+
model: meta?.model ?? null,
|
|
1064
|
+
usage: meta?.usage ?? null,
|
|
1065
|
+
finish_reason: meta?.finish_reason ?? null
|
|
1066
|
+
};
|
|
1067
|
+
this.appendLine(sessionId, stripNulls(entry));
|
|
1068
|
+
this.leafCache.set(sessionId, entry.uuid);
|
|
1069
|
+
return entry;
|
|
1070
|
+
}
|
|
1071
|
+
/** 追加一条压缩行,与 append 同款接到当前链尾(parent 链线性穿过压缩行) */
|
|
1072
|
+
appendCompaction(sessionId, result) {
|
|
1073
|
+
const parent = this.currentLeaf(sessionId);
|
|
1074
|
+
const entry = {
|
|
1075
|
+
type: "compaction",
|
|
1076
|
+
uuid: entryUuid(),
|
|
1077
|
+
parent_uuid: parent,
|
|
1078
|
+
timestamp: nowIso(),
|
|
1079
|
+
summary: result.summary,
|
|
1080
|
+
replacement_history: result.replacement_history,
|
|
1081
|
+
tokens_before: result.tokens_before,
|
|
1082
|
+
tokens_after: result.tokens_after
|
|
1083
|
+
};
|
|
1084
|
+
this.appendLine(sessionId, stripNulls(entry));
|
|
1085
|
+
this.leafCache.set(sessionId, entry.uuid);
|
|
1086
|
+
return entry;
|
|
1087
|
+
}
|
|
1088
|
+
/** 追加一条提权请求行(granted=null 待确认) */
|
|
1089
|
+
appendEscalation(sessionId, info) {
|
|
1090
|
+
const parent = this.currentLeaf(sessionId);
|
|
1091
|
+
const entry = {
|
|
1092
|
+
type: "escalation",
|
|
1093
|
+
uuid: info.escalation_id,
|
|
1094
|
+
parent_uuid: parent,
|
|
1095
|
+
timestamp: nowIso(),
|
|
1096
|
+
requested_path: info.requested_path,
|
|
1097
|
+
resolved_path: info.resolved_path,
|
|
1098
|
+
tool_name: info.tool_name,
|
|
1099
|
+
resource_type: info.resource_type ?? "path",
|
|
1100
|
+
requested_command: info.requested_command,
|
|
1101
|
+
granted: null
|
|
1102
|
+
};
|
|
1103
|
+
this.appendLine(sessionId, stripNulls(entry));
|
|
1104
|
+
this.leafCache.set(sessionId, entry.uuid);
|
|
1105
|
+
return entry;
|
|
1106
|
+
}
|
|
1107
|
+
/** 标记提权请求的确认结果(granted=true 授权 / false 拒绝)。
|
|
1108
|
+
* 这是 append-only 原则的第二个例外(另一个是 discardFromUserMessage):
|
|
1109
|
+
* 提权行的 granted 字段需要就地更新,否则确认状态无法在回放时保留。
|
|
1110
|
+
* 实现上仍是整文件原子重写(tmp + rename),与 discard 同款安全保证。 */
|
|
1111
|
+
resolveEscalation(sessionId, escalationId, granted) {
|
|
1112
|
+
const { header, entries } = this.read(sessionId);
|
|
1113
|
+
const index = entries.findIndex((e) => isEscalationEntry(e) && e.uuid === escalationId);
|
|
1114
|
+
if (index === -1) return false;
|
|
1115
|
+
const target = entries[index];
|
|
1116
|
+
if (!isEscalationEntry(target)) return false;
|
|
1117
|
+
entries[index] = { ...target, granted };
|
|
1118
|
+
const filePath = this.pathOf(sessionId);
|
|
1119
|
+
const tmp = `${filePath}.tmp`;
|
|
1120
|
+
const lines = [JSON.stringify(header), ...entries.map((e) => JSON.stringify(stripNulls(e)))];
|
|
1121
|
+
fs.writeFileSync(tmp, lines.join("\n") + "\n");
|
|
1122
|
+
fs.renameSync(tmp, filePath);
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* 中断收尾:给没有结果的 tool_call 补写错误回执,返回补写条数。
|
|
1127
|
+
*
|
|
1128
|
+
* 保证文件里 assistant 的 tool_calls 与 tool 消息任何时刻都配对完整,
|
|
1129
|
+
* resume 直接回喂 API 不需要修复逻辑(在写入侧一次做对)。
|
|
1130
|
+
* 只检查最后一条压缩行之后的消息:更早的往返已被压缩挡在上下文之外,
|
|
1131
|
+
* 给死上下文补回执毫无意义。
|
|
1132
|
+
*/
|
|
1133
|
+
sealPendingToolCalls(sessionId) {
|
|
1134
|
+
const { entries } = this.read(sessionId);
|
|
1135
|
+
const messages = messagesAfterLastCompaction(entries);
|
|
1136
|
+
const answered = new Set(
|
|
1137
|
+
messages.filter((m) => m.role === "tool").map((m) => m.tool_call_id)
|
|
1138
|
+
);
|
|
1139
|
+
let sealed = 0;
|
|
1140
|
+
for (const message of messages) {
|
|
1141
|
+
for (const tc of message.tool_calls ?? []) {
|
|
1142
|
+
if (answered.has(tc.id)) continue;
|
|
1143
|
+
this.append(sessionId, {
|
|
1144
|
+
role: "tool",
|
|
1145
|
+
content: "\u64CD\u4F5C\u5DF2\u88AB\u4E2D\u65AD\uFF0C\u5DE5\u5177\u672A\u6267\u884C\u5B8C\u6210\u3002",
|
|
1146
|
+
tool_call_id: tc.id,
|
|
1147
|
+
name: tc.name,
|
|
1148
|
+
is_error: true
|
|
1149
|
+
});
|
|
1150
|
+
sealed += 1;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return sealed;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* 删除指定 user message 及其之后的全部 entry,返回删除条数。
|
|
1157
|
+
*
|
|
1158
|
+
* 这是本模块唯一改写历史行的方法,与「append-only」约定相悖,属于刻意的
|
|
1159
|
+
* 例外:用户要的就是「这些记录不该再存在」。整文件重写,写临时文件后
|
|
1160
|
+
* rename 原子换入:中途崩溃要么旧文件完好、要么新文件完整。
|
|
1161
|
+
* 只允许从 user message 切:从 assistant/tool 中间切会留下没有回执的
|
|
1162
|
+
* tool_call,重建出的上下文喂回模型直接 400。
|
|
1163
|
+
*/
|
|
1164
|
+
discardFromUserMessage(sessionId, messageId) {
|
|
1165
|
+
const { header, entries } = this.read(sessionId);
|
|
1166
|
+
const index = entries.findIndex((e) => e.uuid === messageId);
|
|
1167
|
+
if (index === -1) throw new Error("\u4F1A\u8BDD\u4E2D\u6CA1\u6709\u8FD9\u6761\u8BB0\u5F55\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6539\u5199");
|
|
1168
|
+
const target = entries[index];
|
|
1169
|
+
if (!isMessageEntry(target) || target.message.role !== "user") {
|
|
1170
|
+
throw new Error("\u53EA\u80FD\u91CD\u8BD5\u7528\u6237\u6D88\u606F");
|
|
1171
|
+
}
|
|
1172
|
+
const kept = entries.slice(0, index);
|
|
1173
|
+
const filePath = this.pathOf(sessionId);
|
|
1174
|
+
const tmp = `${filePath}.tmp`;
|
|
1175
|
+
const lines = [JSON.stringify(header), ...kept.map((e) => JSON.stringify(stripNulls(e)))];
|
|
1176
|
+
fs.writeFileSync(tmp, lines.join("\n") + "\n");
|
|
1177
|
+
fs.renameSync(tmp, filePath);
|
|
1178
|
+
this.leafCache.set(sessionId, kept.length > 0 ? kept[kept.length - 1].uuid : null);
|
|
1179
|
+
return entries.length - kept.length;
|
|
1180
|
+
}
|
|
1181
|
+
/** 删除会话文件(幂等) */
|
|
1182
|
+
delete(sessionId) {
|
|
1183
|
+
fs.rmSync(this.pathOf(sessionId), { force: true });
|
|
1184
|
+
this.leafCache.delete(sessionId);
|
|
1185
|
+
}
|
|
1186
|
+
// ------------------------------------------------------------------
|
|
1187
|
+
// 读取
|
|
1188
|
+
// ------------------------------------------------------------------
|
|
1189
|
+
/** 读取整个会话(头 + 全部 entry,含压缩行),坏行静默跳过 */
|
|
1190
|
+
read(sessionId) {
|
|
1191
|
+
const filePath = this.pathOf(sessionId);
|
|
1192
|
+
if (!fs.existsSync(filePath)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u8F6C\u5F55\u6587\u4EF6\u5DF2\u88AB\u5220\u9664");
|
|
1193
|
+
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
1194
|
+
let header = null;
|
|
1195
|
+
const entries = [];
|
|
1196
|
+
let bad = 0;
|
|
1197
|
+
for (const [lineNo, raw] of lines.entries()) {
|
|
1198
|
+
const line = raw.trim();
|
|
1199
|
+
if (!line) continue;
|
|
1200
|
+
if (lineNo === 0) {
|
|
1201
|
+
header = sessionHeaderSchema.parse(JSON.parse(line));
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
const parsed = JSON.parse(line);
|
|
1206
|
+
const type = parsed.type;
|
|
1207
|
+
entries.push(
|
|
1208
|
+
type === "compaction" ? sessionCompactionEntrySchema.parse(parsed) : type === "escalation" ? sessionEscalationEntrySchema.parse(parsed) : sessionMessageEntrySchema.parse(parsed)
|
|
1209
|
+
);
|
|
1210
|
+
} catch {
|
|
1211
|
+
bad += 1;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (!header) throw new Error("\u4F1A\u8BDD\u6587\u4EF6\u4E3A\u7A7A\u6216\u5934\u8BB0\u5F55\u635F\u574F");
|
|
1215
|
+
if (bad > 0) {
|
|
1216
|
+
console.warn(`\u4F1A\u8BDD\u6587\u4EF6\u5B58\u5728 ${bad} \u884C\u65E0\u6CD5\u89E3\u6790\u7684\u8BB0\u5F55\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u53EF\u80FD\u6765\u81EA\u5F02\u5E38\u9000\u51FA\uFF09\uFF1A${filePath}`);
|
|
1217
|
+
}
|
|
1218
|
+
return { header, entries, badLines: bad };
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* 把会话重建成模型上下文消息列表(resume 喂回模型用)。
|
|
1222
|
+
* 有压缩行时,从最后一条压缩行的替换历史起步、只追加其后的增量消息;
|
|
1223
|
+
* system 提示词不入库(随代码版本演进),由 runner 每次运行时重新拼装。
|
|
1224
|
+
*/
|
|
1225
|
+
buildHistory(sessionId) {
|
|
1226
|
+
const { entries } = this.read(sessionId);
|
|
1227
|
+
const last = lastCompactionIndex(entries);
|
|
1228
|
+
if (last < 0) return entries.filter(isMessageEntry).map((e) => e.message);
|
|
1229
|
+
const compaction = entries[last];
|
|
1230
|
+
return [
|
|
1231
|
+
...compaction.replacement_history,
|
|
1232
|
+
...entries.slice(last + 1).filter(isMessageEntry).map((e) => e.message)
|
|
1233
|
+
];
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* 重建提权中断前的上下文:保留原始 user 输入,但丢弃被中断的
|
|
1237
|
+
* assistant/tool 往返,授权后由 runner 从原输入继续推理。
|
|
1238
|
+
*/
|
|
1239
|
+
buildHistoryBeforeEscalation(sessionId, escalationId) {
|
|
1240
|
+
const { entries } = this.read(sessionId);
|
|
1241
|
+
const escalationIndex = entries.findIndex(
|
|
1242
|
+
(entry) => isEscalationEntry(entry) && entry.uuid === escalationId
|
|
1243
|
+
);
|
|
1244
|
+
if (escalationIndex < 0) return this.buildHistory(sessionId);
|
|
1245
|
+
const escalation = entries[escalationIndex];
|
|
1246
|
+
const assistantIndex = entries.findIndex(
|
|
1247
|
+
(entry) => entry.uuid === escalation.parent_uuid && isMessageEntry(entry) && entry.message.role === "assistant"
|
|
1248
|
+
);
|
|
1249
|
+
const cut = assistantIndex >= 0 ? assistantIndex : escalationIndex;
|
|
1250
|
+
const last = lastCompactionIndex(entries);
|
|
1251
|
+
if (last >= 0 && last < cut) {
|
|
1252
|
+
const compaction = entries[last];
|
|
1253
|
+
return [
|
|
1254
|
+
...compaction.replacement_history,
|
|
1255
|
+
...entries.slice(last + 1, cut).filter(isMessageEntry).map((e) => e.message)
|
|
1256
|
+
];
|
|
1257
|
+
}
|
|
1258
|
+
return entries.slice(0, cut).filter(isMessageEntry).map((e) => e.message);
|
|
1259
|
+
}
|
|
1260
|
+
/** 扫描单个会话文件生成索引摘要 */
|
|
1261
|
+
summarize(sessionId) {
|
|
1262
|
+
const { header, entries } = this.read(sessionId);
|
|
1263
|
+
const userTexts = entries.filter(isMessageEntry).filter((e) => e.message.role === "user" && (e.message.content ?? "").trim()).map((e) => (e.message.content ?? "").trim());
|
|
1264
|
+
const last = entries[entries.length - 1];
|
|
1265
|
+
return {
|
|
1266
|
+
session_id: header.session_id,
|
|
1267
|
+
created_at: header.created_at,
|
|
1268
|
+
entry_count: entries.length,
|
|
1269
|
+
leaf_uuid: last?.uuid ?? null,
|
|
1270
|
+
title: userTexts[0]?.slice(0, PREVIEW_MAX_CHARS) ?? null,
|
|
1271
|
+
last_prompt: userTexts[userTexts.length - 1]?.slice(0, PREVIEW_MAX_CHARS) ?? null,
|
|
1272
|
+
last_timestamp: last?.timestamp ?? header.created_at
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
/** 遍历目录下全部会话文件生成摘要(索引整体重建用;单文件损坏只跳过) */
|
|
1276
|
+
scanAll() {
|
|
1277
|
+
if (!fs.existsSync(this.root)) return [];
|
|
1278
|
+
const summaries = [];
|
|
1279
|
+
for (const name of fs.readdirSync(this.root).sort()) {
|
|
1280
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
1281
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
1282
|
+
try {
|
|
1283
|
+
summaries.push(this.summarize(sessionId));
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
console.warn(`\u4F1A\u8BDD\u6587\u4EF6\u65E0\u6CD5\u89E3\u6790\uFF0C\u91CD\u5EFA\u7D22\u5F15\u65F6\u5DF2\u8DF3\u8FC7\uFF1A${name}\uFF08${err instanceof Error ? err.message : err}\uFF09`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return summaries;
|
|
1289
|
+
}
|
|
1290
|
+
// ------------------------------------------------------------------
|
|
1291
|
+
currentLeaf(sessionId) {
|
|
1292
|
+
if (!this.leafCache.has(sessionId)) {
|
|
1293
|
+
const { entries } = this.read(sessionId);
|
|
1294
|
+
this.leafCache.set(sessionId, entries[entries.length - 1]?.uuid ?? null);
|
|
1295
|
+
}
|
|
1296
|
+
return this.leafCache.get(sessionId) ?? null;
|
|
1297
|
+
}
|
|
1298
|
+
appendLine(sessionId, entry) {
|
|
1299
|
+
const filePath = this.pathOf(sessionId);
|
|
1300
|
+
if (!fs.existsSync(filePath)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u8F6C\u5F55\u6587\u4EF6\u5DF2\u88AB\u5220\u9664");
|
|
1301
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
function stripNulls(obj) {
|
|
1305
|
+
return JSON.parse(
|
|
1306
|
+
JSON.stringify(obj, (_key, value) => value === null || value === void 0 ? void 0 : value)
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/agent/runner.ts
|
|
1311
|
+
import crypto2 from "crypto";
|
|
1312
|
+
|
|
1313
|
+
// src/tools/types.ts
|
|
1314
|
+
var TOOL_OUTPUT_MAX_BYTES = 5e4;
|
|
1315
|
+
function truncateMiddle2(text, maxBytes = TOOL_OUTPUT_MAX_BYTES) {
|
|
1316
|
+
const data = Buffer.from(text, "utf-8");
|
|
1317
|
+
if (data.length <= maxBytes) return text;
|
|
1318
|
+
const half = Math.floor(maxBytes / 2);
|
|
1319
|
+
const head = data.subarray(0, half).toString("utf-8");
|
|
1320
|
+
const tail = data.subarray(data.length - half).toString("utf-8");
|
|
1321
|
+
const omitted = Math.floor((data.length - maxBytes) / 4);
|
|
1322
|
+
return `${head}
|
|
1323
|
+
\u2026\uFF08\u4E2D\u95F4\u7EA6 ${omitted} token \u5DF2\u622A\u65AD\uFF09\u2026
|
|
1324
|
+
${tail}`;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/agent/escalation.ts
|
|
1328
|
+
var COMMAND_EXECUTION_GRANT = "capability://exec_command";
|
|
1329
|
+
var EscalationRequiredError = class extends Error {
|
|
1330
|
+
constructor(requestedPath, resolvedPath, message, resourceType = "path", requestedCommand) {
|
|
1331
|
+
super(message ?? (resourceType === "command" ? "\u6267\u884C\u672C\u673A\u547D\u4EE4\u9700\u8981\u7528\u6237\u6388\u6743" : `\u8DEF\u5F84\u8D85\u51FA\u5DE5\u4F5C\u533A\u8303\u56F4\uFF0C\u9700\u8981\u7528\u6237\u6388\u6743\uFF1A${resolvedPath}`));
|
|
1332
|
+
this.requestedPath = requestedPath;
|
|
1333
|
+
this.resolvedPath = resolvedPath;
|
|
1334
|
+
this.resourceType = resourceType;
|
|
1335
|
+
this.requestedCommand = requestedCommand;
|
|
1336
|
+
this.name = "EscalationRequiredError";
|
|
1337
|
+
}
|
|
1338
|
+
requestedPath;
|
|
1339
|
+
resolvedPath;
|
|
1340
|
+
resourceType;
|
|
1341
|
+
requestedCommand;
|
|
1342
|
+
};
|
|
1343
|
+
var EscalationGrants = class {
|
|
1344
|
+
grants = /* @__PURE__ */ new Map();
|
|
1345
|
+
grant(sessionId, resolvedPath) {
|
|
1346
|
+
let set = this.grants.get(sessionId);
|
|
1347
|
+
if (!set) {
|
|
1348
|
+
set = /* @__PURE__ */ new Set();
|
|
1349
|
+
this.grants.set(sessionId, set);
|
|
1350
|
+
}
|
|
1351
|
+
set.add(resolvedPath);
|
|
1352
|
+
}
|
|
1353
|
+
grantCapability(sessionId, capability) {
|
|
1354
|
+
this.grant(sessionId, capability);
|
|
1355
|
+
}
|
|
1356
|
+
/** 已授权路径或其子路径视为已放开 */
|
|
1357
|
+
isGranted(sessionId, resolvedPath) {
|
|
1358
|
+
const set = this.grants.get(sessionId);
|
|
1359
|
+
if (!set) return false;
|
|
1360
|
+
for (const prefix of set) {
|
|
1361
|
+
if (resolvedPath === prefix || resolvedPath.startsWith(prefix + "/")) return true;
|
|
1362
|
+
}
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
isCapabilityGranted(sessionId, capability) {
|
|
1366
|
+
return this.grants.get(sessionId)?.has(capability) ?? false;
|
|
1367
|
+
}
|
|
1368
|
+
/** 会话删除时清掉授权(授权随会话生命周期) */
|
|
1369
|
+
drop(sessionId) {
|
|
1370
|
+
this.grants.delete(sessionId);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
// src/agent/runner.ts
|
|
1375
|
+
var EVENT_OUTPUT_LIMIT = 2e3;
|
|
1376
|
+
var AgentRunner = class {
|
|
1377
|
+
protocol;
|
|
1378
|
+
resolver;
|
|
1379
|
+
tools;
|
|
1380
|
+
toolsByName;
|
|
1381
|
+
maxSteps;
|
|
1382
|
+
onMessage;
|
|
1383
|
+
onCompaction;
|
|
1384
|
+
onEscalation;
|
|
1385
|
+
logger;
|
|
1386
|
+
/** 提权事件里展示的工作区根(用户判断边界用);由构造方从工具配置传入 */
|
|
1387
|
+
workdirHint;
|
|
1388
|
+
constructor(opts) {
|
|
1389
|
+
if (!opts.protocol && !opts.resolver) {
|
|
1390
|
+
throw new Error("AgentRunner \u9700\u8981 protocol \u6216 resolver \u4E4B\u4E00");
|
|
1391
|
+
}
|
|
1392
|
+
this.protocol = opts.protocol;
|
|
1393
|
+
this.resolver = opts.resolver;
|
|
1394
|
+
this.tools = opts.tools ?? [];
|
|
1395
|
+
this.toolsByName = new Map(this.tools.map((t) => [t.definition.name, t]));
|
|
1396
|
+
this.maxSteps = opts.maxSteps ?? 200;
|
|
1397
|
+
this.onMessage = opts.onMessage;
|
|
1398
|
+
this.onCompaction = opts.onCompaction;
|
|
1399
|
+
this.onEscalation = opts.onEscalation;
|
|
1400
|
+
this.logger = opts.logger ?? console;
|
|
1401
|
+
this.workdirHint = opts.workdirHint ?? "";
|
|
1402
|
+
}
|
|
1403
|
+
async *start(params, runOpts = {}) {
|
|
1404
|
+
const runId = runOpts.runId ?? crypto2.randomUUID().replaceAll("-", "").slice(0, 12);
|
|
1405
|
+
const started = Date.now();
|
|
1406
|
+
let protocol;
|
|
1407
|
+
let providerName;
|
|
1408
|
+
let modelId;
|
|
1409
|
+
let contextWindow;
|
|
1410
|
+
try {
|
|
1411
|
+
if (this.resolver) {
|
|
1412
|
+
const resolved = this.resolver.resolve(params.model ?? "");
|
|
1413
|
+
protocol = resolved.protocol;
|
|
1414
|
+
providerName = resolved.endpoint.name;
|
|
1415
|
+
modelId = resolved.modelId;
|
|
1416
|
+
contextWindow = resolved.contextWindow;
|
|
1417
|
+
} else {
|
|
1418
|
+
protocol = this.protocol;
|
|
1419
|
+
providerName = protocol.name;
|
|
1420
|
+
modelId = params.model ?? "";
|
|
1421
|
+
contextWindow = void 0;
|
|
1422
|
+
}
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
if (err instanceof LlmError) {
|
|
1425
|
+
yield { type: "agent_error", run_id: runId, error: err.message };
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
throw err;
|
|
1429
|
+
}
|
|
1430
|
+
if (!contextWindow) {
|
|
1431
|
+
this.logger.info(`\u6A21\u578B\u672A\u58F0\u660E\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF0C\u81EA\u52A8\u538B\u7F29\u505C\u7528 model=${modelId}`);
|
|
1432
|
+
}
|
|
1433
|
+
const history = (params.history ?? []).map((m) => chatMessageSchema.parse(m));
|
|
1434
|
+
const settings = modelSettingsSchema.parse(params.settings ?? {});
|
|
1435
|
+
const definitions = this.tools.map((t) => t.definition);
|
|
1436
|
+
const availableToolsPrompt = buildAvailableToolsPrompt(
|
|
1437
|
+
definitions.map(({ name, description }) => ({ name, description }))
|
|
1438
|
+
);
|
|
1439
|
+
const messages = [
|
|
1440
|
+
{
|
|
1441
|
+
role: "system",
|
|
1442
|
+
content: params.system_prompt ? `${params.system_prompt.trim()}
|
|
1443
|
+
|
|
1444
|
+
${availableToolsPrompt}` : buildSystemPrompt({ availableTools: definitions.map(({ name, description }) => ({ name, description })) })
|
|
1445
|
+
},
|
|
1446
|
+
...history,
|
|
1447
|
+
...params.include_input === false ? [] : [{ role: "user", content: params.input }]
|
|
1448
|
+
];
|
|
1449
|
+
yield { type: "agent_start", run_id: runId, provider: providerName, model: modelId };
|
|
1450
|
+
const preCompact = await this.maybeCompact(
|
|
1451
|
+
protocol,
|
|
1452
|
+
runId,
|
|
1453
|
+
modelId,
|
|
1454
|
+
messages,
|
|
1455
|
+
estimateTokens(messages),
|
|
1456
|
+
contextWindow,
|
|
1457
|
+
settings
|
|
1458
|
+
);
|
|
1459
|
+
if (preCompact) yield preCompact;
|
|
1460
|
+
let usage = emptyUsage();
|
|
1461
|
+
for (let step = 1; step <= this.maxSteps; step++) {
|
|
1462
|
+
if (runOpts.signal?.aborted) return;
|
|
1463
|
+
let final = null;
|
|
1464
|
+
for await (const event of protocol.chatStream({
|
|
1465
|
+
model: modelId,
|
|
1466
|
+
messages,
|
|
1467
|
+
tools: definitions.length ? definitions : void 0,
|
|
1468
|
+
settings,
|
|
1469
|
+
signal: runOpts.signal
|
|
1470
|
+
})) {
|
|
1471
|
+
if (event.type === "thinking_delta") {
|
|
1472
|
+
yield { type: "thinking_delta", run_id: runId, delta: event.delta };
|
|
1473
|
+
} else if (event.type === "text_delta") {
|
|
1474
|
+
yield { type: "text_delta", run_id: runId, delta: event.delta };
|
|
1475
|
+
} else if (event.type === "toolcall_start") {
|
|
1476
|
+
yield { type: "tool_call_start", run_id: runId, tool_call: event.tool_call };
|
|
1477
|
+
} else if (event.type === "toolcall_delta") {
|
|
1478
|
+
yield {
|
|
1479
|
+
type: "tool_call_delta",
|
|
1480
|
+
run_id: runId,
|
|
1481
|
+
delta: event.delta,
|
|
1482
|
+
tool_call_id: event.tool_call.id
|
|
1483
|
+
};
|
|
1484
|
+
} else if (event.type === "toolcall_end") {
|
|
1485
|
+
yield { type: "tool_call", run_id: runId, tool_call: event.tool_call };
|
|
1486
|
+
} else if (event.type === "error") {
|
|
1487
|
+
this.logger.warn(`Agent \u8FD0\u884C\u5931\u8D25 run=${runId}\uFF1A${event.error}`);
|
|
1488
|
+
yield { type: "agent_error", run_id: runId, error: event.error || "\u6A21\u578B\u8C03\u7528\u5931\u8D25\uFF0C\u539F\u56E0\u672A\u77E5" };
|
|
1489
|
+
return;
|
|
1490
|
+
} else if (event.type === "done") {
|
|
1491
|
+
final = event.response;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (runOpts.signal?.aborted) return;
|
|
1495
|
+
if (!final) {
|
|
1496
|
+
yield { type: "agent_error", run_id: runId, error: "\u6A21\u578B\u6D41\u5F02\u5E38\u7EC8\u6B62\uFF0C\u672A\u8FD4\u56DE\u7ED3\u679C" };
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
usage = addUsage(usage, final.usage);
|
|
1500
|
+
if (final.tool_calls.length === 0) {
|
|
1501
|
+
await this.notify(responseToMessage(final), final);
|
|
1502
|
+
const result = {
|
|
1503
|
+
text: final.content,
|
|
1504
|
+
thinking: final.thinking,
|
|
1505
|
+
finish_reason: final.finish_reason,
|
|
1506
|
+
usage,
|
|
1507
|
+
steps: step,
|
|
1508
|
+
model: final.model,
|
|
1509
|
+
provider: final.provider,
|
|
1510
|
+
elapsed_ms: Date.now() - started
|
|
1511
|
+
};
|
|
1512
|
+
yield { type: "agent_done", run_id: runId, result };
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
messages.push(responseToMessage(final));
|
|
1516
|
+
await this.notify(responseToMessage(final), final);
|
|
1517
|
+
const stepToolMessages = [];
|
|
1518
|
+
let callIndex = 0;
|
|
1519
|
+
while (callIndex < final.tool_calls.length) {
|
|
1520
|
+
const firstCall = final.tool_calls[callIndex];
|
|
1521
|
+
const firstTool = this.toolsByName.get(firstCall.name);
|
|
1522
|
+
const canRunParallel = firstTool?.annotations?.readOnly === true && firstTool.annotations.parallelSafe === true;
|
|
1523
|
+
const batch = [firstCall];
|
|
1524
|
+
callIndex += 1;
|
|
1525
|
+
if (canRunParallel) {
|
|
1526
|
+
while (callIndex < final.tool_calls.length) {
|
|
1527
|
+
const candidate = final.tool_calls[callIndex];
|
|
1528
|
+
const tool = this.toolsByName.get(candidate.name);
|
|
1529
|
+
if (tool?.annotations?.readOnly !== true || tool.annotations.parallelSafe !== true) break;
|
|
1530
|
+
batch.push(candidate);
|
|
1531
|
+
callIndex += 1;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const settled = await Promise.allSettled(
|
|
1535
|
+
batch.map((tc) => this.executeTool(tc, definitions, {
|
|
1536
|
+
signal: runOpts.signal,
|
|
1537
|
+
runId
|
|
1538
|
+
}))
|
|
1539
|
+
);
|
|
1540
|
+
for (const [index, outcome] of settled.entries()) {
|
|
1541
|
+
const tc = batch[index];
|
|
1542
|
+
let result;
|
|
1543
|
+
if (outcome.status === "rejected") {
|
|
1544
|
+
const err = outcome.reason;
|
|
1545
|
+
if (err instanceof EscalationRequiredError) {
|
|
1546
|
+
const escalation = {
|
|
1547
|
+
escalation_id: crypto2.randomUUID().replaceAll("-", "").slice(0, 12),
|
|
1548
|
+
requested_path: err.requestedPath,
|
|
1549
|
+
resolved_path: err.resolvedPath,
|
|
1550
|
+
tool_name: tc.name,
|
|
1551
|
+
workdir: this.workdirHint,
|
|
1552
|
+
resource_type: err.resourceType,
|
|
1553
|
+
requested_command: err.requestedCommand
|
|
1554
|
+
};
|
|
1555
|
+
await this.notifyEscalation(escalation);
|
|
1556
|
+
yield { type: "escalation_request", run_id: runId, escalation };
|
|
1557
|
+
yield { type: "agent_cancelled", run_id: runId };
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
throw err;
|
|
1561
|
+
} else {
|
|
1562
|
+
result = outcome.value;
|
|
1563
|
+
}
|
|
1564
|
+
const toolResult = {
|
|
1565
|
+
tool_call_id: result.tool_call_id,
|
|
1566
|
+
name: result.name,
|
|
1567
|
+
output: result.output.slice(0, EVENT_OUTPUT_LIMIT),
|
|
1568
|
+
is_error: result.is_error,
|
|
1569
|
+
elapsed_ms: result.elapsed_ms
|
|
1570
|
+
};
|
|
1571
|
+
yield { type: "tool_result", run_id: runId, tool_result: toolResult };
|
|
1572
|
+
const toolMessage = {
|
|
1573
|
+
role: "tool",
|
|
1574
|
+
content: result.output,
|
|
1575
|
+
tool_call_id: tc.id,
|
|
1576
|
+
name: tc.name,
|
|
1577
|
+
...result.is_error ? { is_error: true } : {}
|
|
1578
|
+
};
|
|
1579
|
+
messages.push(toolMessage);
|
|
1580
|
+
stepToolMessages.push(toolMessage);
|
|
1581
|
+
await this.notify(toolMessage, null);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const stepUsage = final.usage.prompt_tokens + final.usage.completion_tokens;
|
|
1585
|
+
const contextTokens = stepUsage > 0 ? stepUsage + estimateTokens(stepToolMessages) : estimateTokens(messages);
|
|
1586
|
+
const midCompact = await this.maybeCompact(
|
|
1587
|
+
protocol,
|
|
1588
|
+
runId,
|
|
1589
|
+
modelId,
|
|
1590
|
+
messages,
|
|
1591
|
+
contextTokens,
|
|
1592
|
+
contextWindow,
|
|
1593
|
+
settings
|
|
1594
|
+
);
|
|
1595
|
+
if (midCompact) yield midCompact;
|
|
1596
|
+
}
|
|
1597
|
+
this.logger.warn(`Agent \u8FBE\u5230\u6700\u5927\u6B65\u6570\u4E0A\u9650 run=${runId} steps=${this.maxSteps}`);
|
|
1598
|
+
yield {
|
|
1599
|
+
type: "agent_error",
|
|
1600
|
+
run_id: runId,
|
|
1601
|
+
error: `\u5DF2\u8FBE\u5230\u6700\u5927\u6267\u884C\u6B65\u6570\u4E0A\u9650\uFF08${this.maxSteps} \u6B65\uFF09\u4ECD\u672A\u5B8C\u6210\uFF0C\u8FD0\u884C\u7EC8\u6B62\u3002\u8BF7\u628A\u4EFB\u52A1\u62C6\u5C0F\u540E\u91CD\u8BD5\uFF0C\u6216\u68C0\u67E5\u662F\u5426\u9677\u5165\u4E86\u5FAA\u73AF\u3002`
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
/** 达到水位线时压缩上下文并原地替换 messages,返回压缩事件。
|
|
1605
|
+
* 任何失败都降级为「本次不压缩」:记日志后照常返回 null,运行继续。 */
|
|
1606
|
+
async maybeCompact(protocol, runId, modelId, messages, contextTokens, contextWindow, settings) {
|
|
1607
|
+
if (!shouldCompact(contextTokens, contextWindow)) return null;
|
|
1608
|
+
let result = null;
|
|
1609
|
+
try {
|
|
1610
|
+
result = await compact(protocol, modelId, messages, settings);
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
this.logger.warn(`\u4E0A\u4E0B\u6587\u538B\u7F29\u53D1\u751F\u610F\u5916\u9519\u8BEF\uFF0C\u672C\u6B21\u8DF3\u8FC7\u538B\u7F29 run=${runId}\uFF1A${err instanceof Error ? err.message : err}`);
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
if (!result) return null;
|
|
1616
|
+
messages.splice(0, messages.length, messages[0], ...result.replacement_history);
|
|
1617
|
+
await this.notifyCompaction(result);
|
|
1618
|
+
this.logger.info(
|
|
1619
|
+
`\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29 run=${runId} tokens=${result.tokens_before}\u2192${result.tokens_after}`
|
|
1620
|
+
);
|
|
1621
|
+
return {
|
|
1622
|
+
type: "context_compacted",
|
|
1623
|
+
run_id: runId,
|
|
1624
|
+
compaction: {
|
|
1625
|
+
summary: result.summary,
|
|
1626
|
+
tokens_before: result.tokens_before,
|
|
1627
|
+
tokens_after: result.tokens_after
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
async notifyCompaction(result) {
|
|
1632
|
+
if (!this.onCompaction) return;
|
|
1633
|
+
try {
|
|
1634
|
+
await this.onCompaction(result);
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
this.logger.warn(`\u538B\u7F29\u8BB0\u5F55\u6301\u4E45\u5316\u56DE\u8C03\u5931\u8D25\uFF08\u672C\u6B21\u538B\u7F29\u53EF\u80FD\u672A\u843D\u76D8\uFF09\uFF1A${err instanceof Error ? err.message : err}`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
async notifyEscalation(info) {
|
|
1640
|
+
if (!this.onEscalation) return;
|
|
1641
|
+
try {
|
|
1642
|
+
await this.onEscalation(info);
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
this.logger.warn(`\u63D0\u6743\u8BF7\u6C42\u6301\u4E45\u5316\u56DE\u8C03\u5931\u8D25\uFF08\u672C\u6B21\u8BF7\u6C42\u53EF\u80FD\u672A\u843D\u76D8\uFF09\uFF1A${err instanceof Error ? err.message : err}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
async notify(message, response) {
|
|
1648
|
+
if (!this.onMessage) return;
|
|
1649
|
+
try {
|
|
1650
|
+
await this.onMessage(message, response);
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
this.logger.warn(`Agent \u6D88\u606F\u6301\u4E45\u5316\u56DE\u8C03\u5931\u8D25\uFF08\u672C\u6761\u6D88\u606F\u53EF\u80FD\u672A\u843D\u76D8\uFF09\uFF1A${err instanceof Error ? err.message : err}`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/** 执行单个工具调用:校验 → 执行;任何失败都转为回喂文本,不抛异常 */
|
|
1656
|
+
async executeTool(tc, definitions, context = {}) {
|
|
1657
|
+
const started = Date.now();
|
|
1658
|
+
const { args, error } = validateToolCall(definitions, tc);
|
|
1659
|
+
let output;
|
|
1660
|
+
let isError;
|
|
1661
|
+
if (error !== null) {
|
|
1662
|
+
output = error;
|
|
1663
|
+
isError = true;
|
|
1664
|
+
} else {
|
|
1665
|
+
const tool = this.toolsByName.get(tc.name);
|
|
1666
|
+
try {
|
|
1667
|
+
const handled = await tool.handler(args, context);
|
|
1668
|
+
output = truncateMiddle2(typeof handled === "string" ? handled : handled.content);
|
|
1669
|
+
isError = typeof handled === "string" ? false : handled.isError ?? false;
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
if (err instanceof EscalationRequiredError) throw err;
|
|
1672
|
+
this.logger.warn(`\u5DE5\u5177\u6267\u884C\u5931\u8D25 tool=${tc.name}\uFF1A${err instanceof Error ? err.message : err}`);
|
|
1673
|
+
output = `\u5DE5\u5177\u6267\u884C\u5931\u8D25\uFF1A${err instanceof Error ? err.message : err}`;
|
|
1674
|
+
isError = true;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
tool_call_id: tc.id,
|
|
1679
|
+
name: tc.name,
|
|
1680
|
+
output,
|
|
1681
|
+
is_error: isError,
|
|
1682
|
+
elapsed_ms: Date.now() - started
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
// src/tools/workdir.ts
|
|
1688
|
+
import fs2 from "fs";
|
|
1689
|
+
import path2 from "path";
|
|
1690
|
+
function resolveSandboxed(workdir, raw, opts = {}) {
|
|
1691
|
+
const baseReal = fs2.realpathSync(workdir);
|
|
1692
|
+
const candidate = path2.resolve(path2.isAbsolute(raw) ? raw : path2.join(baseReal, raw));
|
|
1693
|
+
if (opts.allowOutsideWorkdir) return candidate;
|
|
1694
|
+
let existing = candidate;
|
|
1695
|
+
const missing = [];
|
|
1696
|
+
while (!fs2.existsSync(existing)) {
|
|
1697
|
+
missing.unshift(path2.basename(existing));
|
|
1698
|
+
const parent = path2.dirname(existing);
|
|
1699
|
+
if (parent === existing) break;
|
|
1700
|
+
existing = parent;
|
|
1701
|
+
}
|
|
1702
|
+
const existingReal = fs2.realpathSync(existing);
|
|
1703
|
+
const realTarget = path2.join(existingReal, ...missing);
|
|
1704
|
+
if (realTarget !== baseReal && !realTarget.startsWith(baseReal + path2.sep)) {
|
|
1705
|
+
if (opts.grants && opts.sessionId && opts.grants.isGranted(opts.sessionId, realTarget)) {
|
|
1706
|
+
return realTarget;
|
|
1707
|
+
}
|
|
1708
|
+
throw new EscalationRequiredError(raw, realTarget);
|
|
1709
|
+
}
|
|
1710
|
+
return realTarget;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/tools/apply-patch.ts
|
|
1714
|
+
import crypto3 from "crypto";
|
|
1715
|
+
import fs3 from "fs";
|
|
1716
|
+
import path3 from "path";
|
|
1717
|
+
import { z as z4 } from "zod";
|
|
1718
|
+
var argsSchema = z4.object({ patch: z4.string().min(1) });
|
|
1719
|
+
function makeApplyPatchTool(workdir, sandbox = {}) {
|
|
1720
|
+
return {
|
|
1721
|
+
definition: {
|
|
1722
|
+
name: "apply_patch",
|
|
1723
|
+
description: "\u5E94\u7528 Codex \u98CE\u683C\u7684\u591A\u6587\u4EF6\u8865\u4E01\u3002\u8F93\u5165\u987B\u4EE5 *** Begin Patch \u5F00\u59CB\u3001*** End Patch \u7ED3\u675F\uFF0C\u652F\u6301 *** Add File\u3001*** Update File\u3001*** Delete File\u3002\u6240\u6709\u6587\u4EF6\u5148\u6821\u9A8C\uFF0C\u4EFB\u4F55\u4E00\u5904\u5931\u8D25\u90FD\u4E0D\u4F1A\u5199\u5165\u3002",
|
|
1724
|
+
parameters: z4.toJSONSchema(argsSchema)
|
|
1725
|
+
},
|
|
1726
|
+
annotations: { readOnly: false, parallelSafe: false },
|
|
1727
|
+
handler: async (args) => {
|
|
1728
|
+
const { patch } = argsSchema.parse(args);
|
|
1729
|
+
const operations = parsePatch(patch, workdir, sandbox);
|
|
1730
|
+
const writes = [];
|
|
1731
|
+
const deletes = [];
|
|
1732
|
+
for (const operation of operations) {
|
|
1733
|
+
if (operation.kind === "add") {
|
|
1734
|
+
if (fs3.existsSync(operation.file)) throw new Error(`\u65B0\u589E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${operation.rawPath}`);
|
|
1735
|
+
writes.push({ ...operation, kind: "add" });
|
|
1736
|
+
} else if (operation.kind === "delete") {
|
|
1737
|
+
assertRegularFile(operation.rawPath, operation.file);
|
|
1738
|
+
deletes.push(operation);
|
|
1739
|
+
} else {
|
|
1740
|
+
assertRegularFile(operation.rawPath, operation.file);
|
|
1741
|
+
let content = fs3.readFileSync(operation.file, "utf-8");
|
|
1742
|
+
for (const hunk of operation.hunks) content = applyHunk(content, hunk, operation.rawPath);
|
|
1743
|
+
writes.push({ kind: "update", rawPath: operation.rawPath, file: operation.file, content });
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
const staged = [];
|
|
1747
|
+
try {
|
|
1748
|
+
for (const write of writes) staged.push(stageWrite(write));
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
for (const item of staged) fs3.rmSync(item.tmp, { force: true });
|
|
1751
|
+
throw err;
|
|
1752
|
+
}
|
|
1753
|
+
const backups = /* @__PURE__ */ new Map();
|
|
1754
|
+
const installedAdds = [];
|
|
1755
|
+
try {
|
|
1756
|
+
for (const item of staged) {
|
|
1757
|
+
if (item.kind === "update") {
|
|
1758
|
+
const backup = backupPath(item.file);
|
|
1759
|
+
fs3.renameSync(item.file, backup);
|
|
1760
|
+
backups.set(item.file, backup);
|
|
1761
|
+
} else {
|
|
1762
|
+
installedAdds.push(item.file);
|
|
1763
|
+
}
|
|
1764
|
+
fs3.mkdirSync(path3.dirname(item.file), { recursive: true });
|
|
1765
|
+
fs3.renameSync(item.tmp, item.file);
|
|
1766
|
+
}
|
|
1767
|
+
for (const deletion of deletes) {
|
|
1768
|
+
const backup = backupPath(deletion.file);
|
|
1769
|
+
fs3.renameSync(deletion.file, backup);
|
|
1770
|
+
backups.set(deletion.file, backup);
|
|
1771
|
+
}
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
for (const item of staged) fs3.rmSync(item.tmp, { force: true });
|
|
1774
|
+
for (const file of installedAdds) fs3.rmSync(file, { force: true });
|
|
1775
|
+
for (const [file, backup] of [...backups].reverse()) {
|
|
1776
|
+
fs3.rmSync(file, { force: true });
|
|
1777
|
+
if (fs3.existsSync(backup)) fs3.renameSync(backup, file);
|
|
1778
|
+
}
|
|
1779
|
+
throw err;
|
|
1780
|
+
}
|
|
1781
|
+
for (const backup of backups.values()) fs3.rmSync(backup, { force: true });
|
|
1782
|
+
const summary = operations.map((operation) => `${operation.kind} ${operation.rawPath}`).join("\n");
|
|
1783
|
+
return `\u8865\u4E01\u5DF2\u5E94\u7528\uFF08${operations.length} \u4E2A\u6587\u4EF6\uFF09\uFF1A
|
|
1784
|
+
${summary}`;
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function parsePatch(patch, workdir, sandbox) {
|
|
1789
|
+
const lines = patch.replace(/\r\n/g, "\n").split("\n");
|
|
1790
|
+
if (lines[0] !== "*** Begin Patch") throw new Error("\u8865\u4E01\u5FC5\u987B\u4EE5 *** Begin Patch \u5F00\u59CB");
|
|
1791
|
+
const end = lines.lastIndexOf("*** End Patch");
|
|
1792
|
+
if (end < 0) throw new Error("\u8865\u4E01\u7F3A\u5C11 *** End Patch");
|
|
1793
|
+
if (lines.slice(end + 1).some((line) => line.trim())) throw new Error("*** End Patch \u540E\u4E0D\u80FD\u6709\u5185\u5BB9");
|
|
1794
|
+
const operations = [];
|
|
1795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1796
|
+
let index = 1;
|
|
1797
|
+
while (index < end) {
|
|
1798
|
+
if (!lines[index]) {
|
|
1799
|
+
index += 1;
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
const header = lines[index];
|
|
1803
|
+
const matched = header.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
|
|
1804
|
+
if (!matched) throw new Error(`\u65E0\u6CD5\u8BC6\u522B\u7684\u8865\u4E01\u884C ${index + 1}\uFF1A${header}`);
|
|
1805
|
+
const kind = matched[1].toLowerCase();
|
|
1806
|
+
const rawPath = matched[2].trim();
|
|
1807
|
+
if (!rawPath) throw new Error(`\u8865\u4E01\u7B2C ${index + 1} \u884C\u7F3A\u5C11\u6587\u4EF6\u8DEF\u5F84`);
|
|
1808
|
+
const file = resolveSandboxed(workdir, rawPath, sandbox);
|
|
1809
|
+
if (seen.has(file)) throw new Error(`\u540C\u4E00\u8865\u4E01\u4E0D\u80FD\u591A\u6B21\u64CD\u4F5C\u6587\u4EF6\uFF1A${rawPath}`);
|
|
1810
|
+
seen.add(file);
|
|
1811
|
+
index += 1;
|
|
1812
|
+
const body = [];
|
|
1813
|
+
while (index < end && !lines[index].startsWith("*** ")) {
|
|
1814
|
+
body.push(lines[index]);
|
|
1815
|
+
index += 1;
|
|
1816
|
+
}
|
|
1817
|
+
if (kind === "add") {
|
|
1818
|
+
if (body.some((line) => !line.startsWith("+"))) {
|
|
1819
|
+
throw new Error(`\u65B0\u589E\u6587\u4EF6 ${rawPath} \u7684\u6BCF\u4E00\u884C\u90FD\u5FC5\u987B\u4EE5 + \u5F00\u5934`);
|
|
1820
|
+
}
|
|
1821
|
+
operations.push({ kind, rawPath, file, content: body.map((line) => line.slice(1)).join("\n") });
|
|
1822
|
+
} else if (kind === "delete") {
|
|
1823
|
+
if (body.some((line) => line.length > 0)) throw new Error(`\u5220\u9664\u6587\u4EF6 ${rawPath} \u4E0D\u63A5\u53D7\u8865\u4E01\u6B63\u6587`);
|
|
1824
|
+
operations.push({ kind, rawPath, file });
|
|
1825
|
+
} else {
|
|
1826
|
+
const hunks = parseHunks(body, rawPath);
|
|
1827
|
+
operations.push({ kind, rawPath, file, hunks });
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
if (operations.length === 0) throw new Error("\u8865\u4E01\u4E0D\u5305\u542B\u4EFB\u4F55\u6587\u4EF6\u64CD\u4F5C");
|
|
1831
|
+
return operations;
|
|
1832
|
+
}
|
|
1833
|
+
function parseHunks(lines, rawPath) {
|
|
1834
|
+
const hunks = [];
|
|
1835
|
+
let current = null;
|
|
1836
|
+
for (const line of lines) {
|
|
1837
|
+
if (line.startsWith("@@")) {
|
|
1838
|
+
current = { before: [], after: [] };
|
|
1839
|
+
hunks.push(current);
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
if (!current) throw new Error(`\u66F4\u65B0\u6587\u4EF6 ${rawPath} \u7684\u6B63\u6587\u5FC5\u987B\u4ECE @@ \u5F00\u59CB`);
|
|
1843
|
+
const marker = line[0];
|
|
1844
|
+
const text = line.slice(1);
|
|
1845
|
+
if (marker === " ") {
|
|
1846
|
+
current.before.push(text);
|
|
1847
|
+
current.after.push(text);
|
|
1848
|
+
} else if (marker === "-") {
|
|
1849
|
+
current.before.push(text);
|
|
1850
|
+
} else if (marker === "+") {
|
|
1851
|
+
current.after.push(text);
|
|
1852
|
+
} else {
|
|
1853
|
+
throw new Error(`\u66F4\u65B0\u6587\u4EF6 ${rawPath} \u5305\u542B\u975E\u6CD5\u8865\u4E01\u884C\uFF1A${line}`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (hunks.length === 0) throw new Error(`\u66F4\u65B0\u6587\u4EF6 ${rawPath} \u7F3A\u5C11 @@ hunk`);
|
|
1857
|
+
return hunks;
|
|
1858
|
+
}
|
|
1859
|
+
function applyHunk(content, hunk, rawPath) {
|
|
1860
|
+
const before = hunk.before.join("\n");
|
|
1861
|
+
const after = hunk.after.join("\n");
|
|
1862
|
+
if (!before) throw new Error(`\u66F4\u65B0\u6587\u4EF6 ${rawPath} \u7684 hunk \u7F3A\u5C11\u4E0A\u4E0B\u6587\u6216\u5220\u9664\u884C`);
|
|
1863
|
+
const first = content.indexOf(before);
|
|
1864
|
+
if (first < 0) throw new Error(`\u66F4\u65B0\u6587\u4EF6 ${rawPath} \u65F6\u672A\u627E\u5230 hunk \u4E0A\u4E0B\u6587`);
|
|
1865
|
+
if (content.indexOf(before, first + before.length) >= 0) {
|
|
1866
|
+
throw new Error(`\u66F4\u65B0\u6587\u4EF6 ${rawPath} \u7684 hunk \u4E0A\u4E0B\u6587\u4E0D\u552F\u4E00`);
|
|
1867
|
+
}
|
|
1868
|
+
return content.slice(0, first) + after + content.slice(first + before.length);
|
|
1869
|
+
}
|
|
1870
|
+
function assertRegularFile(rawPath, file) {
|
|
1871
|
+
if (!fs3.existsSync(file)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${rawPath}`);
|
|
1872
|
+
if (!fs3.statSync(file).isFile()) throw new Error(`${rawPath} \u4E0D\u662F\u666E\u901A\u6587\u4EF6`);
|
|
1873
|
+
}
|
|
1874
|
+
function stageWrite(write) {
|
|
1875
|
+
fs3.mkdirSync(path3.dirname(write.file), { recursive: true });
|
|
1876
|
+
const tmp = path3.join(path3.dirname(write.file), `.xlyra-tmp-${crypto3.randomUUID()}${path3.extname(write.file)}`);
|
|
1877
|
+
fs3.writeFileSync(tmp, write.content);
|
|
1878
|
+
return { ...write, tmp };
|
|
1879
|
+
}
|
|
1880
|
+
function backupPath(file) {
|
|
1881
|
+
return path3.join(path3.dirname(file), `.xlyra-backup-${crypto3.randomUUID()}${path3.extname(file)}`);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/tools/create.ts
|
|
1885
|
+
import fs4 from "fs";
|
|
1886
|
+
import path4 from "path";
|
|
1887
|
+
import { z as z5 } from "zod";
|
|
1888
|
+
var argsSchema2 = z5.object({
|
|
1889
|
+
path: z5.string(),
|
|
1890
|
+
content: z5.string()
|
|
1891
|
+
});
|
|
1892
|
+
function makeCreateTool(workdir, sandbox = {}) {
|
|
1893
|
+
return {
|
|
1894
|
+
definition: {
|
|
1895
|
+
name: "create",
|
|
1896
|
+
description: "\u65B0\u5EFA\u6587\u4EF6\u5E76\u5199\u5165\u5185\u5BB9\uFF08\u81EA\u52A8\u521B\u5EFA\u7236\u76EE\u5F55\uFF09\u3002\u76EE\u6807\u5DF2\u5B58\u5728\u65F6\u5931\u8D25\u2014\u2014\u5982\u9700\u4FEE\u6539\u5DF2\u6709\u6587\u4EF6\u8BF7\u7528 write \u5DE5\u5177\u3002",
|
|
1897
|
+
parameters: z5.toJSONSchema(argsSchema2)
|
|
1898
|
+
},
|
|
1899
|
+
annotations: { readOnly: false, parallelSafe: false },
|
|
1900
|
+
handler: async (args) => {
|
|
1901
|
+
const { path: rawPath, content } = argsSchema2.parse(args);
|
|
1902
|
+
const file = resolveSandboxed(workdir, rawPath, sandbox);
|
|
1903
|
+
fs4.mkdirSync(path4.dirname(file), { recursive: true });
|
|
1904
|
+
try {
|
|
1905
|
+
fs4.writeFileSync(file, content, { flag: "wx" });
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
if (err.code === "EEXIST") {
|
|
1908
|
+
throw new Error(`\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${rawPath}\uFF1B\u5982\u9700\u4FEE\u6539\u8BF7\u7528 write \u5DE5\u5177`);
|
|
1909
|
+
}
|
|
1910
|
+
throw err;
|
|
1911
|
+
}
|
|
1912
|
+
return `\u5DF2\u521B\u5EFA ${rawPath}\uFF08${content.length} \u5B57\u7B26\uFF0C${content.split("\n").length} \u884C\uFF09`;
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/tools/edit.ts
|
|
1918
|
+
import crypto4 from "crypto";
|
|
1919
|
+
import fs5 from "fs";
|
|
1920
|
+
import path5 from "path";
|
|
1921
|
+
import { z as z6 } from "zod";
|
|
1922
|
+
var argsSchema3 = z6.object({
|
|
1923
|
+
path: z6.string(),
|
|
1924
|
+
old_text: z6.string().min(1),
|
|
1925
|
+
new_text: z6.string(),
|
|
1926
|
+
replace_all: z6.boolean().default(false)
|
|
1927
|
+
});
|
|
1928
|
+
function makeEditTool(workdir, sandbox = {}) {
|
|
1929
|
+
return {
|
|
1930
|
+
definition: {
|
|
1931
|
+
name: "edit",
|
|
1932
|
+
description: "\u7CBE\u786E\u4FEE\u6539\u5DF2\u6709\u6587\u672C\u6587\u4EF6\u3002\u9ED8\u8BA4\u8981\u6C42 old_text \u5728\u6587\u4EF6\u4E2D\u6070\u597D\u51FA\u73B0\u4E00\u6B21\uFF1B\u672A\u547D\u4E2D\u6216\u591A\u6B21\u547D\u4E2D\u90FD\u4F1A\u5931\u8D25\u3002\u786E\u9700\u66FF\u6362\u5168\u90E8\u5339\u914D\u65F6\u4F20 replace_all=true\u3002",
|
|
1933
|
+
parameters: z6.toJSONSchema(argsSchema3)
|
|
1934
|
+
},
|
|
1935
|
+
annotations: { readOnly: false, parallelSafe: false },
|
|
1936
|
+
handler: async (args) => {
|
|
1937
|
+
const { path: rawPath, old_text: oldText, new_text: newText, replace_all: replaceAll } = argsSchema3.parse(args);
|
|
1938
|
+
const file = resolveSandboxed(workdir, rawPath, sandbox);
|
|
1939
|
+
if (!fs5.existsSync(file)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${rawPath}`);
|
|
1940
|
+
if (fs5.statSync(file).isDirectory()) throw new Error(`${rawPath} \u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6`);
|
|
1941
|
+
const original = fs5.readFileSync(file, "utf-8");
|
|
1942
|
+
const matches = countMatches(original, oldText);
|
|
1943
|
+
if (matches === 0) throw new Error(`\u672A\u627E\u5230\u8981\u66FF\u6362\u7684 old_text\uFF1A${rawPath}`);
|
|
1944
|
+
if (!replaceAll && matches !== 1) {
|
|
1945
|
+
throw new Error(
|
|
1946
|
+
`old_text \u5728 ${rawPath} \u4E2D\u51FA\u73B0 ${matches} \u6B21\uFF1B\u8BF7\u63D0\u4F9B\u66F4\u957F\u7684\u552F\u4E00\u4E0A\u4E0B\u6587\uFF0C\u6216\u660E\u786E\u4F20 replace_all=true`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
const first = original.indexOf(oldText);
|
|
1950
|
+
const updated = replaceAll ? original.split(oldText).join(newText) : original.slice(0, first) + newText + original.slice(first + oldText.length);
|
|
1951
|
+
atomicReplace(file, updated);
|
|
1952
|
+
return `\u5DF2\u7F16\u8F91 ${rawPath}\uFF08\u66FF\u6362 ${replaceAll ? matches : 1} \u5904\uFF09`;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function countMatches(text, needle) {
|
|
1957
|
+
let count = 0;
|
|
1958
|
+
let cursor = 0;
|
|
1959
|
+
while (true) {
|
|
1960
|
+
const index = text.indexOf(needle, cursor);
|
|
1961
|
+
if (index < 0) return count;
|
|
1962
|
+
count += 1;
|
|
1963
|
+
cursor = index + needle.length;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function atomicReplace(file, content) {
|
|
1967
|
+
const tmp = path5.join(path5.dirname(file), `.xlyra-tmp-${crypto4.randomUUID()}${path5.extname(file)}`);
|
|
1968
|
+
try {
|
|
1969
|
+
fs5.writeFileSync(tmp, content);
|
|
1970
|
+
fs5.renameSync(tmp, file);
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
fs5.rmSync(tmp, { force: true });
|
|
1973
|
+
throw err;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/tools/process-manager.ts
|
|
1978
|
+
import crypto5 from "crypto";
|
|
1979
|
+
import { spawn } from "child_process";
|
|
1980
|
+
var TRANSCRIPT_MAX_CHARS = 2e5;
|
|
1981
|
+
var COMPLETED_SESSION_RETENTION_MS = 60 * 60 * 1e3;
|
|
1982
|
+
var MAX_SESSIONS = 128;
|
|
1983
|
+
var ProcessManager = class {
|
|
1984
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1985
|
+
start(options) {
|
|
1986
|
+
this.prune();
|
|
1987
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
1988
|
+
const completed = [...this.sessions.values()].filter((session2) => session2.completedAt !== null).sort((a, b) => a.completedAt - b.completedAt);
|
|
1989
|
+
for (const session2 of completed) {
|
|
1990
|
+
this.sessions.delete(session2.id);
|
|
1991
|
+
if (this.sessions.size < MAX_SESSIONS) break;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
1995
|
+
throw new Error(`\u8FDB\u7A0B\u4F1A\u8BDD\u5DF2\u8FBE\u5230\u4E0A\u9650\uFF08${MAX_SESSIONS}\uFF09\uFF1B\u8BF7\u7B49\u5F85\u5DF2\u6709\u547D\u4EE4\u7ED3\u675F\u540E\u91CD\u8BD5`);
|
|
1996
|
+
}
|
|
1997
|
+
const id = crypto5.randomUUID().replaceAll("-", "").slice(0, 12);
|
|
1998
|
+
const child = spawn(options.command[0], options.command.slice(1), {
|
|
1999
|
+
cwd: options.cwd,
|
|
2000
|
+
env: process.env,
|
|
2001
|
+
stdio: "pipe",
|
|
2002
|
+
shell: false
|
|
2003
|
+
});
|
|
2004
|
+
let resolveDone = () => {
|
|
2005
|
+
};
|
|
2006
|
+
const done = new Promise((resolve) => {
|
|
2007
|
+
resolveDone = resolve;
|
|
2008
|
+
});
|
|
2009
|
+
const session = {
|
|
2010
|
+
id,
|
|
2011
|
+
child,
|
|
2012
|
+
output: "",
|
|
2013
|
+
readCursor: 0,
|
|
2014
|
+
exitCode: null,
|
|
2015
|
+
exitSignal: null,
|
|
2016
|
+
error: null,
|
|
2017
|
+
completedAt: null,
|
|
2018
|
+
done,
|
|
2019
|
+
resolveDone,
|
|
2020
|
+
timeout: setTimeout(() => this.terminate(id, "\u547D\u4EE4\u6267\u884C\u8D85\u65F6"), options.timeoutMs),
|
|
2021
|
+
abortSignal: options.signal
|
|
2022
|
+
};
|
|
2023
|
+
session.timeout.unref?.();
|
|
2024
|
+
this.sessions.set(id, session);
|
|
2025
|
+
const append = (chunk) => {
|
|
2026
|
+
session.output += chunk.toString("utf-8");
|
|
2027
|
+
if (session.output.length > TRANSCRIPT_MAX_CHARS) {
|
|
2028
|
+
const removed = session.output.length - TRANSCRIPT_MAX_CHARS;
|
|
2029
|
+
session.output = session.output.slice(removed);
|
|
2030
|
+
session.readCursor = Math.max(0, session.readCursor - removed);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
child.stdout.on("data", append);
|
|
2034
|
+
child.stderr.on("data", append);
|
|
2035
|
+
child.once("error", (err) => {
|
|
2036
|
+
session.error = err.message;
|
|
2037
|
+
this.finish(session, child.exitCode, child.signalCode);
|
|
2038
|
+
});
|
|
2039
|
+
child.once("close", (code, signal) => this.finish(session, code, signal));
|
|
2040
|
+
if (options.signal) {
|
|
2041
|
+
const onAbort = () => this.terminate(id, "\u547D\u4EE4\u5DF2\u53D6\u6D88");
|
|
2042
|
+
session.abortListener = onAbort;
|
|
2043
|
+
if (options.signal.aborted) onAbort();
|
|
2044
|
+
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
2045
|
+
}
|
|
2046
|
+
return id;
|
|
2047
|
+
}
|
|
2048
|
+
async poll(sessionId, yieldTimeMs, maxOutputBytes) {
|
|
2049
|
+
const session = this.require(sessionId);
|
|
2050
|
+
if (session.child.exitCode === null && session.exitSignal === null && !session.error) {
|
|
2051
|
+
await Promise.race([
|
|
2052
|
+
session.done,
|
|
2053
|
+
new Promise((resolve) => {
|
|
2054
|
+
const timer = setTimeout(resolve, yieldTimeMs);
|
|
2055
|
+
timer.unref?.();
|
|
2056
|
+
})
|
|
2057
|
+
]);
|
|
2058
|
+
}
|
|
2059
|
+
const unread = session.output.slice(session.readCursor);
|
|
2060
|
+
session.readCursor = session.output.length;
|
|
2061
|
+
return {
|
|
2062
|
+
sessionId,
|
|
2063
|
+
output: truncateMiddle2(unread, maxOutputBytes),
|
|
2064
|
+
running: session.child.exitCode === null && session.exitSignal === null && !session.error,
|
|
2065
|
+
exitCode: session.exitCode,
|
|
2066
|
+
exitSignal: session.exitSignal,
|
|
2067
|
+
error: session.error
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
write(sessionId, chars) {
|
|
2071
|
+
const session = this.require(sessionId);
|
|
2072
|
+
if (session.child.exitCode !== null || session.exitSignal !== null || session.error) {
|
|
2073
|
+
throw new Error(`\u8FDB\u7A0B\u4F1A\u8BDD\u5DF2\u7ED3\u675F\uFF1A${sessionId}`);
|
|
2074
|
+
}
|
|
2075
|
+
session.child.stdin.write(chars);
|
|
2076
|
+
}
|
|
2077
|
+
require(sessionId) {
|
|
2078
|
+
this.prune();
|
|
2079
|
+
const session = this.sessions.get(sessionId);
|
|
2080
|
+
if (!session) throw new Error(`\u8FDB\u7A0B\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF1A${sessionId}`);
|
|
2081
|
+
return session;
|
|
2082
|
+
}
|
|
2083
|
+
terminate(sessionId, reason) {
|
|
2084
|
+
const session = this.sessions.get(sessionId);
|
|
2085
|
+
if (!session || session.completedAt !== null) return;
|
|
2086
|
+
session.error = reason;
|
|
2087
|
+
session.child.kill("SIGTERM");
|
|
2088
|
+
const force = setTimeout(() => session.child.kill("SIGKILL"), 1e3);
|
|
2089
|
+
force.unref?.();
|
|
2090
|
+
}
|
|
2091
|
+
finish(session, code, signal) {
|
|
2092
|
+
if (session.completedAt !== null) return;
|
|
2093
|
+
clearTimeout(session.timeout);
|
|
2094
|
+
session.exitCode = code;
|
|
2095
|
+
session.exitSignal = signal;
|
|
2096
|
+
session.completedAt = Date.now();
|
|
2097
|
+
if (session.abortSignal && session.abortListener) {
|
|
2098
|
+
session.abortSignal.removeEventListener("abort", session.abortListener);
|
|
2099
|
+
}
|
|
2100
|
+
session.resolveDone();
|
|
2101
|
+
}
|
|
2102
|
+
prune() {
|
|
2103
|
+
const cutoff = Date.now() - COMPLETED_SESSION_RETENTION_MS;
|
|
2104
|
+
for (const [id, session] of this.sessions) {
|
|
2105
|
+
if (session.completedAt !== null && session.completedAt <= cutoff) this.sessions.delete(id);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
// src/tools/exec-command.ts
|
|
2111
|
+
import fs6 from "fs";
|
|
2112
|
+
import { z as z7 } from "zod";
|
|
2113
|
+
var argsSchema4 = z7.object({
|
|
2114
|
+
command: z7.array(z7.string()).min(1),
|
|
2115
|
+
cwd: z7.string().default("."),
|
|
2116
|
+
timeout_ms: z7.number().int().min(100).max(3e5).default(3e4),
|
|
2117
|
+
yield_time_ms: z7.number().int().min(0).max(3e4).default(1e4),
|
|
2118
|
+
max_output_bytes: z7.number().int().min(1e3).max(1e5).default(5e4)
|
|
2119
|
+
});
|
|
2120
|
+
function makeExecCommandTool(workdir, manager, sandbox = {}) {
|
|
2121
|
+
return {
|
|
2122
|
+
definition: {
|
|
2123
|
+
name: "exec_command",
|
|
2124
|
+
description: '\u76F4\u63A5\u6267\u884C\u547D\u4EE4 argv\uFF0C\u4E0D\u7ECF\u8FC7 shell\u3002command \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\uFF0C\u4F8B\u5982 ["pnpm","test"]\u3002\u547D\u4EE4\u8D85\u8FC7 yield_time_ms \u4ECD\u5728\u8FD0\u884C\u65F6\u8FD4\u56DE session_id\uFF0C\u540E\u7EED\u7528 write_stdin \u8BFB\u53D6\u8F93\u51FA\u6216\u8F93\u5165\u5185\u5BB9\u3002\u6267\u884C\u524D\u53EF\u80FD\u9700\u8981\u7528\u6237\u6388\u6743\u3002',
|
|
2125
|
+
parameters: z7.toJSONSchema(argsSchema4)
|
|
2126
|
+
},
|
|
2127
|
+
annotations: { readOnly: false, parallelSafe: false },
|
|
2128
|
+
handler: async (args, context) => {
|
|
2129
|
+
const parsed = argsSchema4.parse(args);
|
|
2130
|
+
const commandAllowed = sandbox.commandExecutionEnabled === true || sandbox.grants !== void 0 && sandbox.sessionId !== void 0 && sandbox.grants.isCapabilityGranted(sandbox.sessionId, COMMAND_EXECUTION_GRANT);
|
|
2131
|
+
if (!commandAllowed) {
|
|
2132
|
+
throw new EscalationRequiredError(
|
|
2133
|
+
parsed.command.join(" "),
|
|
2134
|
+
COMMAND_EXECUTION_GRANT,
|
|
2135
|
+
"\u6267\u884C\u672C\u673A\u547D\u4EE4\u9700\u8981\u7528\u6237\u6388\u6743",
|
|
2136
|
+
"command",
|
|
2137
|
+
parsed.command
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
const cwd = resolveSandboxed(workdir, parsed.cwd, sandbox);
|
|
2141
|
+
if (!fs6.existsSync(cwd)) throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${parsed.cwd}`);
|
|
2142
|
+
if (!fs6.statSync(cwd).isDirectory()) throw new Error(`cwd \u4E0D\u662F\u76EE\u5F55\uFF1A${parsed.cwd}`);
|
|
2143
|
+
const sessionId = manager.start({
|
|
2144
|
+
command: parsed.command,
|
|
2145
|
+
cwd,
|
|
2146
|
+
timeoutMs: parsed.timeout_ms,
|
|
2147
|
+
signal: context?.signal
|
|
2148
|
+
});
|
|
2149
|
+
const result = await manager.poll(sessionId, parsed.yield_time_ms, parsed.max_output_bytes);
|
|
2150
|
+
return formatProcessResult(result);
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function formatProcessResult(result) {
|
|
2155
|
+
const sections = [
|
|
2156
|
+
`session_id: ${result.sessionId}`,
|
|
2157
|
+
result.running ? "status: running" : `status: exited${result.exitCode !== null ? `
|
|
2158
|
+
exit_code: ${result.exitCode}` : ""}${result.exitSignal ? `
|
|
2159
|
+
exit_signal: ${result.exitSignal}` : ""}`
|
|
2160
|
+
];
|
|
2161
|
+
if (result.output) sections.push(`output:
|
|
2162
|
+
${result.output}`);
|
|
2163
|
+
else sections.push("output: \uFF08\u6682\u65E0\u65B0\u8F93\u51FA\uFF09");
|
|
2164
|
+
if (result.error) sections.push(`error: ${result.error}`);
|
|
2165
|
+
return {
|
|
2166
|
+
content: sections.join("\n"),
|
|
2167
|
+
isError: Boolean(result.error) || !result.running && result.exitCode !== null && result.exitCode !== 0
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// src/tools/list.ts
|
|
2172
|
+
import fs7 from "fs";
|
|
2173
|
+
import path6 from "path";
|
|
2174
|
+
import { z as z8 } from "zod";
|
|
2175
|
+
var MAX_ENTRIES = 500;
|
|
2176
|
+
var IGNORE_NAMES = /* @__PURE__ */ new Set([
|
|
2177
|
+
".git",
|
|
2178
|
+
"node_modules",
|
|
2179
|
+
"dist",
|
|
2180
|
+
".next",
|
|
2181
|
+
"out",
|
|
2182
|
+
"coverage",
|
|
2183
|
+
".turbo",
|
|
2184
|
+
".cache",
|
|
2185
|
+
"__pycache__",
|
|
2186
|
+
".venv"
|
|
2187
|
+
]);
|
|
2188
|
+
var argsSchema5 = z8.object({
|
|
2189
|
+
path: z8.string().default("."),
|
|
2190
|
+
recursive: z8.boolean().default(false),
|
|
2191
|
+
pattern: z8.string().optional()
|
|
2192
|
+
});
|
|
2193
|
+
function globToRegExp(pattern) {
|
|
2194
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(//g, ".*");
|
|
2195
|
+
return new RegExp(`(^|/)${escaped}$|^${escaped}$`);
|
|
2196
|
+
}
|
|
2197
|
+
function makeListTool(workdir, sandbox = {}) {
|
|
2198
|
+
return {
|
|
2199
|
+
definition: {
|
|
2200
|
+
name: "list",
|
|
2201
|
+
description: `\u5217\u51FA\u76EE\u5F55\u5185\u5BB9\uFF08\u6587\u4EF6\u4E0E\u5B50\u76EE\u5F55\uFF0C\u76EE\u5F55\u5E26 / \u540E\u7F00\uFF09\u3002\u9ED8\u8BA4\u53EA\u5217\u4E00\u5C42\uFF0Crecursive=true \u9012\u5F52\u5217\u51FA\u3002\u5185\u7F6E\u5FFD\u7565 .git\u3001node_modules \u7B49\u76EE\u5F55\u3002\u5355\u6B21\u6700\u591A\u8FD4\u56DE ${MAX_ENTRIES} \u6761\uFF0C\u8D85\u51FA\u65F6\u63D0\u793A\u7701\u7565\u6570\u91CF\u3002`,
|
|
2202
|
+
parameters: z8.toJSONSchema(argsSchema5)
|
|
2203
|
+
},
|
|
2204
|
+
annotations: { readOnly: true, parallelSafe: true },
|
|
2205
|
+
handler: async (args) => {
|
|
2206
|
+
const { path: rawPath, recursive, pattern } = argsSchema5.parse(args);
|
|
2207
|
+
const dir = resolveSandboxed(workdir, rawPath, sandbox);
|
|
2208
|
+
if (!fs7.existsSync(dir)) throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${rawPath}`);
|
|
2209
|
+
if (!fs7.statSync(dir).isDirectory()) throw new Error(`${rawPath} \u662F\u6587\u4EF6\u4E0D\u662F\u76EE\u5F55\uFF1B\u8BFB\u53D6\u6587\u4EF6\u8BF7\u7528 read \u5DE5\u5177`);
|
|
2210
|
+
const matcher = pattern ? globToRegExp(pattern) : null;
|
|
2211
|
+
const results = [];
|
|
2212
|
+
let omitted = 0;
|
|
2213
|
+
const walk = (current) => {
|
|
2214
|
+
if (results.length >= MAX_ENTRIES) {
|
|
2215
|
+
omitted += countRemaining(current, recursive);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
let entries;
|
|
2219
|
+
try {
|
|
2220
|
+
entries = fs7.readdirSync(current, { withFileTypes: true });
|
|
2221
|
+
} catch {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2225
|
+
for (const entry of entries) {
|
|
2226
|
+
if (IGNORE_NAMES.has(entry.name)) continue;
|
|
2227
|
+
const full = path6.join(current, entry.name);
|
|
2228
|
+
const rel = path6.relative(dir, full);
|
|
2229
|
+
const display = entry.isDirectory() ? `${rel}/` : rel;
|
|
2230
|
+
if (!matcher || matcher.test(display)) {
|
|
2231
|
+
if (results.length < MAX_ENTRIES) results.push(display);
|
|
2232
|
+
else omitted += 1;
|
|
2233
|
+
}
|
|
2234
|
+
if (recursive && entry.isDirectory()) walk(full);
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
walk(dir);
|
|
2238
|
+
const header = `\u76EE\u5F55 ${rawPath} \u7684\u5185\u5BB9\uFF1A`;
|
|
2239
|
+
if (results.length === 0) return `${header}
|
|
2240
|
+
\uFF08\u7A7A\u76EE\u5F55\u6216\u65E0\u5339\u914D\u9879\uFF09`;
|
|
2241
|
+
const body = results.join("\n");
|
|
2242
|
+
return omitted > 0 ? `${header}
|
|
2243
|
+
${body}
|
|
2244
|
+
\uFF08\u5171 ${results.length + omitted} \u6761\uFF0C\u5DF2\u7701\u7565 ${omitted} \u6761\uFF1B\u53EF\u7528 pattern \u7F29\u5C0F\u8303\u56F4\uFF09` : `${header}
|
|
2245
|
+
${body}`;
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
function countRemaining(dir, recursive) {
|
|
2250
|
+
let count = 0;
|
|
2251
|
+
let entries;
|
|
2252
|
+
try {
|
|
2253
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2254
|
+
} catch {
|
|
2255
|
+
return 0;
|
|
2256
|
+
}
|
|
2257
|
+
for (const entry of entries) {
|
|
2258
|
+
if (IGNORE_NAMES.has(entry.name)) continue;
|
|
2259
|
+
count += 1;
|
|
2260
|
+
if (recursive && entry.isDirectory()) count += countRemaining(path6.join(dir, entry.name), true);
|
|
2261
|
+
}
|
|
2262
|
+
return count;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/tools/read.ts
|
|
2266
|
+
import fs8 from "fs";
|
|
2267
|
+
import path7 from "path";
|
|
2268
|
+
import { z as z9 } from "zod";
|
|
2269
|
+
var MAX_LINES = 2e3;
|
|
2270
|
+
var MAX_BYTES = 5e4;
|
|
2271
|
+
var IMAGE_SUFFIXES = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"]);
|
|
2272
|
+
var argsSchema6 = z9.object({
|
|
2273
|
+
path: z9.string(),
|
|
2274
|
+
offset: z9.number().int().min(1).default(1),
|
|
2275
|
+
limit: z9.number().int().min(1).optional()
|
|
2276
|
+
});
|
|
2277
|
+
function makeReadTool(workdir, sandbox = {}) {
|
|
2278
|
+
return {
|
|
2279
|
+
definition: {
|
|
2280
|
+
name: "read",
|
|
2281
|
+
description: "\u8BFB\u53D6\u6587\u672C\u6587\u4EF6\u5185\u5BB9\u3002\u5355\u6B21\u6700\u591A\u8FD4\u56DE 2000 \u884C\u6216 50KB\uFF08\u5148\u5230\u4E3A\u51C6\uFF09\uFF0C\u5927\u6587\u4EF6\u7528 offset/limit \u5206\u6BB5\u8BFB\u53D6\uFF0C\u76F4\u5230\u8BFB\u5B8C\u4E3A\u6B62\u3002\u4E0D\u652F\u6301\u56FE\u7247\u3002",
|
|
2282
|
+
parameters: z9.toJSONSchema(argsSchema6)
|
|
2283
|
+
},
|
|
2284
|
+
annotations: { readOnly: true, parallelSafe: true },
|
|
2285
|
+
handler: async (args) => {
|
|
2286
|
+
const { path: rawPath, offset, limit } = argsSchema6.parse(args);
|
|
2287
|
+
const file = resolveSandboxed(workdir, rawPath, sandbox);
|
|
2288
|
+
if (!fs8.existsSync(file)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${rawPath}`);
|
|
2289
|
+
if (fs8.statSync(file).isDirectory()) {
|
|
2290
|
+
throw new Error(`${rawPath} \u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\uFF1B\u67E5\u770B\u76EE\u5F55\u5185\u5BB9\u8BF7\u7528 list \u5DE5\u5177`);
|
|
2291
|
+
}
|
|
2292
|
+
if (IMAGE_SUFFIXES.has(path7.extname(file).toLowerCase())) {
|
|
2293
|
+
throw new Error("\u6682\u4E0D\u652F\u6301\u8BFB\u53D6\u56FE\u7247\u6587\u4EF6\uFF0C\u53EA\u652F\u6301\u6587\u672C\u6587\u4EF6");
|
|
2294
|
+
}
|
|
2295
|
+
const lines = fs8.readFileSync(file, "utf-8").split("\n");
|
|
2296
|
+
const total = lines.length;
|
|
2297
|
+
if (offset > total && total > 0) {
|
|
2298
|
+
throw new Error(`offset \u8D85\u51FA\u8303\u56F4\uFF1A\u6587\u4EF6\u5171 ${total} \u884C\uFF0Coffset=${offset}`);
|
|
2299
|
+
}
|
|
2300
|
+
const cap = Math.min(limit ?? MAX_LINES, MAX_LINES);
|
|
2301
|
+
const window = lines.slice(offset - 1, offset - 1 + cap);
|
|
2302
|
+
const picked = [];
|
|
2303
|
+
let size = 0;
|
|
2304
|
+
for (const line of window) {
|
|
2305
|
+
size += Buffer.byteLength(line, "utf-8") + 1;
|
|
2306
|
+
if (picked.length > 0 && size > MAX_BYTES) break;
|
|
2307
|
+
picked.push(line);
|
|
2308
|
+
}
|
|
2309
|
+
const end = offset - 1 + picked.length;
|
|
2310
|
+
let body = picked.join("\n");
|
|
2311
|
+
if (end < total) {
|
|
2312
|
+
body += `
|
|
2313
|
+
\uFF08\u6587\u4EF6\u5171 ${total} \u884C\uFF0C\u672C\u6B21\u8FD4\u56DE\u7B2C ${offset}-${end} \u884C\uFF1B\u7EE7\u7EED\u8BFB\u53D6\u8BF7\u4F20 offset=${end + 1}\uFF09`;
|
|
2314
|
+
}
|
|
2315
|
+
return body || "\uFF08\u7A7A\u6587\u4EF6\uFF09";
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// src/tools/search.ts
|
|
2321
|
+
import fs9 from "fs";
|
|
2322
|
+
import path8 from "path";
|
|
2323
|
+
import { z as z10 } from "zod";
|
|
2324
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
2325
|
+
var HARD_MAX_RESULTS = 500;
|
|
2326
|
+
var IGNORE_NAMES2 = /* @__PURE__ */ new Set([
|
|
2327
|
+
".git",
|
|
2328
|
+
"node_modules",
|
|
2329
|
+
"dist",
|
|
2330
|
+
".next",
|
|
2331
|
+
"out",
|
|
2332
|
+
"coverage",
|
|
2333
|
+
".turbo",
|
|
2334
|
+
".cache",
|
|
2335
|
+
"__pycache__",
|
|
2336
|
+
".venv"
|
|
2337
|
+
]);
|
|
2338
|
+
var argsSchema7 = z10.object({
|
|
2339
|
+
query: z10.string().min(1),
|
|
2340
|
+
path: z10.string().default("."),
|
|
2341
|
+
glob: z10.string().optional(),
|
|
2342
|
+
case_sensitive: z10.boolean().default(false),
|
|
2343
|
+
max_results: z10.number().int().min(1).max(HARD_MAX_RESULTS).default(100)
|
|
2344
|
+
});
|
|
2345
|
+
function makeSearchTool(workdir, sandbox = {}) {
|
|
2346
|
+
return {
|
|
2347
|
+
definition: {
|
|
2348
|
+
name: "search",
|
|
2349
|
+
description: "\u5728\u5DE5\u4F5C\u533A\u6587\u672C\u6587\u4EF6\u4E2D\u641C\u7D22\u5185\u5BB9\uFF0C\u8FD4\u56DE path:line:match\u3002\u53EF\u7528 glob \u6309\u6587\u4EF6\u8DEF\u5F84\u7B5B\u9009\uFF0C\u4F8B\u5982 **/*.ts\uFF1B\u9ED8\u8BA4\u5FFD\u7565 .git\u3001node_modules\u3001dist \u7B49\u76EE\u5F55\u548C\u4E8C\u8FDB\u5236\u6587\u4EF6\u3002",
|
|
2350
|
+
parameters: z10.toJSONSchema(argsSchema7)
|
|
2351
|
+
},
|
|
2352
|
+
annotations: { readOnly: true, parallelSafe: true },
|
|
2353
|
+
handler: async (args, context) => {
|
|
2354
|
+
const parsed = argsSchema7.parse(args);
|
|
2355
|
+
const target = resolveSandboxed(workdir, parsed.path, sandbox);
|
|
2356
|
+
if (!fs9.existsSync(target)) throw new Error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${parsed.path}`);
|
|
2357
|
+
const matcher = parsed.glob ? globToRegExp2(parsed.glob) : null;
|
|
2358
|
+
const needle = parsed.case_sensitive ? parsed.query : parsed.query.toLocaleLowerCase();
|
|
2359
|
+
const results = [];
|
|
2360
|
+
let truncated = false;
|
|
2361
|
+
const visit = (entryPath) => {
|
|
2362
|
+
if (context?.signal?.aborted || results.length >= parsed.max_results) {
|
|
2363
|
+
truncated = results.length >= parsed.max_results;
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
const lstat = fs9.lstatSync(entryPath);
|
|
2367
|
+
if (lstat.isSymbolicLink()) return;
|
|
2368
|
+
const stat = lstat;
|
|
2369
|
+
if (stat.isDirectory()) {
|
|
2370
|
+
for (const entry of fs9.readdirSync(entryPath, { withFileTypes: true })) {
|
|
2371
|
+
if (IGNORE_NAMES2.has(entry.name)) continue;
|
|
2372
|
+
visit(path8.join(entryPath, entry.name));
|
|
2373
|
+
if (results.length >= parsed.max_results || context?.signal?.aborted) break;
|
|
2374
|
+
}
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
const relative = path8.relative(workdir, entryPath) || path8.basename(entryPath);
|
|
2378
|
+
const portable = relative.split(path8.sep).join("/");
|
|
2379
|
+
if (matcher && !matcher.test(portable)) return;
|
|
2380
|
+
if (stat.size > MAX_FILE_BYTES || isBinary(entryPath)) return;
|
|
2381
|
+
let text;
|
|
2382
|
+
try {
|
|
2383
|
+
text = fs9.readFileSync(entryPath, "utf-8");
|
|
2384
|
+
} catch {
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
for (const [index, line] of text.split("\n").entries()) {
|
|
2388
|
+
const haystack = parsed.case_sensitive ? line : line.toLocaleLowerCase();
|
|
2389
|
+
if (!haystack.includes(needle)) continue;
|
|
2390
|
+
results.push(`${portable}:${index + 1}:${line}`);
|
|
2391
|
+
if (results.length >= parsed.max_results) {
|
|
2392
|
+
truncated = true;
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
visit(target);
|
|
2398
|
+
if (context?.signal?.aborted) throw new Error("\u641C\u7D22\u5DF2\u53D6\u6D88");
|
|
2399
|
+
if (results.length === 0) return `\u672A\u627E\u5230\u5339\u914D\uFF1A${parsed.query}`;
|
|
2400
|
+
return results.join("\n") + (truncated ? `
|
|
2401
|
+
\uFF08\u7ED3\u679C\u5DF2\u622A\u65AD\u5230 ${parsed.max_results} \u6761\uFF1B\u8BF7\u7F29\u5C0F path \u6216 glob\uFF09` : "");
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
function isBinary(file) {
|
|
2406
|
+
const fd = fs9.openSync(file, "r");
|
|
2407
|
+
try {
|
|
2408
|
+
const buffer = Buffer.alloc(4096);
|
|
2409
|
+
const read = fs9.readSync(fd, buffer, 0, buffer.length, 0);
|
|
2410
|
+
return buffer.subarray(0, read).includes(0);
|
|
2411
|
+
} finally {
|
|
2412
|
+
fs9.closeSync(fd);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function globToRegExp2(glob) {
|
|
2416
|
+
let source = "";
|
|
2417
|
+
for (let index = 0; index < glob.length; index += 1) {
|
|
2418
|
+
const char = glob[index];
|
|
2419
|
+
if (char === "*") {
|
|
2420
|
+
if (glob[index + 1] === "*") {
|
|
2421
|
+
if (glob[index + 2] === "/") {
|
|
2422
|
+
source += "(?:.*/)?";
|
|
2423
|
+
index += 2;
|
|
2424
|
+
} else {
|
|
2425
|
+
source += ".*";
|
|
2426
|
+
index += 1;
|
|
2427
|
+
}
|
|
2428
|
+
} else {
|
|
2429
|
+
source += "[^/]*";
|
|
2430
|
+
}
|
|
2431
|
+
} else if (char === "?") {
|
|
2432
|
+
source += "[^/]";
|
|
2433
|
+
} else {
|
|
2434
|
+
source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return new RegExp(`^${source}$`);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/tools/write.ts
|
|
2441
|
+
import fs10 from "fs";
|
|
2442
|
+
import path9 from "path";
|
|
2443
|
+
import crypto6 from "crypto";
|
|
2444
|
+
import { z as z11 } from "zod";
|
|
2445
|
+
var argsSchema8 = z11.object({
|
|
2446
|
+
path: z11.string(),
|
|
2447
|
+
content: z11.string()
|
|
2448
|
+
});
|
|
2449
|
+
function makeWriteTool(workdir, sandbox = {}) {
|
|
2450
|
+
return {
|
|
2451
|
+
definition: {
|
|
2452
|
+
name: "write",
|
|
2453
|
+
description: "\u6574\u4F53\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\u7684\u5185\u5BB9\u3002\u76EE\u6807\u4E0D\u5B58\u5728\u65F6\u5931\u8D25\u2014\u2014\u65B0\u5EFA\u6587\u4EF6\u8BF7\u7528 create \u5DE5\u5177\u3002",
|
|
2454
|
+
parameters: z11.toJSONSchema(argsSchema8)
|
|
2455
|
+
},
|
|
2456
|
+
annotations: { readOnly: false, parallelSafe: false },
|
|
2457
|
+
handler: async (args) => {
|
|
2458
|
+
const { path: rawPath, content } = argsSchema8.parse(args);
|
|
2459
|
+
const file = resolveSandboxed(workdir, rawPath, sandbox);
|
|
2460
|
+
if (!fs10.existsSync(file)) {
|
|
2461
|
+
throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${rawPath}\uFF1B\u65B0\u5EFA\u6587\u4EF6\u8BF7\u7528 create \u5DE5\u5177`);
|
|
2462
|
+
}
|
|
2463
|
+
if (fs10.statSync(file).isDirectory()) {
|
|
2464
|
+
throw new Error(`${rawPath} \u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6`);
|
|
2465
|
+
}
|
|
2466
|
+
const tmp = path9.join(
|
|
2467
|
+
path9.dirname(file),
|
|
2468
|
+
`.xlyra-tmp-${crypto6.randomUUID()}${path9.extname(file)}`
|
|
2469
|
+
);
|
|
2470
|
+
try {
|
|
2471
|
+
fs10.writeFileSync(tmp, content);
|
|
2472
|
+
fs10.renameSync(tmp, file);
|
|
2473
|
+
} catch (err) {
|
|
2474
|
+
fs10.rmSync(tmp, { force: true });
|
|
2475
|
+
throw err;
|
|
2476
|
+
}
|
|
2477
|
+
return `\u5DF2\u8986\u76D6 ${rawPath}\uFF08${content.length} \u5B57\u7B26\uFF0C${content.split("\n").length} \u884C\uFF09`;
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/tools/write-stdin.ts
|
|
2483
|
+
import { z as z12 } from "zod";
|
|
2484
|
+
var argsSchema9 = z12.object({
|
|
2485
|
+
session_id: z12.string().min(1),
|
|
2486
|
+
chars: z12.string().optional(),
|
|
2487
|
+
yield_time_ms: z12.number().int().min(0).max(3e4).default(5e3),
|
|
2488
|
+
max_output_bytes: z12.number().int().min(1e3).max(1e5).default(5e4)
|
|
2489
|
+
});
|
|
2490
|
+
function makeWriteStdinTool(manager, sandbox = {}) {
|
|
2491
|
+
return {
|
|
2492
|
+
definition: {
|
|
2493
|
+
name: "write_stdin",
|
|
2494
|
+
description: "\u7EE7\u7EED\u8BFB\u53D6 exec_command \u8FD4\u56DE\u7684\u8FDB\u7A0B\u4F1A\u8BDD\uFF1B\u63D0\u4F9B chars \u65F6\u5148\u5411\u8FDB\u7A0B\u6807\u51C6\u8F93\u5165\u5199\u5165\u5185\u5BB9\u3002\u4EA4\u4E92\u5F0F\u8F93\u5165\u901A\u5E38\u9700\u8981\u81EA\u884C\u9644\u52A0\u6362\u884C\u7B26\u3002\u4F7F\u7528\u524D\u53EF\u80FD\u9700\u8981\u7528\u6237\u6388\u6743\u3002",
|
|
2495
|
+
parameters: z12.toJSONSchema(argsSchema9)
|
|
2496
|
+
},
|
|
2497
|
+
annotations: { readOnly: false, parallelSafe: false },
|
|
2498
|
+
handler: async (args) => {
|
|
2499
|
+
const parsed = argsSchema9.parse(args);
|
|
2500
|
+
const commandAllowed = sandbox.commandExecutionEnabled === true || sandbox.grants !== void 0 && sandbox.sessionId !== void 0 && sandbox.grants.isCapabilityGranted(sandbox.sessionId, COMMAND_EXECUTION_GRANT);
|
|
2501
|
+
if (!commandAllowed) {
|
|
2502
|
+
throw new EscalationRequiredError(
|
|
2503
|
+
`exec_command session ${parsed.session_id}`,
|
|
2504
|
+
COMMAND_EXECUTION_GRANT,
|
|
2505
|
+
"\u7EE7\u7EED\u547D\u4EE4\u4F1A\u8BDD\u9700\u8981\u7528\u6237\u6388\u6743",
|
|
2506
|
+
"command"
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
if (parsed.chars !== void 0) manager.write(parsed.session_id, parsed.chars);
|
|
2510
|
+
const result = await manager.poll(parsed.session_id, parsed.yield_time_ms, parsed.max_output_bytes);
|
|
2511
|
+
return formatProcessResult(result);
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/tools/index.ts
|
|
2517
|
+
import fs11 from "fs";
|
|
2518
|
+
import path10 from "path";
|
|
2519
|
+
function builtinTools(opts) {
|
|
2520
|
+
const workdir = path10.resolve(opts.workdir);
|
|
2521
|
+
fs11.mkdirSync(workdir, { recursive: true });
|
|
2522
|
+
const sandbox = {
|
|
2523
|
+
allowOutsideWorkdir: opts.allowOutsideWorkdir ?? false,
|
|
2524
|
+
grants: opts.grants,
|
|
2525
|
+
sessionId: opts.sessionId
|
|
2526
|
+
};
|
|
2527
|
+
const tools = [
|
|
2528
|
+
makeListTool(workdir, sandbox),
|
|
2529
|
+
makeReadTool(workdir, sandbox),
|
|
2530
|
+
makeSearchTool(workdir, sandbox),
|
|
2531
|
+
makeCreateTool(workdir, sandbox),
|
|
2532
|
+
makeEditTool(workdir, sandbox),
|
|
2533
|
+
makeWriteTool(workdir, sandbox),
|
|
2534
|
+
makeApplyPatchTool(workdir, sandbox)
|
|
2535
|
+
];
|
|
2536
|
+
const commandSandbox = { ...sandbox, commandExecutionEnabled: opts.enableCommandExecution ?? false };
|
|
2537
|
+
const processes = new ProcessManager();
|
|
2538
|
+
tools.push(
|
|
2539
|
+
makeExecCommandTool(workdir, processes, commandSandbox),
|
|
2540
|
+
makeWriteStdinTool(processes, commandSandbox)
|
|
2541
|
+
);
|
|
2542
|
+
return tools;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/server/config.ts
|
|
2546
|
+
import fs12 from "fs";
|
|
2547
|
+
import path11 from "path";
|
|
2548
|
+
import { z as z13 } from "zod";
|
|
2549
|
+
var endpointSchema = z13.object({
|
|
2550
|
+
name: z13.string().min(1, "\u7AEF\u70B9\u540D\u4E0D\u80FD\u4E3A\u7A7A"),
|
|
2551
|
+
protocol: z13.enum(["openai-responses", "anthropic-messages"]),
|
|
2552
|
+
base_url: z13.string().min(1, "base_url \u4E0D\u80FD\u4E3A\u7A7A"),
|
|
2553
|
+
api_key: z13.string().min(1, "api_key \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u53EF\u7528 ${ENV_VAR} \u5F15\u7528\u73AF\u5883\u53D8\u91CF\uFF09"),
|
|
2554
|
+
models: z13.record(z13.string(), z13.object({ context_window: z13.number().positive().optional() })).optional(),
|
|
2555
|
+
default_model: z13.string().optional()
|
|
2556
|
+
});
|
|
2557
|
+
var appConfigSchema = z13.object({
|
|
2558
|
+
endpoints: z13.array(endpointSchema).min(1, "endpoints \u4E3A\u7A7A\uFF1A\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u6A21\u578B\u7AEF\u70B9"),
|
|
2559
|
+
server: z13.object({
|
|
2560
|
+
port: z13.number().int().positive().optional(),
|
|
2561
|
+
token: z13.string().optional()
|
|
2562
|
+
}).optional(),
|
|
2563
|
+
agent: z13.object({
|
|
2564
|
+
workdir: z13.string().optional(),
|
|
2565
|
+
agent_name: z13.string().optional(),
|
|
2566
|
+
persona: z13.string().optional(),
|
|
2567
|
+
/** 本机命令可访问当前用户权限范围内的系统资源,必须显式开启。 */
|
|
2568
|
+
enable_command_execution: z13.boolean().optional()
|
|
2569
|
+
}).optional()
|
|
2570
|
+
});
|
|
2571
|
+
function defaultConfigPath() {
|
|
2572
|
+
return path11.join(defaultDataDir(), "config.json");
|
|
2573
|
+
}
|
|
2574
|
+
function loadConfig(configPath) {
|
|
2575
|
+
const file = configPath ?? defaultConfigPath();
|
|
2576
|
+
if (!fs12.existsSync(file)) {
|
|
2577
|
+
throw new Error(
|
|
2578
|
+
`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${file}
|
|
2579
|
+
\u5148\u8FD0\u884C xlyra config init \u4EA4\u4E92\u5F0F\u521B\u5EFA\uFF0C\u6216\u53C2\u8003 README \u624B\u5DE5\u7F16\u5199\u3002`
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
let raw;
|
|
2583
|
+
try {
|
|
2584
|
+
raw = JSON.parse(fs12.readFileSync(file, "utf-8"));
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${file}\uFF08${err instanceof Error ? err.message : err}\uFF09`);
|
|
2587
|
+
}
|
|
2588
|
+
const parsed = appConfigSchema.safeParse(raw);
|
|
2589
|
+
if (!parsed.success) {
|
|
2590
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(\u6839)"}\uFF1A${i.message}`).join("\n");
|
|
2591
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${file}
|
|
2592
|
+
${issues}`);
|
|
2593
|
+
}
|
|
2594
|
+
for (const endpoint of parsed.data.endpoints) {
|
|
2595
|
+
endpoint.api_key = interpolateEnv(endpoint.api_key, file);
|
|
2596
|
+
}
|
|
2597
|
+
return parsed.data;
|
|
2598
|
+
}
|
|
2599
|
+
function saveConfig(config, configPath) {
|
|
2600
|
+
const file = configPath ?? defaultConfigPath();
|
|
2601
|
+
fs12.mkdirSync(path11.dirname(file), { recursive: true });
|
|
2602
|
+
const tmp = `${file}.tmp`;
|
|
2603
|
+
fs12.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
|
|
2604
|
+
fs12.renameSync(tmp, file);
|
|
2605
|
+
}
|
|
2606
|
+
function configExists(configPath) {
|
|
2607
|
+
return fs12.existsSync(configPath ?? defaultConfigPath());
|
|
2608
|
+
}
|
|
2609
|
+
function upsertEndpoint(endpoint, configPath) {
|
|
2610
|
+
const file = configPath ?? defaultConfigPath();
|
|
2611
|
+
let config;
|
|
2612
|
+
if (fs12.existsSync(file)) {
|
|
2613
|
+
const raw = JSON.parse(fs12.readFileSync(file, "utf-8"));
|
|
2614
|
+
config = appConfigSchema.parse({ ...raw, endpoints: raw.endpoints ?? [] });
|
|
2615
|
+
} else {
|
|
2616
|
+
config = { endpoints: [] };
|
|
2617
|
+
}
|
|
2618
|
+
const idx = config.endpoints.findIndex((e) => e.name === endpoint.name);
|
|
2619
|
+
if (idx >= 0) {
|
|
2620
|
+
config.endpoints[idx] = endpoint;
|
|
2621
|
+
} else {
|
|
2622
|
+
config.endpoints.push(endpoint);
|
|
2623
|
+
}
|
|
2624
|
+
const validated = appConfigSchema.parse(config);
|
|
2625
|
+
saveConfig(validated, file);
|
|
2626
|
+
return validated;
|
|
2627
|
+
}
|
|
2628
|
+
function removeEndpoint(name, configPath) {
|
|
2629
|
+
const file = configPath ?? defaultConfigPath();
|
|
2630
|
+
if (!fs12.existsSync(file)) return false;
|
|
2631
|
+
const raw = JSON.parse(fs12.readFileSync(file, "utf-8"));
|
|
2632
|
+
const endpoints = (raw.endpoints ?? []).filter((e) => e.name !== name);
|
|
2633
|
+
if (endpoints.length === (raw.endpoints ?? []).length) return false;
|
|
2634
|
+
if (endpoints.length === 0) {
|
|
2635
|
+
throw new Error(`\u4E0D\u80FD\u5220\u9664\u6700\u540E\u4E00\u4E2A\u7AEF\u70B9\uFF08\u914D\u7F6E\u5FC5\u987B\u4FDD\u7559\u81F3\u5C11\u4E00\u4E2A\u6A21\u578B\u7AEF\u70B9\uFF09\uFF1A${file}`);
|
|
2636
|
+
}
|
|
2637
|
+
saveConfig({ ...raw, endpoints }, file);
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
function interpolateEnv(value, file) {
|
|
2641
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/g, (whole, name) => {
|
|
2642
|
+
const v = process.env[name];
|
|
2643
|
+
if (v === void 0) {
|
|
2644
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${file} \u5F15\u7528\u4E86\u672A\u8BBE\u7F6E\u7684\u73AF\u5883\u53D8\u91CF ${name}\uFF08\u6765\u81EA "${whole}"\uFF09`);
|
|
2645
|
+
}
|
|
2646
|
+
return v;
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/server/run-registry.ts
|
|
2651
|
+
import crypto7 from "crypto";
|
|
2652
|
+
var DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
2653
|
+
var RunRegistry = class {
|
|
2654
|
+
retentionMs;
|
|
2655
|
+
runs = /* @__PURE__ */ new Map();
|
|
2656
|
+
latestBySession = /* @__PURE__ */ new Map();
|
|
2657
|
+
closing = false;
|
|
2658
|
+
constructor(opts = {}) {
|
|
2659
|
+
this.retentionMs = opts.retentionMs ?? DEFAULT_RETENTION_MS;
|
|
2660
|
+
}
|
|
2661
|
+
/** 分配运行编号并把 runner 放入后台执行,立即返回编号 */
|
|
2662
|
+
start(runner, params, opts) {
|
|
2663
|
+
if (this.closing) throw new Error("\u8FD0\u884C\u6CE8\u518C\u8868\u6B63\u5728\u5173\u95ED\uFF0C\u65E0\u6CD5\u521B\u5EFA\u65B0\u8FD0\u884C");
|
|
2664
|
+
this.pruneExpired();
|
|
2665
|
+
const runId = crypto7.randomUUID().replaceAll("-", "").slice(0, 12);
|
|
2666
|
+
const run = {
|
|
2667
|
+
runId,
|
|
2668
|
+
sessionId: opts.sessionId,
|
|
2669
|
+
events: [],
|
|
2670
|
+
terminal: false,
|
|
2671
|
+
completedAt: null,
|
|
2672
|
+
controller: new AbortController(),
|
|
2673
|
+
waiters: /* @__PURE__ */ new Set(),
|
|
2674
|
+
onTerminal: opts.onTerminal ?? null
|
|
2675
|
+
};
|
|
2676
|
+
this.runs.set(runId, run);
|
|
2677
|
+
this.latestBySession.set(opts.sessionId, runId);
|
|
2678
|
+
void this.execute(run, runner, params);
|
|
2679
|
+
return runId;
|
|
2680
|
+
}
|
|
2681
|
+
/** 按公开会话编号读取当前(或最近一轮)事件 */
|
|
2682
|
+
async getSessionEvents(sessionId, afterSequence, timeoutSeconds) {
|
|
2683
|
+
return this.getEvents(this.sessionRun(sessionId).runId, afterSequence, timeoutSeconds);
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* 返回游标后的事件;暂无事件时等待通知,超时返回空列表供 SSE 发心跳。
|
|
2687
|
+
* 调用方应先发送本批事件,再在 terminal=true 且批次已追平时关闭连接。
|
|
2688
|
+
*/
|
|
2689
|
+
async getEvents(runId, afterSequence, timeoutSeconds) {
|
|
2690
|
+
if (afterSequence < 0) throw new HttpError(400, "SSE \u4E8B\u4EF6\u6E38\u6807\u4E0D\u80FD\u4E3A\u8D1F\u6570");
|
|
2691
|
+
const run = this.getRun(runId);
|
|
2692
|
+
if (afterSequence > run.events.length) {
|
|
2693
|
+
throw new HttpError(400, `SSE \u4E8B\u4EF6\u6E38\u6807 ${afterSequence} \u8D85\u51FA\u5F53\u524D\u4E8B\u4EF6\u8303\u56F4`);
|
|
2694
|
+
}
|
|
2695
|
+
if (afterSequence === run.events.length && !run.terminal && timeoutSeconds > 0) {
|
|
2696
|
+
await new Promise((resolve) => {
|
|
2697
|
+
const timer = setTimeout(() => {
|
|
2698
|
+
run.waiters.delete(wake);
|
|
2699
|
+
resolve();
|
|
2700
|
+
}, timeoutSeconds * 1e3);
|
|
2701
|
+
const wake = () => {
|
|
2702
|
+
clearTimeout(timer);
|
|
2703
|
+
run.waiters.delete(wake);
|
|
2704
|
+
resolve();
|
|
2705
|
+
};
|
|
2706
|
+
run.waiters.add(wake);
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
return { events: run.events.slice(afterSequence), terminal: run.terminal };
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* 幂等取消一次运行:**先落可回放的 agent_cancelled 终态事件,再 abort**。
|
|
2713
|
+
* 顺序很重要:运行刚创建、事件循环尚未开始消费时,直接 abort 会让 runner
|
|
2714
|
+
* 一次都不执行,订阅者会永久等待——先落终态事件保证任何情况下订阅者
|
|
2715
|
+
* 都能看到收尾。
|
|
2716
|
+
*/
|
|
2717
|
+
async cancel(runId) {
|
|
2718
|
+
const run = this.getRun(runId);
|
|
2719
|
+
if (run.terminal) return;
|
|
2720
|
+
await this.publish(run, { type: "agent_cancelled", run_id: run.runId });
|
|
2721
|
+
run.controller.abort();
|
|
2722
|
+
}
|
|
2723
|
+
async cancelSession(sessionId) {
|
|
2724
|
+
await this.cancel(this.sessionRun(sessionId).runId);
|
|
2725
|
+
}
|
|
2726
|
+
/** 应用关闭时取消全部活动运行 */
|
|
2727
|
+
async close() {
|
|
2728
|
+
this.closing = true;
|
|
2729
|
+
for (const run of this.runs.values()) {
|
|
2730
|
+
if (!run.terminal) await this.cancel(run.runId);
|
|
2731
|
+
}
|
|
2732
|
+
this.runs.clear();
|
|
2733
|
+
this.latestBySession.clear();
|
|
2734
|
+
}
|
|
2735
|
+
/** 消费 runner 事件并写入日志,兜住所有退出路径补齐终态 */
|
|
2736
|
+
async execute(run, runner, params) {
|
|
2737
|
+
try {
|
|
2738
|
+
for await (const event of runner.start(params, {
|
|
2739
|
+
runId: run.runId,
|
|
2740
|
+
signal: run.controller.signal
|
|
2741
|
+
})) {
|
|
2742
|
+
await this.publish(run, event);
|
|
2743
|
+
}
|
|
2744
|
+
if (!run.terminal) {
|
|
2745
|
+
await this.publish(run, {
|
|
2746
|
+
type: "agent_error",
|
|
2747
|
+
run_id: run.runId,
|
|
2748
|
+
error: "Agent \u8FD0\u884C\u5F02\u5E38\u7ED3\u675F\uFF0C\u672A\u8FD4\u56DE\u7EC8\u6001\u4E8B\u4EF6"
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
} catch (err) {
|
|
2752
|
+
if (!run.terminal) {
|
|
2753
|
+
await this.publish(run, {
|
|
2754
|
+
type: "agent_error",
|
|
2755
|
+
run_id: run.runId,
|
|
2756
|
+
error: `Agent \u8FD0\u884C\u53D1\u751F\u672A\u77E5\u9519\u8BEF\uFF1A${err instanceof Error ? err.message : err}`
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
/** 原子追加事件并广播唤醒;终态之后的迟到事件直接忽略 */
|
|
2762
|
+
async publish(run, event) {
|
|
2763
|
+
if (run.terminal) return;
|
|
2764
|
+
run.events.push({ sequence: run.events.length + 1, event });
|
|
2765
|
+
let becameTerminal = false;
|
|
2766
|
+
if (TERMINAL_EVENT_TYPES.has(event.type)) {
|
|
2767
|
+
run.terminal = true;
|
|
2768
|
+
run.completedAt = Date.now();
|
|
2769
|
+
becameTerminal = true;
|
|
2770
|
+
}
|
|
2771
|
+
for (const wake of [...run.waiters]) wake();
|
|
2772
|
+
if (becameTerminal && run.onTerminal) {
|
|
2773
|
+
try {
|
|
2774
|
+
await run.onTerminal(event);
|
|
2775
|
+
} catch (err) {
|
|
2776
|
+
console.error(`Agent \u8FD0\u884C\u7EC8\u6001\u94A9\u5B50\u6267\u884C\u5931\u8D25 run=${run.runId}\uFF1A${err instanceof Error ? err.message : err}`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
getRun(runId) {
|
|
2781
|
+
this.pruneExpired();
|
|
2782
|
+
const run = this.runs.get(runId);
|
|
2783
|
+
if (!run) throw new HttpError(404, "Agent \u8FD0\u884C\u4E0D\u5B58\u5728\u6216\u4E8B\u4EF6\u5386\u53F2\u5DF2\u8FC7\u671F");
|
|
2784
|
+
return run;
|
|
2785
|
+
}
|
|
2786
|
+
sessionRun(sessionId) {
|
|
2787
|
+
this.pruneExpired();
|
|
2788
|
+
const runId = this.latestBySession.get(sessionId);
|
|
2789
|
+
const run = runId ? this.runs.get(runId) : void 0;
|
|
2790
|
+
if (!run) throw new HttpError(404, "\u4F1A\u8BDD\u6CA1\u6709\u53EF\u8DDF\u968F\u6216\u505C\u6B62\u7684\u8FD0\u884C");
|
|
2791
|
+
return run;
|
|
2792
|
+
}
|
|
2793
|
+
/** 惰性清理超过保留期的终态运行;活动运行永不在这里删除 */
|
|
2794
|
+
pruneExpired() {
|
|
2795
|
+
const cutoff = Date.now() - this.retentionMs;
|
|
2796
|
+
for (const [runId, run] of [...this.runs]) {
|
|
2797
|
+
if (run.completedAt !== null && run.completedAt <= cutoff) {
|
|
2798
|
+
this.runs.delete(runId);
|
|
2799
|
+
if (this.latestBySession.get(run.sessionId) === runId) {
|
|
2800
|
+
this.latestBySession.delete(run.sessionId);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
var HttpError = class extends Error {
|
|
2807
|
+
constructor(status, message) {
|
|
2808
|
+
super(message);
|
|
2809
|
+
this.status = status;
|
|
2810
|
+
this.name = "HttpError";
|
|
2811
|
+
}
|
|
2812
|
+
status;
|
|
2813
|
+
};
|
|
2814
|
+
|
|
2815
|
+
// src/server/recorder.ts
|
|
2816
|
+
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
2817
|
+
var AgentRunRecorder = class {
|
|
2818
|
+
store;
|
|
2819
|
+
index;
|
|
2820
|
+
sessionId;
|
|
2821
|
+
/** 运行开始时会话已有的 entry 数;之后每次落盘递增,避免反复重读文件 */
|
|
2822
|
+
entryCount;
|
|
2823
|
+
heartbeatTimer = null;
|
|
2824
|
+
/** begin 与 onTerminal 分别由路由层和后台任务并发调用:用同一根 Promise
|
|
2825
|
+
* 链串行化,防「极速结束的运行被误标为永远运行中」的竞态 */
|
|
2826
|
+
lifecycle = Promise.resolve();
|
|
2827
|
+
terminated = false;
|
|
2828
|
+
constructor(store, index, sessionId, entryCount) {
|
|
2829
|
+
this.store = store;
|
|
2830
|
+
this.index = index;
|
|
2831
|
+
this.sessionId = sessionId;
|
|
2832
|
+
this.entryCount = entryCount;
|
|
2833
|
+
}
|
|
2834
|
+
/** 运行启动:标记 running 并开启心跳。运行已先一步终态时跳过。 */
|
|
2835
|
+
async begin(runId) {
|
|
2836
|
+
this.lifecycle = this.lifecycle.then(() => {
|
|
2837
|
+
if (this.terminated) return;
|
|
2838
|
+
this.index.markRunning(this.sessionId, runId);
|
|
2839
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2840
|
+
try {
|
|
2841
|
+
this.index.heartbeat(this.sessionId);
|
|
2842
|
+
} catch (err) {
|
|
2843
|
+
console.warn(`\u4F1A\u8BDD\u5FC3\u8DF3\u5237\u65B0\u5931\u8D25 session=${this.sessionId}\uFF1A${err instanceof Error ? err.message : err}`);
|
|
2844
|
+
}
|
|
2845
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
2846
|
+
this.heartbeatTimer.unref?.();
|
|
2847
|
+
});
|
|
2848
|
+
await this.lifecycle;
|
|
2849
|
+
}
|
|
2850
|
+
/** 落盘 user message,刷新标题(仅首条)与最后提示预览,返回 message id */
|
|
2851
|
+
async recordUserMessage(text) {
|
|
2852
|
+
const entry = this.store.append(this.sessionId, { role: "user", content: text });
|
|
2853
|
+
this.entryCount += 1;
|
|
2854
|
+
const preview = text.trim().slice(0, PREVIEW_MAX_CHARS);
|
|
2855
|
+
this.index.touchAfterAppend(this.sessionId, {
|
|
2856
|
+
leaf_uuid: entry.uuid,
|
|
2857
|
+
entry_count: this.entryCount,
|
|
2858
|
+
last_prompt: preview,
|
|
2859
|
+
title: preview
|
|
2860
|
+
});
|
|
2861
|
+
return entry.uuid;
|
|
2862
|
+
}
|
|
2863
|
+
/** runner 定稿消息回调:assistant 带响应元数据,tool 结果不带 */
|
|
2864
|
+
onMessage = async (message, response) => {
|
|
2865
|
+
const entry = this.store.append(this.sessionId, message, {
|
|
2866
|
+
...response?.model ? { model: response.model } : {},
|
|
2867
|
+
...response?.usage ? { usage: response.usage } : {},
|
|
2868
|
+
...response?.finish_reason ? { finish_reason: response.finish_reason } : {}
|
|
2869
|
+
});
|
|
2870
|
+
this.entryCount += 1;
|
|
2871
|
+
this.index.touchAfterAppend(this.sessionId, {
|
|
2872
|
+
leaf_uuid: entry.uuid,
|
|
2873
|
+
entry_count: this.entryCount
|
|
2874
|
+
});
|
|
2875
|
+
};
|
|
2876
|
+
/** runner 压缩定稿回调:压缩行落盘并刷新索引(与 onMessage 同一节奏) */
|
|
2877
|
+
onCompaction = async (result) => {
|
|
2878
|
+
const entry = this.store.appendCompaction(this.sessionId, result);
|
|
2879
|
+
this.entryCount += 1;
|
|
2880
|
+
this.index.touchAfterAppend(this.sessionId, {
|
|
2881
|
+
leaf_uuid: entry.uuid,
|
|
2882
|
+
entry_count: this.entryCount
|
|
2883
|
+
});
|
|
2884
|
+
};
|
|
2885
|
+
/** runner 提权请求回调:提权行落盘并刷新索引(回放时前端渲染确认卡片) */
|
|
2886
|
+
onEscalation = async (info) => {
|
|
2887
|
+
const entry = this.store.appendEscalation(this.sessionId, {
|
|
2888
|
+
escalation_id: info.escalation_id,
|
|
2889
|
+
requested_path: info.requested_path,
|
|
2890
|
+
resolved_path: info.resolved_path,
|
|
2891
|
+
tool_name: info.tool_name,
|
|
2892
|
+
resource_type: info.resource_type,
|
|
2893
|
+
requested_command: info.requested_command
|
|
2894
|
+
});
|
|
2895
|
+
this.entryCount += 1;
|
|
2896
|
+
this.index.touchAfterAppend(this.sessionId, {
|
|
2897
|
+
leaf_uuid: entry.uuid,
|
|
2898
|
+
entry_count: this.entryCount
|
|
2899
|
+
});
|
|
2900
|
+
};
|
|
2901
|
+
/** 运行终态收尾(done / error / cancelled 统一路径) */
|
|
2902
|
+
onTerminal = async (_event) => {
|
|
2903
|
+
this.lifecycle = this.lifecycle.then(() => {
|
|
2904
|
+
this.terminated = true;
|
|
2905
|
+
if (this.heartbeatTimer !== null) {
|
|
2906
|
+
clearInterval(this.heartbeatTimer);
|
|
2907
|
+
this.heartbeatTimer = null;
|
|
2908
|
+
}
|
|
2909
|
+
let sealed = 0;
|
|
2910
|
+
try {
|
|
2911
|
+
sealed = this.store.sealPendingToolCalls(this.sessionId);
|
|
2912
|
+
} catch (err) {
|
|
2913
|
+
console.warn(`\u4F1A\u8BDD\u4E2D\u65AD\u6536\u5C3E\u5931\u8D25 session=${this.sessionId}\uFF1A${err instanceof Error ? err.message : err}`);
|
|
2914
|
+
}
|
|
2915
|
+
if (sealed > 0) {
|
|
2916
|
+
this.entryCount += sealed;
|
|
2917
|
+
const { entries } = this.store.read(this.sessionId);
|
|
2918
|
+
this.index.touchAfterAppend(this.sessionId, {
|
|
2919
|
+
leaf_uuid: entries[entries.length - 1]?.uuid ?? "",
|
|
2920
|
+
entry_count: this.entryCount
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
this.index.finishRun(this.sessionId);
|
|
2924
|
+
});
|
|
2925
|
+
await this.lifecycle;
|
|
2926
|
+
};
|
|
2927
|
+
};
|
|
2928
|
+
|
|
2929
|
+
// src/server/routes.ts
|
|
2930
|
+
import { Hono } from "hono";
|
|
2931
|
+
import { z as z14 } from "zod";
|
|
2932
|
+
function ok(data, message = "") {
|
|
2933
|
+
return { success: true, code: 0, message, data };
|
|
2934
|
+
}
|
|
2935
|
+
function fail(status, message) {
|
|
2936
|
+
throw new HttpError(status, message);
|
|
2937
|
+
}
|
|
2938
|
+
var startPayloadSchema = z14.object({
|
|
2939
|
+
content: z14.string().min(1, "\u6D88\u606F\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A"),
|
|
2940
|
+
session_id: z14.string().optional(),
|
|
2941
|
+
model: z14.string().default("")
|
|
2942
|
+
});
|
|
2943
|
+
var retryPayloadSchema = z14.object({
|
|
2944
|
+
message_id: z14.string().min(1),
|
|
2945
|
+
content: z14.string().optional(),
|
|
2946
|
+
model: z14.string().default("")
|
|
2947
|
+
});
|
|
2948
|
+
function createAgentRoutes(ctx) {
|
|
2949
|
+
const app = new Hono();
|
|
2950
|
+
app.onError((err, c) => {
|
|
2951
|
+
if (err instanceof HttpError) {
|
|
2952
|
+
return c.json({ success: false, code: err.status, message: err.message }, err.status);
|
|
2953
|
+
}
|
|
2954
|
+
console.error(`\u8BF7\u6C42\u5904\u7406\u5931\u8D25\uFF1A${err instanceof Error ? err.stack ?? err.message : err}`);
|
|
2955
|
+
return c.json({ success: false, code: 500, message: "\u670D\u52A1\u5185\u90E8\u9519\u8BEF" }, 500);
|
|
2956
|
+
});
|
|
2957
|
+
if (ctx.token) {
|
|
2958
|
+
app.use("*", async (c, next) => {
|
|
2959
|
+
const header = c.req.header("authorization") ?? "";
|
|
2960
|
+
if (header !== `Bearer ${ctx.token}`) {
|
|
2961
|
+
return c.json({ success: false, code: 401, message: "\u672A\u6388\u6743\uFF1A\u7F3A\u5C11\u6216\u9519\u8BEF\u7684 Bearer token" }, 401);
|
|
2962
|
+
}
|
|
2963
|
+
await next();
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
const buildSystem = () => buildSystemPrompt({ agentName: ctx.agentName, persona: ctx.persona });
|
|
2967
|
+
async function launchUserMessage(args) {
|
|
2968
|
+
const recorder = new AgentRunRecorder(ctx.store, ctx.index, args.sessionId, args.entryCount);
|
|
2969
|
+
const messageId = args.recordUser === false ? "" : await recorder.recordUserMessage(args.content);
|
|
2970
|
+
const runner = new AgentRunner({
|
|
2971
|
+
resolver: ctx.resolver,
|
|
2972
|
+
tools: ctx.makeTools(args.sessionId),
|
|
2973
|
+
onMessage: recorder.onMessage,
|
|
2974
|
+
onCompaction: recorder.onCompaction,
|
|
2975
|
+
onEscalation: recorder.onEscalation,
|
|
2976
|
+
workdirHint: ctx.workdir
|
|
2977
|
+
});
|
|
2978
|
+
const runId = ctx.registry.start(
|
|
2979
|
+
runner,
|
|
2980
|
+
{
|
|
2981
|
+
input: args.content,
|
|
2982
|
+
include_input: args.recordUser !== false,
|
|
2983
|
+
history: args.history,
|
|
2984
|
+
model: args.model,
|
|
2985
|
+
system_prompt: args.systemPrompt ?? buildSystem()
|
|
2986
|
+
},
|
|
2987
|
+
{ sessionId: args.sessionId, onTerminal: recorder.onTerminal }
|
|
2988
|
+
);
|
|
2989
|
+
await recorder.begin(runId);
|
|
2990
|
+
return { sessionId: args.sessionId, messageId };
|
|
2991
|
+
}
|
|
2992
|
+
app.post("/sessions", async (c) => {
|
|
2993
|
+
const payload = startPayloadSchema.parse(await c.req.json());
|
|
2994
|
+
ctx.resolver.resolve(payload.model);
|
|
2995
|
+
let sessionId;
|
|
2996
|
+
let history;
|
|
2997
|
+
let entryCount;
|
|
2998
|
+
if (payload.session_id) {
|
|
2999
|
+
sessionId = payload.session_id;
|
|
3000
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3001
|
+
if (ctx.index.isRunning(sessionId)) {
|
|
3002
|
+
fail(400, "\u8BE5\u4F1A\u8BDD\u5DF2\u6709\u6B63\u5728\u8FDB\u884C\u7684\u8FD0\u884C\uFF0C\u8BF7\u5148\u505C\u6B62\u6216\u7B49\u5F85\u5B8C\u6210");
|
|
3003
|
+
}
|
|
3004
|
+
history = ctx.store.buildHistory(sessionId);
|
|
3005
|
+
entryCount = ctx.store.read(sessionId).entries.length;
|
|
3006
|
+
} else {
|
|
3007
|
+
const header = ctx.store.create();
|
|
3008
|
+
sessionId = header.session_id;
|
|
3009
|
+
ctx.index.create(sessionId);
|
|
3010
|
+
history = [];
|
|
3011
|
+
entryCount = 0;
|
|
3012
|
+
}
|
|
3013
|
+
const { messageId } = await launchUserMessage({
|
|
3014
|
+
sessionId,
|
|
3015
|
+
content: payload.content,
|
|
3016
|
+
model: payload.model,
|
|
3017
|
+
history,
|
|
3018
|
+
entryCount
|
|
3019
|
+
});
|
|
3020
|
+
return c.json(ok({ session_id: sessionId, message_id: messageId }, "\u7528\u6237\u6D88\u606F\u5DF2\u63D0\u4EA4"), 202);
|
|
3021
|
+
});
|
|
3022
|
+
app.get("/sessions", (c) => {
|
|
3023
|
+
const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
|
|
3024
|
+
const offset = Math.max(Number(c.req.query("offset") ?? 0), 0);
|
|
3025
|
+
const list = ctx.index.list(limit, offset).map((meta) => ({
|
|
3026
|
+
...meta,
|
|
3027
|
+
running: ctx.index.isRunning(meta.session_id)
|
|
3028
|
+
}));
|
|
3029
|
+
return c.json(ok(list));
|
|
3030
|
+
});
|
|
3031
|
+
app.get("/sessions/:id/transcript", (c) => {
|
|
3032
|
+
const sessionId = c.req.param("id");
|
|
3033
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3034
|
+
const { entries } = ctx.store.read(sessionId);
|
|
3035
|
+
return c.json(
|
|
3036
|
+
ok({
|
|
3037
|
+
session: ctx.index.get(sessionId) ?? null,
|
|
3038
|
+
entries: entries.map(
|
|
3039
|
+
(e) => isCompactionEntry(e) ? {
|
|
3040
|
+
type: "compaction",
|
|
3041
|
+
compaction_id: e.uuid,
|
|
3042
|
+
parent_id: e.parent_uuid,
|
|
3043
|
+
timestamp: e.timestamp,
|
|
3044
|
+
summary: e.summary,
|
|
3045
|
+
replacement_history: e.replacement_history,
|
|
3046
|
+
tokens_before: e.tokens_before,
|
|
3047
|
+
tokens_after: e.tokens_after
|
|
3048
|
+
} : isEscalationEntry(e) ? {
|
|
3049
|
+
type: "escalation",
|
|
3050
|
+
escalation_id: e.uuid,
|
|
3051
|
+
parent_id: e.parent_uuid,
|
|
3052
|
+
timestamp: e.timestamp,
|
|
3053
|
+
requested_path: e.requested_path,
|
|
3054
|
+
resolved_path: e.resolved_path,
|
|
3055
|
+
tool_name: e.tool_name,
|
|
3056
|
+
resource_type: e.resource_type,
|
|
3057
|
+
requested_command: e.requested_command,
|
|
3058
|
+
workdir: ctx.workdir,
|
|
3059
|
+
granted: e.granted
|
|
3060
|
+
} : {
|
|
3061
|
+
type: "message",
|
|
3062
|
+
message_id: e.uuid,
|
|
3063
|
+
parent_id: e.parent_uuid,
|
|
3064
|
+
timestamp: e.timestamp,
|
|
3065
|
+
message: e.message,
|
|
3066
|
+
model: e.model,
|
|
3067
|
+
usage: e.usage,
|
|
3068
|
+
finish_reason: e.finish_reason
|
|
3069
|
+
}
|
|
3070
|
+
)
|
|
3071
|
+
})
|
|
3072
|
+
);
|
|
3073
|
+
});
|
|
3074
|
+
app.patch("/sessions/:id", async (c) => {
|
|
3075
|
+
const sessionId = c.req.param("id");
|
|
3076
|
+
const { title } = z14.object({ title: z14.string().min(1) }).parse(await c.req.json());
|
|
3077
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3078
|
+
ctx.index.rename(sessionId, title);
|
|
3079
|
+
return c.json(ok(ctx.index.get(sessionId), "\u4F1A\u8BDD\u5DF2\u91CD\u547D\u540D"));
|
|
3080
|
+
});
|
|
3081
|
+
app.post("/sessions/:id/compact-context", async (c) => {
|
|
3082
|
+
const sessionId = c.req.param("id");
|
|
3083
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3084
|
+
if (ctx.index.isRunning(sessionId)) fail(400, "\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u540E\u518D\u538B\u7F29");
|
|
3085
|
+
const history = ctx.store.buildHistory(sessionId);
|
|
3086
|
+
if (history.length === 0) fail(400, "\u4F1A\u8BDD\u6CA1\u6709\u53EF\u538B\u7F29\u7684\u5185\u5BB9");
|
|
3087
|
+
const { entries } = ctx.store.read(sessionId);
|
|
3088
|
+
const lastModel = [...entries].reverse().find((e) => isMessageEntry(e) && e.model);
|
|
3089
|
+
const modelRef = lastModel && isMessageEntry(lastModel) && lastModel.model || "";
|
|
3090
|
+
const resolved = ctx.resolver.resolve(modelRef);
|
|
3091
|
+
const result = await compact(
|
|
3092
|
+
resolved.protocol,
|
|
3093
|
+
resolved.modelId,
|
|
3094
|
+
[{ role: "system", content: buildSystem() }, ...history],
|
|
3095
|
+
{}
|
|
3096
|
+
);
|
|
3097
|
+
if (!result) fail(502, "\u538B\u7F29\u5931\u8D25\uFF1A\u6A21\u578B\u672A\u80FD\u751F\u6210\u6458\u8981\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
3098
|
+
const entry = ctx.store.appendCompaction(sessionId, result);
|
|
3099
|
+
ctx.index.touchAfterAppend(sessionId, {
|
|
3100
|
+
leaf_uuid: entry.uuid,
|
|
3101
|
+
entry_count: entries.length + 1
|
|
3102
|
+
});
|
|
3103
|
+
return c.json(
|
|
3104
|
+
ok(
|
|
3105
|
+
{
|
|
3106
|
+
summary: result.summary,
|
|
3107
|
+
tokens_before: result.tokens_before,
|
|
3108
|
+
tokens_after: result.tokens_after,
|
|
3109
|
+
compaction_id: entry.uuid
|
|
3110
|
+
},
|
|
3111
|
+
"\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29"
|
|
3112
|
+
)
|
|
3113
|
+
);
|
|
3114
|
+
});
|
|
3115
|
+
app.post("/sessions/:id/retry", async (c) => {
|
|
3116
|
+
const sessionId = c.req.param("id");
|
|
3117
|
+
const payload = retryPayloadSchema.parse(await c.req.json());
|
|
3118
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3119
|
+
if (ctx.index.isRunning(sessionId)) fail(400, "\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u8FD0\u884C\u518D\u91CD\u8BD5");
|
|
3120
|
+
const { entries } = ctx.store.read(sessionId);
|
|
3121
|
+
const target = entries.find((e) => e.uuid === payload.message_id);
|
|
3122
|
+
if (!target) fail(404, "\u4F1A\u8BDD\u4E2D\u6CA1\u6709\u8FD9\u6761\u8BB0\u5F55\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6539\u5199");
|
|
3123
|
+
if (!isMessageEntry(target) || target.message.role !== "user") {
|
|
3124
|
+
fail(400, "\u53EA\u80FD\u91CD\u8BD5\u7528\u6237\u6D88\u606F");
|
|
3125
|
+
}
|
|
3126
|
+
const content = payload.content ?? (target.message.content ?? "");
|
|
3127
|
+
ctx.resolver.resolve(payload.model);
|
|
3128
|
+
ctx.store.discardFromUserMessage(sessionId, payload.message_id);
|
|
3129
|
+
const history = ctx.store.buildHistory(sessionId);
|
|
3130
|
+
const remaining = ctx.store.read(sessionId).entries.length;
|
|
3131
|
+
const { messageId } = await launchUserMessage({
|
|
3132
|
+
sessionId,
|
|
3133
|
+
content,
|
|
3134
|
+
model: payload.model,
|
|
3135
|
+
history,
|
|
3136
|
+
entryCount: remaining
|
|
3137
|
+
});
|
|
3138
|
+
return c.json(ok({ session_id: sessionId, message_id: messageId }, "\u5DF2\u91CD\u65B0\u63D0\u4EA4"), 202);
|
|
3139
|
+
});
|
|
3140
|
+
app.post("/sessions/:id/grant-access", async (c) => {
|
|
3141
|
+
const sessionId = c.req.param("id");
|
|
3142
|
+
const { escalation_id, granted, resolved_path } = z14.object({
|
|
3143
|
+
escalation_id: z14.string().min(1),
|
|
3144
|
+
granted: z14.boolean(),
|
|
3145
|
+
resolved_path: z14.string().min(1)
|
|
3146
|
+
}).parse(await c.req.json());
|
|
3147
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3148
|
+
if (ctx.index.isRunning(sessionId)) fail(400, "\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
|
|
3149
|
+
const { entries } = ctx.store.read(sessionId);
|
|
3150
|
+
const esc = entries.find(
|
|
3151
|
+
(e) => e.type === "escalation" && e.uuid === escalation_id
|
|
3152
|
+
);
|
|
3153
|
+
if (!esc || esc.type !== "escalation") fail(404, "\u63D0\u6743\u8BF7\u6C42\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F");
|
|
3154
|
+
if (esc.resolved_path !== resolved_path) fail(400, "\u6388\u6743\u8DEF\u5F84\u4E0E\u63D0\u6743\u8BF7\u6C42\u4E0D\u4E00\u81F4");
|
|
3155
|
+
if (esc.granted !== null && esc.granted !== void 0) fail(400, "\u8BE5\u63D0\u6743\u8BF7\u6C42\u5DF2\u5904\u7406\u8FC7");
|
|
3156
|
+
ctx.store.resolveEscalation(sessionId, escalation_id, granted);
|
|
3157
|
+
if (!granted) {
|
|
3158
|
+
return c.json(ok({ granted: false }, "\u5DF2\u62D2\u7EDD\u6388\u6743"));
|
|
3159
|
+
}
|
|
3160
|
+
if (esc.resource_type === "command") {
|
|
3161
|
+
ctx.grants.grantCapability(sessionId, resolved_path);
|
|
3162
|
+
} else {
|
|
3163
|
+
ctx.grants.grant(sessionId, resolved_path);
|
|
3164
|
+
}
|
|
3165
|
+
const lastUser = [...entries].reverse().find(
|
|
3166
|
+
(e) => isMessageEntry(e) && e.message.role === "user"
|
|
3167
|
+
);
|
|
3168
|
+
if (!lastUser || !isMessageEntry(lastUser)) fail(400, "\u4F1A\u8BDD\u4E2D\u6CA1\u6709\u53EF\u7EED\u8DD1\u7684\u7528\u6237\u6D88\u606F");
|
|
3169
|
+
ctx.resolver.resolve("");
|
|
3170
|
+
const history = ctx.store.buildHistoryBeforeEscalation(sessionId, escalation_id);
|
|
3171
|
+
const { messageId } = await launchUserMessage({
|
|
3172
|
+
sessionId,
|
|
3173
|
+
content: "",
|
|
3174
|
+
model: "",
|
|
3175
|
+
history,
|
|
3176
|
+
entryCount: entries.length,
|
|
3177
|
+
recordUser: false
|
|
3178
|
+
});
|
|
3179
|
+
return c.json(
|
|
3180
|
+
ok({ session_id: sessionId, message_id: messageId, granted_path: resolved_path }, "\u5DF2\u6388\u6743\u5E76\u7EE7\u7EED\u6267\u884C"),
|
|
3181
|
+
202
|
|
3182
|
+
);
|
|
3183
|
+
});
|
|
3184
|
+
app.delete("/sessions/:id", (c) => {
|
|
3185
|
+
const sessionId = c.req.param("id");
|
|
3186
|
+
if (!ctx.store.exists(sessionId)) fail(404, "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
3187
|
+
if (ctx.index.isRunning(sessionId)) fail(400, "\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u8FD0\u884C\u518D\u5220\u9664");
|
|
3188
|
+
ctx.store.delete(sessionId);
|
|
3189
|
+
ctx.index.remove(sessionId);
|
|
3190
|
+
ctx.grants.drop(sessionId);
|
|
3191
|
+
return c.json(ok({}, "\u4F1A\u8BDD\u5DF2\u5220\u9664"));
|
|
3192
|
+
});
|
|
3193
|
+
app.get("/sessions/:id/events", async (c) => {
|
|
3194
|
+
const sessionId = c.req.param("id");
|
|
3195
|
+
const cursor = Number(c.req.header("last-event-id") ?? 0);
|
|
3196
|
+
const initial = await ctx.registry.getSessionEvents(sessionId, cursor, 0);
|
|
3197
|
+
const stream = new ReadableStream({
|
|
3198
|
+
async start(controller) {
|
|
3199
|
+
const encoder = new TextEncoder();
|
|
3200
|
+
let current = cursor;
|
|
3201
|
+
let { events, terminal } = initial;
|
|
3202
|
+
try {
|
|
3203
|
+
for (; ; ) {
|
|
3204
|
+
for (const stored of events) {
|
|
3205
|
+
current = stored.sequence;
|
|
3206
|
+
const { run_id: _omit, ...payload } = stored.event;
|
|
3207
|
+
controller.enqueue(
|
|
3208
|
+
encoder.encode(
|
|
3209
|
+
`id: ${stored.sequence}
|
|
3210
|
+
event: ${stored.event.type}
|
|
3211
|
+
data: ${JSON.stringify(payload)}
|
|
3212
|
+
|
|
3213
|
+
`
|
|
3214
|
+
)
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
if (terminal) return;
|
|
3218
|
+
({ events, terminal } = await ctx.registry.getSessionEvents(sessionId, current, 15));
|
|
3219
|
+
if (events.length === 0 && !terminal) {
|
|
3220
|
+
controller.enqueue(encoder.encode(": heartbeat\n\n"));
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
} finally {
|
|
3224
|
+
controller.close();
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
return new Response(stream, {
|
|
3229
|
+
headers: {
|
|
3230
|
+
"content-type": "text/event-stream",
|
|
3231
|
+
"cache-control": "no-cache, no-transform",
|
|
3232
|
+
connection: "keep-alive",
|
|
3233
|
+
"x-accel-buffering": "no"
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
});
|
|
3237
|
+
app.post("/sessions/:id/stop", async (c) => {
|
|
3238
|
+
await ctx.registry.cancelSession(c.req.param("id"));
|
|
3239
|
+
return c.json(ok({}, "\u5DF2\u8BF7\u6C42\u505C\u6B62\u4F1A\u8BDD"));
|
|
3240
|
+
});
|
|
3241
|
+
app.get("/health", (c) => c.json(ok({ status: "ok" })));
|
|
3242
|
+
return app;
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/server/session-index.ts
|
|
3246
|
+
import fs13 from "fs";
|
|
3247
|
+
import path12 from "path";
|
|
3248
|
+
var HEARTBEAT_TIMEOUT_MS = 3e4;
|
|
3249
|
+
var SessionIndex = class {
|
|
3250
|
+
file;
|
|
3251
|
+
metas = /* @__PURE__ */ new Map();
|
|
3252
|
+
constructor(indexFile) {
|
|
3253
|
+
this.file = indexFile;
|
|
3254
|
+
this.load();
|
|
3255
|
+
}
|
|
3256
|
+
/** 启动时加载;文件不存在/损坏则从空开始(rebuild 会补齐) */
|
|
3257
|
+
load() {
|
|
3258
|
+
try {
|
|
3259
|
+
const raw = JSON.parse(fs13.readFileSync(this.file, "utf-8"));
|
|
3260
|
+
for (const meta of raw.sessions ?? []) this.metas.set(meta.session_id, meta);
|
|
3261
|
+
} catch {
|
|
3262
|
+
this.metas.clear();
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
/** 从事实源整体重建(启动自愈):扫描结果单向覆盖索引 */
|
|
3266
|
+
rebuild(summaries) {
|
|
3267
|
+
for (const s of summaries) {
|
|
3268
|
+
const existing = this.metas.get(s.session_id);
|
|
3269
|
+
this.metas.set(s.session_id, {
|
|
3270
|
+
session_id: s.session_id,
|
|
3271
|
+
created_at: s.created_at,
|
|
3272
|
+
// 已有人工标题时保留(文件里不存标题,扫描回填的是首条 user 消息)
|
|
3273
|
+
title: existing?.title ?? s.title,
|
|
3274
|
+
last_prompt: s.last_prompt,
|
|
3275
|
+
entry_count: s.entry_count,
|
|
3276
|
+
leaf_uuid: s.leaf_uuid,
|
|
3277
|
+
last_active: s.last_timestamp,
|
|
3278
|
+
running: null
|
|
3279
|
+
// 进程刚启动,任何「运行中」都是上一世的残留
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
this.persist();
|
|
3283
|
+
}
|
|
3284
|
+
create(sessionId) {
|
|
3285
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3286
|
+
this.metas.set(sessionId, {
|
|
3287
|
+
session_id: sessionId,
|
|
3288
|
+
created_at: now,
|
|
3289
|
+
title: null,
|
|
3290
|
+
last_prompt: null,
|
|
3291
|
+
entry_count: 0,
|
|
3292
|
+
leaf_uuid: null,
|
|
3293
|
+
last_active: now,
|
|
3294
|
+
running: null
|
|
3295
|
+
});
|
|
3296
|
+
this.persist();
|
|
3297
|
+
}
|
|
3298
|
+
get(sessionId) {
|
|
3299
|
+
return this.metas.get(sessionId);
|
|
3300
|
+
}
|
|
3301
|
+
/** 每次追加 entry 后刷新链尾/计数/预览;title 仅在传入时更新(首条消息) */
|
|
3302
|
+
touchAfterAppend(sessionId, update) {
|
|
3303
|
+
const meta = this.metas.get(sessionId);
|
|
3304
|
+
if (!meta) return;
|
|
3305
|
+
meta.leaf_uuid = update.leaf_uuid;
|
|
3306
|
+
meta.entry_count = update.entry_count;
|
|
3307
|
+
meta.last_active = (/* @__PURE__ */ new Date()).toISOString();
|
|
3308
|
+
if (update.last_prompt !== void 0) {
|
|
3309
|
+
meta.last_prompt = update.last_prompt.slice(0, PREVIEW_MAX_CHARS);
|
|
3310
|
+
}
|
|
3311
|
+
if (update.title !== void 0 && meta.title === null) {
|
|
3312
|
+
meta.title = update.title.slice(0, PREVIEW_MAX_CHARS);
|
|
3313
|
+
}
|
|
3314
|
+
this.persist();
|
|
3315
|
+
}
|
|
3316
|
+
rename(sessionId, title) {
|
|
3317
|
+
const meta = this.metas.get(sessionId);
|
|
3318
|
+
if (!meta) return false;
|
|
3319
|
+
meta.title = title.slice(0, PREVIEW_MAX_CHARS);
|
|
3320
|
+
this.persist();
|
|
3321
|
+
return true;
|
|
3322
|
+
}
|
|
3323
|
+
markRunning(sessionId, runId) {
|
|
3324
|
+
const meta = this.metas.get(sessionId);
|
|
3325
|
+
if (!meta) return;
|
|
3326
|
+
meta.running = { run_id: runId, last_heartbeat: Date.now() };
|
|
3327
|
+
this.persist();
|
|
3328
|
+
}
|
|
3329
|
+
heartbeat(sessionId) {
|
|
3330
|
+
const meta = this.metas.get(sessionId);
|
|
3331
|
+
if (!meta?.running) return;
|
|
3332
|
+
meta.running.last_heartbeat = Date.now();
|
|
3333
|
+
this.persist();
|
|
3334
|
+
}
|
|
3335
|
+
finishRun(sessionId) {
|
|
3336
|
+
const meta = this.metas.get(sessionId);
|
|
3337
|
+
if (!meta) return;
|
|
3338
|
+
meta.running = null;
|
|
3339
|
+
this.persist();
|
|
3340
|
+
}
|
|
3341
|
+
/** 是否有正在进行的消息处理:运行标记存在且心跳未超时 */
|
|
3342
|
+
isRunning(sessionId) {
|
|
3343
|
+
const running = this.metas.get(sessionId)?.running;
|
|
3344
|
+
if (!running) return false;
|
|
3345
|
+
return Date.now() - running.last_heartbeat < HEARTBEAT_TIMEOUT_MS;
|
|
3346
|
+
}
|
|
3347
|
+
/** 按最后活跃时间倒序分页 */
|
|
3348
|
+
list(limit, offset) {
|
|
3349
|
+
return [...this.metas.values()].sort((a, b) => b.last_active.localeCompare(a.last_active)).slice(offset, offset + limit);
|
|
3350
|
+
}
|
|
3351
|
+
remove(sessionId) {
|
|
3352
|
+
this.metas.delete(sessionId);
|
|
3353
|
+
this.persist();
|
|
3354
|
+
}
|
|
3355
|
+
persist() {
|
|
3356
|
+
const dir = path12.dirname(this.file);
|
|
3357
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
3358
|
+
const tmp = `${this.file}.tmp`;
|
|
3359
|
+
fs13.writeFileSync(tmp, JSON.stringify({ sessions: [...this.metas.values()] }, null, 2));
|
|
3360
|
+
fs13.renameSync(tmp, this.file);
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
|
|
3364
|
+
// src/server/index.ts
|
|
3365
|
+
import path13 from "path";
|
|
3366
|
+
import { serve } from "@hono/node-server";
|
|
3367
|
+
|
|
3368
|
+
// src/server/config-routes.ts
|
|
3369
|
+
import { Hono as Hono2 } from "hono";
|
|
3370
|
+
import { z as z15 } from "zod";
|
|
3371
|
+
function maskApiKey(key) {
|
|
3372
|
+
if (key.includes("${")) return key;
|
|
3373
|
+
if (key.length <= 8) return "****";
|
|
3374
|
+
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
3375
|
+
}
|
|
3376
|
+
function maskedConfig(config) {
|
|
3377
|
+
return {
|
|
3378
|
+
...config,
|
|
3379
|
+
endpoints: config.endpoints.map((e) => ({ ...e, api_key: maskApiKey(e.api_key) }))
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
var putPayloadSchema = z15.object({
|
|
3383
|
+
endpoints: z15.array(
|
|
3384
|
+
z15.object({
|
|
3385
|
+
name: z15.string().min(1),
|
|
3386
|
+
protocol: z15.enum(["openai-responses", "anthropic-messages"]),
|
|
3387
|
+
base_url: z15.string().min(1),
|
|
3388
|
+
api_key: z15.string().min(1),
|
|
3389
|
+
models: z15.record(z15.string(), z15.object({ context_window: z15.number().positive().optional() })).optional(),
|
|
3390
|
+
default_model: z15.string().optional()
|
|
3391
|
+
})
|
|
3392
|
+
).min(1, "endpoints \u4E3A\u7A7A\uFF1A\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u6A21\u578B\u7AEF\u70B9"),
|
|
3393
|
+
server: z15.object({ port: z15.number().int().positive().optional(), token: z15.string().optional() }).optional(),
|
|
3394
|
+
agent: z15.object({
|
|
3395
|
+
workdir: z15.string().optional(),
|
|
3396
|
+
agent_name: z15.string().optional(),
|
|
3397
|
+
persona: z15.string().optional(),
|
|
3398
|
+
enable_command_execution: z15.boolean().optional()
|
|
3399
|
+
}).optional()
|
|
3400
|
+
});
|
|
3401
|
+
function createConfigRoutes(ctx) {
|
|
3402
|
+
const app = new Hono2();
|
|
3403
|
+
app.onError((err, c) => {
|
|
3404
|
+
if (err instanceof HttpError) {
|
|
3405
|
+
return c.json({ success: false, code: err.status, message: err.message }, err.status);
|
|
3406
|
+
}
|
|
3407
|
+
console.error(`\u914D\u7F6E\u63A5\u53E3\u5904\u7406\u5931\u8D25\uFF1A${err instanceof Error ? err.message : err}`);
|
|
3408
|
+
return c.json({ success: false, code: 500, message: err instanceof Error ? err.message : "\u670D\u52A1\u5185\u90E8\u9519\u8BEF" }, 500);
|
|
3409
|
+
});
|
|
3410
|
+
app.get("/config", (c) => {
|
|
3411
|
+
const config = loadConfig(ctx.configPath);
|
|
3412
|
+
return c.json({ success: true, code: 0, message: "", data: maskedConfig(config) });
|
|
3413
|
+
});
|
|
3414
|
+
app.put("/config", async (c) => {
|
|
3415
|
+
const payload = putPayloadSchema.parse(await c.req.json());
|
|
3416
|
+
const current = loadConfig(ctx.configPath);
|
|
3417
|
+
const endpoints = payload.endpoints.map((incoming) => {
|
|
3418
|
+
const existing = current.endpoints.find((e) => e.name === incoming.name);
|
|
3419
|
+
if (existing && incoming.api_key === maskApiKey(existing.api_key)) {
|
|
3420
|
+
return { ...incoming, api_key: existing.api_key };
|
|
3421
|
+
}
|
|
3422
|
+
return incoming;
|
|
3423
|
+
});
|
|
3424
|
+
const next = {
|
|
3425
|
+
endpoints,
|
|
3426
|
+
...payload.server ? { server: payload.server } : {},
|
|
3427
|
+
...payload.agent ? { agent: payload.agent } : {}
|
|
3428
|
+
};
|
|
3429
|
+
saveConfig(next, ctx.configPath);
|
|
3430
|
+
return c.json({ success: true, code: 0, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF08\u90E8\u5206\u542F\u52A8\u671F\u88C5\u914D\u9879\u9700\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\uFF09", data: maskedConfig(next) });
|
|
3431
|
+
});
|
|
3432
|
+
app.post("/config/test", async (c) => {
|
|
3433
|
+
const { name } = z15.object({ name: z15.string().min(1) }).parse(await c.req.json());
|
|
3434
|
+
const config = loadConfig(ctx.configPath);
|
|
3435
|
+
const endpoint = config.endpoints.find((e) => e.name === name);
|
|
3436
|
+
if (!endpoint) throw new HttpError(404, `\u7AEF\u70B9\u4E0D\u5B58\u5728\uFF1A${name}`);
|
|
3437
|
+
const model = endpoint.default_model ?? Object.keys(endpoint.models ?? {})[0];
|
|
3438
|
+
if (!model) throw new HttpError(400, `\u7AEF\u70B9 ${name} \u672A\u914D\u7F6E default_model \u6216 models\uFF0C\u65E0\u6CD5\u6D4B\u8BD5`);
|
|
3439
|
+
const protocol = endpoint.protocol === "anthropic-messages" ? new AnthropicMessagesProtocol({ baseUrl: endpoint.base_url, apiKey: endpoint.api_key, providerName: endpoint.name }) : new OpenAIResponsesProtocol({ baseUrl: endpoint.base_url, apiKey: endpoint.api_key, providerName: endpoint.name });
|
|
3440
|
+
const started = Date.now();
|
|
3441
|
+
for await (const event of protocol.chatStream({
|
|
3442
|
+
model,
|
|
3443
|
+
messages: [{ role: "user", content: "ping" }],
|
|
3444
|
+
settings: { max_tokens: endpoint.protocol === "anthropic-messages" ? 16 : 16 }
|
|
3445
|
+
})) {
|
|
3446
|
+
if (event.type === "error") {
|
|
3447
|
+
return c.json({ success: false, code: 502, message: `\u8FDE\u901A\u6027\u6D4B\u8BD5\u5931\u8D25\uFF1A${event.error}`, data: null }, 502);
|
|
3448
|
+
}
|
|
3449
|
+
if (event.type === "done") {
|
|
3450
|
+
return c.json({
|
|
3451
|
+
success: true,
|
|
3452
|
+
code: 0,
|
|
3453
|
+
message: `\u7AEF\u70B9 ${name} \u8FDE\u901A\u6B63\u5E38\uFF08${Date.now() - started}ms\uFF09`,
|
|
3454
|
+
data: { elapsed_ms: Date.now() - started, model: event.response.model || model }
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
return c.json({ success: false, code: 502, message: "\u8FDE\u901A\u6027\u6D4B\u8BD5\u5931\u8D25\uFF1A\u6D41\u5F02\u5E38\u7EC8\u6B62", data: null }, 502);
|
|
3459
|
+
});
|
|
3460
|
+
return app;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// src/server/index.ts
|
|
3464
|
+
function createAgentServer(config, opts = {}) {
|
|
3465
|
+
const dataDir = path13.resolve(opts.dataDir ?? defaultDataDir());
|
|
3466
|
+
const configPath = opts.configPath ?? path13.join(dataDir, "config.json");
|
|
3467
|
+
const resolver = new EndpointResolver(config.endpoints);
|
|
3468
|
+
const store = new AgentSessionStore(path13.join(dataDir, "sessions"));
|
|
3469
|
+
const index = new SessionIndex(path13.join(dataDir, "index.json"));
|
|
3470
|
+
index.rebuild(store.scanAll());
|
|
3471
|
+
const registry = new RunRegistry();
|
|
3472
|
+
const workdir = path13.resolve(config.agent?.workdir ?? path13.join(dataDir, "workspace"));
|
|
3473
|
+
const grants = new EscalationGrants();
|
|
3474
|
+
const app = createAgentRoutes({
|
|
3475
|
+
resolver,
|
|
3476
|
+
store,
|
|
3477
|
+
index,
|
|
3478
|
+
registry,
|
|
3479
|
+
grants,
|
|
3480
|
+
makeTools: (sessionId) => builtinTools({
|
|
3481
|
+
workdir,
|
|
3482
|
+
grants,
|
|
3483
|
+
sessionId,
|
|
3484
|
+
enableCommandExecution: config.agent?.enable_command_execution ?? false
|
|
3485
|
+
}),
|
|
3486
|
+
workdir,
|
|
3487
|
+
...config.agent?.agent_name ? { agentName: config.agent.agent_name } : {},
|
|
3488
|
+
...config.agent?.persona ? { persona: config.agent.persona } : {},
|
|
3489
|
+
...config.server?.token ? { token: config.server.token } : {}
|
|
3490
|
+
});
|
|
3491
|
+
const configRoutes = createConfigRoutes({ configPath });
|
|
3492
|
+
if (config.server?.token) {
|
|
3493
|
+
configRoutes.use("*", async (c, next) => {
|
|
3494
|
+
const header = c.req.header("authorization") ?? "";
|
|
3495
|
+
if (header !== `Bearer ${config.server.token}`) {
|
|
3496
|
+
return c.json({ success: false, code: 401, message: "\u672A\u6388\u6743\uFF1A\u7F3A\u5C11\u6216\u9519\u8BEF\u7684 Bearer token" }, 401);
|
|
3497
|
+
}
|
|
3498
|
+
await next();
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
app.route("/", configRoutes);
|
|
3502
|
+
return { app, registry, store, index };
|
|
3503
|
+
}
|
|
3504
|
+
function serveFromConfig(opts) {
|
|
3505
|
+
const configPath = opts.configPath ?? path13.join(defaultDataDir(), "config.json");
|
|
3506
|
+
const config = loadConfig(configPath);
|
|
3507
|
+
const { app } = createAgentServer(config, { configPath });
|
|
3508
|
+
const port = opts.port ?? config.server?.port ?? 3210;
|
|
3509
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
3510
|
+
console.log(`xlyra-agent \u670D\u52A1\u5DF2\u542F\u52A8\uFF1Ahttp://127.0.0.1:${info.port}`);
|
|
3511
|
+
console.log(`\u6570\u636E\u76EE\u5F55\uFF1A${defaultDataDir()}\uFF08sessions/ \u4F1A\u8BDD\u8F6C\u5F55\uFF0Cworkspace/ \u5DE5\u5177\u5DE5\u4F5C\u533A\uFF09`);
|
|
3512
|
+
if (!config.server?.token) {
|
|
3513
|
+
console.warn("\u26A0 \u672A\u914D\u7F6E server.token\uFF0C\u63A5\u53E3\u65E0\u9274\u6743\u2014\u2014\u8BF7\u52FF\u66B4\u9732\u5230\u516C\u7F51");
|
|
3514
|
+
}
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
export {
|
|
3519
|
+
toolCallSchema,
|
|
3520
|
+
chatMessageSchema,
|
|
3521
|
+
messageText,
|
|
3522
|
+
tokenUsageSchema,
|
|
3523
|
+
emptyUsage,
|
|
3524
|
+
addUsage,
|
|
3525
|
+
modelSettingsSchema,
|
|
3526
|
+
responseToMessage,
|
|
3527
|
+
LlmError,
|
|
3528
|
+
parseSse,
|
|
3529
|
+
ToolCallBuffer,
|
|
3530
|
+
AnthropicMessagesProtocol,
|
|
3531
|
+
OpenAIResponsesProtocol,
|
|
3532
|
+
builtinContextWindow,
|
|
3533
|
+
EndpointResolver,
|
|
3534
|
+
validateToolCall,
|
|
3535
|
+
agentStartParamsSchema,
|
|
3536
|
+
agentToolResultSchema,
|
|
3537
|
+
agentCompactionSchema,
|
|
3538
|
+
agentEscalationSchema,
|
|
3539
|
+
agentDoneSchema,
|
|
3540
|
+
agentEventSchema,
|
|
3541
|
+
TERMINAL_EVENT_TYPES,
|
|
3542
|
+
SUMMARY_PREFIX,
|
|
3543
|
+
COMPACT_PROMPT,
|
|
3544
|
+
buildAvailableToolsPrompt,
|
|
3545
|
+
buildSystemPrompt,
|
|
3546
|
+
COMPACT_TRIGGER_RATIO,
|
|
3547
|
+
RETAINED_USER_TOKEN_BUDGET,
|
|
3548
|
+
APPROX_BYTES_PER_TOKEN,
|
|
3549
|
+
estimateTokens,
|
|
3550
|
+
shouldCompact,
|
|
3551
|
+
isSummaryMessage,
|
|
3552
|
+
buildReplacementHistory,
|
|
3553
|
+
compact,
|
|
3554
|
+
SESSION_FORMAT_VERSION,
|
|
3555
|
+
PREVIEW_MAX_CHARS,
|
|
3556
|
+
defaultDataDir,
|
|
3557
|
+
defaultSessionsDir,
|
|
3558
|
+
isMessageEntry,
|
|
3559
|
+
isCompactionEntry,
|
|
3560
|
+
isEscalationEntry,
|
|
3561
|
+
AgentSessionStore,
|
|
3562
|
+
AgentRunner,
|
|
3563
|
+
resolveSandboxed,
|
|
3564
|
+
makeApplyPatchTool,
|
|
3565
|
+
makeCreateTool,
|
|
3566
|
+
makeEditTool,
|
|
3567
|
+
ProcessManager,
|
|
3568
|
+
makeExecCommandTool,
|
|
3569
|
+
formatProcessResult,
|
|
3570
|
+
makeListTool,
|
|
3571
|
+
makeReadTool,
|
|
3572
|
+
makeSearchTool,
|
|
3573
|
+
makeWriteTool,
|
|
3574
|
+
makeWriteStdinTool,
|
|
3575
|
+
builtinTools,
|
|
3576
|
+
defaultConfigPath,
|
|
3577
|
+
loadConfig,
|
|
3578
|
+
saveConfig,
|
|
3579
|
+
configExists,
|
|
3580
|
+
upsertEndpoint,
|
|
3581
|
+
removeEndpoint,
|
|
3582
|
+
RunRegistry,
|
|
3583
|
+
HttpError,
|
|
3584
|
+
AgentRunRecorder,
|
|
3585
|
+
createAgentRoutes,
|
|
3586
|
+
SessionIndex,
|
|
3587
|
+
createAgentServer,
|
|
3588
|
+
serveFromConfig
|
|
3589
|
+
};
|
|
3590
|
+
//# sourceMappingURL=chunk-QH6SEOO6.js.map
|