deepccc 0.2.10 → 0.2.12

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
@@ -8,6 +8,8 @@ DeepCCC 是一个本地优先的开源 Coding Agent,同时提供浏览器多
8
8
 
9
9
  ## 安装与快速开始
10
10
 
11
+ 模型原生流未收到完成事件、返回错误结束原因或达到输出长度限制时,会报告异常并保留已经输出的内容,不将不完整回复判为成功。没有回复且没有工具执行的空结果也会明确报错。用户主动取消仍按中断处理。
12
+
11
13
  全局安装:
12
14
 
13
15
  ```bash
package/dist/context.js CHANGED
@@ -392,38 +392,35 @@ function replayStructuredAssistantMessage(message, messageIndex) {
392
392
  ];
393
393
  const messages = [];
394
394
  let assistantParts = [];
395
- let toolParts = [];
396
- const pendingCalls = new Map();
397
- const knownNames = new Map();
395
+ const batchCalls = new Map();
396
+ const batchResults = new Map();
398
397
  const emittedCallIds = new Set();
399
- const flushAssistant = () => {
400
- if (!assistantParts.length)
398
+ const completeToolBatch = () => {
399
+ if (assistantParts.length > 0) {
400
+ messages.push({ role: "assistant", content: assistantParts });
401
+ assistantParts = [];
402
+ }
403
+ if (batchCalls.size === 0) {
404
+ batchResults.clear();
401
405
  return;
402
- messages.push({ role: "assistant", content: assistantParts });
403
- assistantParts = [];
404
- };
405
- const completeToolStep = () => {
406
- // OpenAI/LiteLLM requires every tool message to immediately follow the
407
- // assistant message that declared all matching tool_calls for that step.
408
- flushAssistant();
409
- for (const [toolCallId, toolName] of pendingCalls) {
410
- toolParts.push({
406
+ }
407
+ const toolParts = [];
408
+ for (const [toolCallId, toolName] of batchCalls) {
409
+ toolParts.push(batchResults.get(toolCallId) ?? {
411
410
  type: "tool-result",
412
411
  toolCallId,
413
412
  toolName,
414
413
  output: { type: "text", value: "[tool execution interrupted; result unavailable]" },
415
414
  });
416
415
  }
417
- pendingCalls.clear();
418
- if (!toolParts.length)
419
- return;
420
416
  messages.push({ role: "tool", content: toolParts });
421
- toolParts = [];
417
+ batchCalls.clear();
418
+ batchResults.clear();
422
419
  };
423
420
  for (const entry of timeline) {
424
421
  if (entry.type === "text") {
425
- if (pendingCalls.size > 0 || toolParts.length > 0)
426
- completeToolStep();
422
+ if (batchCalls.size > 0 || batchResults.size > 0)
423
+ completeToolBatch();
427
424
  const previous = assistantParts[assistantParts.length - 1];
428
425
  if (previous?.type === "text")
429
426
  previous.text += entry.text;
@@ -431,12 +428,13 @@ function replayStructuredAssistantMessage(message, messageIndex) {
431
428
  assistantParts.push({ type: "text", text: entry.text });
432
429
  }
433
430
  else if (entry.type === "tool_use") {
434
- // Consecutive tool calls belong to one assistant step. Only close the
435
- // previous step after at least one result has arrived.
436
- if (toolParts.length > 0)
437
- completeToolStep();
438
- knownNames.set(entry.id, entry.name);
439
- pendingCalls.set(entry.id, entry.name);
431
+ // Providers may interleave parallel tool results with later tool-call
432
+ // events from the same assistant step. Keep the whole contiguous tool
433
+ // segment together until text resumes; closing on the first result can
434
+ // emit a later real result as an orphaned `role: tool` message.
435
+ if (emittedCallIds.has(entry.id))
436
+ continue;
437
+ batchCalls.set(entry.id, entry.name);
440
438
  emittedCallIds.add(entry.id);
441
439
  assistantParts.push({
442
440
  type: "tool-call",
@@ -446,14 +444,12 @@ function replayStructuredAssistantMessage(message, messageIndex) {
446
444
  });
447
445
  }
448
446
  else {
449
- if (!emittedCallIds.has(entry.tool_use_id))
447
+ if (!batchCalls.has(entry.tool_use_id))
450
448
  continue;
451
- const toolName = entry.name ?? knownNames.get(entry.tool_use_id);
449
+ const toolName = entry.name ?? batchCalls.get(entry.tool_use_id);
452
450
  if (!toolName)
453
451
  continue;
454
- flushAssistant();
455
- pendingCalls.delete(entry.tool_use_id);
456
- toolParts.push({
452
+ batchResults.set(entry.tool_use_id, {
457
453
  type: "tool-result",
458
454
  toolCallId: entry.tool_use_id,
459
455
  toolName,
@@ -461,10 +457,7 @@ function replayStructuredAssistantMessage(message, messageIndex) {
461
457
  });
462
458
  }
463
459
  }
464
- if (pendingCalls.size > 0 || toolParts.length > 0)
465
- completeToolStep();
466
- else
467
- flushAssistant();
460
+ completeToolBatch();
468
461
  return messages;
469
462
  }
470
463
  function serializeMessagesForCompaction(messages) {
package/dist/index.js CHANGED
@@ -478,16 +478,25 @@ export class ChatSession {
478
478
  messages: attemptMessages,
479
479
  };
480
480
  let stream;
481
+ let requiresFinish = false;
482
+ let receivedFinish = false;
483
+ let finishReason;
481
484
  if (this.streaming) {
482
485
  const result = streamText(generationOptions);
486
+ requiresFinish = result.fullStream != null;
483
487
  stream = result.fullStream ?? textStreamToFullStream(result.textStream);
484
488
  }
485
489
  else {
486
490
  const result = await generateText(generationOptions);
491
+ finishReason = result.finishReason;
487
492
  stream = generateResultToFullStream(result);
488
493
  }
489
494
  for await (const part of stream) {
490
495
  rawLog?.writeLine(safeRawStreamJson(part));
496
+ if (part.type === "finish") {
497
+ receivedFinish = true;
498
+ finishReason = part.finishReason;
499
+ }
491
500
  if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
492
501
  // Reasoning content remains private. A throttled heartbeat is enough
493
502
  // for ChatCCC to distinguish active inference from a stalled stream.
@@ -561,6 +570,14 @@ export class ChatSession {
561
570
  throw new Error(message);
562
571
  }
563
572
  }
573
+ if (!signal?.aborted) {
574
+ if (requiresFinish && !receivedFinish)
575
+ throw new Error("DeepCCC 输出流中断:未收到模型完成事件,回复可能不完整");
576
+ if (finishReason === "error" || finishReason === "length")
577
+ throw new Error(`DeepCCC 未正常完成:finishReason=${finishReason},回复可能不完整`);
578
+ if (!fullText.trim() && toolCallOrder.length === 0)
579
+ throw new Error("DeepCCC 本轮未产生有效回复");
580
+ }
564
581
  if (hasMalformedToolProtocolText(fullText)) {
565
582
  console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
566
583
  + `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [