jinzd-ai-cli 0.1.53 → 0.1.55
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/CLAUDE.md
CHANGED
|
@@ -350,6 +350,68 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
350
350
|
- [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
|
|
351
351
|
- [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
|
|
352
352
|
|
|
353
|
+
## 本轮开发完成记录(2026-03-09,v0.1.53 → v0.1.54)
|
|
354
|
+
|
|
355
|
+
### 新增功能:Streaming Tool Use — agentic 循环流式工具调用
|
|
356
|
+
|
|
357
|
+
**背景**:`handleChatWithTools()` agentic 循环中,每轮调用 `provider.chatWithTools()` 使用 `stream: false`,必须等待 API 返回完整响应后才开始处理。AI 生成工具调用参数期间(5-30 秒),用户只看到 spinner,无任何内容反馈。
|
|
358
|
+
|
|
359
|
+
**设计决策**:
|
|
360
|
+
1. 新增 `chatWithToolsStream()` 方法,不修改现有 `chatWithTools()`
|
|
361
|
+
2. 统一事件模型 `AsyncGenerator<ToolStreamEvent>`(8 种事件类型)
|
|
362
|
+
3. 等待全部工具调用完成后再执行(不做 early execution)
|
|
363
|
+
4. Phase 1 覆盖:OpenAI + Zhipu(继承基类)+ Claude;DeepSeek/Kimi 继续用非流式(虚假声明检测需完整响应)
|
|
364
|
+
|
|
365
|
+
**变更文件**:
|
|
366
|
+
|
|
367
|
+
| 文件 | 变更类型 | 说明 |
|
|
368
|
+
|------|---------|------|
|
|
369
|
+
| `src/core/types.ts` | 修改 | 新增 `ToolStreamEvent` 联合类型(8 变体)+ `StreamedToolCallResult` 接口 |
|
|
370
|
+
| `src/providers/base.ts` | 修改 | 新增可选方法 `chatWithToolsStream?()` + 导入 `ToolStreamEvent`/`ToolDefinition` |
|
|
371
|
+
| `src/providers/openai-compatible.ts` | 修改 | 新增 `enableStreamingToolCalls` 标志 + `chatWithToolsStream()` 完整实现(~140 行) |
|
|
372
|
+
| `src/providers/claude.ts` | 修改 | 新增 `chatWithToolsStream()` 实现(~120 行),收集 `rawContentBlocks` 用于 `buildToolResultMessages` |
|
|
373
|
+
| `src/providers/deepseek.ts` | 修改 | `enableStreamingToolCalls = false`(虚假声明检测需完整响应) |
|
|
374
|
+
| `src/providers/kimi.ts` | 修改 | `enableStreamingToolCalls = false`(XML 伪调用 + 虚假声明检测需完整响应) |
|
|
375
|
+
| `src/repl/repl.ts` | 修改 | 新增 `consumeToolStream()` 方法 + `handleChatWithTools()` 流式/非流式双路径 |
|
|
376
|
+
| `src/repl/renderer.ts` | 修改 | `/about` 新增特性条目 |
|
|
377
|
+
| `src/core/constants.ts` | 修改 | VERSION 0.1.53 → 0.1.54 |
|
|
378
|
+
| `package.json` | 修改 | version 0.1.53 → 0.1.54 |
|
|
379
|
+
|
|
380
|
+
**实现细节**:
|
|
381
|
+
|
|
382
|
+
*类型层*(`types.ts`):
|
|
383
|
+
- `ToolStreamEvent`:`text_delta` / `thinking_start` / `thinking_delta` / `thinking_end` / `tool_call_start` / `tool_call_delta` / `tool_call_end` / `done`
|
|
384
|
+
- `StreamedToolCallResult`:`textContent` + `toolCalls` + `usage` + `rawContent`(Claude 专用)
|
|
385
|
+
|
|
386
|
+
*OpenAI 兼容 Provider*(`openai-compatible.ts`):
|
|
387
|
+
- `enableStreamingToolCalls = true` 保护标志,子类可 override 为 false 禁用
|
|
388
|
+
- 流式路径:`stream: true` + `stream_options: { include_usage: true }`
|
|
389
|
+
- `delta.tool_calls[i]` 首次出现(含 `id`+`name`)发 `tool_call_start`,后续发 `tool_call_delta`
|
|
390
|
+
- 非流式降级路径:`enableStreamingToolCalls = false` 时调用 `chatWithTools()` 并转换为事件序列
|
|
391
|
+
|
|
392
|
+
*Claude Provider*(`claude.ts`):
|
|
393
|
+
- 使用 `this.client.messages.stream()` + AbortSignal 支持
|
|
394
|
+
- 收集 `rawContentBlocks` 数组,通过 `done` 事件的 `rawContent` 传递给 `buildToolResultMessages`
|
|
395
|
+
- 事件映射:`content_block_start`(tool_use) → `tool_call_start`;`input_json_delta` → `tool_call_delta`;`content_block_stop` → `tool_call_end`
|
|
396
|
+
- thinking 块正确处理(`thinking_start`/`thinking_delta`/`thinking_end`)
|
|
397
|
+
|
|
398
|
+
*REPL 层*(`repl.ts`):
|
|
399
|
+
- `consumeToolStream()`:消费事件生成器,`text_delta` 实时输出到 stdout(停 spinner),`tool_call_start` 显示 `⚙ Streaming: <name>...`,累积 arguments JSON 碎片,`tool_call_end` 时 JSON.parse
|
|
400
|
+
- `handleChatWithTools()` 循环:检测 `supportsStreamingTools`(`useStreaming && typeof provider.chatWithToolsStream === 'function'`),流式路径用 `setupStreamInterrupt()`/`teardownStreamInterrupt()` 包裹支持 Escape/Ctrl+C 中断
|
|
401
|
+
- `alreadyRendered` 标志防止文本内容双重渲染
|
|
402
|
+
|
|
403
|
+
**用户体验对比**:
|
|
404
|
+
- Before:Spinner 等待 10-30 秒 → 突然全部出现
|
|
405
|
+
- After:短暂 Spinner → 文本实时流出 → 工具名即时显示 → 参数完整后执行
|
|
406
|
+
|
|
407
|
+
### 版本与收尾
|
|
408
|
+
- `src/core/constants.ts`:VERSION `0.1.53` → `0.1.54`
|
|
409
|
+
- `package.json`:version 同步
|
|
410
|
+
- 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
|
|
411
|
+
- 发布:`npm publish` → `jinzd-ai-cli@0.1.54`
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
353
415
|
## 本轮开发完成记录(2026-03-08,v0.1.51 → v0.1.52)
|
|
354
416
|
|
|
355
417
|
### Bug 修复:DeepSeek 虚假完成声明(方案 C)+ 虚假声明检测共享重构
|
|
@@ -606,9 +668,9 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
606
668
|
| `package.json` | 修改 | version 0.1.38 → 0.1.40 |
|
|
607
669
|
|
|
608
670
|
### 下一步建议
|
|
609
|
-
1.
|
|
610
|
-
2.
|
|
611
|
-
3.
|
|
671
|
+
1. ~~**`/config set` 快捷配置**~~:✅ 已实现(v0.1.49)
|
|
672
|
+
2. ~~**流式工具调用(Streaming Tool Use)**~~:✅ 已在 v0.1.54 实现(OpenAI/Claude 流式 + DeepSeek/Kimi 非流式降级)
|
|
673
|
+
3. ~~**`/diff` 命令**~~:✅ 已实现(v0.1.49)
|
|
612
674
|
|
|
613
675
|
---
|
|
614
676
|
|
|
@@ -704,10 +766,10 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
704
766
|
|
|
705
767
|
### 下一步建议
|
|
706
768
|
|
|
707
|
-
#### Tier 1 —
|
|
708
|
-
1.
|
|
709
|
-
2.
|
|
710
|
-
3.
|
|
769
|
+
#### Tier 1 — 高价值功能(已全部完成)
|
|
770
|
+
1. ~~**`/config set` 快捷配置**~~:✅ 已实现(v0.1.49)
|
|
771
|
+
2. ~~**流式工具调用(Streaming Tool Use)**~~:✅ 已在 v0.1.54 实现(OpenAI/Claude 流式 + DeepSeek/Kimi 非流式降级)
|
|
772
|
+
3. ~~**`/diff` 命令**~~:✅ 已实现(v0.1.49)
|
|
711
773
|
|
|
712
774
|
#### Tier 2 — 体验增强
|
|
713
775
|
4. ~~**L1 低危**~~:✅ 已在 v0.1.50 修复(safeReadPackageJson + detectNodeTestFramework)
|
|
@@ -8,7 +8,7 @@ import { platform } from "os";
|
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
|
|
10
10
|
// src/core/constants.ts
|
|
11
|
-
var VERSION = "0.1.
|
|
11
|
+
var VERSION = "0.1.55";
|
|
12
12
|
var APP_NAME = "ai-cli";
|
|
13
13
|
var CONFIG_DIR_NAME = ".aicli";
|
|
14
14
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -73,6 +73,12 @@ var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
|
|
|
73
73
|
]);
|
|
74
74
|
var CONTEXT_PRESSURE_THRESHOLD = 0.8;
|
|
75
75
|
var TEST_TIMEOUT = 3e5;
|
|
76
|
+
var AGENTIC_BEHAVIOR_GUIDELINE = `# \u91CD\u8981\u884C\u4E3A\u51C6\u5219
|
|
77
|
+
|
|
78
|
+
**\u533A\u5206"\u7406\u89E3"\u4E0E"\u6267\u884C"**\uFF1A
|
|
79
|
+
- \u5F53\u7528\u6237\u8981\u6C42\u4F60"\u9605\u8BFB"\u3001"\u7406\u89E3"\u3001"\u4E86\u89E3"\u3001"\u5206\u6790"\u3001"\u5BA1\u67E5"\u3001"\u770B\u4E00\u4E0B"\u6587\u4EF6\u6216\u9879\u76EE\u65F6\uFF0C\u4F60\u7684\u4EFB\u52A1\u4EC5\u662F**\u8BFB\u53D6\u5E76\u603B\u7ED3**\uFF0C\u7136\u540E\u7B49\u5F85\u7528\u6237\u7684\u4E0B\u4E00\u6B65\u6307\u793A\u3002\u4E0D\u8981\u81EA\u52A8\u5F00\u59CB\u6267\u884C\u9879\u76EE\u4E2D\u63CF\u8FF0\u7684\u4EFB\u52A1\u3002
|
|
80
|
+
- \u53EA\u6709\u5F53\u7528\u6237**\u660E\u786E\u8981\u6C42**\u4F60\u6267\u884C\u67D0\u4E2A\u64CD\u4F5C\uFF08\u5982"\u751F\u6210"\u3001"\u521B\u5EFA"\u3001"\u4FEE\u6539"\u3001"\u8FD0\u884C"\u3001"\u5F00\u59CB"\u7B49\u52A8\u4F5C\u8BCD\uFF09\u65F6\uFF0C\u624D\u5F00\u59CB\u4F7F\u7528\u5199\u5165/\u6267\u884C\u7C7B\u5DE5\u5177\u3002
|
|
81
|
+
- \u5982\u679C\u4E0D\u786E\u5B9A\u7528\u6237\u610F\u56FE\uFF0C\u4F7F\u7528 ask_user \u5DE5\u5177\u5411\u7528\u6237\u786E\u8BA4\uFF0C\u800C\u4E0D\u662F\u81EA\u884C\u5047\u8BBE\u5E76\u6267\u884C\u3002`;
|
|
76
82
|
var AUTHOR = "\u664B\u6B63\u4E1C";
|
|
77
83
|
var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
|
|
78
84
|
var DESCRIPTION = "\u8DE8\u5E73\u53F0 REPL \u98CE\u683C AI \u5BF9\u8BDD\u5DE5\u5177\uFF0C\u652F\u6301\u591A Provider \u4E0E Agentic \u5DE5\u5177\u8C03\u7528";
|
|
@@ -457,6 +463,7 @@ export {
|
|
|
457
463
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
458
464
|
SUBAGENT_ALLOWED_TOOLS,
|
|
459
465
|
CONTEXT_PRESSURE_THRESHOLD,
|
|
466
|
+
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
460
467
|
AUTHOR,
|
|
461
468
|
AUTHOR_EMAIL,
|
|
462
469
|
DESCRIPTION,
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
3
4
|
APP_NAME,
|
|
4
5
|
AUTHOR,
|
|
5
6
|
AUTHOR_EMAIL,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
30
31
|
VERSION,
|
|
31
32
|
runTestsTool
|
|
32
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-OBC56PVG.js";
|
|
33
34
|
|
|
34
35
|
// src/index.ts
|
|
35
36
|
import { program } from "commander";
|
|
@@ -632,6 +633,111 @@ var ClaudeProvider = class extends BaseProvider {
|
|
|
632
633
|
throw this.wrapError(err);
|
|
633
634
|
}
|
|
634
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* 流式工具调用:文本/thinking 实时输出、工具名称/参数逐块发射。
|
|
638
|
+
* 同时收集原始 content blocks 供 buildToolResultMessages 使用。
|
|
639
|
+
*/
|
|
640
|
+
async *chatWithToolsStream(request, tools) {
|
|
641
|
+
const anthropicTools = tools.map((t) => ({
|
|
642
|
+
name: t.name,
|
|
643
|
+
description: t.description,
|
|
644
|
+
input_schema: {
|
|
645
|
+
type: "object",
|
|
646
|
+
properties: Object.fromEntries(
|
|
647
|
+
Object.entries(t.parameters).map(([key, schema]) => [
|
|
648
|
+
key,
|
|
649
|
+
schemaToJsonSchema(schema)
|
|
650
|
+
])
|
|
651
|
+
),
|
|
652
|
+
required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
|
|
653
|
+
}
|
|
654
|
+
}));
|
|
655
|
+
const baseMessages = request.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: this.contentToClaudeParts(m.content) }));
|
|
656
|
+
const extraMessages = request._extraMessages ?? [];
|
|
657
|
+
const allMessages = [...baseMessages, ...extraMessages];
|
|
658
|
+
const { thinking, temperature } = this.buildThinkingParams(request);
|
|
659
|
+
try {
|
|
660
|
+
const stream = this.client.messages.stream({
|
|
661
|
+
model: request.model,
|
|
662
|
+
messages: allMessages,
|
|
663
|
+
tools: anthropicTools,
|
|
664
|
+
system: request.systemPrompt,
|
|
665
|
+
max_tokens: request.maxTokens ?? 8192,
|
|
666
|
+
temperature,
|
|
667
|
+
thinking
|
|
668
|
+
}, { signal: request.signal });
|
|
669
|
+
let currentBlockType = null;
|
|
670
|
+
let currentToolIndex = 0;
|
|
671
|
+
const rawContentBlocks = [];
|
|
672
|
+
let currentBlockData = {};
|
|
673
|
+
for await (const event of stream) {
|
|
674
|
+
if (event.type === "content_block_start") {
|
|
675
|
+
const block = event.content_block;
|
|
676
|
+
currentBlockType = block.type;
|
|
677
|
+
currentBlockData = { type: block.type };
|
|
678
|
+
if (block.type === "thinking") {
|
|
679
|
+
yield { type: "thinking_start" };
|
|
680
|
+
currentBlockData = { type: "thinking", thinking: "", signature: "" };
|
|
681
|
+
} else if (block.type === "text") {
|
|
682
|
+
currentBlockData = { type: "text", text: "" };
|
|
683
|
+
} else if (block.type === "tool_use") {
|
|
684
|
+
const tuBlock = block;
|
|
685
|
+
yield {
|
|
686
|
+
type: "tool_call_start",
|
|
687
|
+
index: currentToolIndex,
|
|
688
|
+
id: tuBlock.id,
|
|
689
|
+
name: tuBlock.name
|
|
690
|
+
};
|
|
691
|
+
currentBlockData = { type: "tool_use", id: tuBlock.id, name: tuBlock.name, input: {} };
|
|
692
|
+
} else if (block.type === "redacted_thinking") {
|
|
693
|
+
currentBlockData = { type: "redacted_thinking", data: block.data };
|
|
694
|
+
}
|
|
695
|
+
} else if (event.type === "content_block_delta") {
|
|
696
|
+
if (event.delta.type === "text_delta") {
|
|
697
|
+
yield { type: "text_delta", delta: event.delta.text };
|
|
698
|
+
currentBlockData.text += event.delta.text;
|
|
699
|
+
} else if (event.delta.type === "thinking_delta") {
|
|
700
|
+
const thinkingDelta = event.delta.thinking;
|
|
701
|
+
yield { type: "thinking_delta", delta: thinkingDelta };
|
|
702
|
+
currentBlockData.thinking += thinkingDelta;
|
|
703
|
+
} else if (event.delta.type === "input_json_delta") {
|
|
704
|
+
const jsonDelta = event.delta.partial_json;
|
|
705
|
+
yield {
|
|
706
|
+
type: "tool_call_delta",
|
|
707
|
+
index: currentToolIndex,
|
|
708
|
+
argumentsDelta: jsonDelta
|
|
709
|
+
};
|
|
710
|
+
} else if (event.delta.type === "signature_delta") {
|
|
711
|
+
currentBlockData.signature += event.delta.signature ?? "";
|
|
712
|
+
}
|
|
713
|
+
} else if (event.type === "content_block_stop") {
|
|
714
|
+
if (currentBlockType === "thinking") {
|
|
715
|
+
yield { type: "thinking_end" };
|
|
716
|
+
} else if (currentBlockType === "tool_use") {
|
|
717
|
+
yield { type: "tool_call_end", index: currentToolIndex };
|
|
718
|
+
currentToolIndex++;
|
|
719
|
+
}
|
|
720
|
+
rawContentBlocks.push(currentBlockData);
|
|
721
|
+
currentBlockType = null;
|
|
722
|
+
currentBlockData = {};
|
|
723
|
+
} else if (event.type === "message_delta") {
|
|
724
|
+
const usage = event.usage;
|
|
725
|
+
if (usage) {
|
|
726
|
+
yield {
|
|
727
|
+
type: "done",
|
|
728
|
+
usage: {
|
|
729
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
730
|
+
outputTokens: usage.output_tokens ?? 0
|
|
731
|
+
},
|
|
732
|
+
rawContent: rawContentBlocks
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch (err) {
|
|
738
|
+
throw this.wrapError(err);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
635
741
|
buildToolResultMessages(assistantToolCalls, results) {
|
|
636
742
|
const rawContent = assistantToolCalls._rawContent;
|
|
637
743
|
let assistantContent;
|
|
@@ -989,6 +1095,8 @@ var OpenAICompatibleProvider = class extends BaseProvider {
|
|
|
989
1095
|
client;
|
|
990
1096
|
defaultTimeout = 6e4;
|
|
991
1097
|
// ms
|
|
1098
|
+
/** 子类设为 false 可禁用流式工具调用(虚假声明检测需要完整响应) */
|
|
1099
|
+
enableStreamingToolCalls = true;
|
|
992
1100
|
async initialize(apiKey, options) {
|
|
993
1101
|
if (options?.timeout !== void 0) {
|
|
994
1102
|
this.defaultTimeout = options.timeout;
|
|
@@ -1159,6 +1267,122 @@ var OpenAICompatibleProvider = class extends BaseProvider {
|
|
|
1159
1267
|
throw this.wrapError(err);
|
|
1160
1268
|
}
|
|
1161
1269
|
}
|
|
1270
|
+
/**
|
|
1271
|
+
* 流式工具调用:文本内容实时输出、工具名称/参数逐块发射。
|
|
1272
|
+
* 子类(DeepSeek / Kimi)因虚假声明检测需要完整响应,故不继承此方法。
|
|
1273
|
+
*/
|
|
1274
|
+
async *chatWithToolsStream(request, tools) {
|
|
1275
|
+
if (!this.enableStreamingToolCalls) {
|
|
1276
|
+
const result = await this.chatWithTools(request, tools);
|
|
1277
|
+
if ("toolCalls" in result) {
|
|
1278
|
+
for (let i = 0; i < result.toolCalls.length; i++) {
|
|
1279
|
+
const tc = result.toolCalls[i];
|
|
1280
|
+
yield { type: "tool_call_start", index: i, id: tc.id, name: tc.name };
|
|
1281
|
+
yield { type: "tool_call_delta", index: i, argumentsDelta: JSON.stringify(tc.arguments) };
|
|
1282
|
+
yield { type: "tool_call_end", index: i };
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
yield { type: "text_delta", delta: result.content };
|
|
1286
|
+
}
|
|
1287
|
+
yield { type: "done", usage: result.usage };
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const openaiTools = tools.map((t) => ({
|
|
1291
|
+
type: "function",
|
|
1292
|
+
function: {
|
|
1293
|
+
name: t.name,
|
|
1294
|
+
description: t.description,
|
|
1295
|
+
parameters: {
|
|
1296
|
+
type: "object",
|
|
1297
|
+
properties: Object.fromEntries(
|
|
1298
|
+
Object.entries(t.parameters).map(([key, schema]) => [
|
|
1299
|
+
key,
|
|
1300
|
+
schemaToJsonSchema(schema)
|
|
1301
|
+
])
|
|
1302
|
+
),
|
|
1303
|
+
required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}));
|
|
1307
|
+
const baseMessages = this.buildMessages(request);
|
|
1308
|
+
const extraMessages = request._extraMessages ?? [];
|
|
1309
|
+
const allMessages = [...baseMessages, ...extraMessages];
|
|
1310
|
+
try {
|
|
1311
|
+
const stream = await this.client.chat.completions.create({
|
|
1312
|
+
model: request.model,
|
|
1313
|
+
messages: allMessages,
|
|
1314
|
+
tools: openaiTools,
|
|
1315
|
+
tool_choice: "auto",
|
|
1316
|
+
temperature: request.temperature,
|
|
1317
|
+
max_tokens: request.maxTokens,
|
|
1318
|
+
stream: true,
|
|
1319
|
+
stream_options: { include_usage: true },
|
|
1320
|
+
...request.thinking ? { thinking: { type: "enabled" } } : {}
|
|
1321
|
+
}, {
|
|
1322
|
+
timeout: request.timeout ?? this.defaultTimeout,
|
|
1323
|
+
signal: request.signal
|
|
1324
|
+
});
|
|
1325
|
+
const toolCallAccumulators = /* @__PURE__ */ new Map();
|
|
1326
|
+
let toolCallsEnded = false;
|
|
1327
|
+
for await (const chunk of stream) {
|
|
1328
|
+
const choice = chunk.choices[0];
|
|
1329
|
+
if (!choice && chunk.usage) {
|
|
1330
|
+
if (!toolCallsEnded && toolCallAccumulators.size > 0) {
|
|
1331
|
+
for (const [idx] of toolCallAccumulators) {
|
|
1332
|
+
yield { type: "tool_call_end", index: idx };
|
|
1333
|
+
}
|
|
1334
|
+
toolCallsEnded = true;
|
|
1335
|
+
}
|
|
1336
|
+
yield {
|
|
1337
|
+
type: "done",
|
|
1338
|
+
usage: {
|
|
1339
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
1340
|
+
outputTokens: chunk.usage.completion_tokens
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (!choice) continue;
|
|
1346
|
+
const delta = choice.delta;
|
|
1347
|
+
if (delta?.content) {
|
|
1348
|
+
yield { type: "text_delta", delta: delta.content };
|
|
1349
|
+
}
|
|
1350
|
+
if (delta?.tool_calls) {
|
|
1351
|
+
for (const tc of delta.tool_calls) {
|
|
1352
|
+
const idx = tc.index;
|
|
1353
|
+
if (tc.id && tc.function?.name) {
|
|
1354
|
+
toolCallAccumulators.set(idx, {
|
|
1355
|
+
id: tc.id,
|
|
1356
|
+
name: tc.function.name,
|
|
1357
|
+
arguments: tc.function.arguments ?? ""
|
|
1358
|
+
});
|
|
1359
|
+
yield { type: "tool_call_start", index: idx, id: tc.id, name: tc.function.name };
|
|
1360
|
+
} else if (tc.function?.arguments) {
|
|
1361
|
+
const acc = toolCallAccumulators.get(idx);
|
|
1362
|
+
if (acc) {
|
|
1363
|
+
acc.arguments += tc.function.arguments;
|
|
1364
|
+
yield { type: "tool_call_delta", index: idx, argumentsDelta: tc.function.arguments };
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (choice.finish_reason && !toolCallsEnded && toolCallAccumulators.size > 0) {
|
|
1370
|
+
for (const [idx] of toolCallAccumulators) {
|
|
1371
|
+
yield { type: "tool_call_end", index: idx };
|
|
1372
|
+
}
|
|
1373
|
+
toolCallsEnded = true;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (!toolCallsEnded && toolCallAccumulators.size > 0) {
|
|
1377
|
+
for (const [idx] of toolCallAccumulators) {
|
|
1378
|
+
yield { type: "tool_call_end", index: idx };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
yield { type: "done" };
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
throw this.wrapError(err);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1162
1386
|
/**
|
|
1163
1387
|
* 将工具结果作为 tool_call 消息追加,供下一轮使用
|
|
1164
1388
|
*/
|
|
@@ -1257,6 +1481,8 @@ var DEEPSEEK_TOOL_CALL_REMINDER = `
|
|
|
1257
1481
|
\u5982\u679C\u9700\u8981\u751F\u6210\u591A\u4E2A\u6587\u4EF6\uFF0C\u5FC5\u987B\u5BF9\u6BCF\u4E2A\u6587\u4EF6\u5206\u522B\u8C03\u7528 write_file \u5DE5\u5177\uFF0C\u4E0D\u53EF\u7701\u7565\u4EFB\u4F55\u4E00\u4E2A\u3002`;
|
|
1258
1482
|
var DeepSeekProvider = class extends OpenAICompatibleProvider {
|
|
1259
1483
|
defaultBaseUrl = "https://api.deepseek.com/v1";
|
|
1484
|
+
// 禁用流式工具调用:DeepSeek 的虚假声明检测(方案 C)需要完整响应
|
|
1485
|
+
enableStreamingToolCalls = false;
|
|
1260
1486
|
info = {
|
|
1261
1487
|
id: "deepseek",
|
|
1262
1488
|
displayName: "DeepSeek",
|
|
@@ -1415,6 +1641,8 @@ var KIMI_TOOL_CALL_REMINDER = `
|
|
|
1415
1641
|
\u4EC5\u5728\u6587\u672C\u4E2D\u63CF\u8FF0\u6587\u4EF6\u5185\u5BB9\u800C\u4E0D\u8C03\u7528\u5DE5\u5177 = \u6587\u4EF6\u4E0D\u5B58\u5728 = \u4EFB\u52A1\u5931\u8D25\u3002`;
|
|
1416
1642
|
var KimiProvider = class extends OpenAICompatibleProvider {
|
|
1417
1643
|
defaultBaseUrl = "https://api.moonshot.ai/v1";
|
|
1644
|
+
// 禁用流式工具调用:Kimi 的 XML 伪调用检测(方案 A)和虚假声明检测(方案 C)需要完整响应
|
|
1645
|
+
enableStreamingToolCalls = false;
|
|
1418
1646
|
info = {
|
|
1419
1647
|
id: "kimi",
|
|
1420
1648
|
displayName: "Kimi (Moonshot AI)",
|
|
@@ -2402,6 +2630,7 @@ var Renderer = class {
|
|
|
2402
2630
|
console.log(feat("\u5DE5\u5177\u8C03\u7528\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u5316\uFF1A\u6A21\u62DF\u6253\u5B57\u673A\u6548\u679C\u9010\u5757\u8F93\u51FA\uFF0C\u96F6\u989D\u5916 API \u8C03\u7528\uFF0C\u652F\u6301 Escape \u4E2D\u65AD"));
|
|
2403
2631
|
console.log(feat("/diff \u547D\u4EE4\uFF1A\u663E\u793A\u5F53\u524D session \u5185\u6240\u6709\u6587\u4EF6\u4FEE\u6539\u7684\u6C47\u603B diff\uFF08\u5408\u5E76\u540C\u6587\u4EF6\u591A\u6B21\u4FEE\u6539\uFF09"));
|
|
2404
2632
|
console.log(feat("/fork \u5BF9\u8BDD\u5206\u652F\uFF1A\u4ECE\u5F53\u524D\u4F4D\u7F6E\u6216\u6307\u5B9A checkpoint \u5206\u53C9\u4E3A\u65B0 session\uFF0C\u63A2\u7D22\u4E0D\u540C\u65B9\u6848"));
|
|
2633
|
+
console.log(feat("Streaming Tool Use\uFF1Aagentic \u5FAA\u73AF\u4E2D\u6587\u672C\u5B9E\u65F6\u6D41\u51FA + \u5DE5\u5177\u540D\u79F0\u5373\u65F6\u663E\u793A\uFF08OpenAI/Claude\uFF09"));
|
|
2405
2634
|
console.log();
|
|
2406
2635
|
}
|
|
2407
2636
|
printPrompt(provider, _model) {
|
|
@@ -4363,7 +4592,7 @@ ${hint}` : "")
|
|
|
4363
4592
|
description: "Run project tests and show structured report",
|
|
4364
4593
|
usage: "/test [command|filter]",
|
|
4365
4594
|
async execute(args, _ctx) {
|
|
4366
|
-
const { executeTests } = await import("./run-tests-
|
|
4595
|
+
const { executeTests } = await import("./run-tests-JS64U54Q.js");
|
|
4367
4596
|
const argStr = args.join(" ").trim();
|
|
4368
4597
|
let testArgs = {};
|
|
4369
4598
|
if (argStr) {
|
|
@@ -9169,7 +9398,7 @@ ${projectContext}`);
|
|
|
9169
9398
|
const envInfo = `\u64CD\u4F5C\u7CFB\u7EDF\uFF1A${osName}
|
|
9170
9399
|
${shellInfo}
|
|
9171
9400
|
\u5DE5\u4F5C\u76EE\u5F55\uFF1A${process.cwd()}`;
|
|
9172
|
-
const parts = [dateTimeInfo + "\n" + envInfo];
|
|
9401
|
+
const parts = [dateTimeInfo + "\n" + envInfo, AGENTIC_BEHAVIOR_GUIDELINE];
|
|
9173
9402
|
const memory = this.loadMemoryContent();
|
|
9174
9403
|
if (memory) {
|
|
9175
9404
|
parts.push(`# Persistent Memory
|
|
@@ -9990,6 +10219,107 @@ Session '${this.resumeSessionId}' not found.
|
|
|
9990
10219
|
}
|
|
9991
10220
|
await this.checkContextPressure();
|
|
9992
10221
|
}
|
|
10222
|
+
/**
|
|
10223
|
+
* 消费流式工具调用事件生成器,实时渲染文本内容和工具名称,
|
|
10224
|
+
* 累积完整工具调用参数后返回结构化结果。
|
|
10225
|
+
*/
|
|
10226
|
+
async consumeToolStream(stream, spinner) {
|
|
10227
|
+
const textParts = [];
|
|
10228
|
+
const toolCallAccumulators = /* @__PURE__ */ new Map();
|
|
10229
|
+
let usage;
|
|
10230
|
+
let rawContent;
|
|
10231
|
+
let spinnerStopped = false;
|
|
10232
|
+
const stopSpinner = () => {
|
|
10233
|
+
if (!spinnerStopped) {
|
|
10234
|
+
spinner.stop();
|
|
10235
|
+
spinnerStopped = true;
|
|
10236
|
+
}
|
|
10237
|
+
};
|
|
10238
|
+
try {
|
|
10239
|
+
for await (const event of stream) {
|
|
10240
|
+
switch (event.type) {
|
|
10241
|
+
case "text_delta":
|
|
10242
|
+
stopSpinner();
|
|
10243
|
+
process.stdout.write(event.delta);
|
|
10244
|
+
textParts.push(event.delta);
|
|
10245
|
+
break;
|
|
10246
|
+
case "thinking_start":
|
|
10247
|
+
stopSpinner();
|
|
10248
|
+
process.stdout.write(theme.dim("<think>"));
|
|
10249
|
+
break;
|
|
10250
|
+
case "thinking_delta":
|
|
10251
|
+
break;
|
|
10252
|
+
case "thinking_end":
|
|
10253
|
+
process.stdout.write(theme.dim("</think>"));
|
|
10254
|
+
break;
|
|
10255
|
+
case "tool_call_start":
|
|
10256
|
+
stopSpinner();
|
|
10257
|
+
process.stdout.write(
|
|
10258
|
+
theme.dim(`
|
|
10259
|
+
\u2699 Streaming: `) + theme.toolCall(event.name) + theme.dim("...\n")
|
|
10260
|
+
);
|
|
10261
|
+
toolCallAccumulators.set(event.index, {
|
|
10262
|
+
id: event.id,
|
|
10263
|
+
name: event.name,
|
|
10264
|
+
arguments: ""
|
|
10265
|
+
});
|
|
10266
|
+
break;
|
|
10267
|
+
case "tool_call_delta": {
|
|
10268
|
+
const acc = toolCallAccumulators.get(event.index);
|
|
10269
|
+
if (acc) {
|
|
10270
|
+
acc.arguments += event.argumentsDelta;
|
|
10271
|
+
}
|
|
10272
|
+
break;
|
|
10273
|
+
}
|
|
10274
|
+
case "tool_call_end":
|
|
10275
|
+
break;
|
|
10276
|
+
case "done":
|
|
10277
|
+
if (event.usage) usage = event.usage;
|
|
10278
|
+
if (event.rawContent) rawContent = event.rawContent;
|
|
10279
|
+
break;
|
|
10280
|
+
}
|
|
10281
|
+
}
|
|
10282
|
+
} catch (err) {
|
|
10283
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"))) {
|
|
10284
|
+
stopSpinner();
|
|
10285
|
+
process.stdout.write(theme.dim("\n[interrupted]\n"));
|
|
10286
|
+
return {
|
|
10287
|
+
textContent: textParts.join(""),
|
|
10288
|
+
toolCalls: [],
|
|
10289
|
+
usage,
|
|
10290
|
+
rawContent
|
|
10291
|
+
};
|
|
10292
|
+
}
|
|
10293
|
+
throw err;
|
|
10294
|
+
}
|
|
10295
|
+
const toolCalls = [];
|
|
10296
|
+
for (const [, acc] of toolCallAccumulators) {
|
|
10297
|
+
let parsedArgs;
|
|
10298
|
+
try {
|
|
10299
|
+
parsedArgs = JSON.parse(acc.arguments || "{}");
|
|
10300
|
+
} catch {
|
|
10301
|
+
const truncated = acc.arguments.trimEnd();
|
|
10302
|
+
const lastComma = truncated.lastIndexOf(",");
|
|
10303
|
+
const fixed = lastComma > 0 ? truncated.slice(0, lastComma) + "}" : truncated.slice(0, truncated.indexOf("{") + 1) + "}";
|
|
10304
|
+
try {
|
|
10305
|
+
parsedArgs = JSON.parse(fixed);
|
|
10306
|
+
} catch {
|
|
10307
|
+
parsedArgs = {};
|
|
10308
|
+
}
|
|
10309
|
+
}
|
|
10310
|
+
toolCalls.push({
|
|
10311
|
+
id: acc.id,
|
|
10312
|
+
name: acc.name,
|
|
10313
|
+
arguments: parsedArgs
|
|
10314
|
+
});
|
|
10315
|
+
}
|
|
10316
|
+
return {
|
|
10317
|
+
textContent: textParts.join(""),
|
|
10318
|
+
toolCalls,
|
|
10319
|
+
usage,
|
|
10320
|
+
rawContent
|
|
10321
|
+
};
|
|
10322
|
+
}
|
|
9993
10323
|
async handleChatWithTools(provider, messages) {
|
|
9994
10324
|
const session = this.sessions.current;
|
|
9995
10325
|
let toolDefs;
|
|
@@ -10016,24 +10346,48 @@ Session '${this.resumeSessionId}' not found.
|
|
|
10016
10346
|
const useStreaming = this.config.get("ui").streaming;
|
|
10017
10347
|
const spinner = this.renderer.showSpinner("Thinking...");
|
|
10018
10348
|
const roundUsage = { inputTokens: 0, outputTokens: 0 };
|
|
10349
|
+
const supportsStreamingTools = useStreaming && typeof provider.chatWithToolsStream === "function";
|
|
10019
10350
|
try {
|
|
10020
10351
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
10021
10352
|
this.toolExecutor.setRoundInfo(round + 1, MAX_TOOL_ROUNDS);
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
}
|
|
10035
|
-
|
|
10036
|
-
)
|
|
10353
|
+
let result;
|
|
10354
|
+
let alreadyRendered = false;
|
|
10355
|
+
const chatRequest = {
|
|
10356
|
+
messages: apiMessages,
|
|
10357
|
+
model: this.currentModel,
|
|
10358
|
+
systemPrompt,
|
|
10359
|
+
stream: false,
|
|
10360
|
+
temperature: modelParams.temperature,
|
|
10361
|
+
maxTokens: modelParams.maxTokens,
|
|
10362
|
+
timeout: modelParams.timeout,
|
|
10363
|
+
thinking: modelParams.thinking,
|
|
10364
|
+
thinkingBudget: modelParams.thinkingBudget,
|
|
10365
|
+
...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
|
|
10366
|
+
};
|
|
10367
|
+
if (supportsStreamingTools) {
|
|
10368
|
+
const streamAc = this.setupStreamInterrupt();
|
|
10369
|
+
try {
|
|
10370
|
+
const streamGen = provider.chatWithToolsStream(
|
|
10371
|
+
{ ...chatRequest, signal: streamAc.signal },
|
|
10372
|
+
toolDefs
|
|
10373
|
+
);
|
|
10374
|
+
const streamResult = await this.consumeToolStream(streamGen, spinner);
|
|
10375
|
+
if (streamResult.toolCalls.length > 0) {
|
|
10376
|
+
const toolCalls = streamResult.toolCalls;
|
|
10377
|
+
if (streamResult.rawContent) {
|
|
10378
|
+
toolCalls._rawContent = streamResult.rawContent;
|
|
10379
|
+
}
|
|
10380
|
+
result = { toolCalls, usage: streamResult.usage };
|
|
10381
|
+
} else {
|
|
10382
|
+
result = { content: streamResult.textContent, usage: streamResult.usage };
|
|
10383
|
+
alreadyRendered = true;
|
|
10384
|
+
}
|
|
10385
|
+
} finally {
|
|
10386
|
+
this.teardownStreamInterrupt();
|
|
10387
|
+
}
|
|
10388
|
+
} else {
|
|
10389
|
+
result = await provider.chatWithTools(chatRequest, toolDefs);
|
|
10390
|
+
}
|
|
10037
10391
|
if (result.usage) {
|
|
10038
10392
|
roundUsage.inputTokens += result.usage.inputTokens;
|
|
10039
10393
|
roundUsage.outputTokens += result.usage.outputTokens;
|
|
@@ -10041,15 +10395,21 @@ Session '${this.resumeSessionId}' not found.
|
|
|
10041
10395
|
if ("content" in result) {
|
|
10042
10396
|
spinner.stop();
|
|
10043
10397
|
const finalContent = result.content;
|
|
10044
|
-
if (
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10398
|
+
if (!alreadyRendered) {
|
|
10399
|
+
if (useStreaming) {
|
|
10400
|
+
const streamAc = this.setupStreamInterrupt();
|
|
10401
|
+
try {
|
|
10402
|
+
await this.renderer.renderContentAsStream(finalContent, { signal: streamAc.signal });
|
|
10403
|
+
} finally {
|
|
10404
|
+
this.teardownStreamInterrupt();
|
|
10405
|
+
}
|
|
10406
|
+
} else {
|
|
10407
|
+
this.renderer.renderResponse(finalContent);
|
|
10050
10408
|
}
|
|
10051
10409
|
} else {
|
|
10052
|
-
|
|
10410
|
+
if (finalContent.trim()) {
|
|
10411
|
+
process.stdout.write("\n\n");
|
|
10412
|
+
}
|
|
10053
10413
|
}
|
|
10054
10414
|
lastResponseStore.content = finalContent;
|
|
10055
10415
|
session.addMessage({
|