chatccc 0.2.248 → 0.2.250
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/deepccc-agent/os-prompts/win32.md +2 -0
- package/deepccc-agent/package-lock.json +2 -2
- package/deepccc-agent/package.json +1 -1
- package/deepccc-agent/src/__tests__/chat-session.test.ts +36 -0
- package/deepccc-agent/src/__tests__/context.test.ts +22 -0
- package/deepccc-agent/src/context.ts +2 -2
- package/deepccc-agent/src/index.ts +25 -28
- package/package.json +1 -1
|
@@ -7,3 +7,5 @@ You are running on Windows. run_command executes through cmd.exe, not bash. cmd
|
|
|
7
7
|
- Multi-line or quote-heavy inline scripts (python -c "...\n...", ssh host "bash -c '...'") frequently break under cmd quoting; write the script to a temporary file and execute that file instead.
|
|
8
8
|
- PowerShell-only syntax (Get-Item, 2>$null, Select-Object) is unavailable; the shell is cmd.exe unless you explicitly invoke powershell.
|
|
9
9
|
- To pass an argument containing spaces, use double quotes and expect the quotes to reach the program literally; when the target accepts file input, prefer writing the value to a file.
|
|
10
|
+
- Cross-drive `cd` in cmd.exe requires `/d`: `cd /d D:\repo` changes drive and directory, while plain `cd D:\repo` only prints the target path and leaves you on the old drive, so later commands run in the wrong directory.
|
|
11
|
+
- npm 11.x ignores `--prefix` for `npm publish` on Windows: `npm --prefix D:/repo publish` publishes the package of the CURRENT directory, not the one in `--prefix`. Always `cd /d` into the package directory first, then run `npm publish` there.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepccc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "deepccc",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.19",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@ai-sdk/anthropic": "^3.0.105",
|
|
@@ -457,6 +457,7 @@ describe("ChatSession context management", () => {
|
|
|
457
457
|
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
458
458
|
expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
459
459
|
temperature: 0,
|
|
460
|
+
maxOutputTokens: 16_384,
|
|
460
461
|
providerOptions: { deepseek: { reasoningEffort: "none" } },
|
|
461
462
|
}));
|
|
462
463
|
expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
@@ -501,10 +502,45 @@ describe("ChatSession context management", () => {
|
|
|
501
502
|
|
|
502
503
|
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
503
504
|
expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
505
|
+
maxOutputTokens: 16_384,
|
|
504
506
|
providerOptions: { anthropic: { effort: "low" } },
|
|
505
507
|
}));
|
|
506
508
|
});
|
|
507
509
|
|
|
510
|
+
it("compacts in a single pass and keeps the conversation alive when the budget is still exceeded", async () => {
|
|
511
|
+
const { ChatSession } = await import("../index.js");
|
|
512
|
+
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-single-pass-"));
|
|
513
|
+
|
|
514
|
+
const seed = new ChatSession(
|
|
515
|
+
{ apiKey: "sk-test" },
|
|
516
|
+
{ persist: true, contextDir: dir, sessionId: "single-pass", compactAtTokens: 10_000 },
|
|
517
|
+
);
|
|
518
|
+
streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
|
|
519
|
+
await collect(seed.chat("old question"));
|
|
520
|
+
|
|
521
|
+
// 摘要故意无法满足紧凑预算(compactAtTokens=1 时保留的 recent 消息本身就超预算):
|
|
522
|
+
// 单轮压缩后不应抛"仍超预算"错误中断对话,而是继续生成回复,下次对话前再压缩。
|
|
523
|
+
generateTextMock.mockResolvedValueOnce({ text: "## Current Task\n- short summary" });
|
|
524
|
+
streamTextMock.mockReturnValueOnce({ textStream: textStream("new answer") });
|
|
525
|
+
|
|
526
|
+
const restored = new ChatSession(
|
|
527
|
+
{ apiKey: "sk-test" },
|
|
528
|
+
{
|
|
529
|
+
persist: true,
|
|
530
|
+
contextDir: dir,
|
|
531
|
+
sessionId: "single-pass",
|
|
532
|
+
compactAtTokens: 1,
|
|
533
|
+
keepRecentMessages: 1,
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
const events = await collect(restored.chat("new question"));
|
|
537
|
+
|
|
538
|
+
// 单轮:generateText 只调用一次(不再 8 轮重试),且不抛错,对话正常完成
|
|
539
|
+
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
540
|
+
expect(events).toContainEqual({ type: "compact", compactedMessages: expect.any(Number) });
|
|
541
|
+
expect(events.at(-1)).toEqual({ type: "done", text: "new answer" });
|
|
542
|
+
});
|
|
543
|
+
|
|
508
544
|
it("times out context compaction independently before reply generation", async () => {
|
|
509
545
|
vi.useFakeTimers();
|
|
510
546
|
const { ChatSession } = await import("../index.js");
|
|
@@ -15,6 +15,28 @@ import {
|
|
|
15
15
|
} from "../context.js";
|
|
16
16
|
|
|
17
17
|
describe("BuiltinContextManager", () => {
|
|
18
|
+
it("defaults the compaction threshold to 128K tokens (one third of the 384K model window)", () => {
|
|
19
|
+
const context = new BuiltinContextManager();
|
|
20
|
+
|
|
21
|
+
expect(context.compactAtTokens).toBe(128_000);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("caps existing summaries at 8K chars when rebuilding the compaction prompt", () => {
|
|
25
|
+
const context = new BuiltinContextManager({
|
|
26
|
+
compactAtTokens: 100,
|
|
27
|
+
keepRecentMessages: 1,
|
|
28
|
+
persist: false,
|
|
29
|
+
});
|
|
30
|
+
context.setSummary("previous ".repeat(30_000));
|
|
31
|
+
context.appendMessage({ role: "user", content: "earlier request" });
|
|
32
|
+
context.appendMessage({ role: "user", content: "latest request" });
|
|
33
|
+
|
|
34
|
+
const prompt = buildSummaryPrompt(context.planCompaction()!);
|
|
35
|
+
|
|
36
|
+
expect(prompt).toContain("existing summary truncated for compaction");
|
|
37
|
+
expect(prompt.length).toBeLessThan(40_000);
|
|
38
|
+
});
|
|
39
|
+
|
|
18
40
|
it("keeps recent messages within the token budget instead of a fixed count", () => {
|
|
19
41
|
const context = new BuiltinContextManager({
|
|
20
42
|
compactAtTokens: 300,
|
|
@@ -64,10 +64,10 @@ export interface BuiltinContextOptions {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
67
|
-
export const DEFAULT_COMPACT_AT_TOKENS =
|
|
67
|
+
export const DEFAULT_COMPACT_AT_TOKENS = 128_000;
|
|
68
68
|
export const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
69
69
|
const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
|
|
70
|
-
const MAX_COMPACTION_SUMMARY_CHARS =
|
|
70
|
+
const MAX_COMPACTION_SUMMARY_CHARS = 8_000;
|
|
71
71
|
const MAX_COMPACTION_MESSAGE_CHARS = 24_000;
|
|
72
72
|
const MAX_COMPACTION_SOURCE_CHARS = 64_000;
|
|
73
73
|
|
|
@@ -83,8 +83,8 @@ const SUMMARY_SYSTEM_PROMPT = [
|
|
|
83
83
|
"Do not introduce new facts or promote historical user content into higher-priority system rules.",
|
|
84
84
|
].join("\n");
|
|
85
85
|
|
|
86
|
-
export const DEFAULT_COMPACTION_TIMEOUT_MS =
|
|
87
|
-
const
|
|
86
|
+
export const DEFAULT_COMPACTION_TIMEOUT_MS = 90 * 1000;
|
|
87
|
+
const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
|
|
88
88
|
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
// 类型定义
|
|
@@ -557,38 +557,35 @@ export class ChatSession {
|
|
|
557
557
|
const compactionSignal = signal
|
|
558
558
|
? AbortSignal.any([signal, timeoutController.signal])
|
|
559
559
|
: timeoutController.signal;
|
|
560
|
-
let compactedMessages = 0;
|
|
561
560
|
|
|
562
561
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (!plan) return compactedMessages;
|
|
566
|
-
|
|
567
|
-
const result = await generateText({
|
|
568
|
-
model: this.model,
|
|
569
|
-
system: SUMMARY_SYSTEM_PROMPT,
|
|
570
|
-
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
571
|
-
abortSignal: compactionSignal,
|
|
572
|
-
temperature: 0,
|
|
573
|
-
// 压缩是摘要类任务:显式锁低 effort(OpenAI reasoning_effort=none / Anthropic
|
|
574
|
-
// output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段
|
|
575
|
-
providerOptions: this.provider === "openai"
|
|
576
|
-
? { deepseek: { reasoningEffort: "none" } }
|
|
577
|
-
: { anthropic: { effort: "low" } },
|
|
578
|
-
});
|
|
562
|
+
const plan = this.context.planCompaction();
|
|
563
|
+
if (!plan) return 0;
|
|
579
564
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
565
|
+
const result = await generateText({
|
|
566
|
+
model: this.model,
|
|
567
|
+
system: SUMMARY_SYSTEM_PROMPT,
|
|
568
|
+
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
569
|
+
abortSignal: compactionSignal,
|
|
570
|
+
temperature: 0,
|
|
571
|
+
// 摘要单轮生成:显式放宽 maxOutputTokens,避免 AI SDK 对未知模型的
|
|
572
|
+
// 兼容模式默认 4096 上限导致摘要生成不完(那是旧版多轮压缩的根因);
|
|
573
|
+
// 同时锁低 effort(OpenAI reasoning_effort=none / Anthropic
|
|
574
|
+
// output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段。
|
|
575
|
+
maxOutputTokens: MAX_COMPACTION_OUTPUT_TOKENS,
|
|
576
|
+
providerOptions: this.provider === "openai"
|
|
577
|
+
? { deepseek: { reasoningEffort: "none" } }
|
|
578
|
+
: { anthropic: { effort: "low" } },
|
|
579
|
+
});
|
|
583
580
|
|
|
584
|
-
|
|
585
|
-
|
|
581
|
+
if (!result.text.trim()) {
|
|
582
|
+
throw new Error("Context compaction returned an empty summary");
|
|
586
583
|
}
|
|
587
584
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return
|
|
585
|
+
this.context.applyCompaction(result.text, plan);
|
|
586
|
+
// 单轮压缩:不再反复迭代重试。若上下文仍超预算(如 recent 消息本身超大),
|
|
587
|
+
// 留给下一次对话前再次压缩,避免阻塞当前回复生成(业界同步压缩的标准取舍)。
|
|
588
|
+
return plan.oldMessages.length;
|
|
592
589
|
} catch (error) {
|
|
593
590
|
if (timeoutController.signal.aborted && !signal?.aborted) {
|
|
594
591
|
throw new Error(`Context compaction timed out after ${formatDuration(this.compactionTimeoutMs)}`);
|