chatccc 0.2.272 → 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 +24 -22
- package/config.sample.json +4 -4
- package/deepccc-agent/README.md +147 -79
- package/deepccc-agent/package.json +68 -65
- package/dist/deepccc-agent/src/attachments.js +192 -0
- package/dist/deepccc-agent/src/cli.js +54 -13
- package/dist/deepccc-agent/src/config.js +49 -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 +48 -16
- 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 +1 -0
- 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/web/agent-team-page.js +250 -250
- package/dist/src/cards.js +7 -4
- package/dist/src/im-skills.js +9 -2
- package/dist/src/orchestrator.js +117 -29
- package/dist/src/session-name.js +15 -0
- package/dist/src/session.js +43 -7
- package/dist/src/web-ui.js +68 -51
- 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 +76 -76
|
@@ -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({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
7
7
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
8
|
-
import { generateText, isLoopFinished, stepCountIs, streamText } from "ai";
|
|
8
|
+
import { generateText, isLoopFinished, stepCountIs, streamText, } from "ai";
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { isAbsolute, join, resolve } from "node:path";
|
|
@@ -144,7 +144,7 @@ function addAnthropicToolJsonCompatibilityNote(messages) {
|
|
|
144
144
|
// 部分 Anthropic→OpenAI/Ark 转换器会用 response_format=json_object
|
|
145
145
|
// 实现工具调用,却只在 messages 中校验 JSON 关键词、不读取顶层 system。
|
|
146
146
|
// 这里仅声明工具参数的编码方式,并明确不要求普通最终回复输出 JSON。
|
|
147
|
-
return messages.map((message, index) => (index === lastUserIndex
|
|
147
|
+
return messages.map((message, index) => (index === lastUserIndex && message.role === "user" && typeof message.content === "string"
|
|
148
148
|
? {
|
|
149
149
|
...message,
|
|
150
150
|
content: `${message.content}\n\n${ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE}`,
|
|
@@ -161,11 +161,13 @@ function addAnthropicToolJsonCompatibilityNote(messages) {
|
|
|
161
161
|
function maybeAppendCompactionRecoveryHint(messages, summary, rawLogsEnabled, sessionId) {
|
|
162
162
|
if (!summary.trim())
|
|
163
163
|
return messages;
|
|
164
|
-
const summaryIndex = messages.findIndex((message) => message.role === "user"
|
|
164
|
+
const summaryIndex = messages.findIndex((message) => message.role === "user"
|
|
165
|
+
&& typeof message.content === "string"
|
|
166
|
+
&& message.content.startsWith("以下是更早的对话摘要"));
|
|
165
167
|
if (summaryIndex < 0)
|
|
166
168
|
return messages;
|
|
167
169
|
const hint = rawLogsEnabled ? buildCompactionRecoveryHint(sessionId) : COMPACTION_RECOVERY_HINT_DISABLED;
|
|
168
|
-
return messages.map((message, index) => (index === summaryIndex
|
|
170
|
+
return messages.map((message, index) => (index === summaryIndex && message.role === "user" && typeof message.content === "string"
|
|
169
171
|
? { ...message, content: `${message.content}\n\n${hint}` }
|
|
170
172
|
: message));
|
|
171
173
|
}
|
|
@@ -249,6 +251,7 @@ export class ChatSession {
|
|
|
249
251
|
maxSteps;
|
|
250
252
|
effort;
|
|
251
253
|
maxOutputTokens;
|
|
254
|
+
streaming;
|
|
252
255
|
permissionMode;
|
|
253
256
|
permissionResolver;
|
|
254
257
|
permissionGate;
|
|
@@ -316,6 +319,7 @@ export class ChatSession {
|
|
|
316
319
|
this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
|
|
317
320
|
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
318
321
|
this.maxOutputTokens = normalizeMaxOutputTokens(overrides.maxOutputTokens ?? appConfig.maxOutputTokens);
|
|
322
|
+
this.streaming = overrides.streaming ?? appConfig.streaming;
|
|
319
323
|
this.apiKey = apiKey;
|
|
320
324
|
this.baseURL = baseURL;
|
|
321
325
|
const provider = this.provider === "anthropic"
|
|
@@ -351,6 +355,7 @@ export class ChatSession {
|
|
|
351
355
|
cwd: this.cwd,
|
|
352
356
|
contextWindow: options.contextWindow ?? appConfig.contextWindow,
|
|
353
357
|
compactAtTokens: options.compactAtTokens,
|
|
358
|
+
maxToolContextTokens: options.maxToolContextTokens,
|
|
354
359
|
keepRecentMessages: options.keepRecentMessages,
|
|
355
360
|
});
|
|
356
361
|
this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
|
|
@@ -391,6 +396,8 @@ export class ChatSession {
|
|
|
391
396
|
// assistant 消息 toolCalls 字段;[Tool transcript] 文本视图仍按原格式生成。
|
|
392
397
|
const toolCallsById = new Map();
|
|
393
398
|
const toolCallOrder = [];
|
|
399
|
+
const timeline = [];
|
|
400
|
+
let toolContext = [];
|
|
394
401
|
try {
|
|
395
402
|
if (this.context.planCompaction()) {
|
|
396
403
|
yield { type: "status", phase: "compacting" };
|
|
@@ -458,9 +465,10 @@ export class ChatSession {
|
|
|
458
465
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
459
466
|
fullText = "";
|
|
460
467
|
safeAccumulated = "";
|
|
461
|
-
|
|
468
|
+
toolContext = [];
|
|
462
469
|
toolCallsById.clear();
|
|
463
470
|
toolCallOrder.length = 0;
|
|
471
|
+
timeline.length = 0;
|
|
464
472
|
let lastReasoningProgressAt;
|
|
465
473
|
const attemptMessages = attempt === 0
|
|
466
474
|
? modelMessages
|
|
@@ -470,7 +478,7 @@ export class ChatSession {
|
|
|
470
478
|
messages: attemptMessages,
|
|
471
479
|
};
|
|
472
480
|
let stream;
|
|
473
|
-
if (
|
|
481
|
+
if (this.streaming) {
|
|
474
482
|
const result = streamText(generationOptions);
|
|
475
483
|
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
476
484
|
}
|
|
@@ -491,6 +499,11 @@ export class ChatSession {
|
|
|
491
499
|
}
|
|
492
500
|
else if (part.type === "text-delta") {
|
|
493
501
|
fullText += part.text;
|
|
502
|
+
const previous = timeline[timeline.length - 1];
|
|
503
|
+
if (previous?.type === "text")
|
|
504
|
+
previous.text += part.text;
|
|
505
|
+
else
|
|
506
|
+
timeline.push({ type: "text", text: part.text });
|
|
494
507
|
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
495
508
|
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
496
509
|
const safeText = applyPrivacy(part.text);
|
|
@@ -498,9 +511,11 @@ export class ChatSession {
|
|
|
498
511
|
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
499
512
|
}
|
|
500
513
|
else if (part.type === "tool-call") {
|
|
501
|
-
|
|
502
|
-
|
|
514
|
+
const input = safeJson(part.input);
|
|
515
|
+
toolContext.push(`tool_call ${part.toolName}: ${input}`);
|
|
516
|
+
toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
|
|
503
517
|
toolCallOrder.push(part.toolCallId);
|
|
518
|
+
timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
|
|
504
519
|
yield {
|
|
505
520
|
type: "tool_use",
|
|
506
521
|
id: part.toolCallId,
|
|
@@ -509,10 +524,12 @@ export class ChatSession {
|
|
|
509
524
|
};
|
|
510
525
|
}
|
|
511
526
|
else if (part.type === "tool-result") {
|
|
512
|
-
|
|
527
|
+
const output = truncateToolContext(safeJson(part.output));
|
|
528
|
+
toolContext.push(`tool_result ${part.toolName}: ${output}`);
|
|
513
529
|
const call = toolCallsById.get(part.toolCallId);
|
|
514
530
|
if (call)
|
|
515
|
-
call.output =
|
|
531
|
+
call.output = output;
|
|
532
|
+
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
|
|
516
533
|
yield {
|
|
517
534
|
type: "tool_result",
|
|
518
535
|
tool_use_id: part.toolCallId,
|
|
@@ -529,6 +546,7 @@ export class ChatSession {
|
|
|
529
546
|
call.output = message;
|
|
530
547
|
call.is_error = true;
|
|
531
548
|
}
|
|
549
|
+
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
|
|
532
550
|
yield {
|
|
533
551
|
type: "tool_result",
|
|
534
552
|
tool_use_id: part.toolCallId,
|
|
@@ -544,7 +562,7 @@ export class ChatSession {
|
|
|
544
562
|
}
|
|
545
563
|
}
|
|
546
564
|
if (hasMalformedToolProtocolText(fullText)) {
|
|
547
|
-
console.warn(`[DeepCCC] malformed
|
|
565
|
+
console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
|
|
548
566
|
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
549
567
|
rawLog?.writeLine(safeRawStreamJson({
|
|
550
568
|
type: "deepccc_tool_protocol_recovery",
|
|
@@ -557,8 +575,8 @@ export class ChatSession {
|
|
|
557
575
|
continue;
|
|
558
576
|
}
|
|
559
577
|
throw new Error(toolCallOrder.length > 0
|
|
560
|
-
? "
|
|
561
|
-
: "
|
|
578
|
+
? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
|
|
579
|
+
: "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
|
|
562
580
|
}
|
|
563
581
|
completed = true;
|
|
564
582
|
const collectedToolCalls = toolCallOrder
|
|
@@ -568,6 +586,7 @@ export class ChatSession {
|
|
|
568
586
|
fullText,
|
|
569
587
|
transcriptLines: toolContext,
|
|
570
588
|
toolCalls: collectedToolCalls,
|
|
589
|
+
timeline,
|
|
571
590
|
}));
|
|
572
591
|
yield { type: "done", text: safeAccumulated };
|
|
573
592
|
return;
|
|
@@ -579,9 +598,22 @@ export class ChatSession {
|
|
|
579
598
|
if (malformedProtocolOutput)
|
|
580
599
|
yield { type: "text_reset" };
|
|
581
600
|
if (err.name === "AbortError" || signal?.aborted) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
601
|
+
if ((fullText || toolCallOrder.length > 0) && !malformedProtocolOutput) {
|
|
602
|
+
const collectedToolCalls = toolCallOrder
|
|
603
|
+
.map((id) => toolCallsById.get(id))
|
|
604
|
+
.filter((call) => call !== undefined);
|
|
605
|
+
const interruptedTimeline = timeline.map((entry) => ({ ...entry }));
|
|
606
|
+
const previous = interruptedTimeline[interruptedTimeline.length - 1];
|
|
607
|
+
if (previous?.type === "text")
|
|
608
|
+
previous.text += "\n[interrupted]";
|
|
609
|
+
else
|
|
610
|
+
interruptedTimeline.push({ type: "text", text: "[interrupted]" });
|
|
611
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
612
|
+
fullText: `${fullText}\n[interrupted]`,
|
|
613
|
+
transcriptLines: toolContext,
|
|
614
|
+
toolCalls: collectedToolCalls,
|
|
615
|
+
timeline: interruptedTimeline,
|
|
616
|
+
}));
|
|
585
617
|
}
|
|
586
618
|
yield { type: "done", text: safeAccumulated };
|
|
587
619
|
return;
|