deepccc 0.2.7 → 0.2.9
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 +5 -4
- package/dist/context.d.ts +31 -11
- package/dist/context.js +276 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +45 -15
- package/dist/tool-protocol.d.ts +6 -0
- package/dist/tool-protocol.js +14 -3
- package/dist/web-page.js +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,7 +155,7 @@ deepccc
|
|
|
155
155
|
|
|
156
156
|
Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
|
|
157
157
|
|
|
158
|
-
模型文本、reasoning 心跳和工具事件通过 SSE
|
|
158
|
+
模型文本、reasoning 心跳和工具事件通过 SSE 实时更新。每轮消息按真实发生顺序持久化和回放,因此刷新后仍会保持“阶段说明 → 工具调用 → 后续结论”的交错时间线;旧版会话没有顺序数据时,会兼容显示为“工具调用 → 最终回答”。每次工具调用与对应结果合并成一张卡片,折叠态显示 emoji、工具名、状态和关键参数;展开后调用参数默认保留前 8/后 4 行,工具结果保留前 12/后 6 行,省略内容可继续展开。工具卡及省略行的展开状态保存在当前浏览器标签页的 `sessionStorage`,持续生成、切换会话和刷新页面均不会自动收起。消息正文支持标题、表格、列表、引用、链接和代码块等常用 Markdown。
|
|
159
159
|
|
|
160
160
|
图片始终按本地附件处理,不转换为 Provider 原生多模态消息。Web 支持文件选择、剪贴板粘贴和拖拽,每条消息最多 10 张 PNG/JPEG/WebP、单张最大 20 MB;附件复制到 `~/.deepccc/attachments/<session-id>/`,Agent 收到本地绝对路径后使用可用工具自行处理。Agent 可调用 `present_file` 把当前工作目录或会话附件目录中的图片直接展示在会话中;删除会话时对应附件一并清理。
|
|
161
161
|
|
|
@@ -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 在流式运行与持久化消息之间保持同一张工具卡状态。 */
|
|
@@ -13,11 +14,28 @@ export interface BuiltinContextToolCall {
|
|
|
13
14
|
output?: string;
|
|
14
15
|
is_error?: boolean;
|
|
15
16
|
}
|
|
17
|
+
export type BuiltinContextTimelineEntry = {
|
|
18
|
+
type: "text";
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "tool_use";
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
input?: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "tool_result";
|
|
27
|
+
tool_use_id: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
output?: string;
|
|
30
|
+
is_error?: boolean;
|
|
31
|
+
};
|
|
16
32
|
export interface BuiltinContextMessage {
|
|
17
33
|
role: BuiltinContextRole;
|
|
18
34
|
content: string;
|
|
19
35
|
/** 可选结构化工具调用记录(assistant 消息专用) */
|
|
20
36
|
toolCalls?: BuiltinContextToolCall[];
|
|
37
|
+
/** 文本与工具事件的真实发生顺序,同时用于 UI 与模型的结构化历史重放。 */
|
|
38
|
+
timeline?: BuiltinContextTimelineEntry[];
|
|
21
39
|
}
|
|
22
40
|
export interface BuiltinContextState {
|
|
23
41
|
version: 1;
|
|
@@ -54,6 +72,8 @@ export interface BuiltinContextOptions {
|
|
|
54
72
|
contextWindow?: number;
|
|
55
73
|
/** 直接指定压缩阈值(token),优先于 contextWindow 的 80% 派生。 */
|
|
56
74
|
compactAtTokens?: number;
|
|
75
|
+
/** Independent budget for retained structured tool inputs/results. */
|
|
76
|
+
maxToolContextTokens?: number;
|
|
57
77
|
keepRecentMessages?: number;
|
|
58
78
|
}
|
|
59
79
|
export declare const DEFAULT_BUILTIN_CONTEXT_DIR: string;
|
|
@@ -64,6 +84,7 @@ export declare const COMPACTION_THRESHOLD_RATIO = 0.8;
|
|
|
64
84
|
/** 默认压缩阈值 = 1M × 0.8。 */
|
|
65
85
|
export declare const DEFAULT_COMPACT_AT_TOKENS: number;
|
|
66
86
|
export declare const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
87
|
+
export declare const DEFAULT_MAX_TOOL_CONTEXT_TOKENS = 64000;
|
|
67
88
|
export declare function normalizeBuiltinSessionId(value: string): string;
|
|
68
89
|
export declare function defaultBuiltinSessionId(cwd?: string): string;
|
|
69
90
|
export declare function newBuiltinSessionId(now?: Date, suffix?: string): string;
|
|
@@ -71,25 +92,23 @@ export declare function getBuiltinContextSession(sessionId: string, contextDir?:
|
|
|
71
92
|
export declare function readBuiltinContextState(sessionId: string, contextDir?: string): BuiltinContextState | null;
|
|
72
93
|
export declare function listBuiltinContextSessions(contextDir?: string): BuiltinContextSessionInfo[];
|
|
73
94
|
export declare function latestBuiltinSessionForCwd(cwd: string, contextDir?: string): BuiltinContextSessionInfo | null;
|
|
74
|
-
|
|
75
|
-
* 估算上下文的 token 数(用于决定何时压缩)。
|
|
76
|
-
* 按字符类型加权,比旧版 chars/3 更接近真实:CJK 字符 ≈ 1 token/字,
|
|
77
|
-
* 其他字符 ≈ 3.5 chars/token。避免中文长上下文被严重低估导致压缩过晚。
|
|
78
|
-
*/
|
|
95
|
+
export declare function estimateBuiltinToolContextTokens(messages: readonly BuiltinContextMessage[]): number;
|
|
79
96
|
export declare function estimateBuiltinContextTokens(summary: string, messages: readonly BuiltinContextMessage[]): number;
|
|
80
97
|
export declare function serializeMessagesForSummary(messages: readonly BuiltinContextMessage[]): string;
|
|
81
98
|
export declare const DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS = 32000;
|
|
82
99
|
export declare const DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24000;
|
|
83
100
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
101
|
+
* 构造持久化 assistant 消息。content 仅保存回答正文,工具事件保存在
|
|
102
|
+
* toolCalls/timeline,避免内部 transcript 语法回流并诱导模型伪造工具执行。
|
|
86
103
|
*/
|
|
87
104
|
export declare function buildPersistedAssistantMessage(params: {
|
|
88
105
|
fullText: string;
|
|
89
|
-
/**
|
|
106
|
+
/** @deprecated 兼容旧调用方;工具记录只从 toolCalls/timeline 持久化。 */
|
|
90
107
|
transcriptLines: readonly string[];
|
|
91
108
|
/** 结构化工具调用(按调用顺序);有则写入消息的 toolCalls 字段 */
|
|
92
109
|
toolCalls?: readonly BuiltinContextToolCall[];
|
|
110
|
+
/** 文本和工具事件的有序时间线;不参与模型输入。 */
|
|
111
|
+
timeline?: readonly BuiltinContextTimelineEntry[];
|
|
93
112
|
maxAssistantChars?: number;
|
|
94
113
|
maxTranscriptChars?: number;
|
|
95
114
|
}): BuiltinContextMessage;
|
|
@@ -99,6 +118,7 @@ export declare class BuiltinContextManager {
|
|
|
99
118
|
readonly contextDir: string;
|
|
100
119
|
readonly sessionId: string;
|
|
101
120
|
readonly compactAtTokens: number;
|
|
121
|
+
readonly maxToolContextTokens: number;
|
|
102
122
|
readonly keepRecentMessages: number;
|
|
103
123
|
private readonly cwd?;
|
|
104
124
|
private state;
|
|
@@ -109,7 +129,7 @@ export declare class BuiltinContextManager {
|
|
|
109
129
|
get contextFilePath(): string;
|
|
110
130
|
appendMessage(message: BuiltinContextMessage): void;
|
|
111
131
|
setSummary(summary: string): void;
|
|
112
|
-
buildModelMessages():
|
|
132
|
+
buildModelMessages(): ModelMessage[];
|
|
113
133
|
planCompaction(): BuiltinCompactionPlan | null;
|
|
114
134
|
applyCompaction(summary: string, plan: BuiltinCompactionPlan): void;
|
|
115
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
|
}
|
|
@@ -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 [];
|
|
@@ -175,8 +214,11 @@ export function latestBuiltinSessionForCwd(cwd, contextDir = DEFAULT_BUILTIN_CON
|
|
|
175
214
|
* 按字符类型加权,比旧版 chars/3 更接近真实:CJK 字符 ≈ 1 token/字,
|
|
176
215
|
* 其他字符 ≈ 3.5 chars/token。避免中文长上下文被严重低估导致压缩过晚。
|
|
177
216
|
*/
|
|
178
|
-
|
|
179
|
-
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) {
|
|
180
222
|
let cjk = 0;
|
|
181
223
|
for (const ch of text) {
|
|
182
224
|
if (/[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/.test(ch))
|
|
@@ -185,6 +227,30 @@ export function estimateBuiltinContextTokens(summary, messages) {
|
|
|
185
227
|
const other = text.length - cjk;
|
|
186
228
|
return Math.ceil(cjk + other / 3.5);
|
|
187
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
|
+
}
|
|
188
254
|
export function serializeMessagesForSummary(messages) {
|
|
189
255
|
return messages
|
|
190
256
|
.map((message, index) => `### ${index + 1}. ${message.role}\n${message.content}`)
|
|
@@ -202,23 +268,196 @@ export const DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS = 32_000;
|
|
|
202
268
|
export const DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24_000;
|
|
203
269
|
const ASSISTANT_TRUNCATED_MARKER = "...[助手回复已在上下文中截断]...";
|
|
204
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
|
+
}
|
|
205
289
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
290
|
+
* 构造持久化 assistant 消息。content 仅保存回答正文,工具事件保存在
|
|
291
|
+
* toolCalls/timeline,避免内部 transcript 语法回流并诱导模型伪造工具执行。
|
|
208
292
|
*/
|
|
209
293
|
export function buildPersistedAssistantMessage(params) {
|
|
210
294
|
const maxAssistantChars = params.maxAssistantChars ?? DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS;
|
|
211
295
|
const maxTranscriptChars = params.maxTranscriptChars ?? DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS;
|
|
212
296
|
const persistedAssistantText = truncateMiddle(params.fullText, maxAssistantChars, ASSISTANT_TRUNCATED_MARKER);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
: persistedAssistantText;
|
|
216
|
-
const message = { role: "assistant", content };
|
|
217
|
-
const toolCalls = normalizeToolCalls(params.toolCalls);
|
|
297
|
+
const message = { role: "assistant", content: persistedAssistantText };
|
|
298
|
+
const toolCalls = truncateStructuredToolCalls(normalizeToolCalls(params.toolCalls), maxTranscriptChars);
|
|
218
299
|
if (toolCalls.length > 0)
|
|
219
300
|
message.toolCalls = toolCalls;
|
|
301
|
+
const timeline = truncatePersistedTimeline(normalizeTimeline(params.timeline));
|
|
302
|
+
if (timeline.length > 0)
|
|
303
|
+
message.timeline = timeline;
|
|
220
304
|
return message;
|
|
221
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 flushTools = () => {
|
|
406
|
+
for (const [toolCallId, toolName] of pendingCalls) {
|
|
407
|
+
toolParts.push({
|
|
408
|
+
type: "tool-result",
|
|
409
|
+
toolCallId,
|
|
410
|
+
toolName,
|
|
411
|
+
output: { type: "text", value: "[tool execution interrupted; result unavailable]" },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
pendingCalls.clear();
|
|
415
|
+
if (!toolParts.length)
|
|
416
|
+
return;
|
|
417
|
+
messages.push({ role: "tool", content: toolParts });
|
|
418
|
+
toolParts = [];
|
|
419
|
+
};
|
|
420
|
+
for (const entry of timeline) {
|
|
421
|
+
if (entry.type === "text") {
|
|
422
|
+
flushTools();
|
|
423
|
+
const previous = assistantParts[assistantParts.length - 1];
|
|
424
|
+
if (previous?.type === "text")
|
|
425
|
+
previous.text += entry.text;
|
|
426
|
+
else if (entry.text)
|
|
427
|
+
assistantParts.push({ type: "text", text: entry.text });
|
|
428
|
+
}
|
|
429
|
+
else if (entry.type === "tool_use") {
|
|
430
|
+
flushTools();
|
|
431
|
+
knownNames.set(entry.id, entry.name);
|
|
432
|
+
pendingCalls.set(entry.id, entry.name);
|
|
433
|
+
emittedCallIds.add(entry.id);
|
|
434
|
+
assistantParts.push({
|
|
435
|
+
type: "tool-call",
|
|
436
|
+
toolCallId: entry.id,
|
|
437
|
+
toolName: entry.name,
|
|
438
|
+
input: parseToolInput(entry.input),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
if (!emittedCallIds.has(entry.tool_use_id))
|
|
443
|
+
continue;
|
|
444
|
+
const toolName = entry.name ?? knownNames.get(entry.tool_use_id);
|
|
445
|
+
if (!toolName)
|
|
446
|
+
continue;
|
|
447
|
+
flushAssistant();
|
|
448
|
+
pendingCalls.delete(entry.tool_use_id);
|
|
449
|
+
toolParts.push({
|
|
450
|
+
type: "tool-result",
|
|
451
|
+
toolCallId: entry.tool_use_id,
|
|
452
|
+
toolName,
|
|
453
|
+
output: parseToolOutput(entry.output),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
flushAssistant();
|
|
458
|
+
flushTools();
|
|
459
|
+
return messages;
|
|
460
|
+
}
|
|
222
461
|
function serializeMessagesForCompaction(messages) {
|
|
223
462
|
const sections = [];
|
|
224
463
|
let remaining = MAX_COMPACTION_SOURCE_CHARS;
|
|
@@ -259,6 +498,7 @@ export class BuiltinContextManager {
|
|
|
259
498
|
contextDir;
|
|
260
499
|
sessionId;
|
|
261
500
|
compactAtTokens;
|
|
501
|
+
maxToolContextTokens;
|
|
262
502
|
keepRecentMessages;
|
|
263
503
|
cwd;
|
|
264
504
|
state;
|
|
@@ -269,6 +509,8 @@ export class BuiltinContextManager {
|
|
|
269
509
|
this.cwd = options.cwd;
|
|
270
510
|
this.compactAtTokens = options.compactAtTokens
|
|
271
511
|
?? Math.floor((options.contextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS) * COMPACTION_THRESHOLD_RATIO);
|
|
512
|
+
this.maxToolContextTokens = Math.max(1, options.maxToolContextTokens
|
|
513
|
+
?? Math.min(DEFAULT_MAX_TOOL_CONTEXT_TOKENS, Math.floor(this.compactAtTokens * TOOL_CONTEXT_BUDGET_RATIO)));
|
|
272
514
|
this.keepRecentMessages = Math.max(1, options.keepRecentMessages ?? DEFAULT_KEEP_RECENT_MESSAGES);
|
|
273
515
|
this.state = this.load();
|
|
274
516
|
}
|
|
@@ -308,7 +550,12 @@ export class BuiltinContextManager {
|
|
|
308
550
|
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
309
551
|
// from future prompts so one protocol failure cannot teach the model to
|
|
310
552
|
// repeat the same invalid tool syntax on every later turn.
|
|
311
|
-
|
|
553
|
+
for (const [index, message] of this.modelSafeMessages().entries()) {
|
|
554
|
+
if (message.role === "user")
|
|
555
|
+
messages.push({ role: "user", content: message.content });
|
|
556
|
+
else
|
|
557
|
+
messages.push(...replayStructuredAssistantMessage(message, index));
|
|
558
|
+
}
|
|
312
559
|
return messages;
|
|
313
560
|
}
|
|
314
561
|
planCompaction() {
|
|
@@ -316,16 +563,19 @@ export class BuiltinContextManager {
|
|
|
316
563
|
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
317
564
|
const messages = this.modelSafeMessages();
|
|
318
565
|
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
319
|
-
|
|
566
|
+
const toolEstimated = estimateBuiltinToolContextTokens(messages);
|
|
567
|
+
if (estimated <= this.compactAtTokens && toolEstimated <= this.maxToolContextTokens)
|
|
320
568
|
return null;
|
|
321
569
|
if (messages.length <= 1)
|
|
322
570
|
return null;
|
|
323
571
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
324
572
|
const recentTokenBudget = Math.max(1, Math.floor(this.compactAtTokens * RECENT_CONTEXT_BUDGET_RATIO));
|
|
573
|
+
const recentToolBudget = Math.max(1, Math.floor(this.maxToolContextTokens * RECENT_TOOL_CONTEXT_BUDGET_RATIO));
|
|
325
574
|
let splitAt = messages.length - 1;
|
|
326
575
|
for (let index = messages.length - 2; index >= earliestAllowed; index -= 1) {
|
|
327
576
|
const candidate = messages.slice(index);
|
|
328
|
-
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget
|
|
577
|
+
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget
|
|
578
|
+
|| estimateBuiltinToolContextTokens(candidate) > recentToolBudget)
|
|
329
579
|
break;
|
|
330
580
|
splitAt = index;
|
|
331
581
|
}
|
|
@@ -336,8 +586,8 @@ export class BuiltinContextManager {
|
|
|
336
586
|
splitAt = 1;
|
|
337
587
|
return {
|
|
338
588
|
previousSummary: this.state.summary,
|
|
339
|
-
oldMessages:
|
|
340
|
-
recentMessages:
|
|
589
|
+
oldMessages: messages.slice(0, splitAt),
|
|
590
|
+
recentMessages: messages.slice(splitAt),
|
|
341
591
|
};
|
|
342
592
|
}
|
|
343
593
|
applyCompaction(summary, plan) {
|
|
@@ -361,7 +611,17 @@ export class BuiltinContextManager {
|
|
|
361
611
|
renameSync(tmp, this.contextFilePath);
|
|
362
612
|
}
|
|
363
613
|
modelSafeMessages() {
|
|
364
|
-
return this.state.messages.
|
|
614
|
+
return this.state.messages.map((message) => {
|
|
615
|
+
if (message.role !== "assistant")
|
|
616
|
+
return message;
|
|
617
|
+
const hasStructuredTools = Boolean(message.toolCalls?.length
|
|
618
|
+
|| message.timeline?.some((entry) => entry.type === "tool_use" || entry.type === "tool_result"));
|
|
619
|
+
const copiedStoredTranscript = hasImitatedToolTranscriptText(message.content) && hasStructuredTools;
|
|
620
|
+
if (hasMalformedToolProtocolText(message.content) && !copiedStoredTranscript) {
|
|
621
|
+
return { role: "assistant", content: QUARANTINED_PROTOCOL_REPLY };
|
|
622
|
+
}
|
|
623
|
+
return { ...message, content: stripStoredToolTranscript(message.content) };
|
|
624
|
+
});
|
|
365
625
|
}
|
|
366
626
|
load() {
|
|
367
627
|
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);
|
|
@@ -393,6 +396,8 @@ export class ChatSession {
|
|
|
393
396
|
// assistant 消息 toolCalls 字段;[Tool transcript] 文本视图仍按原格式生成。
|
|
394
397
|
const toolCallsById = new Map();
|
|
395
398
|
const toolCallOrder = [];
|
|
399
|
+
const timeline = [];
|
|
400
|
+
let toolContext = [];
|
|
396
401
|
try {
|
|
397
402
|
if (this.context.planCompaction()) {
|
|
398
403
|
yield { type: "status", phase: "compacting" };
|
|
@@ -460,9 +465,10 @@ export class ChatSession {
|
|
|
460
465
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
461
466
|
fullText = "";
|
|
462
467
|
safeAccumulated = "";
|
|
463
|
-
|
|
468
|
+
toolContext = [];
|
|
464
469
|
toolCallsById.clear();
|
|
465
470
|
toolCallOrder.length = 0;
|
|
471
|
+
timeline.length = 0;
|
|
466
472
|
let lastReasoningProgressAt;
|
|
467
473
|
const attemptMessages = attempt === 0
|
|
468
474
|
? modelMessages
|
|
@@ -493,6 +499,11 @@ export class ChatSession {
|
|
|
493
499
|
}
|
|
494
500
|
else if (part.type === "text-delta") {
|
|
495
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 });
|
|
496
507
|
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
497
508
|
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
498
509
|
const safeText = applyPrivacy(part.text);
|
|
@@ -500,9 +511,11 @@ export class ChatSession {
|
|
|
500
511
|
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
501
512
|
}
|
|
502
513
|
else if (part.type === "tool-call") {
|
|
503
|
-
|
|
504
|
-
|
|
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 });
|
|
505
517
|
toolCallOrder.push(part.toolCallId);
|
|
518
|
+
timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
|
|
506
519
|
yield {
|
|
507
520
|
type: "tool_use",
|
|
508
521
|
id: part.toolCallId,
|
|
@@ -511,10 +524,12 @@ export class ChatSession {
|
|
|
511
524
|
};
|
|
512
525
|
}
|
|
513
526
|
else if (part.type === "tool-result") {
|
|
514
|
-
|
|
527
|
+
const output = truncateToolContext(safeJson(part.output));
|
|
528
|
+
toolContext.push(`tool_result ${part.toolName}: ${output}`);
|
|
515
529
|
const call = toolCallsById.get(part.toolCallId);
|
|
516
530
|
if (call)
|
|
517
|
-
call.output =
|
|
531
|
+
call.output = output;
|
|
532
|
+
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
|
|
518
533
|
yield {
|
|
519
534
|
type: "tool_result",
|
|
520
535
|
tool_use_id: part.toolCallId,
|
|
@@ -531,6 +546,7 @@ export class ChatSession {
|
|
|
531
546
|
call.output = message;
|
|
532
547
|
call.is_error = true;
|
|
533
548
|
}
|
|
549
|
+
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
|
|
534
550
|
yield {
|
|
535
551
|
type: "tool_result",
|
|
536
552
|
tool_use_id: part.toolCallId,
|
|
@@ -546,7 +562,7 @@ export class ChatSession {
|
|
|
546
562
|
}
|
|
547
563
|
}
|
|
548
564
|
if (hasMalformedToolProtocolText(fullText)) {
|
|
549
|
-
console.warn(`[DeepCCC] malformed
|
|
565
|
+
console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
|
|
550
566
|
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
551
567
|
rawLog?.writeLine(safeRawStreamJson({
|
|
552
568
|
type: "deepccc_tool_protocol_recovery",
|
|
@@ -559,8 +575,8 @@ export class ChatSession {
|
|
|
559
575
|
continue;
|
|
560
576
|
}
|
|
561
577
|
throw new Error(toolCallOrder.length > 0
|
|
562
|
-
? "
|
|
563
|
-
: "
|
|
578
|
+
? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
|
|
579
|
+
: "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
|
|
564
580
|
}
|
|
565
581
|
completed = true;
|
|
566
582
|
const collectedToolCalls = toolCallOrder
|
|
@@ -570,6 +586,7 @@ export class ChatSession {
|
|
|
570
586
|
fullText,
|
|
571
587
|
transcriptLines: toolContext,
|
|
572
588
|
toolCalls: collectedToolCalls,
|
|
589
|
+
timeline,
|
|
573
590
|
}));
|
|
574
591
|
yield { type: "done", text: safeAccumulated };
|
|
575
592
|
return;
|
|
@@ -581,9 +598,22 @@ export class ChatSession {
|
|
|
581
598
|
if (malformedProtocolOutput)
|
|
582
599
|
yield { type: "text_reset" };
|
|
583
600
|
if (err.name === "AbortError" || signal?.aborted) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
}));
|
|
587
617
|
}
|
|
588
618
|
yield { type: "done", text: safeAccumulated };
|
|
589
619
|
return;
|
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");
|
package/dist/web-page.js
CHANGED
|
@@ -325,7 +325,10 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
|
|
|
325
325
|
function setToolState(key,value){try{sessionStorage.setItem(toolStorageKey(key),value?'1':'0');}catch(error){}}
|
|
326
326
|
function clearToolSessionState(sessionId){try{var prefix='deepccc:tool-ui:v1:'+sessionId+':';for(var index=sessionStorage.length-1;index>=0;index--){var key=sessionStorage.key(index);if(key&&key.indexOf(prefix)===0)sessionStorage.removeItem(key);}}catch(error){}}
|
|
327
327
|
function toolSectionHtml(call,kind,value,headLines,tailLines){if(value===undefined)return'';var result=truncateToolPayload(value,headLines,tailLines);var stateKey=call.uiKey+':'+kind+'-full';var expanded=toolState(stateKey);var text=expanded?result.full:result.preview;var label=kind==='input'?'调用参数':'工具结果';var button=result.truncated?'<button type="button" class="tool-overflow-toggle" data-tool-overflow="'+escapeHtml(stateKey)+'">'+escapeHtml(expanded?'收起省略内容':(result.omittedLines>0?'展开省略的 '+result.omittedLines+' 行':'展开完整内容'))+'</button>':'';return'<section class="tool-section"><div class="tool-section-title">'+label+'</div><pre>'+escapeHtml(text)+'</pre>'+button+'</section>';}
|
|
328
|
-
function
|
|
328
|
+
function timelineHtml(timeline,messageIndex){var results={};(timeline||[]).forEach(function(item){if(item&&item.type==='tool_result'&&item.tool_use_id)results[item.tool_use_id]=item;});var renderedCalls={};var html='';(timeline||[]).forEach(function(item,index){if(!item)return;if(item.type==='text'){if(item.text)html+='<div class="markdown">'+markdown(item.text)+'</div>';return;}if(item.type==='tool_use'){var id=item.id||('timeline-'+messageIndex+'-'+index);var result=results[id];renderedCalls[id]=true;html+=toolCardHtml({uiKey:'tool:'+id,name:item.name||'tool',input:item.input,output:result?result.output:undefined,isError:!!(result&&result.is_error),pending:!result});return;}if(item.type==='tool_result'&&!renderedCalls[item.tool_use_id]){var fallbackId=item.tool_use_id||('result-'+messageIndex+'-'+index);renderedCalls[fallbackId]=true;html+=toolCardHtml({uiKey:'tool:'+fallbackId,name:item.name||'tool',output:item.output,isError:!!item.is_error,pending:false});}});return html;}
|
|
329
|
+
function liveTimeline(events){var timeline=[];(events||[]).forEach(function(event){if(event.type!=='agent'||!event.data)return;var item=event.data;if(item.type==='text_reset'){timeline.length=0;return;}if(item.type==='text'){var previous=timeline[timeline.length-1];if(previous&&previous.type==='text')previous.text+=(item.text||'');else timeline.push({type:'text',text:item.text||''});return;}if(item.type==='tool_use')timeline.push({type:'tool_use',id:item.id,name:item.name,input:item.input});else if(item.type==='tool_result')timeline.push({type:'tool_result',tool_use_id:item.tool_use_id,name:item.name,output:item.content,is_error:!!item.is_error});});return timeline;}
|
|
330
|
+
function legacyToolsHtml(toolCalls,messageIndex){var tools='';(toolCalls||[]).forEach(function(call,index){tools+=toolCardHtml({uiKey:call.id?'tool:'+call.id:'message:'+messageIndex+':tool:'+index,name:call.name,input:call.input,output:call.output,isError:!!call.is_error,pending:false});});return tools;}
|
|
331
|
+
function messageHtml(role,content,toolCalls,messageIndex,timeline){var decoded=role==='user'?decodeAttachmentMessage(content):{text:withoutToolTranscript(content),attachments:[]};var body;if(role==='user')body='<div class="markdown">'+markdown(decoded.text)+'</div>'+attachmentGallery(decoded.attachments,state.detail.sessionId);else if(timeline&&timeline.length)body=timelineHtml(timeline,messageIndex);else body=legacyToolsHtml(toolCalls,messageIndex)+'<div class="markdown">'+markdown(decoded.text)+'</div>';return '<article class="message '+role+'"><div class="avatar">'+(role==='user'?'你':'D')+'</div><div class="bubble"><div class="role">'+(role==='user'?'YOU':'DEEPCCC')+'</div>'+body+'</div></article>';}
|
|
329
332
|
function buildToolGroups(events){var groups=[];var byId={};var order=[];(events||[]).forEach(function(event){if(event.type!=='agent'||!event.data)return;var item=event.data;if(item.type==='tool_use'){var id=item.id||('call-'+order.length);var group={id:id,uiKey:'tool:'+id,name:item.name||'tool',input:item.input,output:undefined,isError:false,pending:true};byId[id]=group;order.push(id);groups.push(group);}else if(item.type==='tool_result'){var key=item.tool_use_id;var target=key?byId[key]:null;if(target){target.output=item.content;target.isError=!!item.is_error;target.pending=false;}}});return groups;}
|
|
330
333
|
function toolCardHtml(call){var open=toolState(call.uiKey+':card');var present=call.name==='present_file'&&!call.pending?artifactFromOutput(call.output):null;var inputHtml=toolSectionHtml(call,'input',call.input,8,4);var outputHtml=call.pending?'':(present?'<section class="tool-section"><div class="tool-section-title">工具结果</div>'+artifactGallery([{name:'present_file',output:call.output}],state.detail.sessionId)+'</section>':toolSectionHtml(call,'output',call.output==null?'(无输出)':call.output,12,6));return'<details class="tool-card" data-tool-state="'+escapeHtml(call.uiKey+':card')+'"'+(open?' open':'')+'><summary><span class="tool-summary-text">'+escapeHtml(toolSummary(call))+'</span></summary>'+inputHtml+outputHtml+'</details>';}
|
|
331
334
|
function toolCardsHtml(calls){return (calls||[]).map(toolCardHtml).join('');}
|
|
@@ -356,11 +359,10 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
|
|
|
356
359
|
renderAttachmentDrafts();
|
|
357
360
|
var html='<div class="thread">';
|
|
358
361
|
if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class="empty-thread"><strong>准备开始这个会话</strong><span>在下方输入任务,可附加截图。DeepCCC 会读取当前项目指令、调用工具并持续保存上下文。</span></div>';
|
|
359
|
-
(detail.messages||[]).forEach(function(message,index){html+=messageHtml(message.role,message.content,message.toolCalls,index);});
|
|
362
|
+
(detail.messages||[]).forEach(function(message,index){html+=messageHtml(message.role,message.content,message.toolCalls,index,message.timeline);});
|
|
360
363
|
if(detail.status==='running'){
|
|
361
|
-
var
|
|
362
|
-
if(
|
|
363
|
-
html+=toolCardsHtml(buildToolGroups(detail.events));
|
|
364
|
+
var currentTimeline=liveTimeline(detail.events);
|
|
365
|
+
if(currentTimeline.length)html+=messageHtml('assistant','',[],'live',currentTimeline);
|
|
364
366
|
}
|
|
365
367
|
var approvals=(detail.approvals||[]).slice();
|
|
366
368
|
if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));
|