deepccc 0.2.8 → 0.2.10
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 +4 -3
- package/dist/context.d.ts +13 -12
- package/dist/context.js +205 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -7
- package/dist/tool-protocol.d.ts +6 -0
- package/dist/tool-protocol.js +14 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,8 +180,9 @@ Provider 发送 `max_tokens`,使用模型服务端默认值。可通过 `DEEPC
|
|
|
180
180
|
或命令行 `--max-output-tokens` 覆盖;只接受正整数。该限制会同时覆盖模型思考内容、
|
|
181
181
|
工具参数和最终回答,设置过小可能导致工具调用或长回复被截断。
|
|
182
182
|
|
|
183
|
-
`contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
|
|
184
|
-
|
|
183
|
+
`contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
|
|
184
|
+
原生规格);常规上下文压缩阈值自动 = `contextWindow × 0.8`。工具输入与结果另有独立预算:
|
|
185
|
+
默认取 `min(64000, 常规压缩阈值 × 25%)`,超过后会提前压缩,避免长会话积累大量低信号工具输出。
|
|
185
186
|
可通过 `DEEPCCC_CONTEXT_WINDOW` 环境变量覆盖。⚠️ 超过模型/服务端实际上限时请求会被
|
|
186
187
|
API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
|
|
187
188
|
`max_input_tokens`)。
|
|
@@ -324,7 +325,7 @@ deepccc-cli --dangerously-bypass-permissions
|
|
|
324
325
|
|
|
325
326
|
交互模式下,每轮回复渲染为固定"过程区块":状态行(压缩上下文中/生成回复中/完成/已停止/异常结束)+ 折叠工具行 + 原地更新正文,不滚屏刷 JSON。活动状态有心跳点号动画;完成/停止/异常后区块定型留在屏幕上。
|
|
326
327
|
|
|
327
|
-
|
|
328
|
+
持久化上下文达到常规 token 阈值或独立工具预算时,deepccc 会先按预算保留最近消息,再压缩较早内容;工具历史使用标准结构化 tool-call/tool-result 消息重放,不会把内部 `[工具记录]` 文本重复送回模型。若模型仍输出伪工具记录,系统会丢弃并重试一次;重复失败时安全终止,避免把未执行命令当成真实结果。超长历史消息、工具记录和压缩输入会被限长,一次压缩最多等待 5 分钟。
|
|
328
329
|
|
|
329
330
|
如果终端渲染出现异常,可以强制回退为纯文本流式输出:
|
|
330
331
|
|
package/dist/context.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
1
2
|
export type BuiltinContextRole = "user" | "assistant";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* 结构化工具调用存档。content 只保存面向用户的回答正文;工具输入与结果
|
|
5
|
+
* 独立保存,供模型按标准 tool-call/tool-result 消息重放以及 UI 检索、回放。
|
|
5
6
|
*/
|
|
6
7
|
export interface BuiltinContextToolCall {
|
|
7
8
|
/** Provider tool-call id,用于 Web 在流式运行与持久化消息之间保持同一张工具卡状态。 */
|
|
@@ -33,7 +34,7 @@ export interface BuiltinContextMessage {
|
|
|
33
34
|
content: string;
|
|
34
35
|
/** 可选结构化工具调用记录(assistant 消息专用) */
|
|
35
36
|
toolCalls?: BuiltinContextToolCall[];
|
|
36
|
-
/**
|
|
37
|
+
/** 文本与工具事件的真实发生顺序,同时用于 UI 与模型的结构化历史重放。 */
|
|
37
38
|
timeline?: BuiltinContextTimelineEntry[];
|
|
38
39
|
}
|
|
39
40
|
export interface BuiltinContextState {
|
|
@@ -71,6 +72,8 @@ export interface BuiltinContextOptions {
|
|
|
71
72
|
contextWindow?: number;
|
|
72
73
|
/** 直接指定压缩阈值(token),优先于 contextWindow 的 80% 派生。 */
|
|
73
74
|
compactAtTokens?: number;
|
|
75
|
+
/** Independent budget for retained structured tool inputs/results. */
|
|
76
|
+
maxToolContextTokens?: number;
|
|
74
77
|
keepRecentMessages?: number;
|
|
75
78
|
}
|
|
76
79
|
export declare const DEFAULT_BUILTIN_CONTEXT_DIR: string;
|
|
@@ -81,6 +84,7 @@ export declare const COMPACTION_THRESHOLD_RATIO = 0.8;
|
|
|
81
84
|
/** 默认压缩阈值 = 1M × 0.8。 */
|
|
82
85
|
export declare const DEFAULT_COMPACT_AT_TOKENS: number;
|
|
83
86
|
export declare const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
87
|
+
export declare const DEFAULT_MAX_TOOL_CONTEXT_TOKENS = 64000;
|
|
84
88
|
export declare function normalizeBuiltinSessionId(value: string): string;
|
|
85
89
|
export declare function defaultBuiltinSessionId(cwd?: string): string;
|
|
86
90
|
export declare function newBuiltinSessionId(now?: Date, suffix?: string): string;
|
|
@@ -88,22 +92,18 @@ export declare function getBuiltinContextSession(sessionId: string, contextDir?:
|
|
|
88
92
|
export declare function readBuiltinContextState(sessionId: string, contextDir?: string): BuiltinContextState | null;
|
|
89
93
|
export declare function listBuiltinContextSessions(contextDir?: string): BuiltinContextSessionInfo[];
|
|
90
94
|
export declare function latestBuiltinSessionForCwd(cwd: string, contextDir?: string): BuiltinContextSessionInfo | null;
|
|
91
|
-
|
|
92
|
-
* 估算上下文的 token 数(用于决定何时压缩)。
|
|
93
|
-
* 按字符类型加权,比旧版 chars/3 更接近真实:CJK 字符 ≈ 1 token/字,
|
|
94
|
-
* 其他字符 ≈ 3.5 chars/token。避免中文长上下文被严重低估导致压缩过晚。
|
|
95
|
-
*/
|
|
95
|
+
export declare function estimateBuiltinToolContextTokens(messages: readonly BuiltinContextMessage[]): number;
|
|
96
96
|
export declare function estimateBuiltinContextTokens(summary: string, messages: readonly BuiltinContextMessage[]): number;
|
|
97
97
|
export declare function serializeMessagesForSummary(messages: readonly BuiltinContextMessage[]): string;
|
|
98
98
|
export declare const DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS = 32000;
|
|
99
99
|
export declare const DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24000;
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
101
|
+
* 构造持久化 assistant 消息。content 仅保存回答正文,工具事件保存在
|
|
102
|
+
* toolCalls/timeline,避免内部 transcript 语法回流并诱导模型伪造工具执行。
|
|
103
103
|
*/
|
|
104
104
|
export declare function buildPersistedAssistantMessage(params: {
|
|
105
105
|
fullText: string;
|
|
106
|
-
/**
|
|
106
|
+
/** @deprecated 兼容旧调用方;工具记录只从 toolCalls/timeline 持久化。 */
|
|
107
107
|
transcriptLines: readonly string[];
|
|
108
108
|
/** 结构化工具调用(按调用顺序);有则写入消息的 toolCalls 字段 */
|
|
109
109
|
toolCalls?: readonly BuiltinContextToolCall[];
|
|
@@ -118,6 +118,7 @@ export declare class BuiltinContextManager {
|
|
|
118
118
|
readonly contextDir: string;
|
|
119
119
|
readonly sessionId: string;
|
|
120
120
|
readonly compactAtTokens: number;
|
|
121
|
+
readonly maxToolContextTokens: number;
|
|
121
122
|
readonly keepRecentMessages: number;
|
|
122
123
|
private readonly cwd?;
|
|
123
124
|
private state;
|
|
@@ -128,7 +129,7 @@ export declare class BuiltinContextManager {
|
|
|
128
129
|
get contextFilePath(): string;
|
|
129
130
|
appendMessage(message: BuiltinContextMessage): void;
|
|
130
131
|
setSummary(summary: string): void;
|
|
131
|
-
buildModelMessages():
|
|
132
|
+
buildModelMessages(): ModelMessage[];
|
|
132
133
|
planCompaction(): BuiltinCompactionPlan | null;
|
|
133
134
|
applyCompaction(summary: string, plan: BuiltinCompactionPlan): void;
|
|
134
135
|
reset(): void;
|
package/dist/context.js
CHANGED
|
@@ -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
|
}
|
|
@@ -209,8 +214,11 @@ export function latestBuiltinSessionForCwd(cwd, contextDir = DEFAULT_BUILTIN_CON
|
|
|
209
214
|
* 按字符类型加权,比旧版 chars/3 更接近真实:CJK 字符 ≈ 1 token/字,
|
|
210
215
|
* 其他字符 ≈ 3.5 chars/token。避免中文长上下文被严重低估导致压缩过晚。
|
|
211
216
|
*/
|
|
212
|
-
|
|
213
|
-
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) {
|
|
214
222
|
let cjk = 0;
|
|
215
223
|
for (const ch of text) {
|
|
216
224
|
if (/[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/.test(ch))
|
|
@@ -219,6 +227,30 @@ export function estimateBuiltinContextTokens(summary, messages) {
|
|
|
219
227
|
const other = text.length - cjk;
|
|
220
228
|
return Math.ceil(cjk + other / 3.5);
|
|
221
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
|
+
}
|
|
222
254
|
export function serializeMessagesForSummary(messages) {
|
|
223
255
|
return messages
|
|
224
256
|
.map((message, index) => `### ${index + 1}. ${message.role}\n${message.content}`)
|
|
@@ -236,19 +268,34 @@ export const DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS = 32_000;
|
|
|
236
268
|
export const DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24_000;
|
|
237
269
|
const ASSISTANT_TRUNCATED_MARKER = "...[助手回复已在上下文中截断]...";
|
|
238
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
|
+
}
|
|
239
289
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
290
|
+
* 构造持久化 assistant 消息。content 仅保存回答正文,工具事件保存在
|
|
291
|
+
* toolCalls/timeline,避免内部 transcript 语法回流并诱导模型伪造工具执行。
|
|
242
292
|
*/
|
|
243
293
|
export function buildPersistedAssistantMessage(params) {
|
|
244
294
|
const maxAssistantChars = params.maxAssistantChars ?? DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS;
|
|
245
295
|
const maxTranscriptChars = params.maxTranscriptChars ?? DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS;
|
|
246
296
|
const persistedAssistantText = truncateMiddle(params.fullText, maxAssistantChars, ASSISTANT_TRUNCATED_MARKER);
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
: persistedAssistantText;
|
|
250
|
-
const message = { role: "assistant", content };
|
|
251
|
-
const toolCalls = normalizeToolCalls(params.toolCalls);
|
|
297
|
+
const message = { role: "assistant", content: persistedAssistantText };
|
|
298
|
+
const toolCalls = truncateStructuredToolCalls(normalizeToolCalls(params.toolCalls), maxTranscriptChars);
|
|
252
299
|
if (toolCalls.length > 0)
|
|
253
300
|
message.toolCalls = toolCalls;
|
|
254
301
|
const timeline = truncatePersistedTimeline(normalizeTimeline(params.timeline));
|
|
@@ -299,6 +346,127 @@ function truncatePersistedTimeline(entries) {
|
|
|
299
346
|
return entry;
|
|
300
347
|
});
|
|
301
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
|
+
}
|
|
302
470
|
function serializeMessagesForCompaction(messages) {
|
|
303
471
|
const sections = [];
|
|
304
472
|
let remaining = MAX_COMPACTION_SOURCE_CHARS;
|
|
@@ -339,6 +507,7 @@ export class BuiltinContextManager {
|
|
|
339
507
|
contextDir;
|
|
340
508
|
sessionId;
|
|
341
509
|
compactAtTokens;
|
|
510
|
+
maxToolContextTokens;
|
|
342
511
|
keepRecentMessages;
|
|
343
512
|
cwd;
|
|
344
513
|
state;
|
|
@@ -349,6 +518,8 @@ export class BuiltinContextManager {
|
|
|
349
518
|
this.cwd = options.cwd;
|
|
350
519
|
this.compactAtTokens = options.compactAtTokens
|
|
351
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)));
|
|
352
523
|
this.keepRecentMessages = Math.max(1, options.keepRecentMessages ?? DEFAULT_KEEP_RECENT_MESSAGES);
|
|
353
524
|
this.state = this.load();
|
|
354
525
|
}
|
|
@@ -388,7 +559,12 @@ export class BuiltinContextManager {
|
|
|
388
559
|
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
389
560
|
// from future prompts so one protocol failure cannot teach the model to
|
|
390
561
|
// repeat the same invalid tool syntax on every later turn.
|
|
391
|
-
|
|
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
|
+
}
|
|
392
568
|
return messages;
|
|
393
569
|
}
|
|
394
570
|
planCompaction() {
|
|
@@ -396,16 +572,19 @@ export class BuiltinContextManager {
|
|
|
396
572
|
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
397
573
|
const messages = this.modelSafeMessages();
|
|
398
574
|
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
399
|
-
|
|
575
|
+
const toolEstimated = estimateBuiltinToolContextTokens(messages);
|
|
576
|
+
if (estimated <= this.compactAtTokens && toolEstimated <= this.maxToolContextTokens)
|
|
400
577
|
return null;
|
|
401
578
|
if (messages.length <= 1)
|
|
402
579
|
return null;
|
|
403
580
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
404
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));
|
|
405
583
|
let splitAt = messages.length - 1;
|
|
406
584
|
for (let index = messages.length - 2; index >= earliestAllowed; index -= 1) {
|
|
407
585
|
const candidate = messages.slice(index);
|
|
408
|
-
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget
|
|
586
|
+
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget
|
|
587
|
+
|| estimateBuiltinToolContextTokens(candidate) > recentToolBudget)
|
|
409
588
|
break;
|
|
410
589
|
splitAt = index;
|
|
411
590
|
}
|
|
@@ -416,8 +595,8 @@ export class BuiltinContextManager {
|
|
|
416
595
|
splitAt = 1;
|
|
417
596
|
return {
|
|
418
597
|
previousSummary: this.state.summary,
|
|
419
|
-
oldMessages:
|
|
420
|
-
recentMessages:
|
|
598
|
+
oldMessages: messages.slice(0, splitAt),
|
|
599
|
+
recentMessages: messages.slice(splitAt),
|
|
421
600
|
};
|
|
422
601
|
}
|
|
423
602
|
applyCompaction(summary, plan) {
|
|
@@ -441,7 +620,17 @@ export class BuiltinContextManager {
|
|
|
441
620
|
renameSync(tmp, this.contextFilePath);
|
|
442
621
|
}
|
|
443
622
|
modelSafeMessages() {
|
|
444
|
-
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
|
+
});
|
|
445
634
|
}
|
|
446
635
|
load() {
|
|
447
636
|
if (!this.persist || !existsSync(this.contextFilePath))
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface ChatSessionOptions {
|
|
|
55
55
|
sessionId?: string;
|
|
56
56
|
/** Compact older context when the rough token estimate exceeds this value. */
|
|
57
57
|
compactAtTokens?: number;
|
|
58
|
+
/** Independent retained-tool budget; defaults to min(64K, compaction threshold x 25%). */
|
|
59
|
+
maxToolContextTokens?: number;
|
|
58
60
|
/**
|
|
59
61
|
* 模型上下文窗口(token),默认 1048576(1M)。压缩阈值自动 = contextWindow × 0.8;
|
|
60
62
|
* 显式 compactAtTokens 优先于该派生值。
|
package/dist/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -353,6 +355,7 @@ export class ChatSession {
|
|
|
353
355
|
cwd: this.cwd,
|
|
354
356
|
contextWindow: options.contextWindow ?? appConfig.contextWindow,
|
|
355
357
|
compactAtTokens: options.compactAtTokens,
|
|
358
|
+
maxToolContextTokens: options.maxToolContextTokens,
|
|
356
359
|
keepRecentMessages: options.keepRecentMessages,
|
|
357
360
|
});
|
|
358
361
|
this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
|
|
@@ -559,7 +562,7 @@ export class ChatSession {
|
|
|
559
562
|
}
|
|
560
563
|
}
|
|
561
564
|
if (hasMalformedToolProtocolText(fullText)) {
|
|
562
|
-
console.warn(`[DeepCCC] malformed
|
|
565
|
+
console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
|
|
563
566
|
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
564
567
|
rawLog?.writeLine(safeRawStreamJson({
|
|
565
568
|
type: "deepccc_tool_protocol_recovery",
|
|
@@ -572,8 +575,8 @@ export class ChatSession {
|
|
|
572
575
|
continue;
|
|
573
576
|
}
|
|
574
577
|
throw new Error(toolCallOrder.length > 0
|
|
575
|
-
? "
|
|
576
|
-
: "
|
|
578
|
+
? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
|
|
579
|
+
: "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
|
|
577
580
|
}
|
|
578
581
|
completed = true;
|
|
579
582
|
const collectedToolCalls = toolCallOrder
|
package/dist/tool-protocol.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a model copying DeepCCC's persisted/debug transcript syntax into
|
|
3
|
+
* normal assistant text. Both the dedicated header and an event line are
|
|
4
|
+
* required so documentation and ordinary discussion remain valid.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hasImitatedToolTranscriptText(text: string): boolean;
|
|
1
7
|
export declare function hasMalformedToolProtocolText(text: string): boolean;
|
|
2
8
|
export declare const TOOL_PROTOCOL_RECOVERY_PROMPT: string;
|
package/dist/tool-protocol.js
CHANGED
|
@@ -6,12 +6,23 @@
|
|
|
6
6
|
const MALFORMED_DSML_TRAILER = /<\/[||]{2}DSML[||]{2}(?:parameter|invoke)>\s*$/iu;
|
|
7
7
|
const RENDERED_TOOL_CALL = /\[调用\s+[A-Za-z_][\w.-]*\]/u;
|
|
8
8
|
const DSML_INVOKE_TAG = /<[||]{2}DSML[||]{2}invoke\b/iu;
|
|
9
|
+
const TOOL_TRANSCRIPT_HEADER = /(?:^|\n)\s*\[工具记录\]\s*(?:\n|$)/u;
|
|
10
|
+
const TOOL_TRANSCRIPT_EVENT = /(?:^|\n)\s*tool_(?:call|result|error)\s+[A-Za-z_][\w.-]*\s*:/u;
|
|
11
|
+
/**
|
|
12
|
+
* Detects a model copying DeepCCC's persisted/debug transcript syntax into
|
|
13
|
+
* normal assistant text. Both the dedicated header and an event line are
|
|
14
|
+
* required so documentation and ordinary discussion remain valid.
|
|
15
|
+
*/
|
|
16
|
+
export function hasImitatedToolTranscriptText(text) {
|
|
17
|
+
return TOOL_TRANSCRIPT_HEADER.test(text) && TOOL_TRANSCRIPT_EVENT.test(text);
|
|
18
|
+
}
|
|
9
19
|
export function hasMalformedToolProtocolText(text) {
|
|
10
|
-
|
|
20
|
+
const malformedDsml = MALFORMED_DSML_TRAILER.test(text)
|
|
11
21
|
&& (RENDERED_TOOL_CALL.test(text) || DSML_INVOKE_TAG.test(text));
|
|
22
|
+
return malformedDsml || hasImitatedToolTranscriptText(text);
|
|
12
23
|
}
|
|
13
24
|
export const TOOL_PROTOCOL_RECOVERY_PROMPT = [
|
|
14
25
|
"[系统恢复提示] 上一次响应泄漏了内部工具调用协议,因此已被丢弃。",
|
|
15
|
-
"请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML 或工具参数作为普通文本输出。",
|
|
16
|
-
"
|
|
26
|
+
"请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML、[工具调用]、[工具记录] 或工具参数作为普通文本输出。",
|
|
27
|
+
"不要提及本恢复提示,不要伪造工具已执行,也不要根据未执行的命令声称得到了结果。",
|
|
17
28
|
].join("\n");
|