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. **`/config set` 快捷配置**:REPL 内直接 `/config set ui.theme light` 无需进入向导
610
- 2. **流式工具调用(Streaming Tool Use)**:流式解析 tool_use 事件,更早启动工具执行
611
- 3. **`/diff` 命令**:显示当前 session 内所有文件修改的汇总 diff
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. **`/config set` 快捷配置**:REPL 内直接 `/config set ui.theme light` 无需进入向导,key-path 语法读写 config.json
709
- 2. **流式工具调用(Streaming Tool Use)**:Claude/OpenAI 均支持流式返回 tool_use 事件,当前等待完整 response 才开始执行,改为流式解析可更早启动工具执行
710
- 3. **`/diff` 命令**:显示当前 session 内所有文件修改的汇总 diff,便于 AI 操作后快速审查变更
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.53";
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-E23DQUHQ.js";
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-VV7FXC5Z.js");
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
- const result = await provider.chatWithTools(
10023
- {
10024
- messages: apiMessages,
10025
- model: this.currentModel,
10026
- systemPrompt,
10027
- stream: false,
10028
- temperature: modelParams.temperature,
10029
- maxTokens: modelParams.maxTokens,
10030
- timeout: modelParams.timeout,
10031
- thinking: modelParams.thinking,
10032
- thinkingBudget: modelParams.thinkingBudget,
10033
- ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
10034
- },
10035
- toolDefs
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 (useStreaming) {
10045
- const streamAc = this.setupStreamInterrupt();
10046
- try {
10047
- await this.renderer.renderContentAsStream(finalContent, { signal: streamAc.signal });
10048
- } finally {
10049
- this.teardownStreamInterrupt();
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
- this.renderer.renderResponse(finalContent);
10410
+ if (finalContent.trim()) {
10411
+ process.stdout.write("\n\n");
10412
+ }
10053
10413
  }
10054
10414
  lastResponseStore.content = finalContent;
10055
10415
  session.addMessage({
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-E23DQUHQ.js";
5
+ } from "./chunk-OBC56PVG.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",