deepccc 0.1.25 → 0.1.26

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 CHANGED
@@ -75,6 +75,7 @@ $env:DEEPCCC_STREAMING="true"
75
75
  "apiKey": "sk-...",
76
76
  "baseURL": "https://api.deepseek.com/v1",
77
77
  "model": "deepseek-v4-pro",
78
+ "subModel": "",
78
79
  "effort": "",
79
80
  "streaming": true,
80
81
  "contextWindow": 1048576,
@@ -102,6 +103,16 @@ $env:DEEPCCC_STREAMING="true"
102
103
  API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
103
104
  `max_input_tokens`)。
104
105
 
106
+ `subModel` 是子模型(选填),默认 `""`(留空跟随主模型)。配置后,DeepCCC 内部的轻量
107
+ 环节——上下文压缩摘要生成、`task` 子代理任务——使用子模型执行,主对话仍用主模型。
108
+ 典型用法:主模型用 pro 承担复杂推理,子模型用 flash 做高频廉价的摘要与子任务。
109
+ 可通过 `DEEPCCC_SUB_MODEL` 环境变量或命令行 `--sub-model` 覆盖。
110
+
111
+ `task` 子代理工具:主模型可把边界清晰的独立子任务(仓库调研、长文档阅读、独立模块生成)
112
+ 委派给子代理执行——子代理使用子模型、独立上下文,不污染主对话上下文;结果截断回传。
113
+ 子代理**不能再次委派**(禁止嵌套),单轮最多 20 个工具步,超时与主会话压缩超时一致。
114
+ 仅在配置了子模型时建议使用(未配置时子代理跟随主模型,节省有限)。
115
+
105
116
  `rawStreamLogs.enabled` 默认 `true`,通过 `DEEPCCC_RAW_STREAM_LOGS` 环境变量或配置 JSON 关闭。
106
117
  开启时,每次对话的原始流按 gzip JSONL 落到 `~/.deepccc/raw-stream-logs/`,供
107
118
  `session_search` 工具在会话被压缩后找回被压缩消息的精确原文(检索时设置
package/dist/cli.js CHANGED
@@ -57,6 +57,10 @@ function parseArgs(argv = process.argv.slice(2)) {
57
57
  config.model = next;
58
58
  i++;
59
59
  }
60
+ else if (arg === "--sub-model" && next !== undefined) {
61
+ config.subModel = next;
62
+ i++;
63
+ }
60
64
  else if (arg === "--effort" && next !== undefined) {
61
65
  config.effort = next;
62
66
  i++;
@@ -125,6 +129,7 @@ function printHelp(appConfig) {
125
129
  "Options:",
126
130
  ` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
127
131
  ` --model <name> Model name (current default ${appConfig.model})`,
132
+ ` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
128
133
  ` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
129
134
  ` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
130
135
  " --api-key <key> API key",
package/dist/config.d.ts CHANGED
@@ -5,6 +5,11 @@ export interface DeepCccConfig {
5
5
  apiKey: string;
6
6
  baseURL: string;
7
7
  model: string;
8
+ /**
9
+ * 子模型(可选):用于内部轻量环节(上下文压缩摘要生成、task 子代理任务)。
10
+ * 留空("")时跟随主模型,行为与旧版完全一致。
11
+ */
12
+ subModel: string;
8
13
  /** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
9
14
  effort: string;
10
15
  /** 主对话是否使用流式请求;默认开启 */
package/dist/config.js CHANGED
@@ -12,6 +12,7 @@ export const DEFAULT_CONFIG = {
12
12
  apiKey: "",
13
13
  baseURL: "https://api.deepseek.com/v1",
14
14
  model: "deepseek-v4-pro",
15
+ subModel: "",
15
16
  effort: "",
16
17
  streaming: true,
17
18
  contextWindow: 1_048_576,
@@ -66,6 +67,7 @@ function loadConfig() {
66
67
  apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
67
68
  baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
68
69
  model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
70
+ subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
69
71
  effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
70
72
  streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
71
73
  contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
@@ -1,5 +1,7 @@
1
1
  import { type ToolSet } from "ai";
2
2
  import { type PermissionGate } from "./permissions.js";
3
+ /** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
4
+ export declare const MAX_TASK_OUTPUT_CHARS = 32000;
3
5
  export interface ReadFileInput {
4
6
  path: string;
5
7
  startLine?: number;
@@ -155,5 +157,21 @@ export interface BuiltinFileToolsOptions {
155
157
  contextDir?: string;
156
158
  rawLogsDir?: string;
157
159
  };
160
+ /**
161
+ * task 子代理工具的执行回调:把独立子任务委派给子会话(使用子模型、独立上下文)。
162
+ * 未提供时 task 工具不可用(抛出明确错误),主会话之外的工具集不会意外开启子代理。
163
+ */
164
+ runTask?: TaskRunner;
165
+ }
166
+ export interface TaskRunnerInput {
167
+ /** 子任务描述:包含目标、约束与交付物,子代理将以此作为唯一指令。 */
168
+ description: string;
169
+ /** 可选子任务工作目录(相对主会话 cwd 或绝对路径),默认继承主会话工作目录。 */
170
+ cwd?: string;
171
+ }
172
+ export interface TaskRunnerOutput {
173
+ /** 子代理最终文本回复(已截断到 MAX_TASK_OUTPUT_CHARS,已过隐私替换)。 */
174
+ result: string;
158
175
  }
176
+ export type TaskRunner = (input: TaskRunnerInput, signal?: AbortSignal) => Promise<string>;
159
177
  export declare function createBuiltinFileTools(cwd: string, options?: BuiltinFileToolsOptions): ToolSet;
@@ -22,6 +22,8 @@ const SEARCH_TIMEOUT_MS = 15_000;
22
22
  const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
23
23
  const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
24
24
  const MAX_COMMAND_TIMEOUT_MS = 900_000;
25
+ /** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
26
+ export const MAX_TASK_OUTPUT_CHARS = 32_000;
25
27
  const requireFromHere = createRequire(import.meta.url);
26
28
  const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
27
29
  /**
@@ -939,6 +941,7 @@ export async function applyPatchForTool(cwd, input) {
939
941
  }
940
942
  export function createBuiltinFileTools(cwd, options = {}) {
941
943
  const gate = options.permissionGate;
944
+ const runTask = options.runTask;
942
945
  /** 文件路径的候选匹配键:原始路径 + 绝对路径 + 相对 cwd 路径(正/反斜杠双版本,规则可任选其一) */
943
946
  const pathKeys = (p) => {
944
947
  const abs = resolveToolPath(cwd, p);
@@ -1023,6 +1026,25 @@ export function createBuiltinFileTools(cwd, options = {}) {
1023
1026
  return runCommandForTool(cwd, input, options.abortSignal);
1024
1027
  },
1025
1028
  }),
1029
+ task: tool({
1030
+ description: "把独立子任务委派给子代理执行:子代理使用子模型、拥有独立上下文,不污染主对话上下文。适合边界清晰、可独立交付的调研/代码生成子任务(如扫描整个仓库、阅读长文档、生成独立模块)。子代理不能再次委派任务(禁止嵌套),结果会截断回传。",
1031
+ inputSchema: jsonSchema({
1032
+ type: "object",
1033
+ additionalProperties: false,
1034
+ properties: {
1035
+ description: { type: "string", description: "子任务描述:目标、约束与交付物。请写清楚子代理需要返回什么。" },
1036
+ cwd: { type: "string", description: "可选子任务工作目录(绝对路径或相对主会话 cwd),默认继承主会话工作目录。" },
1037
+ },
1038
+ required: ["description"],
1039
+ }),
1040
+ execute: async (input, execOptions) => {
1041
+ if (!runTask) {
1042
+ throw new Error("task 工具不可用:当前环境未启用子代理执行器");
1043
+ }
1044
+ const result = await runTask(input, execOptions.abortSignal);
1045
+ return { result };
1046
+ },
1047
+ }),
1026
1048
  edit_file: tool({
1027
1049
  description: "通过精确的 oldText -> newText 替换编辑现有 UTF-8 文本文件。可行时使用 SHA-256 前置条件以避免覆盖并发编辑。",
1028
1050
  inputSchema: jsonSchema({
package/dist/index.d.ts CHANGED
@@ -28,6 +28,10 @@ export interface ChatSessionConfig {
28
28
  apiKey?: string;
29
29
  /** Model id. Defaults to DEEPCCC_MODEL/config. */
30
30
  model?: string;
31
+ /**
32
+ * 子模型 id(可选):用于压缩摘要生成与 task 子代理任务。留空跟随主模型。
33
+ */
34
+ subModel?: string;
31
35
  /**
32
36
  * Reasoning effort (none/minimal/low/medium/high/xhigh/max);
33
37
  * overrides config.effort; empty omits the reasoning_effort request field.
@@ -115,17 +119,30 @@ interface ChatMessage {
115
119
  }
116
120
  export declare class ChatSession {
117
121
  private model;
122
+ /** 子模型实例;未配置 subModel 时与主模型同一实例 */
123
+ private subModel;
118
124
  private provider;
125
+ private apiKey;
126
+ private baseURL;
127
+ private modelId;
128
+ private subModelId;
119
129
  private cwd;
120
130
  private context;
121
131
  private compactionTimeoutMs;
122
132
  private maxSteps?;
123
133
  private effort;
134
+ private permissionMode;
135
+ private permissionResolver?;
124
136
  private permissionGate;
125
137
  private skillDirs;
126
138
  private customSystemPrompt;
127
139
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
128
140
  private systemPrompt;
141
+ /**
142
+ * task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
143
+ * 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
144
+ */
145
+ private runTask;
129
146
  constructor(overrides?: ChatSessionConfig, options?: ChatSessionOptions);
130
147
  /**
131
148
  * 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
package/dist/index.js CHANGED
@@ -8,12 +8,12 @@ import { createAnthropic } from "@ai-sdk/anthropic";
8
8
  import { generateText, isLoopFinished, stepCountIs, streamText } from "ai";
9
9
  import { existsSync, readFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
- import { join } from "node:path";
11
+ import { isAbsolute, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { config as appConfig, normalizeDeepCccProvider, RAW_STREAM_LOGS_DIR, } from "./config.js";
14
14
  import { createRawStreamLog, } from "./raw-stream-log.js";
15
15
  import { buildPersistedAssistantMessage, buildSummaryPrompt, BuiltinContextManager, defaultBuiltinSessionId, } from "./context.js";
16
- import { createBuiltinFileTools } from "./file-tools.js";
16
+ import { createBuiltinFileTools, MAX_TASK_OUTPUT_CHARS } from "./file-tools.js";
17
17
  import { PermissionGate } from "./permissions.js";
18
18
  import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
19
19
  import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
@@ -76,6 +76,15 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
76
76
  ].join("\n");
77
77
  export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
78
78
  const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
79
+ /** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
80
+ const TASK_MAX_STEPS = 20;
81
+ /** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
82
+ function truncateTaskOutput(text) {
83
+ if (text.length <= MAX_TASK_OUTPUT_CHARS)
84
+ return text;
85
+ return (text.slice(0, MAX_TASK_OUTPUT_CHARS) +
86
+ `\n…[子代理输出已截断,共 ${text.length} 字符,仅保留前 ${MAX_TASK_OUTPUT_CHARS} 字符]`);
87
+ }
79
88
  const ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE = [
80
89
  "[Protocol compatibility note]",
81
90
  "tool-call arguments use JSON encoding; the final reply does not need to be JSON unless the user requests it.",
@@ -217,17 +226,71 @@ function normalizeAnthropicBaseURL(baseURL) {
217
226
  }
218
227
  export class ChatSession {
219
228
  model;
229
+ /** 子模型实例;未配置 subModel 时与主模型同一实例 */
230
+ subModel;
220
231
  provider;
232
+ apiKey;
233
+ baseURL;
234
+ modelId;
235
+ subModelId;
221
236
  cwd;
222
237
  context;
223
238
  compactionTimeoutMs;
224
239
  maxSteps;
225
240
  effort;
241
+ permissionMode;
242
+ permissionResolver;
226
243
  permissionGate;
227
244
  skillDirs;
228
245
  customSystemPrompt;
229
246
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
230
247
  systemPrompt = "";
248
+ /**
249
+ * task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
250
+ * 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
251
+ */
252
+ runTask = async (input, signal) => {
253
+ const rawCwd = input.cwd?.trim();
254
+ const taskCwd = rawCwd ? (isAbsolute(rawCwd) ? rawCwd : resolve(this.cwd, rawCwd)) : this.cwd;
255
+ const timeoutController = new AbortController();
256
+ const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
257
+ timeout.unref?.();
258
+ const taskSignal = signal
259
+ ? AbortSignal.any([signal, timeoutController.signal])
260
+ : timeoutController.signal;
261
+ try {
262
+ const child = new ChatSession({
263
+ provider: this.provider,
264
+ apiKey: this.apiKey,
265
+ baseURL: this.baseURL,
266
+ model: this.subModelId || this.modelId,
267
+ effort: this.effort,
268
+ }, {
269
+ cwd: taskCwd,
270
+ persist: false,
271
+ permissionMode: this.permissionMode,
272
+ permissionResolver: this.permissionResolver,
273
+ maxSteps: TASK_MAX_STEPS,
274
+ compactionTimeoutMs: this.compactionTimeoutMs,
275
+ skillsDirs: this.skillDirs.map((s) => s.dir),
276
+ });
277
+ let full = "";
278
+ for await (const event of child.chat(input.description, taskSignal)) {
279
+ if (event.type === "text") {
280
+ full += event.text;
281
+ }
282
+ else if (event.type === "error") {
283
+ throw new Error(`task 子代理执行失败: ${event.message}`);
284
+ }
285
+ }
286
+ if (!full.trim())
287
+ return "(子代理未返回文本内容)";
288
+ return applyPrivacy(truncateTaskOutput(full));
289
+ }
290
+ finally {
291
+ clearTimeout(timeout);
292
+ }
293
+ };
231
294
  constructor(overrides = {}, options = {}) {
232
295
  const apiKey = overrides.apiKey ?? appConfig.apiKey;
233
296
  if (!apiKey) {
@@ -235,28 +298,32 @@ export class ChatSession {
235
298
  }
236
299
  const baseURL = overrides.baseURL ?? appConfig.baseURL;
237
300
  const modelId = overrides.model ?? appConfig.model;
301
+ this.modelId = modelId;
302
+ this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
238
303
  this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
239
304
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
240
- if (this.provider === "anthropic") {
241
- const provider = createAnthropic({
305
+ this.apiKey = apiKey;
306
+ this.baseURL = baseURL;
307
+ const provider = this.provider === "anthropic"
308
+ ? createAnthropic({
242
309
  baseURL: normalizeAnthropicBaseURL(baseURL),
243
310
  apiKey,
244
- });
245
- this.model = provider(modelId);
246
- }
247
- else {
248
- const provider = createOpenAICompatible({
311
+ })
312
+ : createOpenAICompatible({
249
313
  name: "deepccc",
250
314
  baseURL,
251
315
  apiKey,
252
316
  includeUsage: true,
253
317
  });
254
- this.model = provider(modelId);
255
- }
318
+ this.model = provider(modelId);
319
+ // 子模型:留空时与主模型共用同一实例(行为与旧版完全一致,零开销)
320
+ this.subModel = this.subModelId ? provider(this.subModelId) : this.model;
256
321
  this.cwd = options.cwd ?? process.cwd();
257
322
  this.maxSteps = normalizeMaxSteps(options.maxSteps);
258
323
  this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
259
324
  this.customSystemPrompt = options.systemPrompt ?? "";
325
+ this.permissionMode = options.permissionMode ?? "ask";
326
+ this.permissionResolver = options.permissionResolver;
260
327
  // 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
261
328
  // 因此创建/修改技能后下一次对话自动生效,无需重启。
262
329
  this.skillDirs =
@@ -271,7 +338,7 @@ export class ChatSession {
271
338
  compactAtTokens: options.compactAtTokens,
272
339
  keepRecentMessages: options.keepRecentMessages,
273
340
  });
274
- this.permissionGate = new PermissionGate(options.permissionMode ?? "ask", options.permissionResolver);
341
+ this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
275
342
  }
276
343
  /**
277
344
  * 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
@@ -360,7 +427,10 @@ export class ChatSession {
360
427
  model: this.model,
361
428
  system,
362
429
  messages: modelMessages,
363
- tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
430
+ tools: createBuiltinFileTools(this.cwd, {
431
+ permissionGate: this.permissionGate,
432
+ runTask: this.runTask,
433
+ }),
364
434
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
365
435
  abortSignal: signal,
366
436
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
@@ -500,7 +570,7 @@ export class ChatSession {
500
570
  if (!plan)
501
571
  return 0;
502
572
  const result = await generateText({
503
- model: this.model,
573
+ model: this.subModel,
504
574
  system: SUMMARY_SYSTEM_PROMPT,
505
575
  messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
506
576
  abortSignal: compactionSignal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [