deepccc 0.1.26 → 0.1.27
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/dist/context.d.ts +1 -0
- package/dist/context.js +12 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +116 -73
- package/dist/progress/reducer.js +4 -0
- package/dist/tool-protocol.d.ts +2 -0
- package/dist/tool-protocol.js +17 -0
- package/package.json +1 -1
package/dist/context.d.ts
CHANGED
package/dist/context.js
CHANGED
|
@@ -2,6 +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
6
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
6
7
|
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
7
8
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
@@ -290,14 +291,19 @@ export class BuiltinContextManager {
|
|
|
290
291
|
].join("\n"),
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
+
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
295
|
+
// from future prompts so one protocol failure cannot teach the model to
|
|
296
|
+
// repeat the same invalid tool syntax on every later turn.
|
|
297
|
+
messages.push(...this.modelSafeMessages());
|
|
294
298
|
return messages;
|
|
295
299
|
}
|
|
296
300
|
planCompaction() {
|
|
297
|
-
|
|
301
|
+
// The compaction model is still a model input: exclude quarantined protocol
|
|
302
|
+
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
303
|
+
const messages = this.modelSafeMessages();
|
|
304
|
+
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
298
305
|
if (estimated <= this.compactAtTokens)
|
|
299
306
|
return null;
|
|
300
|
-
const messages = this.state.messages;
|
|
301
307
|
if (messages.length <= 1)
|
|
302
308
|
return null;
|
|
303
309
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
@@ -340,6 +346,9 @@ export class BuiltinContextManager {
|
|
|
340
346
|
writeFileSync(tmp, content, "utf8");
|
|
341
347
|
renameSync(tmp, this.contextFilePath);
|
|
342
348
|
}
|
|
349
|
+
modelSafeMessages() {
|
|
350
|
+
return this.state.messages.filter((message) => message.role !== "assistant" || !hasMalformedToolProtocolText(message.content));
|
|
351
|
+
}
|
|
343
352
|
load() {
|
|
344
353
|
if (!this.persist || !existsSync(this.contextFilePath))
|
|
345
354
|
return emptyState(this.sessionId, this.cwd);
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,9 @@ export interface ChatSessionOptions {
|
|
|
85
85
|
export type ChatEvent = {
|
|
86
86
|
type: "status";
|
|
87
87
|
phase: "compacting" | "generating";
|
|
88
|
+
} | {
|
|
89
|
+
type: "progress";
|
|
90
|
+
phase: "reasoning";
|
|
88
91
|
} | {
|
|
89
92
|
type: "compact";
|
|
90
93
|
compactedMessages: number;
|
|
@@ -103,6 +106,8 @@ export type ChatEvent = {
|
|
|
103
106
|
type: "text";
|
|
104
107
|
text: string;
|
|
105
108
|
accumulated: string;
|
|
109
|
+
} | {
|
|
110
|
+
type: "text_reset";
|
|
106
111
|
} | {
|
|
107
112
|
type: "done";
|
|
108
113
|
text: string;
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createRawStreamLog, } from "./raw-stream-log.js";
|
|
|
15
15
|
import { buildPersistedAssistantMessage, buildSummaryPrompt, BuiltinContextManager, defaultBuiltinSessionId, } from "./context.js";
|
|
16
16
|
import { createBuiltinFileTools, MAX_TASK_OUTPUT_CHARS } from "./file-tools.js";
|
|
17
17
|
import { PermissionGate } from "./permissions.js";
|
|
18
|
+
import { hasMalformedToolProtocolText, TOOL_PROTOCOL_RECOVERY_PROMPT, } from "./tool-protocol.js";
|
|
18
19
|
import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
|
|
19
20
|
import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -400,7 +401,6 @@ export class ChatSession {
|
|
|
400
401
|
catch (err) {
|
|
401
402
|
console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
|
|
402
403
|
}
|
|
403
|
-
const toolContext = [];
|
|
404
404
|
const maxSteps = this.maxSteps;
|
|
405
405
|
// 每次对话前重新扫描技能索引(并行 + mtime 缓存,开销极小):
|
|
406
406
|
// 新技能/修改的技能在下一次对话自动生效(热加载)。
|
|
@@ -423,10 +423,9 @@ export class ChatSession {
|
|
|
423
423
|
? { deepseek: { reasoningEffort: this.effort } }
|
|
424
424
|
: { anthropic: { effort: this.effort } };
|
|
425
425
|
}
|
|
426
|
-
const
|
|
426
|
+
const baseGenerationOptions = {
|
|
427
427
|
model: this.model,
|
|
428
428
|
system,
|
|
429
|
-
messages: modelMessages,
|
|
430
429
|
tools: createBuiltinFileTools(this.cwd, {
|
|
431
430
|
permissionGate: this.permissionGate,
|
|
432
431
|
runTask: this.runTask,
|
|
@@ -435,88 +434,132 @@ export class ChatSession {
|
|
|
435
434
|
abortSignal: signal,
|
|
436
435
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
437
436
|
};
|
|
438
|
-
let
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
437
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
438
|
+
fullText = "";
|
|
439
|
+
safeAccumulated = "";
|
|
440
|
+
const toolContext = [];
|
|
441
|
+
toolCallsById.clear();
|
|
442
|
+
toolCallOrder.length = 0;
|
|
443
|
+
let lastReasoningProgressAt;
|
|
444
|
+
const attemptMessages = attempt === 0
|
|
445
|
+
? modelMessages
|
|
446
|
+
: [...modelMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
|
|
447
|
+
const generationOptions = {
|
|
448
|
+
...baseGenerationOptions,
|
|
449
|
+
messages: attemptMessages,
|
|
450
|
+
};
|
|
451
|
+
let stream;
|
|
452
|
+
if (appConfig.streaming) {
|
|
453
|
+
const result = streamText(generationOptions);
|
|
454
|
+
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
456
455
|
}
|
|
457
|
-
else
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
toolCallOrder.push(part.toolCallId);
|
|
461
|
-
yield {
|
|
462
|
-
type: "tool_use",
|
|
463
|
-
id: part.toolCallId,
|
|
464
|
-
name: part.toolName,
|
|
465
|
-
input: applyPrivacyToJson(part.input),
|
|
466
|
-
};
|
|
456
|
+
else {
|
|
457
|
+
const result = await generateText(generationOptions);
|
|
458
|
+
stream = generateResultToFullStream(result);
|
|
467
459
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
for await (const part of stream) {
|
|
461
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
462
|
+
if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
|
|
463
|
+
// Reasoning content remains private. A throttled heartbeat is enough
|
|
464
|
+
// for ChatCCC to distinguish active inference from a stalled stream.
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
|
|
467
|
+
lastReasoningProgressAt = now;
|
|
468
|
+
yield { type: "progress", phase: "reasoning" };
|
|
469
|
+
}
|
|
473
470
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
471
|
+
else if (part.type === "text-delta") {
|
|
472
|
+
fullText += part.text;
|
|
473
|
+
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
474
|
+
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
475
|
+
const safeText = applyPrivacy(part.text);
|
|
476
|
+
safeAccumulated += safeText;
|
|
477
|
+
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
478
|
+
}
|
|
479
|
+
else if (part.type === "tool-call") {
|
|
480
|
+
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
481
|
+
toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
|
|
482
|
+
toolCallOrder.push(part.toolCallId);
|
|
483
|
+
yield {
|
|
484
|
+
type: "tool_use",
|
|
485
|
+
id: part.toolCallId,
|
|
486
|
+
name: part.toolName,
|
|
487
|
+
input: applyPrivacyToJson(part.input),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
else if (part.type === "tool-result") {
|
|
491
|
+
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
492
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
493
|
+
if (call)
|
|
494
|
+
call.output = truncateToolContext(safeJson(part.output));
|
|
495
|
+
yield {
|
|
496
|
+
type: "tool_result",
|
|
497
|
+
tool_use_id: part.toolCallId,
|
|
498
|
+
name: part.toolName,
|
|
499
|
+
content: applyPrivacyToJson(part.output),
|
|
500
|
+
is_error: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
else if (part.type === "tool-error") {
|
|
504
|
+
const message = errorMessage(part.error);
|
|
505
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
506
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
507
|
+
if (call) {
|
|
508
|
+
call.output = message;
|
|
509
|
+
call.is_error = true;
|
|
510
|
+
}
|
|
511
|
+
yield {
|
|
512
|
+
type: "tool_result",
|
|
513
|
+
tool_use_id: part.toolCallId,
|
|
514
|
+
name: part.toolName,
|
|
515
|
+
content: applyPrivacy(message),
|
|
516
|
+
is_error: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
else if (part.type === "error") {
|
|
520
|
+
const message = errorMessage(part.error);
|
|
521
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
522
|
+
throw new Error(message);
|
|
489
523
|
}
|
|
490
|
-
yield {
|
|
491
|
-
type: "tool_result",
|
|
492
|
-
tool_use_id: part.toolCallId,
|
|
493
|
-
name: part.toolName,
|
|
494
|
-
content: applyPrivacy(message),
|
|
495
|
-
is_error: true,
|
|
496
|
-
};
|
|
497
524
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
525
|
+
if (hasMalformedToolProtocolText(fullText)) {
|
|
526
|
+
console.warn(`[DeepCCC] malformed DSML tool output detected for ${this.context.sessionId} `
|
|
527
|
+
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
528
|
+
rawLog?.writeLine(safeRawStreamJson({
|
|
529
|
+
type: "deepccc_tool_protocol_recovery",
|
|
530
|
+
attempt: attempt + 1,
|
|
531
|
+
structuredToolCalls: toolCallOrder.length,
|
|
532
|
+
}));
|
|
533
|
+
yield { type: "text_reset" };
|
|
534
|
+
if (attempt === 0 && toolCallOrder.length === 0) {
|
|
535
|
+
yield { type: "status", phase: "generating" };
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
throw new Error(toolCallOrder.length > 0
|
|
539
|
+
? "工具调用协议异常:检测到混合的结构化与 DSML 文本调用,为避免重复执行工具,本轮已安全终止"
|
|
540
|
+
: "工具调用协议异常:模型重试后仍输出了无效的 DSML 工具调用");
|
|
502
541
|
}
|
|
542
|
+
completed = true;
|
|
543
|
+
const collectedToolCalls = toolCallOrder
|
|
544
|
+
.map((id) => toolCallsById.get(id))
|
|
545
|
+
.filter((call) => call !== undefined);
|
|
546
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
547
|
+
fullText,
|
|
548
|
+
transcriptLines: toolContext,
|
|
549
|
+
toolCalls: collectedToolCalls,
|
|
550
|
+
}));
|
|
551
|
+
yield { type: "done", text: safeAccumulated };
|
|
552
|
+
return;
|
|
503
553
|
}
|
|
504
|
-
completed = true;
|
|
505
|
-
const collectedToolCalls = toolCallOrder
|
|
506
|
-
.map((id) => toolCallsById.get(id))
|
|
507
|
-
.filter((call) => call !== undefined);
|
|
508
|
-
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
509
|
-
fullText,
|
|
510
|
-
transcriptLines: toolContext,
|
|
511
|
-
toolCalls: collectedToolCalls,
|
|
512
|
-
}));
|
|
513
|
-
yield { type: "done", text: safeAccumulated };
|
|
514
554
|
}
|
|
515
555
|
catch (err) {
|
|
516
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
557
|
+
const malformedProtocolOutput = hasMalformedToolProtocolText(fullText);
|
|
558
|
+
if (malformedProtocolOutput)
|
|
559
|
+
yield { type: "text_reset" };
|
|
517
560
|
if (err.name === "AbortError" || signal?.aborted) {
|
|
518
561
|
// 被中断时,不保存不完整的助手消息
|
|
519
|
-
if (fullText) {
|
|
562
|
+
if (fullText && !malformedProtocolOutput) {
|
|
520
563
|
this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
|
|
521
564
|
}
|
|
522
565
|
yield { type: "done", text: safeAccumulated };
|
package/dist/progress/reducer.js
CHANGED
|
@@ -57,6 +57,10 @@ export function reduceProgress(prev, event) {
|
|
|
57
57
|
case "text":
|
|
58
58
|
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
59
59
|
return withProgressView(prev, { text: event.accumulated });
|
|
60
|
+
case "progress":
|
|
61
|
+
return withProgressView(prev, { headerTitle: "思考中..." });
|
|
62
|
+
case "text_reset":
|
|
63
|
+
return withProgressView(prev, { text: "", tools: [] });
|
|
60
64
|
case "tool_use": {
|
|
61
65
|
const tool = {
|
|
62
66
|
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects provider output that leaked DeepSeek's internal DSML tool-call syntax
|
|
3
|
+
* into normal assistant text. A trailing closing tag is intentionally required:
|
|
4
|
+
* mentioning DSML in prose or a code sample must not trigger recovery.
|
|
5
|
+
*/
|
|
6
|
+
const MALFORMED_DSML_TRAILER = /<\/[||]{2}DSML[||]{2}(?:parameter|invoke)>\s*$/iu;
|
|
7
|
+
const RENDERED_TOOL_CALL = /\[调用\s+[A-Za-z_][\w.-]*\]/u;
|
|
8
|
+
const DSML_INVOKE_TAG = /<[||]{2}DSML[||]{2}invoke\b/iu;
|
|
9
|
+
export function hasMalformedToolProtocolText(text) {
|
|
10
|
+
return MALFORMED_DSML_TRAILER.test(text)
|
|
11
|
+
&& (RENDERED_TOOL_CALL.test(text) || DSML_INVOKE_TAG.test(text));
|
|
12
|
+
}
|
|
13
|
+
export const TOOL_PROTOCOL_RECOVERY_PROMPT = [
|
|
14
|
+
"[系统恢复提示] 上一次响应泄漏了内部工具调用协议,因此已被丢弃。",
|
|
15
|
+
"请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML 或工具参数作为普通文本输出。",
|
|
16
|
+
"不要提及本恢复提示,也不要假装工具已经执行。",
|
|
17
|
+
].join("\n");
|