chatccc 0.2.252 → 0.2.254
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 +10 -1
- package/bin/cccagent.mjs +12 -3
- package/bin/chatccc.mjs +15 -6
- package/config.sample.json +2 -1
- package/deepccc-agent/README.md +40 -35
- package/deepccc-agent/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +11 -11
- package/deepccc-agent/package.json +65 -65
- package/dist/deepccc-agent/src/cli.js +634 -0
- package/dist/deepccc-agent/src/config.js +81 -0
- package/dist/deepccc-agent/src/context.js +348 -0
- package/dist/deepccc-agent/src/file-log.js +34 -0
- package/dist/deepccc-agent/src/file-tools.js +1208 -0
- package/dist/deepccc-agent/src/index.js +601 -0
- package/dist/deepccc-agent/src/permissions.js +170 -0
- package/dist/deepccc-agent/src/privacy.js +124 -0
- package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
- package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
- package/dist/deepccc-agent/src/progress/reducer.js +102 -0
- package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
- package/dist/deepccc-agent/src/progress/view.js +30 -0
- package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
- package/dist/deepccc-agent/src/session-search.js +276 -0
- package/dist/deepccc-agent/src/session-select.js +23 -0
- package/dist/deepccc-agent/src/sigint.js +26 -0
- package/dist/deepccc-agent/src/skills.js +178 -0
- package/dist/deepccc-agent/src/web-tools.js +246 -0
- package/dist/src/adapters/adapter-interface.js +19 -0
- package/dist/src/adapters/ccc-adapter.js +112 -0
- package/dist/src/adapters/claude-adapter.js +497 -0
- package/dist/src/adapters/claude-session-meta-store.js +92 -0
- package/dist/src/adapters/codex-adapter.js +279 -0
- package/dist/src/adapters/codex-session-meta-store.js +94 -0
- package/dist/src/adapters/cursor-adapter.js +491 -0
- package/dist/src/adapters/cursor-session-meta-store.js +116 -0
- package/dist/src/adapters/jsonl-stream.js +104 -0
- package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
- package/dist/src/adapters/raw-stream-log.js +106 -0
- package/dist/src/adapters/resource-monitor.js +113 -0
- package/dist/src/agent-activity.js +133 -0
- package/dist/src/agent-delegate-task-rpc.js +129 -0
- package/dist/src/agent-delegate-task.js +48 -0
- package/dist/src/agent-file-rpc.js +152 -0
- package/dist/src/agent-image-rpc.js +148 -0
- package/dist/src/agent-platform-routing.js +13 -0
- package/dist/src/agent-reload-config-rpc.js +23 -0
- package/dist/src/agent-rpc-body.js +87 -0
- package/dist/src/agent-stop-stuck.js +110 -0
- package/dist/src/agent-team/application/board-service.js +142 -0
- package/dist/src/agent-team/application/main-agent-service.js +151 -0
- package/dist/src/agent-team/domain/board.js +69 -0
- package/dist/src/agent-team/http/board-routes.js +213 -0
- package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
- package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
- package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
- package/dist/src/agent-team/repositories/board-repository.js +10 -0
- package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
- package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
- package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
- package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
- package/dist/src/agent-team/web/agent-team-page.js +219 -0
- package/dist/src/agent-tool.js +10 -0
- package/dist/src/card-action-routing.js +7 -0
- package/dist/src/card-plain-text.js +101 -0
- package/dist/src/cardkit.js +158 -0
- package/dist/src/cards.js +573 -0
- package/dist/src/chatgpt-subscription-rpc.js +18 -0
- package/dist/src/chatgpt-subscription.js +199 -0
- package/dist/src/chrome-devtools-guard.js +238 -0
- package/dist/src/claude-sdk-installer.js +249 -0
- package/dist/src/codex-reset-actions.js +143 -0
- package/dist/src/config-utils.js +149 -0
- package/dist/src/config.js +808 -0
- package/dist/src/cursor-usage.js +77 -0
- package/dist/src/exit-banner.js +28 -0
- package/dist/src/feishu-api.js +1404 -0
- package/dist/src/feishu-message-ingress.js +137 -0
- package/dist/src/feishu-platform.js +97 -0
- package/dist/src/format-message.js +252 -0
- package/dist/src/git-command.js +155 -0
- package/dist/src/im-skills.js +121 -0
- package/dist/src/index.js +835 -0
- package/dist/src/litellm-proxy.js +300 -0
- package/dist/src/orchestrator.js +2096 -0
- package/dist/src/package-root.js +26 -0
- package/dist/src/platform-adapter.js +7 -0
- package/dist/src/platform-startup.js +6 -0
- package/dist/src/privacy.js +100 -0
- package/dist/src/progress/reducer.js +102 -0
- package/dist/src/progress/terminal-renderer.js +264 -0
- package/dist/src/progress/view.js +30 -0
- package/dist/src/response-stall.js +14 -0
- package/dist/src/runtime-entry.js +13 -0
- package/dist/src/runtime-reload.js +19 -0
- package/dist/src/session-chat-binding.js +183 -0
- package/dist/src/session-name.js +7 -0
- package/dist/src/session.js +2147 -0
- package/dist/src/shared-prefix.js +16 -0
- package/dist/src/shared.js +493 -0
- package/dist/src/sim-agent.js +105 -0
- package/dist/src/sim-platform.js +142 -0
- package/dist/src/sim-store.js +231 -0
- package/dist/src/simplify.js +99 -0
- package/dist/src/startup-lifecycle.js +209 -0
- package/dist/src/stream-state.js +141 -0
- package/dist/src/terminal-error.js +100 -0
- package/dist/src/trace.js +50 -0
- package/dist/src/turn-cards.js +92 -0
- package/dist/src/update-command-guard.js +114 -0
- package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
- package/dist/src/wechat-platform.js +545 -0
- package/package.json +76 -74
- package/deepccc-agent/LICENSE +0 -201
- package/deepccc-agent/bin/deepccc.mjs +0 -26
- package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
- package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
- package/deepccc-agent/package-lock.json +0 -2027
- package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
- package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
- package/deepccc-agent/src/__tests__/config.test.ts +0 -34
- package/deepccc-agent/src/__tests__/context.test.ts +0 -341
- package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
- package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
- package/deepccc-agent/src/cli.ts +0 -682
- package/deepccc-agent/src/config.ts +0 -101
- package/deepccc-agent/src/context.ts +0 -465
- package/deepccc-agent/src/file-log.ts +0 -38
- package/deepccc-agent/src/file-tools.ts +0 -1493
- package/deepccc-agent/src/index.ts +0 -710
- package/deepccc-agent/src/permissions.ts +0 -226
- package/deepccc-agent/src/privacy.ts +0 -141
- package/deepccc-agent/src/proc-tree-kill.ts +0 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
- package/deepccc-agent/src/progress/reducer.ts +0 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
- package/deepccc-agent/src/progress/view.ts +0 -77
- package/deepccc-agent/src/raw-stream-log.ts +0 -124
- package/deepccc-agent/src/session-search.ts +0 -370
- package/deepccc-agent/src/session-select.ts +0 -48
- package/deepccc-agent/src/sigint.ts +0 -50
- package/deepccc-agent/src/skills.ts +0 -205
- package/deepccc-agent/src/web-tools.ts +0 -313
- package/deepccc-agent/tsconfig.build.json +0 -13
- package/deepccc-agent/tsconfig.json +0 -13
- package/deepccc-agent/vitest.config.ts +0 -7
- package/src/__tests__/adapter-interface.test.ts +0 -152
- package/src/__tests__/agent-activity.test.ts +0 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
- package/src/__tests__/agent-image-rpc.test.ts +0 -34
- package/src/__tests__/agent-platform-routing.test.ts +0 -26
- package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
- package/src/__tests__/agent-rpc-body.test.ts +0 -42
- package/src/__tests__/builtin-chat-session.test.ts +0 -532
- package/src/__tests__/builtin-cli-json.test.ts +0 -39
- package/src/__tests__/builtin-config.test.ts +0 -26
- package/src/__tests__/builtin-context.test.ts +0 -319
- package/src/__tests__/builtin-file-tools.test.ts +0 -240
- package/src/__tests__/builtin-permissions.test.ts +0 -219
- package/src/__tests__/builtin-session-search.test.ts +0 -262
- package/src/__tests__/builtin-session-select.test.ts +0 -116
- package/src/__tests__/builtin-sigint.test.ts +0 -56
- package/src/__tests__/builtin-skills.test.ts +0 -284
- package/src/__tests__/builtin-web-tools.test.ts +0 -220
- package/src/__tests__/card-action-routing.test.ts +0 -18
- package/src/__tests__/card-plain-text.test.ts +0 -45
- package/src/__tests__/cardkit.test.ts +0 -60
- package/src/__tests__/cards.test.ts +0 -607
- package/src/__tests__/ccc-adapter.test.ts +0 -194
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
- package/src/__tests__/chatgpt-subscription.test.ts +0 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
- package/src/__tests__/claude-adapter.test.ts +0 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
- package/src/__tests__/claude-sdk-installer.test.ts +0 -285
- package/src/__tests__/codex-adapter.test.ts +0 -331
- package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
- package/src/__tests__/codex-reset-actions.test.ts +0 -146
- package/src/__tests__/config-reload.test.ts +0 -284
- package/src/__tests__/config-sample.test.ts +0 -97
- package/src/__tests__/config-utils.test.ts +0 -40
- package/src/__tests__/config.test.ts +0 -395
- package/src/__tests__/crash-logging.test.ts +0 -360
- package/src/__tests__/cursor-adapter.test.ts +0 -890
- package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
- package/src/__tests__/feishu-api.test.ts +0 -60
- package/src/__tests__/feishu-avatar.test.ts +0 -504
- package/src/__tests__/feishu-message-ingress.test.ts +0 -138
- package/src/__tests__/feishu-platform.test.ts +0 -75
- package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
- package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
- package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
- package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
- package/src/__tests__/format-message.test.ts +0 -316
- package/src/__tests__/git-command.test.ts +0 -288
- package/src/__tests__/im-skills.test.ts +0 -125
- package/src/__tests__/jsonl-stream.test.ts +0 -79
- package/src/__tests__/orchestrator.test.ts +0 -1268
- package/src/__tests__/package-files.test.ts +0 -24
- package/src/__tests__/platform-startup.test.ts +0 -19
- package/src/__tests__/privacy.test.ts +0 -198
- package/src/__tests__/proc-tree-kill.test.ts +0 -108
- package/src/__tests__/progress-reducer.test.ts +0 -121
- package/src/__tests__/raw-stream-log.test.ts +0 -106
- package/src/__tests__/response-stall.test.ts +0 -49
- package/src/__tests__/restart.test.ts +0 -232
- package/src/__tests__/session-ccc-config.test.ts +0 -66
- package/src/__tests__/session.test.ts +0 -3004
- package/src/__tests__/shared-prefix.test.ts +0 -36
- package/src/__tests__/sim-agent.test.ts +0 -174
- package/src/__tests__/sim-platform.test.ts +0 -93
- package/src/__tests__/sim-store.test.ts +0 -214
- package/src/__tests__/simplify.test.ts +0 -283
- package/src/__tests__/startup-lifecycle.test.ts +0 -231
- package/src/__tests__/stop-session.test.ts +0 -162
- package/src/__tests__/stream-state.test.ts +0 -164
- package/src/__tests__/terminal-error.test.ts +0 -54
- package/src/__tests__/terminal-renderer.test.ts +0 -247
- package/src/__tests__/update-command-guard.test.ts +0 -144
- package/src/__tests__/web-ui.test.ts +0 -438
- package/src/__tests__/wechat-platform.test.ts +0 -111
- package/src/adapters/adapter-interface.ts +0 -217
- package/src/adapters/ccc-adapter.ts +0 -150
- package/src/adapters/claude-adapter.ts +0 -673
- package/src/adapters/claude-session-meta-store.ts +0 -120
- package/src/adapters/codex-adapter.ts +0 -426
- package/src/adapters/codex-session-meta-store.ts +0 -131
- package/src/adapters/cursor-adapter.ts +0 -681
- package/src/adapters/cursor-session-meta-store.ts +0 -154
- package/src/adapters/jsonl-stream.ts +0 -157
- package/src/adapters/raw-stream-log.ts +0 -124
- package/src/adapters/resource-monitor.ts +0 -141
- package/src/agent-activity.ts +0 -175
- package/src/agent-delegate-task-rpc.ts +0 -153
- package/src/agent-delegate-task.ts +0 -91
- package/src/agent-file-rpc.ts +0 -172
- package/src/agent-image-rpc.ts +0 -168
- package/src/agent-platform-routing.ts +0 -28
- package/src/agent-reload-config-rpc.ts +0 -34
- package/src/agent-rpc-body.ts +0 -92
- package/src/agent-stop-stuck.ts +0 -129
- package/src/card-action-routing.ts +0 -14
- package/src/card-plain-text.ts +0 -108
- package/src/cardkit.ts +0 -179
- package/src/cards.ts +0 -684
- package/src/chatgpt-subscription-rpc.ts +0 -27
- package/src/chatgpt-subscription.ts +0 -299
- package/src/chrome-devtools-guard.ts +0 -318
- package/src/claude-sdk-installer.ts +0 -324
- package/src/codex-reset-actions.ts +0 -184
- package/src/config-utils.ts +0 -211
- package/src/config.ts +0 -1063
- package/src/cursor-usage.ts +0 -128
- package/src/exit-banner.ts +0 -33
- package/src/feishu-api.ts +0 -1616
- package/src/feishu-message-ingress.ts +0 -195
- package/src/feishu-platform.ts +0 -159
- package/src/format-message.ts +0 -293
- package/src/git-command.ts +0 -202
- package/src/im-skills.ts +0 -149
- package/src/index.ts +0 -1089
- package/src/litellm-proxy.ts +0 -374
- package/src/orchestrator.ts +0 -2543
- package/src/platform-adapter.ts +0 -70
- package/src/platform-startup.ts +0 -16
- package/src/privacy.ts +0 -118
- package/src/progress/reducer.ts +0 -113
- package/src/progress/terminal-renderer.ts +0 -294
- package/src/progress/view.ts +0 -77
- package/src/response-stall.ts +0 -28
- package/src/runtime-reload.ts +0 -34
- package/src/session-chat-binding.ts +0 -292
- package/src/session-name.ts +0 -8
- package/src/session.ts +0 -2659
- package/src/shared-prefix.ts +0 -29
- package/src/shared.ts +0 -552
- package/src/sim-agent.ts +0 -167
- package/src/sim-platform.ts +0 -177
- package/src/sim-store.ts +0 -317
- package/src/simplify.ts +0 -120
- package/src/startup-lifecycle.ts +0 -250
- package/src/stream-state.ts +0 -177
- package/src/terminal-error.ts +0 -129
- package/src/trace.ts +0 -51
- package/src/turn-cards.ts +0 -118
- package/src/update-command-guard.ts +0 -165
- package/src/wechat-platform.ts +0 -680
|
@@ -1,877 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
|
|
8
|
-
import { config } from "../config.js";
|
|
9
|
-
import { estimateBuiltinContextTokens } from "../context.js";
|
|
10
|
-
|
|
11
|
-
const streamTextMock = vi.fn();
|
|
12
|
-
const generateTextMock = vi.fn();
|
|
13
|
-
const createRawStreamLogMock = vi.fn();
|
|
14
|
-
const rawLogWriteLineMock = vi.fn();
|
|
15
|
-
const rawLogCloseMock = vi.fn();
|
|
16
|
-
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
17
|
-
const originalStreaming = config.streaming;
|
|
18
|
-
const originalProvider = config.provider;
|
|
19
|
-
const originalEffort = config.effort;
|
|
20
|
-
const createOpenAICompatibleMock = vi.fn(() => (modelId: string) => ({ modelId }));
|
|
21
|
-
const createAnthropicMock = vi.fn(() => (modelId: string) => ({ modelId, provider: "anthropic" }));
|
|
22
|
-
|
|
23
|
-
vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
24
|
-
createOpenAICompatible: createOpenAICompatibleMock,
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
vi.mock("@ai-sdk/anthropic", () => ({
|
|
28
|
-
createAnthropic: createAnthropicMock,
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
vi.mock("ai", () => ({
|
|
32
|
-
streamText: streamTextMock,
|
|
33
|
-
generateText: generateTextMock,
|
|
34
|
-
isLoopFinished: vi.fn(() => ({ loopFinished: true })),
|
|
35
|
-
stepCountIs: vi.fn((count: number) => ({ count })),
|
|
36
|
-
jsonSchema: vi.fn((schema: unknown) => schema),
|
|
37
|
-
tool: vi.fn((definition: unknown) => definition),
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
vi.mock("../raw-stream-log.js", () => ({
|
|
41
|
-
createRawStreamLog: createRawStreamLogMock,
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
45
|
-
const events: unknown[] = [];
|
|
46
|
-
for await (const event of iterable) events.push(event);
|
|
47
|
-
return events;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function* textStream(...chunks: string[]): AsyncIterable<string> {
|
|
51
|
-
for (const chunk of chunks) yield chunk;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
|
|
55
|
-
for (const part of parts) yield part;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
config.provider = "openai";
|
|
60
|
-
config.streaming = true;
|
|
61
|
-
config.effort = "";
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
streamTextMock.mockReset();
|
|
66
|
-
generateTextMock.mockReset();
|
|
67
|
-
createRawStreamLogMock.mockReset();
|
|
68
|
-
rawLogWriteLineMock.mockReset();
|
|
69
|
-
rawLogCloseMock.mockReset();
|
|
70
|
-
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
71
|
-
config.provider = originalProvider;
|
|
72
|
-
config.streaming = originalStreaming;
|
|
73
|
-
config.effort = originalEffort;
|
|
74
|
-
createOpenAICompatibleMock.mockClear();
|
|
75
|
-
createAnthropicMock.mockClear();
|
|
76
|
-
vi.useRealTimers();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe("ChatSession response transport", () => {
|
|
80
|
-
it("uses the OpenAI-compatible provider by default", async () => {
|
|
81
|
-
const { ChatSession } = await import("../index.js");
|
|
82
|
-
|
|
83
|
-
new ChatSession({ apiKey: "sk-test", baseURL: "https://gateway.example", model: "model-a" });
|
|
84
|
-
|
|
85
|
-
expect(createOpenAICompatibleMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
86
|
-
baseURL: "https://gateway.example",
|
|
87
|
-
apiKey: "sk-test",
|
|
88
|
-
}));
|
|
89
|
-
expect(createAnthropicMock).not.toHaveBeenCalled();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("uses Anthropic Messages with the same base URL and appends /v1 when needed", async () => {
|
|
93
|
-
const { ChatSession } = await import("../index.js");
|
|
94
|
-
|
|
95
|
-
new ChatSession({
|
|
96
|
-
provider: "anthropic",
|
|
97
|
-
apiKey: "sk-test",
|
|
98
|
-
baseURL: "https://gateway.example/",
|
|
99
|
-
model: "model-a",
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(createAnthropicMock).toHaveBeenLastCalledWith({
|
|
103
|
-
baseURL: "https://gateway.example/v1",
|
|
104
|
-
apiKey: "sk-test",
|
|
105
|
-
});
|
|
106
|
-
expect(createOpenAICompatibleMock).not.toHaveBeenCalled();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("keeps an existing /v1 suffix and maps Anthropic effort to output_config.effort", async () => {
|
|
110
|
-
const { ChatSession } = await import("../index.js");
|
|
111
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
112
|
-
const session = new ChatSession({
|
|
113
|
-
provider: "anthropic",
|
|
114
|
-
apiKey: "sk-test",
|
|
115
|
-
baseURL: "https://gateway.example/v1/",
|
|
116
|
-
model: "model-a",
|
|
117
|
-
effort: "high",
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
await collect(session.chat("hello"));
|
|
121
|
-
|
|
122
|
-
expect(createAnthropicMock).toHaveBeenLastCalledWith({
|
|
123
|
-
baseURL: "https://gateway.example/v1",
|
|
124
|
-
apiKey: "sk-test",
|
|
125
|
-
});
|
|
126
|
-
expect(streamTextMock).toHaveBeenCalledOnce();
|
|
127
|
-
expect(streamTextMock.mock.calls[0]?.[0]).toMatchObject({
|
|
128
|
-
providerOptions: { anthropic: { effort: "high" } },
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("omits providerOptions when effort is empty for the Anthropic protocol", async () => {
|
|
133
|
-
const { ChatSession } = await import("../index.js");
|
|
134
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
135
|
-
const session = new ChatSession({
|
|
136
|
-
provider: "anthropic",
|
|
137
|
-
apiKey: "sk-test",
|
|
138
|
-
baseURL: "https://gateway.example",
|
|
139
|
-
model: "model-a",
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
await collect(session.chat("hello"));
|
|
143
|
-
|
|
144
|
-
expect(streamTextMock).toHaveBeenCalledOnce();
|
|
145
|
-
expect(streamTextMock.mock.calls[0]?.[0]).not.toHaveProperty("providerOptions");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("adds the JSON tool compatibility note to Anthropic user messages", async () => {
|
|
149
|
-
const { ChatSession } = await import("../index.js");
|
|
150
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
151
|
-
const session = new ChatSession({
|
|
152
|
-
provider: "anthropic",
|
|
153
|
-
apiKey: "sk-test",
|
|
154
|
-
baseURL: "https://gateway.example",
|
|
155
|
-
model: "model-a",
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await collect(session.chat("delete the orphan directory"));
|
|
159
|
-
|
|
160
|
-
const messages = streamTextMock.mock.calls[0]?.[0].messages as Array<{
|
|
161
|
-
role: string;
|
|
162
|
-
content: string;
|
|
163
|
-
}>;
|
|
164
|
-
expect(messages.at(-1)).toMatchObject({
|
|
165
|
-
role: "user",
|
|
166
|
-
content: expect.stringContaining("tool-call arguments use JSON encoding"),
|
|
167
|
-
});
|
|
168
|
-
expect(messages.at(-1)?.content).toContain(
|
|
169
|
-
"final reply does not need to be JSON",
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("maps OpenAI-compatible effort to DeepSeek reasoningEffort", async () => {
|
|
174
|
-
const { ChatSession } = await import("../index.js");
|
|
175
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
176
|
-
const session = new ChatSession({
|
|
177
|
-
provider: "openai",
|
|
178
|
-
apiKey: "sk-test",
|
|
179
|
-
baseURL: "https://gateway.example",
|
|
180
|
-
model: "model-a",
|
|
181
|
-
effort: "max",
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
await collect(session.chat("hello"));
|
|
185
|
-
|
|
186
|
-
expect(streamTextMock).toHaveBeenCalledOnce();
|
|
187
|
-
expect(streamTextMock.mock.calls[0]?.[0]).toMatchObject({
|
|
188
|
-
providerOptions: { deepseek: { reasoningEffort: "max" } },
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("asks the provider to include usage in streaming responses", async () => {
|
|
193
|
-
const { ChatSession } = await import("../index.js");
|
|
194
|
-
|
|
195
|
-
new ChatSession({ apiKey: "sk-test" });
|
|
196
|
-
|
|
197
|
-
expect(createOpenAICompatibleMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
198
|
-
includeUsage: true,
|
|
199
|
-
}));
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("uses generateText and preserves text/tool events when streaming is disabled", async () => {
|
|
203
|
-
const { ChatSession } = await import("../index.js");
|
|
204
|
-
config.streaming = false;
|
|
205
|
-
generateTextMock.mockResolvedValueOnce({
|
|
206
|
-
text: "done",
|
|
207
|
-
steps: [{
|
|
208
|
-
text: "done",
|
|
209
|
-
toolCalls: [{
|
|
210
|
-
type: "tool-call",
|
|
211
|
-
toolCallId: "call-1",
|
|
212
|
-
toolName: "read_file",
|
|
213
|
-
input: { path: "package.json" },
|
|
214
|
-
}],
|
|
215
|
-
toolResults: [{
|
|
216
|
-
type: "tool-result",
|
|
217
|
-
toolCallId: "call-1",
|
|
218
|
-
toolName: "read_file",
|
|
219
|
-
input: { path: "package.json" },
|
|
220
|
-
output: { content: "{}" },
|
|
221
|
-
}],
|
|
222
|
-
}],
|
|
223
|
-
});
|
|
224
|
-
const session = new ChatSession({ apiKey: "sk-test" });
|
|
225
|
-
|
|
226
|
-
const events = await collect(session.chat("read package"));
|
|
227
|
-
|
|
228
|
-
expect(streamTextMock).not.toHaveBeenCalled();
|
|
229
|
-
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
230
|
-
expect(events).toContainEqual({
|
|
231
|
-
type: "tool_use",
|
|
232
|
-
id: "call-1",
|
|
233
|
-
name: "read_file",
|
|
234
|
-
input: { path: "package.json" },
|
|
235
|
-
});
|
|
236
|
-
expect(events).toContainEqual({
|
|
237
|
-
type: "tool_result",
|
|
238
|
-
tool_use_id: "call-1",
|
|
239
|
-
name: "read_file",
|
|
240
|
-
content: { content: "{}" },
|
|
241
|
-
is_error: false,
|
|
242
|
-
});
|
|
243
|
-
expect(events).toContainEqual({ type: "text", text: "done", accumulated: "done" });
|
|
244
|
-
expect(events).toContainEqual({ type: "done", text: "done" });
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe("ChatSession context management", () => {
|
|
249
|
-
it("keeps the generalized evidence gate in the stable system prompt prefix", async () => {
|
|
250
|
-
const { ChatSession } = await import("../index.js");
|
|
251
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-evidence-gate-"));
|
|
252
|
-
await writeFile(join(dir, "AGENTS.md"), "PROJECT GUIDANCE MARKER", "utf-8");
|
|
253
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
254
|
-
|
|
255
|
-
const session = new ChatSession(
|
|
256
|
-
{ apiKey: "sk-test" },
|
|
257
|
-
{
|
|
258
|
-
cwd: dir,
|
|
259
|
-
sessionId: "evidence-gate",
|
|
260
|
-
systemPrompt: "CUSTOM PROMPT MARKER",
|
|
261
|
-
},
|
|
262
|
-
);
|
|
263
|
-
await collect(session.chat("diagnose a consequential problem"));
|
|
264
|
-
|
|
265
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
266
|
-
expect(system).toContain("## 证据门控结论");
|
|
267
|
-
expect(system).toContain("权威事实来源");
|
|
268
|
-
expect(system).toContain("区分直接观察与推断");
|
|
269
|
-
expect(system).toContain("合理的替代解释");
|
|
270
|
-
expect(system).toContain("运行时结论用运行时行为");
|
|
271
|
-
expect(system).toContain("说明不确定性");
|
|
272
|
-
expect(system).toContain("一旦已有决定性证据");
|
|
273
|
-
expect(system).not.toContain("CodesForUnity");
|
|
274
|
-
expect(system.indexOf("## 证据门控结论")).toBeLessThan(
|
|
275
|
-
system.indexOf("PROJECT GUIDANCE MARKER"),
|
|
276
|
-
);
|
|
277
|
-
expect(system.indexOf("## 证据门控结论")).toBeLessThan(
|
|
278
|
-
system.indexOf("当前工作目录"),
|
|
279
|
-
);
|
|
280
|
-
expect(system.indexOf("## 证据门控结论")).toBeLessThan(
|
|
281
|
-
system.indexOf("CUSTOM PROMPT MARKER"),
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("keeps execution discipline sections in the stable system prompt prefix", async () => {
|
|
286
|
-
const { ChatSession } = await import("../index.js");
|
|
287
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-discipline-"));
|
|
288
|
-
await writeFile(join(dir, "AGENTS.md"), "PROJECT GUIDANCE MARKER", "utf-8");
|
|
289
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
290
|
-
|
|
291
|
-
const session = new ChatSession(
|
|
292
|
-
{ apiKey: "sk-test" },
|
|
293
|
-
{
|
|
294
|
-
cwd: dir,
|
|
295
|
-
sessionId: "execution-discipline",
|
|
296
|
-
systemPrompt: "CUSTOM PROMPT MARKER",
|
|
297
|
-
},
|
|
298
|
-
);
|
|
299
|
-
await collect(session.chat("build a feature"));
|
|
300
|
-
|
|
301
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
302
|
-
// 先盘点再动手:动手前低开销盘点 + 输出含验证策略的计划
|
|
303
|
-
expect(system).toContain("## 行动前先调查");
|
|
304
|
-
expect(system).toContain("以低成本盘点环境");
|
|
305
|
-
expect(system).toContain("如何验证结果");
|
|
306
|
-
// 授权自主:用户委托决策后只问真正阻塞项,不抛实现级选择题
|
|
307
|
-
expect(system).toContain("## 授权范围");
|
|
308
|
-
expect(system).toContain("不可逆操作");
|
|
309
|
-
expect(system).toContain("不要把实现级选择题抛回给用户");
|
|
310
|
-
// 交付自检:声明做了什么/如何验证/未验证项
|
|
311
|
-
expect(system).toContain("## 交付前自检");
|
|
312
|
-
expect(system).toContain("未验证或有风险");
|
|
313
|
-
// 稳定前缀全部位于项目指令与 runtime 上下文之前
|
|
314
|
-
for (const section of ["## 行动前先调查", "## 授权范围", "## 交付前自检"]) {
|
|
315
|
-
expect(system.indexOf(section)).toBeGreaterThan(0);
|
|
316
|
-
expect(system.indexOf(section)).toBeLessThan(system.indexOf("PROJECT GUIDANCE MARKER"));
|
|
317
|
-
expect(system.indexOf(section)).toBeLessThan(system.indexOf("当前工作目录"));
|
|
318
|
-
expect(system.indexOf(section)).toBeLessThan(system.indexOf("CUSTOM PROMPT MARKER"));
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("injects cwd project instruction files before runtime workspace details", async () => {
|
|
323
|
-
const { ChatSession } = await import("../index.js");
|
|
324
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-instructions-"));
|
|
325
|
-
await writeFile(join(dir, "AGENTS.md"), "agents root guidance", "utf-8");
|
|
326
|
-
await writeFile(join(dir, "AGENTS.local.md"), "agents local guidance", "utf-8");
|
|
327
|
-
await writeFile(join(dir, "CLAUDE.md"), "claude root guidance", "utf-8");
|
|
328
|
-
await writeFile(join(dir, "CLAUDE.local.md"), "claude local guidance", "utf-8");
|
|
329
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream() });
|
|
330
|
-
|
|
331
|
-
const session = new ChatSession(
|
|
332
|
-
{ apiKey: "sk-test" },
|
|
333
|
-
{
|
|
334
|
-
cwd: dir,
|
|
335
|
-
sessionId: "project-instructions",
|
|
336
|
-
},
|
|
337
|
-
);
|
|
338
|
-
await collect(session.chat("hi"));
|
|
339
|
-
|
|
340
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
341
|
-
expect(system).toContain("## 项目指令");
|
|
342
|
-
expect(system).toContain("### AGENTS.md");
|
|
343
|
-
expect(system).toContain("agents root guidance");
|
|
344
|
-
expect(system).toContain("### AGENTS.local.md");
|
|
345
|
-
expect(system).toContain("agents local guidance");
|
|
346
|
-
expect(system).toContain("### CLAUDE.md");
|
|
347
|
-
expect(system).toContain("claude root guidance");
|
|
348
|
-
expect(system).toContain("### CLAUDE.local.md");
|
|
349
|
-
expect(system).toContain("claude local guidance");
|
|
350
|
-
|
|
351
|
-
expect(system.indexOf("agents root guidance")).toBeLessThan(system.indexOf("agents local guidance"));
|
|
352
|
-
expect(system.indexOf("agents local guidance")).toBeLessThan(system.indexOf("claude root guidance"));
|
|
353
|
-
expect(system.indexOf("claude root guidance")).toBeLessThan(system.indexOf("claude local guidance"));
|
|
354
|
-
expect(system.indexOf("claude local guidance")).toBeLessThan(system.indexOf(dir));
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("places the volatile skills index at the very end of system prompt", async () => {
|
|
358
|
-
const { ChatSession } = await import("../index.js");
|
|
359
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-skill-order-"));
|
|
360
|
-
const skillsDir = join(dir, "skills");
|
|
361
|
-
await mkdir(join(skillsDir, "demo-skill"), { recursive: true });
|
|
362
|
-
await writeFile(
|
|
363
|
-
join(skillsDir, "demo-skill", "SKILL.md"),
|
|
364
|
-
"---\nname: demo-skill\ndescription: demo description\n---\n\nbody\n",
|
|
365
|
-
"utf-8",
|
|
366
|
-
);
|
|
367
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("ok") });
|
|
368
|
-
|
|
369
|
-
const session = new ChatSession(
|
|
370
|
-
{ apiKey: "sk-test" },
|
|
371
|
-
{
|
|
372
|
-
cwd: dir,
|
|
373
|
-
sessionId: "skill-order",
|
|
374
|
-
systemPrompt: "CUSTOM PROMPT MARKER",
|
|
375
|
-
skillsDirs: [skillsDir],
|
|
376
|
-
},
|
|
377
|
-
);
|
|
378
|
-
await collect(session.chat("hi"));
|
|
379
|
-
|
|
380
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
381
|
-
expect(system).toContain("demo-skill");
|
|
382
|
-
expect(system).toContain("CUSTOM PROMPT MARKER");
|
|
383
|
-
expect(system).toContain("当前工作目录");
|
|
384
|
-
// 稳定性排序:固定规则 → 项目指令 → runtime → custom → 技能索引(最后)
|
|
385
|
-
expect(system.indexOf("CUSTOM PROMPT MARKER")).toBeGreaterThan(
|
|
386
|
-
system.indexOf("当前工作目录"),
|
|
387
|
-
);
|
|
388
|
-
expect(system.indexOf("demo-skill")).toBeGreaterThan(system.indexOf("CUSTOM PROMPT MARKER"));
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("does not read project instruction files from parent directories", async () => {
|
|
392
|
-
const { ChatSession } = await import("../index.js");
|
|
393
|
-
const parent = await mkdtemp(join(tmpdir(), "deepccc-session-parent-instructions-"));
|
|
394
|
-
const child = join(parent, "child");
|
|
395
|
-
await mkdir(child);
|
|
396
|
-
await writeFile(join(parent, "AGENTS.md"), "parent-only guidance", "utf-8");
|
|
397
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream() });
|
|
398
|
-
|
|
399
|
-
const session = new ChatSession(
|
|
400
|
-
{ apiKey: "sk-test" },
|
|
401
|
-
{
|
|
402
|
-
cwd: child,
|
|
403
|
-
sessionId: "no-parent-instructions",
|
|
404
|
-
},
|
|
405
|
-
);
|
|
406
|
-
await collect(session.chat("hi"));
|
|
407
|
-
|
|
408
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
409
|
-
expect(system).not.toContain("parent-only guidance");
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("uses loop-finished stopping by default", async () => {
|
|
413
|
-
const { ChatSession } = await import("../index.js");
|
|
414
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-unlimited-"));
|
|
415
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
416
|
-
|
|
417
|
-
const session = new ChatSession(
|
|
418
|
-
{ apiKey: "sk-test" },
|
|
419
|
-
{
|
|
420
|
-
cwd: dir,
|
|
421
|
-
sessionId: "unlimited-steps",
|
|
422
|
-
},
|
|
423
|
-
);
|
|
424
|
-
await collect(session.chat("run a multi-stage workflow"));
|
|
425
|
-
|
|
426
|
-
expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
427
|
-
stopWhen: { loopFinished: true },
|
|
428
|
-
}));
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it("uses a configured tool step limit when provided", async () => {
|
|
432
|
-
const { ChatSession } = await import("../index.js");
|
|
433
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-step-budget-"));
|
|
434
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
435
|
-
|
|
436
|
-
const session = new ChatSession(
|
|
437
|
-
{ apiKey: "sk-test" },
|
|
438
|
-
{
|
|
439
|
-
cwd: dir,
|
|
440
|
-
sessionId: "step-budget",
|
|
441
|
-
maxSteps: 7,
|
|
442
|
-
},
|
|
443
|
-
);
|
|
444
|
-
await collect(session.chat("run a bounded workflow"));
|
|
445
|
-
|
|
446
|
-
expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
447
|
-
stopWhen: { count: 7 },
|
|
448
|
-
}));
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("loads persisted context, compacts older messages, and persists the new assistant reply", async () => {
|
|
452
|
-
const { ChatSession } = await import("../index.js");
|
|
453
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-context-"));
|
|
454
|
-
|
|
455
|
-
const seed = new ChatSession(
|
|
456
|
-
{ apiKey: "sk-test" },
|
|
457
|
-
{
|
|
458
|
-
persist: true,
|
|
459
|
-
contextDir: dir,
|
|
460
|
-
sessionId: "integration",
|
|
461
|
-
compactAtTokens: 10_000,
|
|
462
|
-
},
|
|
463
|
-
);
|
|
464
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
|
|
465
|
-
await collect(seed.chat("old question"));
|
|
466
|
-
|
|
467
|
-
generateTextMock.mockResolvedValueOnce({ text: "## Current Task\n- old question summarized" });
|
|
468
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("new answer") });
|
|
469
|
-
|
|
470
|
-
const restored = new ChatSession(
|
|
471
|
-
{ apiKey: "sk-test" },
|
|
472
|
-
{
|
|
473
|
-
persist: true,
|
|
474
|
-
contextDir: dir,
|
|
475
|
-
sessionId: "integration",
|
|
476
|
-
compactAtTokens: 1,
|
|
477
|
-
keepRecentMessages: 1,
|
|
478
|
-
},
|
|
479
|
-
);
|
|
480
|
-
const events = await collect(restored.chat("new question"));
|
|
481
|
-
|
|
482
|
-
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
483
|
-
expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
484
|
-
temperature: 0,
|
|
485
|
-
maxOutputTokens: 16_384,
|
|
486
|
-
providerOptions: { deepseek: { reasoningEffort: "none" } },
|
|
487
|
-
}));
|
|
488
|
-
expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
489
|
-
messages: expect.arrayContaining([
|
|
490
|
-
expect.objectContaining({ content: expect.stringContaining("old question summarized") }),
|
|
491
|
-
expect.objectContaining({ role: "user", content: "new question" }),
|
|
492
|
-
]),
|
|
493
|
-
}));
|
|
494
|
-
expect(events.slice(0, 3)).toEqual([
|
|
495
|
-
{ type: "status", phase: "compacting" },
|
|
496
|
-
{ type: "compact", compactedMessages: 2 },
|
|
497
|
-
{ type: "status", phase: "generating" },
|
|
498
|
-
]);
|
|
499
|
-
expect(restored.history.map((m) => m.content).join("\n")).toContain("new answer");
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it("locks compaction to low effort under the Anthropic protocol", async () => {
|
|
503
|
-
const { ChatSession } = await import("../index.js");
|
|
504
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-anthropic-effort-"));
|
|
505
|
-
const base = { apiKey: "sk-test", provider: "anthropic" as const, baseURL: "https://gateway.example", model: "model-a" };
|
|
506
|
-
|
|
507
|
-
const seed = new ChatSession(base, {
|
|
508
|
-
persist: true,
|
|
509
|
-
contextDir: dir,
|
|
510
|
-
sessionId: "compaction-anthropic-effort",
|
|
511
|
-
compactAtTokens: 10_000,
|
|
512
|
-
});
|
|
513
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
|
|
514
|
-
await collect(seed.chat("old question"));
|
|
515
|
-
|
|
516
|
-
generateTextMock.mockResolvedValueOnce({ text: "## Current Task\n- old question summarized" });
|
|
517
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("new answer") });
|
|
518
|
-
|
|
519
|
-
const restored = new ChatSession(base, {
|
|
520
|
-
persist: true,
|
|
521
|
-
contextDir: dir,
|
|
522
|
-
sessionId: "compaction-anthropic-effort",
|
|
523
|
-
compactAtTokens: 1,
|
|
524
|
-
keepRecentMessages: 1,
|
|
525
|
-
});
|
|
526
|
-
await collect(restored.chat("new question"));
|
|
527
|
-
|
|
528
|
-
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
529
|
-
expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
530
|
-
maxOutputTokens: 16_384,
|
|
531
|
-
providerOptions: { anthropic: { effort: "low" } },
|
|
532
|
-
}));
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
it("compacts in a single pass and keeps the conversation alive when the budget is still exceeded", async () => {
|
|
536
|
-
const { ChatSession } = await import("../index.js");
|
|
537
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-single-pass-"));
|
|
538
|
-
|
|
539
|
-
const seed = new ChatSession(
|
|
540
|
-
{ apiKey: "sk-test" },
|
|
541
|
-
{ persist: true, contextDir: dir, sessionId: "single-pass", compactAtTokens: 10_000 },
|
|
542
|
-
);
|
|
543
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
|
|
544
|
-
await collect(seed.chat("old question"));
|
|
545
|
-
|
|
546
|
-
// 摘要故意无法满足紧凑预算(compactAtTokens=1 时保留的 recent 消息本身就超预算):
|
|
547
|
-
// 单轮压缩后不应抛"仍超预算"错误中断对话,而是继续生成回复,下次对话前再压缩。
|
|
548
|
-
generateTextMock.mockResolvedValueOnce({ text: "## Current Task\n- short summary" });
|
|
549
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("new answer") });
|
|
550
|
-
|
|
551
|
-
const restored = new ChatSession(
|
|
552
|
-
{ apiKey: "sk-test" },
|
|
553
|
-
{
|
|
554
|
-
persist: true,
|
|
555
|
-
contextDir: dir,
|
|
556
|
-
sessionId: "single-pass",
|
|
557
|
-
compactAtTokens: 1,
|
|
558
|
-
keepRecentMessages: 1,
|
|
559
|
-
},
|
|
560
|
-
);
|
|
561
|
-
const events = await collect(restored.chat("new question"));
|
|
562
|
-
|
|
563
|
-
// 单轮:generateText 只调用一次(不再 8 轮重试),且不抛错,对话正常完成
|
|
564
|
-
expect(generateTextMock).toHaveBeenCalledOnce();
|
|
565
|
-
expect(events).toContainEqual({ type: "compact", compactedMessages: expect.any(Number) });
|
|
566
|
-
expect(events.at(-1)).toEqual({ type: "done", text: "new answer" });
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it("times out context compaction independently before reply generation", async () => {
|
|
570
|
-
vi.useFakeTimers();
|
|
571
|
-
const { ChatSession } = await import("../index.js");
|
|
572
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-timeout-"));
|
|
573
|
-
const seed = new ChatSession(
|
|
574
|
-
{ apiKey: "sk-test" },
|
|
575
|
-
{ persist: true, contextDir: dir, sessionId: "timeout", compactAtTokens: 10_000 },
|
|
576
|
-
);
|
|
577
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
|
|
578
|
-
await collect(seed.chat("old question"));
|
|
579
|
-
|
|
580
|
-
generateTextMock.mockImplementationOnce(({ abortSignal }: { abortSignal: AbortSignal }) =>
|
|
581
|
-
new Promise((_resolve, reject) => {
|
|
582
|
-
abortSignal.addEventListener("abort", () => reject(abortSignal.reason), { once: true });
|
|
583
|
-
}));
|
|
584
|
-
const restored = new ChatSession(
|
|
585
|
-
{ apiKey: "sk-test" },
|
|
586
|
-
{
|
|
587
|
-
persist: true,
|
|
588
|
-
contextDir: dir,
|
|
589
|
-
sessionId: "timeout",
|
|
590
|
-
compactAtTokens: 1,
|
|
591
|
-
keepRecentMessages: 1,
|
|
592
|
-
compactionTimeoutMs: 100,
|
|
593
|
-
},
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
const result = collect(restored.chat("new question"));
|
|
597
|
-
const timeoutAssertion = expect(result).rejects.toThrow("Context compaction timed out");
|
|
598
|
-
await vi.waitFor(() => expect(generateTextMock).toHaveBeenCalledOnce());
|
|
599
|
-
await vi.advanceTimersByTimeAsync(101);
|
|
600
|
-
await timeoutAssertion;
|
|
601
|
-
expect(streamTextMock).toHaveBeenCalledTimes(1);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it("streams tool calls and tool results from fullStream", async () => {
|
|
605
|
-
const { ChatSession } = await import("../index.js");
|
|
606
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-tools-"));
|
|
607
|
-
const session = new ChatSession(
|
|
608
|
-
{ apiKey: "sk-test" },
|
|
609
|
-
{
|
|
610
|
-
persist: true,
|
|
611
|
-
contextDir: dir,
|
|
612
|
-
sessionId: "tools",
|
|
613
|
-
},
|
|
614
|
-
);
|
|
615
|
-
|
|
616
|
-
streamTextMock.mockReturnValueOnce({
|
|
617
|
-
fullStream: fullStream(
|
|
618
|
-
{ type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "package.json" } },
|
|
619
|
-
{ type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "{}" } },
|
|
620
|
-
{ type: "text-delta", text: "done" },
|
|
621
|
-
),
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const events = await collect(session.chat("read package"));
|
|
625
|
-
|
|
626
|
-
expect(events).toContainEqual({
|
|
627
|
-
type: "tool_use",
|
|
628
|
-
id: "call-1",
|
|
629
|
-
name: "read_file",
|
|
630
|
-
input: { path: "package.json" },
|
|
631
|
-
});
|
|
632
|
-
expect(events).toContainEqual({
|
|
633
|
-
type: "tool_result",
|
|
634
|
-
tool_use_id: "call-1",
|
|
635
|
-
name: "read_file",
|
|
636
|
-
content: { content: "{}" },
|
|
637
|
-
is_error: false,
|
|
638
|
-
});
|
|
639
|
-
expect(events).toContainEqual({ type: "text", text: "done", accumulated: "done" });
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
it("caps the total persisted tool transcript for a turn", async () => {
|
|
643
|
-
const { ChatSession } = await import("../index.js");
|
|
644
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-tool-cap-"));
|
|
645
|
-
const session = new ChatSession(
|
|
646
|
-
{ apiKey: "sk-test" },
|
|
647
|
-
{ persist: true, contextDir: dir, sessionId: "tool-cap" },
|
|
648
|
-
);
|
|
649
|
-
const parts = Array.from({ length: 12 }, (_, index) => ({
|
|
650
|
-
type: "tool-result",
|
|
651
|
-
toolCallId: `call-${index}`,
|
|
652
|
-
toolName: "read_file",
|
|
653
|
-
output: { content: "x".repeat(8_000) },
|
|
654
|
-
}));
|
|
655
|
-
streamTextMock.mockReturnValueOnce({
|
|
656
|
-
fullStream: fullStream(...parts, { type: "text-delta", text: "done" }),
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
await collect(session.chat("read many files"));
|
|
660
|
-
|
|
661
|
-
const persisted = session.history.at(-1)?.content ?? "";
|
|
662
|
-
expect(persisted.length).toBeLessThan(40_000);
|
|
663
|
-
expect(persisted).toContain("工具记录已截断");
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it("persists structured tool calls alongside the text transcript", async () => {
|
|
667
|
-
const { ChatSession } = await import("../index.js");
|
|
668
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-structured-tools-"));
|
|
669
|
-
const session = new ChatSession(
|
|
670
|
-
{ apiKey: "sk-test" },
|
|
671
|
-
{ persist: true, contextDir: dir, sessionId: "structured-tools" },
|
|
672
|
-
);
|
|
673
|
-
|
|
674
|
-
streamTextMock.mockReturnValueOnce({
|
|
675
|
-
fullStream: fullStream(
|
|
676
|
-
{ type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "package.json" } },
|
|
677
|
-
{ type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "{}" } },
|
|
678
|
-
{ type: "text-delta", text: "done" },
|
|
679
|
-
),
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
await collect(session.chat("read package"));
|
|
683
|
-
|
|
684
|
-
const raw = await readFile(join(dir, "structured-tools", "context.json"), "utf8");
|
|
685
|
-
const state = JSON.parse(raw) as { messages: Array<{ content: string; toolCalls?: unknown }> };
|
|
686
|
-
expect(state.messages).toHaveLength(2);
|
|
687
|
-
expect(state.messages[1].toolCalls).toEqual([
|
|
688
|
-
{ name: "read_file", input: "{\"path\":\"package.json\"}", output: "{\"content\":\"{}\"}" },
|
|
689
|
-
]);
|
|
690
|
-
expect(state.messages[1].content).toContain("[工具记录]");
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
it("records tool errors and preserves tool call order in structured tool calls", async () => {
|
|
694
|
-
const { ChatSession } = await import("../index.js");
|
|
695
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-structured-tools-order-"));
|
|
696
|
-
const session = new ChatSession(
|
|
697
|
-
{ apiKey: "sk-test" },
|
|
698
|
-
{ persist: true, contextDir: dir, sessionId: "structured-tools-order" },
|
|
699
|
-
);
|
|
700
|
-
|
|
701
|
-
// AI SDK 流中工具调用先全部到达(tool-call),结果后到达(tool-result/tool-error)
|
|
702
|
-
streamTextMock.mockReturnValueOnce({
|
|
703
|
-
fullStream: fullStream(
|
|
704
|
-
{ type: "tool-call", toolCallId: "c1", toolName: "run_command", input: { command: "npm test" } },
|
|
705
|
-
{ type: "tool-call", toolCallId: "c2", toolName: "read_file", input: { path: "a.ts" } },
|
|
706
|
-
{ type: "tool-result", toolCallId: "c1", toolName: "run_command", output: { exitCode: 0 } },
|
|
707
|
-
{ type: "tool-error", toolCallId: "c2", toolName: "read_file", error: new Error("boom") },
|
|
708
|
-
{ type: "text-delta", text: "failed" },
|
|
709
|
-
),
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
await collect(session.chat("run tests"));
|
|
713
|
-
|
|
714
|
-
const raw = await readFile(join(dir, "structured-tools-order", "context.json"), "utf8");
|
|
715
|
-
const state = JSON.parse(raw) as { messages: Array<{ toolCalls?: unknown[] }> };
|
|
716
|
-
expect(state.messages[1].toolCalls).toEqual([
|
|
717
|
-
{ name: "run_command", input: "{\"command\":\"npm test\"}", output: "{\"exitCode\":0}" },
|
|
718
|
-
{ name: "read_file", input: "{\"path\":\"a.ts\"}", output: "boom", is_error: true },
|
|
719
|
-
]);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it("writes raw DeepCCC fullStream parts when raw stream logs are enabled", async () => {
|
|
723
|
-
const { ChatSession } = await import("../index.js");
|
|
724
|
-
config.rawStreamLogs = {
|
|
725
|
-
enabled: true,
|
|
726
|
-
maxBytesPerTurn: 4096,
|
|
727
|
-
retentionDays: 3,
|
|
728
|
-
keepCompleted: true,
|
|
729
|
-
};
|
|
730
|
-
createRawStreamLogMock.mockResolvedValueOnce({
|
|
731
|
-
filePath: "raw.jsonl.gz",
|
|
732
|
-
writeLine: rawLogWriteLineMock,
|
|
733
|
-
close: rawLogCloseMock,
|
|
734
|
-
});
|
|
735
|
-
const session = new ChatSession(
|
|
736
|
-
{ apiKey: "sk-test" },
|
|
737
|
-
{
|
|
738
|
-
sessionId: "raw-log-session",
|
|
739
|
-
},
|
|
740
|
-
);
|
|
741
|
-
const textPart = { type: "text-delta", text: "hello" };
|
|
742
|
-
const toolPart = {
|
|
743
|
-
type: "tool-call",
|
|
744
|
-
toolCallId: "call-1",
|
|
745
|
-
toolName: "read_file",
|
|
746
|
-
input: { path: "package.json" },
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
streamTextMock.mockReturnValueOnce({
|
|
750
|
-
fullStream: fullStream(textPart, toolPart),
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
await collect(session.chat("hi"));
|
|
754
|
-
|
|
755
|
-
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
756
|
-
enabled: true,
|
|
757
|
-
tool: "deepccc",
|
|
758
|
-
sessionId: "raw-log-session",
|
|
759
|
-
label: "prompt",
|
|
760
|
-
maxBytesPerTurn: 4096,
|
|
761
|
-
retentionDays: 3,
|
|
762
|
-
}));
|
|
763
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, JSON.stringify(textPart));
|
|
764
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, JSON.stringify(toolPart));
|
|
765
|
-
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: true });
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
describe("platform-specific system prompt injection", () => {
|
|
770
|
-
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
771
|
-
|
|
772
|
-
const setPlatform = (value: string) => {
|
|
773
|
-
Object.defineProperty(process, "platform", { value, configurable: true });
|
|
774
|
-
};
|
|
775
|
-
|
|
776
|
-
afterEach(() => {
|
|
777
|
-
if (originalPlatform) {
|
|
778
|
-
Object.defineProperty(process, "platform", originalPlatform);
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it("injects Windows command-line guidance only on win32", async () => {
|
|
783
|
-
const { ChatSession } = await import("../index.js");
|
|
784
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-win32-prompt-"));
|
|
785
|
-
setPlatform("win32");
|
|
786
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("ok") });
|
|
787
|
-
|
|
788
|
-
const session = new ChatSession(
|
|
789
|
-
{ apiKey: "sk-test" },
|
|
790
|
-
{ cwd: dir, sessionId: "win32-prompt" },
|
|
791
|
-
);
|
|
792
|
-
await collect(session.chat("hi"));
|
|
793
|
-
|
|
794
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
795
|
-
expect(system).toContain("## Windows 命令行提示");
|
|
796
|
-
expect(system).toContain("cmd.exe");
|
|
797
|
-
expect(system).toMatch(/双引号/);
|
|
798
|
-
expect(system).toMatch(/单引号/);
|
|
799
|
-
// 平台指引属于固定规则区,位于 runtime workspace 上下文之前
|
|
800
|
-
expect(system.indexOf("## Windows 命令行提示")).toBeGreaterThan(
|
|
801
|
-
system.indexOf("## 固定规则"),
|
|
802
|
-
);
|
|
803
|
-
expect(system.indexOf("## Windows 命令行提示")).toBeLessThan(
|
|
804
|
-
system.indexOf("当前工作目录"),
|
|
805
|
-
);
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
it("omits Windows guidance on non-Windows platforms", async () => {
|
|
809
|
-
const { ChatSession } = await import("../index.js");
|
|
810
|
-
const dir = await mkdtemp(join(tmpdir(), "deepccc-posix-prompt-"));
|
|
811
|
-
setPlatform("linux");
|
|
812
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("ok") });
|
|
813
|
-
|
|
814
|
-
const session = new ChatSession(
|
|
815
|
-
{ apiKey: "sk-test" },
|
|
816
|
-
{ cwd: dir, sessionId: "posix-prompt" },
|
|
817
|
-
);
|
|
818
|
-
await collect(session.chat("hi"));
|
|
819
|
-
|
|
820
|
-
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
821
|
-
expect(system).not.toContain("## Windows 命令行提示");
|
|
822
|
-
});
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
describe("estimateBuiltinContextTokens", () => {
|
|
826
|
-
it("weights CJK characters closer to one token per character", () => {
|
|
827
|
-
const messages = [{ role: "user" as const, content: "你好".repeat(100) }];
|
|
828
|
-
const estimate = estimateBuiltinContextTokens("", messages);
|
|
829
|
-
// 200 个汉字 ≈ 200 tokens(旧算法 chars/3 只估到 ~70,明显低估)
|
|
830
|
-
expect(estimate).toBeGreaterThanOrEqual(180);
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it("keeps latin text near chars/3.5", () => {
|
|
834
|
-
const messages = [{ role: "user" as const, content: "a".repeat(1000) }];
|
|
835
|
-
const estimate = estimateBuiltinContextTokens("", messages);
|
|
836
|
-
expect(estimate).toBeGreaterThan(250);
|
|
837
|
-
expect(estimate).toBeLessThan(340);
|
|
838
|
-
});
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
describe("loadPlatformCommandPrompt", () => {
|
|
842
|
-
// 测试文件在 src/__tests__/,上两级即包根 os-prompts/
|
|
843
|
-
const builtinDir = fileURLToPath(new URL("../../os-prompts/", import.meta.url));
|
|
844
|
-
|
|
845
|
-
async function loadPrompt(
|
|
846
|
-
platform: string,
|
|
847
|
-
dirs?: { builtinDir?: string; userDir?: string },
|
|
848
|
-
): Promise<string> {
|
|
849
|
-
const mod = (await import("../index.js")) as {
|
|
850
|
-
loadPlatformCommandPrompt: (
|
|
851
|
-
platform: string,
|
|
852
|
-
dirs?: { builtinDir?: string; userDir?: string },
|
|
853
|
-
) => string;
|
|
854
|
-
};
|
|
855
|
-
return mod.loadPlatformCommandPrompt(platform, dirs);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
it("loads builtin guidance from os-prompts/<platform>.md", async () => {
|
|
859
|
-
const text = await loadPrompt("win32", { builtinDir });
|
|
860
|
-
expect(text).toContain("## Windows 命令行提示");
|
|
861
|
-
expect(text).toContain("cmd.exe");
|
|
862
|
-
expect(text).toMatch(/双引号/);
|
|
863
|
-
expect(text).toMatch(/单引号/);
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
it("prefers the user override at ~/.deepccc/prompts/<platform>.md", async () => {
|
|
867
|
-
const userDir = await mkdtemp(join(tmpdir(), "deepccc-os-prompt-override-"));
|
|
868
|
-
await writeFile(join(userDir, "win32.md"), "CUSTOM OVERRIDE GUIDANCE", "utf-8");
|
|
869
|
-
const text = await loadPrompt("win32", { builtinDir, userDir });
|
|
870
|
-
expect(text).toBe("CUSTOM OVERRIDE GUIDANCE");
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
it("returns empty string for unknown platforms or missing files", async () => {
|
|
874
|
-
expect(await loadPrompt("freebsd", { builtinDir })).toBe("");
|
|
875
|
-
expect(await loadPrompt("linux", { builtinDir: join(builtinDir, "missing") })).toBe("");
|
|
876
|
-
});
|
|
877
|
-
});
|