deepccc 0.1.24 → 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,8 +75,10 @@ $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,
81
+ "contextWindow": 1048576,
80
82
  "rawStreamLogs": {
81
83
  "enabled": true,
82
84
  "maxBytesPerTurn": 1048576,
@@ -90,11 +92,27 @@ $env:DEEPCCC_STREAMING="true"
90
92
  `DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
91
93
 
92
94
  - `openai` 使用 OpenAI-compatible Chat Completions 协议,兼容 DeepSeek、OpenAI、LiteLLM、vLLM 等服务。
93
- - `anthropic` 使用 Anthropic Messages 协议。配置中的 `baseURL` 可以继续填写同一个服务基址;DeepCCC 会为不带 `/v1` 的地址自动补上 `/v1`。`effort` 是 OpenAI/DeepSeek 扩展参数,在 Anthropic 模式下不会发送。
95
+ - `anthropic` 使用 Anthropic Messages 协议。配置中的 `baseURL` **完全按填写值使用,不自动补 `/v1`**,请填写到完整版本化地址,例如 DeepSeek 官方 Anthropic 端点为 `https://api.deepseek.com/anthropic/v1`(官方 Anthropic SDK 使用的 `https://api.deepseek.com/anthropic` 基址会拼接为 `.../anthropic/v1/messages`)。`effort` 是 OpenAI/DeepSeek 扩展参数,在 Anthropic 模式下不会发送。
94
96
 
95
97
  `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
96
98
  `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
97
99
 
100
+ `contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
101
+ 原生规格);上下文压缩阈值自动 = `contextWindow × 0.8`(超出即把较早消息压缩为摘要)。
102
+ 可通过 `DEEPCCC_CONTEXT_WINDOW` 环境变量覆盖。⚠️ 超过模型/服务端实际上限时请求会被
103
+ API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
104
+ `max_input_tokens`)。
105
+
106
+ `subModel` 是子模型(选填),默认 `""`(留空跟随主模型)。配置后,DeepCCC 内部的轻量
107
+ 环节——上下文压缩摘要生成、`task` 子代理任务——使用子模型执行,主对话仍用主模型。
108
+ 典型用法:主模型用 pro 承担复杂推理,子模型用 flash 做高频廉价的摘要与子任务。
109
+ 可通过 `DEEPCCC_SUB_MODEL` 环境变量或命令行 `--sub-model` 覆盖。
110
+
111
+ `task` 子代理工具:主模型可把边界清晰的独立子任务(仓库调研、长文档阅读、独立模块生成)
112
+ 委派给子代理执行——子代理使用子模型、独立上下文,不污染主对话上下文;结果截断回传。
113
+ 子代理**不能再次委派**(禁止嵌套),单轮最多 20 个工具步,超时与主会话压缩超时一致。
114
+ 仅在配置了子模型时建议使用(未配置时子代理跟随主模型,节省有限)。
115
+
98
116
  `rawStreamLogs.enabled` 默认 `true`,通过 `DEEPCCC_RAW_STREAM_LOGS` 环境变量或配置 JSON 关闭。
99
117
  开启时,每次对话的原始流按 gzip JSONL 落到 `~/.deepccc/raw-stream-logs/`,供
100
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,10 +5,20 @@ 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
  /** 主对话是否使用流式请求;默认开启 */
11
16
  streaming: boolean;
17
+ /**
18
+ * 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格)。
19
+ * 上下文压缩阈值自动 = contextWindow × 0.8;超过模型/服务端实际上限会被 API 拒绝。
20
+ */
21
+ contextWindow: number;
12
22
  rawStreamLogs: {
13
23
  enabled: boolean;
14
24
  maxBytesPerTurn: number;
package/dist/config.js CHANGED
@@ -12,8 +12,10 @@ 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,
18
+ contextWindow: 1_048_576,
17
19
  rawStreamLogs: {
18
20
  // 默认开启:压缩后可通过 session_search(include_raw_logs=true)找回原文。
19
21
  // 如需关闭,在 ~/.deepccc/config.json 中设置 rawStreamLogs.enabled=false。
@@ -65,8 +67,10 @@ function loadConfig() {
65
67
  apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
66
68
  baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
67
69
  model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
70
+ subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
68
71
  effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
69
72
  streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
73
+ contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
70
74
  rawStreamLogs: {
71
75
  enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
72
76
  maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
package/dist/context.d.ts CHANGED
@@ -48,11 +48,19 @@ export interface BuiltinContextOptions {
48
48
  contextDir?: string;
49
49
  sessionId?: string;
50
50
  cwd?: string;
51
+ /** 模型上下文窗口(token)。缺省时压缩阈值 = contextWindow × 0.8。 */
52
+ contextWindow?: number;
53
+ /** 直接指定压缩阈值(token),优先于 contextWindow 的 80% 派生。 */
51
54
  compactAtTokens?: number;
52
55
  keepRecentMessages?: number;
53
56
  }
54
57
  export declare const DEFAULT_BUILTIN_CONTEXT_DIR: string;
55
- export declare const DEFAULT_COMPACT_AT_TOKENS = 128000;
58
+ /** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
59
+ export declare const DEFAULT_CONTEXT_WINDOW_TOKENS = 1048576;
60
+ /** 压缩阈值 = 窗口 × 0.8(业界惯例:在窗口接近上限前主动压缩,留出安全余量)。 */
61
+ export declare const COMPACTION_THRESHOLD_RATIO = 0.8;
62
+ /** 默认压缩阈值 = 1M × 0.8。 */
63
+ export declare const DEFAULT_COMPACT_AT_TOKENS: number;
56
64
  export declare const DEFAULT_KEEP_RECENT_MESSAGES = 16;
57
65
  export declare function normalizeBuiltinSessionId(value: string): string;
58
66
  export declare function defaultBuiltinSessionId(cwd?: string): string;
package/dist/context.js CHANGED
@@ -3,7 +3,12 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFile
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
6
- export const DEFAULT_COMPACT_AT_TOKENS = 128_000;
6
+ /** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
7
+ export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
8
+ /** 压缩阈值 = 窗口 × 0.8(业界惯例:在窗口接近上限前主动压缩,留出安全余量)。 */
9
+ export const COMPACTION_THRESHOLD_RATIO = 0.8;
10
+ /** 默认压缩阈值 = 1M × 0.8。 */
11
+ export const DEFAULT_COMPACT_AT_TOKENS = Math.floor(DEFAULT_CONTEXT_WINDOW_TOKENS * COMPACTION_THRESHOLD_RATIO);
7
12
  export const DEFAULT_KEEP_RECENT_MESSAGES = 16;
8
13
  const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
9
14
  const MAX_COMPACTION_SUMMARY_CHARS = 8_000;
@@ -247,7 +252,8 @@ export class BuiltinContextManager {
247
252
  this.contextDir = options.contextDir ?? DEFAULT_BUILTIN_CONTEXT_DIR;
248
253
  this.sessionId = normalizeBuiltinSessionId(options.sessionId ?? defaultBuiltinSessionId());
249
254
  this.cwd = options.cwd;
250
- this.compactAtTokens = options.compactAtTokens ?? DEFAULT_COMPACT_AT_TOKENS;
255
+ this.compactAtTokens = options.compactAtTokens
256
+ ?? Math.floor((options.contextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS) * COMPACTION_THRESHOLD_RATIO);
251
257
  this.keepRecentMessages = Math.max(1, options.keepRecentMessages ?? DEFAULT_KEEP_RECENT_MESSAGES);
252
258
  this.state = this.load();
253
259
  }
@@ -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.
@@ -47,6 +51,11 @@ export interface ChatSessionOptions {
47
51
  sessionId?: string;
48
52
  /** Compact older context when the rough token estimate exceeds this value. */
49
53
  compactAtTokens?: number;
54
+ /**
55
+ * 模型上下文窗口(token),默认 1048576(1M)。压缩阈值自动 = contextWindow × 0.8;
56
+ * 显式 compactAtTokens 优先于该派生值。
57
+ */
58
+ contextWindow?: number;
50
59
  /** Number of recent raw messages retained after compaction. */
51
60
  keepRecentMessages?: number;
52
61
  /** Hard deadline for all context-compaction passes in one turn. */
@@ -110,17 +119,30 @@ interface ChatMessage {
110
119
  }
111
120
  export declare class ChatSession {
112
121
  private model;
122
+ /** 子模型实例;未配置 subModel 时与主模型同一实例 */
123
+ private subModel;
113
124
  private provider;
125
+ private apiKey;
126
+ private baseURL;
127
+ private modelId;
128
+ private subModelId;
114
129
  private cwd;
115
130
  private context;
116
131
  private compactionTimeoutMs;
117
132
  private maxSteps?;
118
133
  private effort;
134
+ private permissionMode;
135
+ private permissionResolver?;
119
136
  private permissionGate;
120
137
  private skillDirs;
121
138
  private customSystemPrompt;
122
139
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
123
140
  private systemPrompt;
141
+ /**
142
+ * task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
143
+ * 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
144
+ */
145
+ private runTask;
124
146
  constructor(overrides?: ChatSessionConfig, options?: ChatSessionOptions);
125
147
  /**
126
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.",
@@ -210,22 +219,78 @@ function normalizeMaxSteps(value) {
210
219
  return value;
211
220
  }
212
221
  function normalizeAnthropicBaseURL(baseURL) {
213
- const normalized = baseURL.trim().replace(/\/+$/, "");
214
- return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
222
+ // 完全按用户填写的地址使用,不自动补 /v1(AI SDK 仅对官方 api.anthropic.com
223
+ // 特判补一次 /v1,其他地址原样拼接 /messages)。
224
+ // DeepSeek Anthropic 端点示例:https://api.deepseek.com/anthropic/v1。
225
+ return baseURL.trim().replace(/\/+$/, "");
215
226
  }
216
227
  export class ChatSession {
217
228
  model;
229
+ /** 子模型实例;未配置 subModel 时与主模型同一实例 */
230
+ subModel;
218
231
  provider;
232
+ apiKey;
233
+ baseURL;
234
+ modelId;
235
+ subModelId;
219
236
  cwd;
220
237
  context;
221
238
  compactionTimeoutMs;
222
239
  maxSteps;
223
240
  effort;
241
+ permissionMode;
242
+ permissionResolver;
224
243
  permissionGate;
225
244
  skillDirs;
226
245
  customSystemPrompt;
227
246
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
228
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
+ };
229
294
  constructor(overrides = {}, options = {}) {
230
295
  const apiKey = overrides.apiKey ?? appConfig.apiKey;
231
296
  if (!apiKey) {
@@ -233,28 +298,32 @@ export class ChatSession {
233
298
  }
234
299
  const baseURL = overrides.baseURL ?? appConfig.baseURL;
235
300
  const modelId = overrides.model ?? appConfig.model;
301
+ this.modelId = modelId;
302
+ this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
236
303
  this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
237
304
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
238
- if (this.provider === "anthropic") {
239
- const provider = createAnthropic({
305
+ this.apiKey = apiKey;
306
+ this.baseURL = baseURL;
307
+ const provider = this.provider === "anthropic"
308
+ ? createAnthropic({
240
309
  baseURL: normalizeAnthropicBaseURL(baseURL),
241
310
  apiKey,
242
- });
243
- this.model = provider(modelId);
244
- }
245
- else {
246
- const provider = createOpenAICompatible({
311
+ })
312
+ : createOpenAICompatible({
247
313
  name: "deepccc",
248
314
  baseURL,
249
315
  apiKey,
250
316
  includeUsage: true,
251
317
  });
252
- this.model = provider(modelId);
253
- }
318
+ this.model = provider(modelId);
319
+ // 子模型:留空时与主模型共用同一实例(行为与旧版完全一致,零开销)
320
+ this.subModel = this.subModelId ? provider(this.subModelId) : this.model;
254
321
  this.cwd = options.cwd ?? process.cwd();
255
322
  this.maxSteps = normalizeMaxSteps(options.maxSteps);
256
323
  this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
257
324
  this.customSystemPrompt = options.systemPrompt ?? "";
325
+ this.permissionMode = options.permissionMode ?? "ask";
326
+ this.permissionResolver = options.permissionResolver;
258
327
  // 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
259
328
  // 因此创建/修改技能后下一次对话自动生效,无需重启。
260
329
  this.skillDirs =
@@ -265,10 +334,11 @@ export class ChatSession {
265
334
  contextDir: options.contextDir,
266
335
  sessionId: options.sessionId ?? defaultBuiltinSessionId(this.cwd),
267
336
  cwd: this.cwd,
337
+ contextWindow: options.contextWindow ?? appConfig.contextWindow,
268
338
  compactAtTokens: options.compactAtTokens,
269
339
  keepRecentMessages: options.keepRecentMessages,
270
340
  });
271
- this.permissionGate = new PermissionGate(options.permissionMode ?? "ask", options.permissionResolver);
341
+ this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
272
342
  }
273
343
  /**
274
344
  * 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
@@ -357,7 +427,10 @@ export class ChatSession {
357
427
  model: this.model,
358
428
  system,
359
429
  messages: modelMessages,
360
- tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
430
+ tools: createBuiltinFileTools(this.cwd, {
431
+ permissionGate: this.permissionGate,
432
+ runTask: this.runTask,
433
+ }),
361
434
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
362
435
  abortSignal: signal,
363
436
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
@@ -497,7 +570,7 @@ export class ChatSession {
497
570
  if (!plan)
498
571
  return 0;
499
572
  const result = await generateText({
500
- model: this.model,
573
+ model: this.subModel,
501
574
  system: SUMMARY_SYSTEM_PROMPT,
502
575
  messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
503
576
  abortSignal: compactionSignal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.24",
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": [