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 CHANGED
@@ -111,5 +111,6 @@ export declare class BuiltinContextManager {
111
111
  applyCompaction(summary: string, plan: BuiltinCompactionPlan): void;
112
112
  reset(): void;
113
113
  save(): void;
114
+ private modelSafeMessages;
114
115
  private load;
115
116
  }
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
- messages.push(...this.state.messages);
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
- const estimated = estimateBuiltinContextTokens(this.state.summary, this.state.messages);
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 generationOptions = {
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 stream;
439
- if (appConfig.streaming) {
440
- const result = streamText(generationOptions);
441
- stream = result.fullStream ?? textStreamToFullStream(result.textStream);
442
- }
443
- else {
444
- const result = await generateText(generationOptions);
445
- stream = generateResultToFullStream(result);
446
- }
447
- for await (const part of stream) {
448
- rawLog?.writeLine(safeRawStreamJson(part));
449
- if (part.type === "text-delta") {
450
- fullText += part.text;
451
- // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
452
- // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
453
- const safeText = applyPrivacy(part.text);
454
- safeAccumulated += safeText;
455
- yield { type: "text", text: safeText, accumulated: safeAccumulated };
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 if (part.type === "tool-call") {
458
- toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
459
- toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
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
- else if (part.type === "tool-result") {
469
- toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
470
- const call = toolCallsById.get(part.toolCallId);
471
- if (call) {
472
- call.output = truncateToolContext(safeJson(part.output));
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
- yield {
475
- type: "tool_result",
476
- tool_use_id: part.toolCallId,
477
- name: part.toolName,
478
- content: applyPrivacyToJson(part.output),
479
- is_error: false,
480
- };
481
- }
482
- else if (part.type === "tool-error") {
483
- const message = errorMessage(part.error);
484
- toolContext.push(`tool_error ${part.toolName}: ${message}`);
485
- const call = toolCallsById.get(part.toolCallId);
486
- if (call) {
487
- call.output = message;
488
- call.is_error = true;
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
- else if (part.type === "error") {
499
- const message = errorMessage(part.error);
500
- yield { type: "error", message: applyPrivacy(message) };
501
- throw new Error(message);
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 };
@@ -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,2 @@
1
+ export declare function hasMalformedToolProtocolText(text: string): boolean;
2
+ export declare const TOOL_PROTOCOL_RECOVERY_PROMPT: string;
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [