chatccc 0.2.270 → 0.2.276
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/README.md +16 -10
- package/config.sample.json +4 -3
- package/deepccc-agent/README.md +147 -61
- package/deepccc-agent/package.json +5 -2
- package/dist/deepccc-agent/src/attachments.js +192 -0
- package/dist/deepccc-agent/src/cli.js +59 -13
- package/dist/deepccc-agent/src/config.js +57 -4
- package/dist/deepccc-agent/src/context.js +299 -16
- package/dist/deepccc-agent/src/file-tools.js +33 -0
- package/dist/deepccc-agent/src/index.js +68 -21
- package/dist/deepccc-agent/src/tool-protocol.js +14 -3
- package/dist/deepccc-agent/src/web-entry.js +72 -0
- package/dist/deepccc-agent/src/web-page.js +414 -0
- package/dist/deepccc-agent/src/web-runtime.js +331 -0
- package/dist/deepccc-agent/src/web-server.js +476 -0
- package/dist/deepccc-agent/src/web-session-store.js +162 -0
- package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
- package/dist/src/adapters/ccc-adapter.js +5 -1
- package/dist/src/agent-capability-grants.js +26 -0
- package/dist/src/agent-delegate-task.js +5 -2
- package/dist/src/agent-file-rpc.js +6 -1
- package/dist/src/agent-image-rpc.js +6 -1
- package/dist/src/agent-team/application/task-execution-service.js +330 -97
- package/dist/src/agent-team/domain/task-run.js +14 -1
- package/dist/src/agent-team/infrastructure/task-execution-runtime.js +7 -2
- package/dist/src/agent-team/main-agent-bootstrap.js +24 -1
- package/dist/src/agent-team/repositories/json-task-run-repository.js +22 -4
- package/dist/src/agent-team/web/agent-team-page.js +14 -7
- package/dist/src/cards.js +7 -4
- package/dist/src/config.js +12 -0
- package/dist/src/im-skills.js +9 -2
- package/dist/src/orchestrator.js +117 -29
- package/dist/src/safe-maintenance.js +4 -1
- package/dist/src/session-name.js +15 -0
- package/dist/src/session.js +54 -9
- package/dist/src/web-ui.js +76 -32
- package/im-skills/feishu-skill/receive-send-file.md +3 -2
- package/im-skills/feishu-skill/receive-send-image.md +3 -2
- package/im-skills/feishu-skill/send-file.mjs +6 -5
- package/im-skills/feishu-skill/send-image.mjs +6 -5
- package/im-skills/feishu-skill/skill.md +4 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { hasMalformedToolProtocolText } from "./tool-protocol.js";
|
|
5
|
+
import { hasImitatedToolTranscriptText, hasMalformedToolProtocolText, } from "./tool-protocol.js";
|
|
6
6
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
7
7
|
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
8
8
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
@@ -15,6 +15,11 @@ const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
|
|
|
15
15
|
const MAX_COMPACTION_SUMMARY_CHARS = 8_000;
|
|
16
16
|
const MAX_COMPACTION_MESSAGE_CHARS = 24_000;
|
|
17
17
|
const MAX_COMPACTION_SOURCE_CHARS = 64_000;
|
|
18
|
+
export const DEFAULT_MAX_TOOL_CONTEXT_TOKENS = 64_000;
|
|
19
|
+
const TOOL_CONTEXT_BUDGET_RATIO = 0.25;
|
|
20
|
+
const RECENT_TOOL_CONTEXT_BUDGET_RATIO = 0.6;
|
|
21
|
+
const STORED_TOOL_TRANSCRIPT_MARKER = "\n\n[工具记录]\n";
|
|
22
|
+
const QUARANTINED_PROTOCOL_REPLY = "[上一轮响应因工具协议异常已隔离,不能视为已执行;请根据后续用户消息继续。]";
|
|
18
23
|
export function normalizeBuiltinSessionId(value) {
|
|
19
24
|
return value.replace(/[^a-zA-Z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "default";
|
|
20
25
|
}
|
|
@@ -49,8 +54,42 @@ function normalizeMessage(value) {
|
|
|
49
54
|
const toolCalls = normalizeToolCalls(raw.toolCalls);
|
|
50
55
|
if (toolCalls.length > 0)
|
|
51
56
|
message.toolCalls = toolCalls;
|
|
57
|
+
const timeline = normalizeTimeline(raw.timeline);
|
|
58
|
+
if (timeline.length > 0)
|
|
59
|
+
message.timeline = timeline;
|
|
52
60
|
return message;
|
|
53
61
|
}
|
|
62
|
+
function normalizeTimeline(value) {
|
|
63
|
+
if (!Array.isArray(value))
|
|
64
|
+
return [];
|
|
65
|
+
const entries = [];
|
|
66
|
+
for (const item of value.slice(0, 1000)) {
|
|
67
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
68
|
+
continue;
|
|
69
|
+
const raw = item;
|
|
70
|
+
if (raw.type === "text" && typeof raw.text === "string") {
|
|
71
|
+
entries.push({ type: "text", text: raw.text });
|
|
72
|
+
}
|
|
73
|
+
else if (raw.type === "tool_use" && typeof raw.id === "string" && typeof raw.name === "string") {
|
|
74
|
+
entries.push({
|
|
75
|
+
type: "tool_use",
|
|
76
|
+
id: raw.id,
|
|
77
|
+
name: raw.name,
|
|
78
|
+
...(typeof raw.input === "string" ? { input: raw.input } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (raw.type === "tool_result" && typeof raw.tool_use_id === "string") {
|
|
82
|
+
entries.push({
|
|
83
|
+
type: "tool_result",
|
|
84
|
+
tool_use_id: raw.tool_use_id,
|
|
85
|
+
...(typeof raw.name === "string" ? { name: raw.name } : {}),
|
|
86
|
+
...(typeof raw.output === "string" ? { output: raw.output } : {}),
|
|
87
|
+
...(typeof raw.is_error === "boolean" ? { is_error: raw.is_error } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
54
93
|
function normalizeToolCalls(value) {
|
|
55
94
|
if (!Array.isArray(value))
|
|
56
95
|
return [];
|
|
@@ -62,6 +101,8 @@ function normalizeToolCalls(value) {
|
|
|
62
101
|
if (typeof raw.name !== "string" || raw.name.length === 0)
|
|
63
102
|
continue;
|
|
64
103
|
const call = { name: raw.name };
|
|
104
|
+
if (typeof raw.id === "string" && raw.id.length > 0)
|
|
105
|
+
call.id = raw.id;
|
|
65
106
|
if (typeof raw.input === "string")
|
|
66
107
|
call.input = raw.input;
|
|
67
108
|
if (typeof raw.output === "string")
|
|
@@ -135,6 +176,18 @@ function readSessionInfo(contextDir, sessionId) {
|
|
|
135
176
|
export function getBuiltinContextSession(sessionId, contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
|
|
136
177
|
return readSessionInfo(contextDir, sessionId);
|
|
137
178
|
}
|
|
179
|
+
export function readBuiltinContextState(sessionId, contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
|
|
180
|
+
const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
|
|
181
|
+
const filePath = contextFilePath(contextDir, normalizedSessionId);
|
|
182
|
+
if (!existsSync(filePath))
|
|
183
|
+
return null;
|
|
184
|
+
try {
|
|
185
|
+
return normalizeState(JSON.parse(readFileSync(filePath, "utf8")), normalizedSessionId);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
138
191
|
export function listBuiltinContextSessions(contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
|
|
139
192
|
if (!existsSync(contextDir))
|
|
140
193
|
return [];
|
|
@@ -161,8 +214,11 @@ export function latestBuiltinSessionForCwd(cwd, contextDir = DEFAULT_BUILTIN_CON
|
|
|
161
214
|
* 按字符类型加权,比旧版 chars/3 更接近真实:CJK 字符 ≈ 1 token/字,
|
|
162
215
|
* 其他字符 ≈ 3.5 chars/token。避免中文长上下文被严重低估导致压缩过晚。
|
|
163
216
|
*/
|
|
164
|
-
|
|
165
|
-
const
|
|
217
|
+
function stripStoredToolTranscript(content) {
|
|
218
|
+
const markerIndex = content.indexOf(STORED_TOOL_TRANSCRIPT_MARKER);
|
|
219
|
+
return markerIndex >= 0 ? content.slice(0, markerIndex) : content;
|
|
220
|
+
}
|
|
221
|
+
function estimateTextTokens(text) {
|
|
166
222
|
let cjk = 0;
|
|
167
223
|
for (const ch of text) {
|
|
168
224
|
if (/[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/.test(ch))
|
|
@@ -171,6 +227,30 @@ export function estimateBuiltinContextTokens(summary, messages) {
|
|
|
171
227
|
const other = text.length - cjk;
|
|
172
228
|
return Math.ceil(cjk + other / 3.5);
|
|
173
229
|
}
|
|
230
|
+
export function estimateBuiltinToolContextTokens(messages) {
|
|
231
|
+
let text = "";
|
|
232
|
+
for (const message of messages) {
|
|
233
|
+
if (message.role !== "assistant")
|
|
234
|
+
continue;
|
|
235
|
+
if (message.toolCalls?.length) {
|
|
236
|
+
for (const call of message.toolCalls) {
|
|
237
|
+
text += `${call.name}\n${call.input ?? ""}\n${call.output ?? ""}\n`;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (const entry of message.timeline ?? []) {
|
|
242
|
+
if (entry.type === "tool_use")
|
|
243
|
+
text += `${entry.name}\n${entry.input ?? ""}\n`;
|
|
244
|
+
if (entry.type === "tool_result")
|
|
245
|
+
text += `${entry.name ?? ""}\n${entry.output ?? ""}\n`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return estimateTextTokens(text);
|
|
249
|
+
}
|
|
250
|
+
export function estimateBuiltinContextTokens(summary, messages) {
|
|
251
|
+
const text = summary + messages.reduce((sum, message) => sum + `${message.role}\n${stripStoredToolTranscript(message.content)}\n`, "");
|
|
252
|
+
return estimateTextTokens(text) + estimateBuiltinToolContextTokens(messages);
|
|
253
|
+
}
|
|
174
254
|
export function serializeMessagesForSummary(messages) {
|
|
175
255
|
return messages
|
|
176
256
|
.map((message, index) => `### ${index + 1}. ${message.role}\n${message.content}`)
|
|
@@ -188,23 +268,205 @@ export const DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS = 32_000;
|
|
|
188
268
|
export const DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24_000;
|
|
189
269
|
const ASSISTANT_TRUNCATED_MARKER = "...[助手回复已在上下文中截断]...";
|
|
190
270
|
const TOOL_TRANSCRIPT_TRUNCATED_MARKER = "...[工具记录已截断]...";
|
|
271
|
+
function truncateStructuredToolCalls(calls, maxChars) {
|
|
272
|
+
const payloads = calls.flatMap((call) => [call.input ?? "", call.output ?? ""]).filter(Boolean);
|
|
273
|
+
const total = payloads.reduce((sum, value) => sum + value.length, 0);
|
|
274
|
+
if (total <= maxChars)
|
|
275
|
+
return calls;
|
|
276
|
+
const sharedBudget = Math.max(0, maxChars - payloads.length);
|
|
277
|
+
const truncatePayload = (value) => {
|
|
278
|
+
if (value === undefined || value.length === 0)
|
|
279
|
+
return value;
|
|
280
|
+
const budget = 1 + Math.floor(sharedBudget * value.length / Math.max(1, total));
|
|
281
|
+
return truncateMiddle(value, budget, TOOL_TRANSCRIPT_TRUNCATED_MARKER);
|
|
282
|
+
};
|
|
283
|
+
return calls.map((call) => ({
|
|
284
|
+
...call,
|
|
285
|
+
...(call.input !== undefined ? { input: truncatePayload(call.input) } : {}),
|
|
286
|
+
...(call.output !== undefined ? { output: truncatePayload(call.output) } : {}),
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
191
289
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
290
|
+
* 构造持久化 assistant 消息。content 仅保存回答正文,工具事件保存在
|
|
291
|
+
* toolCalls/timeline,避免内部 transcript 语法回流并诱导模型伪造工具执行。
|
|
194
292
|
*/
|
|
195
293
|
export function buildPersistedAssistantMessage(params) {
|
|
196
294
|
const maxAssistantChars = params.maxAssistantChars ?? DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS;
|
|
197
295
|
const maxTranscriptChars = params.maxTranscriptChars ?? DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS;
|
|
198
296
|
const persistedAssistantText = truncateMiddle(params.fullText, maxAssistantChars, ASSISTANT_TRUNCATED_MARKER);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
: persistedAssistantText;
|
|
202
|
-
const message = { role: "assistant", content };
|
|
203
|
-
const toolCalls = normalizeToolCalls(params.toolCalls);
|
|
297
|
+
const message = { role: "assistant", content: persistedAssistantText };
|
|
298
|
+
const toolCalls = truncateStructuredToolCalls(normalizeToolCalls(params.toolCalls), maxTranscriptChars);
|
|
204
299
|
if (toolCalls.length > 0)
|
|
205
300
|
message.toolCalls = toolCalls;
|
|
301
|
+
const timeline = truncatePersistedTimeline(normalizeTimeline(params.timeline));
|
|
302
|
+
if (timeline.length > 0)
|
|
303
|
+
message.timeline = timeline;
|
|
206
304
|
return message;
|
|
207
305
|
}
|
|
306
|
+
function truncatePersistedTimeline(entries) {
|
|
307
|
+
const maxTextChars = DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS;
|
|
308
|
+
const maxToolChars = DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS;
|
|
309
|
+
const totalText = entries.reduce((sum, entry) => sum + (entry.type === "text" ? entry.text.length : 0), 0);
|
|
310
|
+
const textPayloads = entries.filter((entry) => entry.type === "text" && entry.text.length > 0).length;
|
|
311
|
+
const totalTool = entries.reduce((sum, entry) => {
|
|
312
|
+
if (entry.type === "tool_use")
|
|
313
|
+
return sum + (entry.input?.length ?? 0);
|
|
314
|
+
if (entry.type === "tool_result")
|
|
315
|
+
return sum + (entry.output?.length ?? 0);
|
|
316
|
+
return sum;
|
|
317
|
+
}, 0);
|
|
318
|
+
const toolPayloads = entries.filter((entry) => (entry.type === "tool_use" && (entry.input?.length ?? 0) > 0)
|
|
319
|
+
|| (entry.type === "tool_result" && (entry.output?.length ?? 0) > 0)).length;
|
|
320
|
+
const payloadBudget = (length, total, count, maximum) => {
|
|
321
|
+
if (total <= maximum)
|
|
322
|
+
return length;
|
|
323
|
+
const sharedBudget = Math.max(0, maximum - count);
|
|
324
|
+
return length > 0 ? 1 + Math.floor(sharedBudget * length / Math.max(1, total)) : 0;
|
|
325
|
+
};
|
|
326
|
+
const truncatePayload = (value, budget, marker) => {
|
|
327
|
+
if (value.length <= budget)
|
|
328
|
+
return value;
|
|
329
|
+
if (budget <= marker.length + 2)
|
|
330
|
+
return value.slice(0, budget);
|
|
331
|
+
return truncateMiddle(value, budget, marker);
|
|
332
|
+
};
|
|
333
|
+
return entries.map((entry) => {
|
|
334
|
+
if (entry.type === "text") {
|
|
335
|
+
const budget = payloadBudget(entry.text.length, totalText, textPayloads, maxTextChars);
|
|
336
|
+
return { ...entry, text: truncatePayload(entry.text, budget, ASSISTANT_TRUNCATED_MARKER) };
|
|
337
|
+
}
|
|
338
|
+
if (entry.type === "tool_use" && entry.input !== undefined) {
|
|
339
|
+
const budget = payloadBudget(entry.input.length, totalTool, toolPayloads, maxToolChars);
|
|
340
|
+
return { ...entry, input: truncatePayload(entry.input, budget, TOOL_TRANSCRIPT_TRUNCATED_MARKER) };
|
|
341
|
+
}
|
|
342
|
+
if (entry.type === "tool_result" && entry.output !== undefined) {
|
|
343
|
+
const budget = payloadBudget(entry.output.length, totalTool, toolPayloads, maxToolChars);
|
|
344
|
+
return { ...entry, output: truncatePayload(entry.output, budget, TOOL_TRANSCRIPT_TRUNCATED_MARKER) };
|
|
345
|
+
}
|
|
346
|
+
return entry;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function parseToolInput(value) {
|
|
350
|
+
if (!value?.trim())
|
|
351
|
+
return {};
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(value);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return { raw: value };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function parseToolOutput(value) {
|
|
360
|
+
if (!value?.trim())
|
|
361
|
+
return { type: "text", value: "" };
|
|
362
|
+
try {
|
|
363
|
+
return { type: "json", value: JSON.parse(value) };
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return { type: "text", value };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function replayStructuredAssistantMessage(message, messageIndex) {
|
|
370
|
+
const plainText = stripStoredToolTranscript(message.content);
|
|
371
|
+
const hasStructuredTools = Boolean(message.toolCalls?.length
|
|
372
|
+
|| message.timeline?.some((entry) => entry.type === "tool_use" || entry.type === "tool_result"));
|
|
373
|
+
if (!hasStructuredTools)
|
|
374
|
+
return [{ role: "assistant", content: plainText }];
|
|
375
|
+
const timeline = message.timeline?.length
|
|
376
|
+
? message.timeline
|
|
377
|
+
: [
|
|
378
|
+
...(message.toolCalls ?? []).map((call, callIndex) => ({
|
|
379
|
+
type: "tool_use",
|
|
380
|
+
id: call.id ?? `context-tool-${messageIndex}-${callIndex}`,
|
|
381
|
+
name: call.name,
|
|
382
|
+
...(call.input !== undefined ? { input: call.input } : {}),
|
|
383
|
+
})),
|
|
384
|
+
...(message.toolCalls ?? []).map((call, callIndex) => ({
|
|
385
|
+
type: "tool_result",
|
|
386
|
+
tool_use_id: call.id ?? `context-tool-${messageIndex}-${callIndex}`,
|
|
387
|
+
name: call.name,
|
|
388
|
+
...(call.output !== undefined ? { output: call.output } : {}),
|
|
389
|
+
...(call.is_error !== undefined ? { is_error: call.is_error } : {}),
|
|
390
|
+
})),
|
|
391
|
+
...(plainText ? [{ type: "text", text: plainText }] : []),
|
|
392
|
+
];
|
|
393
|
+
const messages = [];
|
|
394
|
+
let assistantParts = [];
|
|
395
|
+
let toolParts = [];
|
|
396
|
+
const pendingCalls = new Map();
|
|
397
|
+
const knownNames = new Map();
|
|
398
|
+
const emittedCallIds = new Set();
|
|
399
|
+
const flushAssistant = () => {
|
|
400
|
+
if (!assistantParts.length)
|
|
401
|
+
return;
|
|
402
|
+
messages.push({ role: "assistant", content: assistantParts });
|
|
403
|
+
assistantParts = [];
|
|
404
|
+
};
|
|
405
|
+
const completeToolStep = () => {
|
|
406
|
+
// OpenAI/LiteLLM requires every tool message to immediately follow the
|
|
407
|
+
// assistant message that declared all matching tool_calls for that step.
|
|
408
|
+
flushAssistant();
|
|
409
|
+
for (const [toolCallId, toolName] of pendingCalls) {
|
|
410
|
+
toolParts.push({
|
|
411
|
+
type: "tool-result",
|
|
412
|
+
toolCallId,
|
|
413
|
+
toolName,
|
|
414
|
+
output: { type: "text", value: "[tool execution interrupted; result unavailable]" },
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
pendingCalls.clear();
|
|
418
|
+
if (!toolParts.length)
|
|
419
|
+
return;
|
|
420
|
+
messages.push({ role: "tool", content: toolParts });
|
|
421
|
+
toolParts = [];
|
|
422
|
+
};
|
|
423
|
+
for (const entry of timeline) {
|
|
424
|
+
if (entry.type === "text") {
|
|
425
|
+
if (pendingCalls.size > 0 || toolParts.length > 0)
|
|
426
|
+
completeToolStep();
|
|
427
|
+
const previous = assistantParts[assistantParts.length - 1];
|
|
428
|
+
if (previous?.type === "text")
|
|
429
|
+
previous.text += entry.text;
|
|
430
|
+
else if (entry.text)
|
|
431
|
+
assistantParts.push({ type: "text", text: entry.text });
|
|
432
|
+
}
|
|
433
|
+
else if (entry.type === "tool_use") {
|
|
434
|
+
// Consecutive tool calls belong to one assistant step. Only close the
|
|
435
|
+
// previous step after at least one result has arrived.
|
|
436
|
+
if (toolParts.length > 0)
|
|
437
|
+
completeToolStep();
|
|
438
|
+
knownNames.set(entry.id, entry.name);
|
|
439
|
+
pendingCalls.set(entry.id, entry.name);
|
|
440
|
+
emittedCallIds.add(entry.id);
|
|
441
|
+
assistantParts.push({
|
|
442
|
+
type: "tool-call",
|
|
443
|
+
toolCallId: entry.id,
|
|
444
|
+
toolName: entry.name,
|
|
445
|
+
input: parseToolInput(entry.input),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
if (!emittedCallIds.has(entry.tool_use_id))
|
|
450
|
+
continue;
|
|
451
|
+
const toolName = entry.name ?? knownNames.get(entry.tool_use_id);
|
|
452
|
+
if (!toolName)
|
|
453
|
+
continue;
|
|
454
|
+
flushAssistant();
|
|
455
|
+
pendingCalls.delete(entry.tool_use_id);
|
|
456
|
+
toolParts.push({
|
|
457
|
+
type: "tool-result",
|
|
458
|
+
toolCallId: entry.tool_use_id,
|
|
459
|
+
toolName,
|
|
460
|
+
output: parseToolOutput(entry.output),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (pendingCalls.size > 0 || toolParts.length > 0)
|
|
465
|
+
completeToolStep();
|
|
466
|
+
else
|
|
467
|
+
flushAssistant();
|
|
468
|
+
return messages;
|
|
469
|
+
}
|
|
208
470
|
function serializeMessagesForCompaction(messages) {
|
|
209
471
|
const sections = [];
|
|
210
472
|
let remaining = MAX_COMPACTION_SOURCE_CHARS;
|
|
@@ -245,6 +507,7 @@ export class BuiltinContextManager {
|
|
|
245
507
|
contextDir;
|
|
246
508
|
sessionId;
|
|
247
509
|
compactAtTokens;
|
|
510
|
+
maxToolContextTokens;
|
|
248
511
|
keepRecentMessages;
|
|
249
512
|
cwd;
|
|
250
513
|
state;
|
|
@@ -255,6 +518,8 @@ export class BuiltinContextManager {
|
|
|
255
518
|
this.cwd = options.cwd;
|
|
256
519
|
this.compactAtTokens = options.compactAtTokens
|
|
257
520
|
?? Math.floor((options.contextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS) * COMPACTION_THRESHOLD_RATIO);
|
|
521
|
+
this.maxToolContextTokens = Math.max(1, options.maxToolContextTokens
|
|
522
|
+
?? Math.min(DEFAULT_MAX_TOOL_CONTEXT_TOKENS, Math.floor(this.compactAtTokens * TOOL_CONTEXT_BUDGET_RATIO)));
|
|
258
523
|
this.keepRecentMessages = Math.max(1, options.keepRecentMessages ?? DEFAULT_KEEP_RECENT_MESSAGES);
|
|
259
524
|
this.state = this.load();
|
|
260
525
|
}
|
|
@@ -294,7 +559,12 @@ export class BuiltinContextManager {
|
|
|
294
559
|
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
295
560
|
// from future prompts so one protocol failure cannot teach the model to
|
|
296
561
|
// repeat the same invalid tool syntax on every later turn.
|
|
297
|
-
|
|
562
|
+
for (const [index, message] of this.modelSafeMessages().entries()) {
|
|
563
|
+
if (message.role === "user")
|
|
564
|
+
messages.push({ role: "user", content: message.content });
|
|
565
|
+
else
|
|
566
|
+
messages.push(...replayStructuredAssistantMessage(message, index));
|
|
567
|
+
}
|
|
298
568
|
return messages;
|
|
299
569
|
}
|
|
300
570
|
planCompaction() {
|
|
@@ -302,16 +572,19 @@ export class BuiltinContextManager {
|
|
|
302
572
|
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
303
573
|
const messages = this.modelSafeMessages();
|
|
304
574
|
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
305
|
-
|
|
575
|
+
const toolEstimated = estimateBuiltinToolContextTokens(messages);
|
|
576
|
+
if (estimated <= this.compactAtTokens && toolEstimated <= this.maxToolContextTokens)
|
|
306
577
|
return null;
|
|
307
578
|
if (messages.length <= 1)
|
|
308
579
|
return null;
|
|
309
580
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
310
581
|
const recentTokenBudget = Math.max(1, Math.floor(this.compactAtTokens * RECENT_CONTEXT_BUDGET_RATIO));
|
|
582
|
+
const recentToolBudget = Math.max(1, Math.floor(this.maxToolContextTokens * RECENT_TOOL_CONTEXT_BUDGET_RATIO));
|
|
311
583
|
let splitAt = messages.length - 1;
|
|
312
584
|
for (let index = messages.length - 2; index >= earliestAllowed; index -= 1) {
|
|
313
585
|
const candidate = messages.slice(index);
|
|
314
|
-
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget
|
|
586
|
+
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget
|
|
587
|
+
|| estimateBuiltinToolContextTokens(candidate) > recentToolBudget)
|
|
315
588
|
break;
|
|
316
589
|
splitAt = index;
|
|
317
590
|
}
|
|
@@ -322,8 +595,8 @@ export class BuiltinContextManager {
|
|
|
322
595
|
splitAt = 1;
|
|
323
596
|
return {
|
|
324
597
|
previousSummary: this.state.summary,
|
|
325
|
-
oldMessages:
|
|
326
|
-
recentMessages:
|
|
598
|
+
oldMessages: messages.slice(0, splitAt),
|
|
599
|
+
recentMessages: messages.slice(splitAt),
|
|
327
600
|
};
|
|
328
601
|
}
|
|
329
602
|
applyCompaction(summary, plan) {
|
|
@@ -347,7 +620,17 @@ export class BuiltinContextManager {
|
|
|
347
620
|
renameSync(tmp, this.contextFilePath);
|
|
348
621
|
}
|
|
349
622
|
modelSafeMessages() {
|
|
350
|
-
return this.state.messages.
|
|
623
|
+
return this.state.messages.map((message) => {
|
|
624
|
+
if (message.role !== "assistant")
|
|
625
|
+
return message;
|
|
626
|
+
const hasStructuredTools = Boolean(message.toolCalls?.length
|
|
627
|
+
|| message.timeline?.some((entry) => entry.type === "tool_use" || entry.type === "tool_result"));
|
|
628
|
+
const copiedStoredTranscript = hasImitatedToolTranscriptText(message.content) && hasStructuredTools;
|
|
629
|
+
if (hasMalformedToolProtocolText(message.content) && !copiedStoredTranscript) {
|
|
630
|
+
return { role: "assistant", content: QUARANTINED_PROTOCOL_REPLY };
|
|
631
|
+
}
|
|
632
|
+
return { ...message, content: stripStoredToolTranscript(message.content) };
|
|
633
|
+
});
|
|
351
634
|
}
|
|
352
635
|
load() {
|
|
353
636
|
if (!this.persist || !existsSync(this.contextFilePath))
|
|
@@ -8,6 +8,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
|
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { jsonSchema, tool } from "ai";
|
|
10
10
|
import { isDangerousCommand } from "./permissions.js";
|
|
11
|
+
import { detectImageMime, MAX_ATTACHMENT_BYTES } from "./attachments.js";
|
|
11
12
|
import { killProcessTree } from "./proc-tree-kill.js";
|
|
12
13
|
import { searchBuiltinSessions, } from "./session-search.js";
|
|
13
14
|
import { webFetchForTool, webSearchForTool, } from "./web-tools.js";
|
|
@@ -74,6 +75,25 @@ async function pathExists(path) {
|
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
export async function presentFileForTool(cwd, input) {
|
|
79
|
+
const path = resolveToolPath(cwd, input.path);
|
|
80
|
+
const info = await stat(path).catch(() => null);
|
|
81
|
+
if (!info?.isFile())
|
|
82
|
+
throw new Error(`Image file not found: ${input.path}`);
|
|
83
|
+
if (info.size > MAX_ATTACHMENT_BYTES)
|
|
84
|
+
throw new Error(`Image file exceeds the 20 MB limit: ${input.path}`);
|
|
85
|
+
const mimeType = detectImageMime(await readFile(path));
|
|
86
|
+
if (!mimeType)
|
|
87
|
+
throw new Error(`present_file only supports PNG, JPEG, or WebP images: ${input.path}`);
|
|
88
|
+
const caption = input.caption?.trim();
|
|
89
|
+
return {
|
|
90
|
+
path,
|
|
91
|
+
name: basename(path),
|
|
92
|
+
mimeType,
|
|
93
|
+
size: info.size,
|
|
94
|
+
...(caption ? { caption: caption.slice(0, 500) } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
77
97
|
function sha256(buffer) {
|
|
78
98
|
return createHash("sha256").update(buffer).digest("hex");
|
|
79
99
|
}
|
|
@@ -1011,6 +1031,19 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1011
1031
|
}),
|
|
1012
1032
|
execute: (input, options) => searchCodeForTool(cwd, input, options.abortSignal),
|
|
1013
1033
|
}),
|
|
1034
|
+
present_file: tool({
|
|
1035
|
+
description: "把本地 PNG、JPEG 或 WebP 图片作为会话图片展示给用户。仅在图片文件已经生成并需要交付时调用。",
|
|
1036
|
+
inputSchema: jsonSchema({
|
|
1037
|
+
type: "object",
|
|
1038
|
+
additionalProperties: false,
|
|
1039
|
+
properties: {
|
|
1040
|
+
path: { type: "string", description: "图片绝对路径或相对于会话工作目录的路径。" },
|
|
1041
|
+
caption: { type: "string", description: "可选的图片说明。" },
|
|
1042
|
+
},
|
|
1043
|
+
required: ["path"],
|
|
1044
|
+
}),
|
|
1045
|
+
execute: (input) => presentFileForTool(cwd, input),
|
|
1046
|
+
}),
|
|
1014
1047
|
run_command: tool({
|
|
1015
1048
|
description: "在本地工作区运行非交互式 shell 命令。用于测试、git 和包脚本。返回 stdout/stderr 和 exitCode;非零退出码不是工具错误。",
|
|
1016
1049
|
inputSchema: jsonSchema({
|