chatccc 0.2.292 → 0.2.294

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.
@@ -198,7 +198,11 @@ API 拒绝(context length exceeded),实际窗口以模型与所用服务
198
198
 
199
199
  `task` 子代理工具:主模型可把边界清晰的独立子任务(仓库调研、长文档阅读、独立模块生成)
200
200
  委派给子代理执行——子代理使用子模型、独立上下文,不污染主对话上下文;结果截断回传。
201
- 子代理**不能再次委派**(禁止嵌套),单轮最多 20 个工具步,超时与主会话压缩超时一致。
201
+ 子代理**不能再次委派**(禁止嵌套)。每次调用默认最多 20 步,也可用 `maxSteps` 在
202
+ 1–100 步内指定预算;若在工具调用后耗尽预算,内核会追加一次禁止调用工具的强制总结,
203
+ 避免只返回过程性开场白。设置 `runInBackground=true` 可让主代理继续工作,并通过
204
+ `task_output` 查看状态,或通过 `task_stop` 停止继续调查并要求子代理基于已有证据总结。
205
+ 超时与主会话压缩超时一致。
202
206
  仅在配置了子模型时建议使用(未配置时子代理跟随主模型,节省有限)。
203
207
 
204
208
  `rawStreamLogs.enabled` 默认 `true`,通过 `DEEPCCC_RAW_STREAM_LOGS` 环境变量或配置 JSON 关闭。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -25,14 +25,27 @@ const SEARCH_TIMEOUT_MS = 15_000;
25
25
  const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
26
26
  const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
27
27
  const MAX_COMMAND_TIMEOUT_MS = 900_000;
28
- /** 让位注入的轮询间隔:run_command 执行期以此频率检查是否有待注入的用户消息 */
29
- const YIELD_POLL_INTERVAL_MS = 250;
28
+ /**
29
+ * 让位注入的轮询间隔:run_command 执行期以此频率检查是否有待注入的用户消息。
30
+ * 取 1s 而非更小值:轮询本身不花 token(只读内存队列,无模型调用),但更密的
31
+ * 定时器唤醒有可测的 CPU 开销(实测 250ms 档约 0.27ms/s,1s 档低于计时精度)。
32
+ * 让位延迟从 250ms 放宽到 1s 对用户无感,却把长任务期间的定时器唤醒降低到
33
+ * 约 1/4,因此取 1s 作为响应速度与空转成本的平衡点。
34
+ */
35
+ const YIELD_POLL_INTERVAL_MS = 1000;
30
36
  /** command_output 单次等待上限,避免代理用等待把 step 又堵死 */
31
37
  const MAX_COMMAND_OUTPUT_WAIT_MS = 30_000;
32
38
  /** 后台命令句柄上限;超出时优先回收已结束的句柄 */
33
39
  const MAX_BACKGROUND_COMMANDS = 32;
34
40
  /** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
35
41
  export const MAX_TASK_OUTPUT_CHARS = 32_000;
42
+ /** task 子代理工具:调用方可选 step 预算的安全边界。 */
43
+ export const MIN_TASK_MAX_STEPS = 1;
44
+ export const MAX_TASK_MAX_STEPS = 100;
45
+ /** task_stop 用此 abort reason 区分“主代理要求收尾”和整个父会话被取消。 */
46
+ export const TASK_STOP_ABORT_REASON = "deepccc_task_stop";
47
+ const MAX_BACKGROUND_TASKS = 32;
48
+ const MAX_TASK_OUTPUT_WAIT_MS = 30_000;
36
49
  const requireFromHere = createRequire(import.meta.url);
37
50
  /** Adds DeepCCC attribution to git commit commands without replacing the user's author. */
38
51
  export function withGitCoAuthor(command, coAuthor) {
@@ -1182,6 +1195,114 @@ export async function applyPatchForTool(cwd, input) {
1182
1195
  }
1183
1196
  return { changedFiles };
1184
1197
  }
1198
+ const backgroundTasks = new Map();
1199
+ let backgroundTaskSeq = 0;
1200
+ function normalizeTaskOutputWaitMs(value) {
1201
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
1202
+ return 0;
1203
+ return Math.min(Math.trunc(value), MAX_TASK_OUTPUT_WAIT_MS);
1204
+ }
1205
+ function snapshotBackgroundTask(entry) {
1206
+ return {
1207
+ taskId: entry.taskId,
1208
+ running: !entry.done,
1209
+ stopped: entry.stopped,
1210
+ ...(entry.result !== undefined ? { result: entry.result } : {}),
1211
+ ...(entry.error !== undefined ? { error: entry.error } : {}),
1212
+ durationMs: Date.now() - entry.startedAt,
1213
+ };
1214
+ }
1215
+ function wakeBackgroundTaskWaiters(entry) {
1216
+ for (const wake of entry.waiters.splice(0))
1217
+ wake();
1218
+ }
1219
+ function pruneBackgroundTasks() {
1220
+ if (backgroundTasks.size <= MAX_BACKGROUND_TASKS)
1221
+ return;
1222
+ for (const [id, entry] of backgroundTasks) {
1223
+ if (!entry.done)
1224
+ continue;
1225
+ backgroundTasks.delete(id);
1226
+ if (backgroundTasks.size <= MAX_BACKGROUND_TASKS)
1227
+ return;
1228
+ }
1229
+ }
1230
+ function waitForBackgroundTask(entry, waitMs) {
1231
+ if (entry.done || waitMs <= 0)
1232
+ return Promise.resolve();
1233
+ return new Promise((resolvePromise) => {
1234
+ let settled = false;
1235
+ const finish = () => {
1236
+ if (settled)
1237
+ return;
1238
+ settled = true;
1239
+ clearTimeout(timer);
1240
+ const index = entry.waiters.indexOf(finish);
1241
+ if (index >= 0)
1242
+ entry.waiters.splice(index, 1);
1243
+ resolvePromise();
1244
+ };
1245
+ const timer = setTimeout(finish, waitMs);
1246
+ timer.unref?.();
1247
+ entry.waiters.push(finish);
1248
+ });
1249
+ }
1250
+ function startBackgroundTask(runTask, input, parentSignal) {
1251
+ const controller = new AbortController();
1252
+ const taskId = `agent-${Date.now().toString(36)}-${(++backgroundTaskSeq).toString(36)}-${randomBytes(3).toString("hex")}`;
1253
+ const entry = {
1254
+ taskId,
1255
+ startedAt: Date.now(),
1256
+ controller,
1257
+ done: false,
1258
+ stopped: false,
1259
+ waiters: [],
1260
+ promise: Promise.resolve(),
1261
+ };
1262
+ const signal = parentSignal
1263
+ ? AbortSignal.any([parentSignal, controller.signal])
1264
+ : controller.signal;
1265
+ entry.promise = runTask(input, signal)
1266
+ .then((result) => {
1267
+ entry.result = result;
1268
+ })
1269
+ .catch((error) => {
1270
+ entry.error = error instanceof Error ? error.message : String(error);
1271
+ })
1272
+ .finally(() => {
1273
+ entry.done = true;
1274
+ wakeBackgroundTaskWaiters(entry);
1275
+ });
1276
+ backgroundTasks.set(taskId, entry);
1277
+ pruneBackgroundTasks();
1278
+ return { taskId, running: true };
1279
+ }
1280
+ export async function taskOutputForTool(input) {
1281
+ const entry = backgroundTasks.get(input.taskId);
1282
+ if (!entry)
1283
+ throw new Error(`未知的后台子代理:${input.taskId}`);
1284
+ await waitForBackgroundTask(entry, normalizeTaskOutputWaitMs(input.waitMs));
1285
+ return snapshotBackgroundTask(entry);
1286
+ }
1287
+ export async function taskStopForTool(input) {
1288
+ const entry = backgroundTasks.get(input.taskId);
1289
+ if (!entry)
1290
+ throw new Error(`未知的后台子代理:${input.taskId}`);
1291
+ if (!entry.done) {
1292
+ entry.stopped = true;
1293
+ entry.controller.abort(TASK_STOP_ABORT_REASON);
1294
+ await entry.promise;
1295
+ }
1296
+ return snapshotBackgroundTask(entry);
1297
+ }
1298
+ /** 中止并清空全部后台子代理句柄(测试收尾或进程退出)。 */
1299
+ export function killAllBackgroundTasks() {
1300
+ for (const entry of backgroundTasks.values()) {
1301
+ if (!entry.done)
1302
+ entry.controller.abort();
1303
+ }
1304
+ backgroundTasks.clear();
1305
+ }
1185
1306
  export function createBuiltinFileTools(cwd, options = {}) {
1186
1307
  const gate = options.permissionGate;
1187
1308
  const runTask = options.runTask;
@@ -1334,13 +1455,23 @@ export function createBuiltinFileTools(cwd, options = {}) {
1334
1455
  execute: (input) => commandKillForTool(input.taskId),
1335
1456
  }),
1336
1457
  task: tool({
1337
- description: "把独立子任务委派给子代理执行:子代理使用子模型、拥有独立上下文,不污染主对话上下文。适合边界清晰、可独立交付的调研/代码生成子任务(如扫描整个仓库、阅读长文档、生成独立模块)。子代理不能再次委派任务(禁止嵌套),结果会截断回传。",
1458
+ description: `把独立子任务委派给子代理执行:子代理使用子模型、拥有独立上下文,不污染主对话上下文。可用 maxSteps 指定 ${MIN_TASK_MAX_STEPS}-${MAX_TASK_MAX_STEPS} 步预算。默认同步等待;runInBackground=true 时立即返回 taskId,之后用 task_output 查看或 task_stop 要求其停止调查并总结。子代理不能再次委派任务(禁止嵌套),结果会截断回传。`,
1338
1459
  inputSchema: jsonSchema({
1339
1460
  type: "object",
1340
1461
  additionalProperties: false,
1341
1462
  properties: {
1342
1463
  description: { type: "string", description: "子任务描述:目标、约束与交付物。请写清楚子代理需要返回什么。" },
1343
1464
  cwd: { type: "string", description: "可选子任务工作目录(绝对路径或相对主会话 cwd),默认继承主会话工作目录。" },
1465
+ maxSteps: {
1466
+ type: "number",
1467
+ minimum: MIN_TASK_MAX_STEPS,
1468
+ maximum: MAX_TASK_MAX_STEPS,
1469
+ description: `可选的子代理最大步骤数,范围 ${MIN_TASK_MAX_STEPS}-${MAX_TASK_MAX_STEPS};复杂全仓调查应显式提高。`,
1470
+ },
1471
+ runInBackground: {
1472
+ type: "boolean",
1473
+ description: "设为 true 后立即返回 taskId,使主代理能继续工作、查看进度或主动结束该子代理。",
1474
+ },
1344
1475
  },
1345
1476
  required: ["description"],
1346
1477
  }),
@@ -1348,10 +1479,43 @@ export function createBuiltinFileTools(cwd, options = {}) {
1348
1479
  if (!runTask) {
1349
1480
  throw new Error("task 工具不可用:当前环境未启用子代理执行器");
1350
1481
  }
1351
- const result = await runTask(input, execOptions.abortSignal);
1482
+ if (input.runInBackground === true) {
1483
+ return startBackgroundTask(runTask, input, execOptions?.abortSignal);
1484
+ }
1485
+ const result = await runTask(input, execOptions?.abortSignal);
1352
1486
  return { result };
1353
1487
  },
1354
1488
  }),
1489
+ task_output: tool({
1490
+ description: "查看后台子代理的状态和最终总结。waitMs 可最多等待 30 秒;running=true 时稍后再查。",
1491
+ inputSchema: jsonSchema({
1492
+ type: "object",
1493
+ additionalProperties: false,
1494
+ properties: {
1495
+ taskId: { type: "string", description: "task(runInBackground=true) 返回的子代理句柄。" },
1496
+ waitMs: {
1497
+ type: "number",
1498
+ minimum: 0,
1499
+ maximum: MAX_TASK_OUTPUT_WAIT_MS,
1500
+ description: `可选等待毫秒数,最大 ${MAX_TASK_OUTPUT_WAIT_MS}。`,
1501
+ },
1502
+ },
1503
+ required: ["taskId"],
1504
+ }),
1505
+ execute: (input) => taskOutputForTool(input),
1506
+ }),
1507
+ task_stop: tool({
1508
+ description: "结束一个后台子代理。子代理会停止继续调查,并基于停止前已收集的证据生成最终总结后返回。",
1509
+ inputSchema: jsonSchema({
1510
+ type: "object",
1511
+ additionalProperties: false,
1512
+ properties: {
1513
+ taskId: { type: "string", description: "task(runInBackground=true) 返回的子代理句柄。" },
1514
+ },
1515
+ required: ["taskId"],
1516
+ }),
1517
+ execute: (input) => taskStopForTool(input),
1518
+ }),
1355
1519
  edit_file: tool({
1356
1520
  description: "通过精确的 oldText -> newText 替换编辑现有 UTF-8 文本文件。可行时使用 SHA-256 前置条件以避免覆盖并发编辑。",
1357
1521
  inputSchema: jsonSchema({
@@ -13,7 +13,7 @@ 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, MAX_TASK_OUTPUT_CHARS } from "./file-tools.js";
16
+ import { createBuiltinFileTools, MAX_TASK_MAX_STEPS, MAX_TASK_OUTPUT_CHARS, MIN_TASK_MAX_STEPS, TASK_STOP_ABORT_REASON, } from "./file-tools.js";
17
17
  import { PermissionGate } from "./permissions.js";
18
18
  import { hasMalformedToolProtocolText, TOOL_PROTOCOL_RECOVERY_PROMPT, } from "./tool-protocol.js";
19
19
  import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
@@ -88,8 +88,26 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
88
88
  export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
89
89
  const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
90
90
  const OPENAI_COMPATIBLE_PROVIDER_NAME = "deepccc";
91
- /** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
92
- const TASK_MAX_STEPS = 20;
91
+ /** task 子代理工具:未指定 maxSteps 时的默认步骤预算。 */
92
+ const DEFAULT_TASK_MAX_STEPS = 20;
93
+ const MAX_TASK_SUMMARY_OUTPUT_TOKENS = 16_384;
94
+ const TASK_FINAL_SUMMARY_SYSTEM_PROMPT = [
95
+ "你是 DeepCCC 子代理的最终总结器。",
96
+ "只能依据消息中已经收集的证据作答,不得调用工具、继续调查或引入新事实。",
97
+ "直接交付原子任务要求的最终结果;说明关键证据、结论、局限和未完成项。",
98
+ "不要输出“我将开始”“接下来调查”等过程性开场白。",
99
+ ].join("\n");
100
+ function normalizeTaskMaxSteps(value) {
101
+ if (value === undefined)
102
+ return DEFAULT_TASK_MAX_STEPS;
103
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
104
+ throw new Error("task maxSteps 必须是整数");
105
+ }
106
+ if (value < MIN_TASK_MAX_STEPS || value > MAX_TASK_MAX_STEPS) {
107
+ throw new Error(`task maxSteps 必须在 ${MIN_TASK_MAX_STEPS}-${MAX_TASK_MAX_STEPS} 之间`);
108
+ }
109
+ return value;
110
+ }
93
111
  /** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
94
112
  function truncateTaskOutput(text) {
95
113
  if (text.length <= MAX_TASK_OUTPUT_CHARS)
@@ -283,6 +301,43 @@ export class ChatSession {
283
301
  customSystemPrompt;
284
302
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
285
303
  systemPrompt = "";
304
+ /** 最近一轮底层模型的结束原因;用于识别 step 上限截停在工具调用之后的情况。 */
305
+ lastTurnFinishReason;
306
+ async summarizeTaskResult(child, reason) {
307
+ const summaryController = new AbortController();
308
+ const timeout = setTimeout(() => summaryController.abort(), this.compactionTimeoutMs);
309
+ timeout.unref?.();
310
+ try {
311
+ const reasonText = reason === "step_limit"
312
+ ? "子代理已用完步骤预算"
313
+ : reason === "stopped"
314
+ ? "主代理已要求子代理停止继续调查"
315
+ : "子代理已达到执行时限";
316
+ const result = await generateText({
317
+ model: child.model,
318
+ system: TASK_FINAL_SUMMARY_SYSTEM_PROMPT,
319
+ messages: [
320
+ ...child.context.buildModelMessages(),
321
+ {
322
+ role: "user",
323
+ content: `${reasonText}。不要再调用工具;请立即根据以上已有证据生成最终总结。`,
324
+ },
325
+ ],
326
+ abortSignal: summaryController.signal,
327
+ temperature: 0,
328
+ maxOutputTokens: Math.min(child.maxOutputTokens ?? MAX_TASK_SUMMARY_OUTPUT_TOKENS, MAX_TASK_SUMMARY_OUTPUT_TOKENS),
329
+ providerOptions: child.provider === "openai"
330
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: "none" } }
331
+ : { anthropic: { effort: "low" } },
332
+ });
333
+ if (!result.text.trim())
334
+ throw new Error("task 子代理最终总结为空");
335
+ return result.text;
336
+ }
337
+ finally {
338
+ clearTimeout(timeout);
339
+ }
340
+ }
286
341
  /**
287
342
  * task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
288
343
  * 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
@@ -290,8 +345,9 @@ export class ChatSession {
290
345
  runTask = async (input, signal) => {
291
346
  const rawCwd = input.cwd?.trim();
292
347
  const taskCwd = rawCwd ? (isAbsolute(rawCwd) ? rawCwd : resolve(this.cwd, rawCwd)) : this.cwd;
348
+ const maxSteps = normalizeTaskMaxSteps(input.maxSteps);
293
349
  const timeoutController = new AbortController();
294
- const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
350
+ const timeout = setTimeout(() => timeoutController.abort("deepccc_task_timeout"), this.compactionTimeoutMs);
295
351
  timeout.unref?.();
296
352
  const taskSignal = signal
297
353
  ? AbortSignal.any([signal, timeoutController.signal])
@@ -309,7 +365,7 @@ export class ChatSession {
309
365
  persist: false,
310
366
  permissionMode: this.permissionMode,
311
367
  permissionResolver: this.permissionResolver,
312
- maxSteps: TASK_MAX_STEPS,
368
+ maxSteps,
313
369
  compactionTimeoutMs: this.compactionTimeoutMs,
314
370
  skillsDirs: this.skillDirs.map((s) => s.dir),
315
371
  });
@@ -322,6 +378,14 @@ export class ChatSession {
322
378
  throw new Error(`task 子代理执行失败: ${event.message}`);
323
379
  }
324
380
  }
381
+ const explicitlyStopped = signal?.aborted === true && signal.reason === TASK_STOP_ABORT_REASON;
382
+ const parentCancelled = signal?.aborted === true && !explicitlyStopped;
383
+ const timedOut = timeoutController.signal.aborted;
384
+ const stepLimitReached = child.lastTurnFinishReason === "tool-calls";
385
+ if (!parentCancelled && (explicitlyStopped || timedOut || stepLimitReached)) {
386
+ const reason = explicitlyStopped ? "stopped" : timedOut ? "timeout" : "step_limit";
387
+ full = await this.summarizeTaskResult(child, reason);
388
+ }
325
389
  if (!full.trim())
326
390
  return "(子代理未返回文本内容)";
327
391
  return applyPrivacy(truncateTaskOutput(full));
@@ -411,6 +475,7 @@ export class ChatSession {
411
475
  return systemContent.join("\n");
412
476
  }
413
477
  async *chat(userMessage, signal, drainInput) {
478
+ this.lastTurnFinishReason = undefined;
414
479
  this.context.appendMessage({ role: "user", content: userMessage });
415
480
  let fullText = "";
416
481
  let safeAccumulated = "";
@@ -664,6 +729,7 @@ export class ChatSession {
664
729
  if (!fullText.trim() && toolCallOrder.length === 0)
665
730
  throw new Error("DeepCCC 本轮未产生有效回复");
666
731
  }
732
+ this.lastTurnFinishReason = finishReason;
667
733
  if (hasMalformedToolProtocolText(fullText)) {
668
734
  console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
669
735
  + `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.292",
3
+ "version": "0.2.294",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",