jinzd-ai-cli 0.4.58 → 0.4.60

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.
@@ -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.4.58";
11
+ var VERSION = "0.4.60";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -6,8 +6,9 @@ import {
6
6
  ProviderError,
7
7
  ProviderNotFoundError,
8
8
  RateLimitError,
9
- schemaToJsonSchema
10
- } from "./chunk-HDNVCYD6.js";
9
+ schemaToJsonSchema,
10
+ truncateForPersist
11
+ } from "./chunk-C32FFHMY.js";
11
12
  import {
12
13
  APP_NAME,
13
14
  CONFIG_DIR_NAME,
@@ -20,7 +21,7 @@ import {
20
21
  MCP_TOOL_PREFIX,
21
22
  PLUGINS_DIR_NAME,
22
23
  VERSION
23
- } from "./chunk-7MQXQDVV.js";
24
+ } from "./chunk-2DWWB4KD.js";
24
25
 
25
26
  // src/config/config-manager.ts
26
27
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -309,9 +310,12 @@ var BaseProvider = class {
309
310
  /**
310
311
  * 将 Message[] 转换为 OpenAI API 格式的消息数组。
311
312
  * content 为 string 时直接传递;为 MessageContentPart[] 时保留数组格式(vision 请求)。
313
+ *
314
+ * 自动跳过 role='tool' 和带 toolCalls 的 assistant 消息——
315
+ * 这些是 v0.4.60+ 持久化的工具历史,由 _extraMessages 机制单独注入。
312
316
  */
313
317
  normalizeMessages(messages) {
314
- return messages.map((m) => ({ role: m.role, content: m.content }));
318
+ return messages.filter((m) => m.role !== "tool" && !m.toolCalls).map((m) => ({ role: m.role, content: m.content }));
315
319
  }
316
320
  };
317
321
 
@@ -475,7 +479,7 @@ var ClaudeProvider = class extends BaseProvider {
475
479
  }
476
480
  async chat(request) {
477
481
  try {
478
- const messages = request.messages.filter((m) => m.role !== "system").map((m) => ({
482
+ const messages = request.messages.filter((m) => m.role !== "system" && m.role !== "tool" && !m.toolCalls).map((m) => ({
479
483
  role: m.role,
480
484
  content: this.contentToClaudeParts(m.content)
481
485
  }));
@@ -500,7 +504,7 @@ var ClaudeProvider = class extends BaseProvider {
500
504
  }
501
505
  async *chatStream(request) {
502
506
  try {
503
- const messages = request.messages.filter((m) => m.role !== "system").map((m) => ({
507
+ const messages = request.messages.filter((m) => m.role !== "system" && m.role !== "tool" && !m.toolCalls).map((m) => ({
504
508
  role: m.role,
505
509
  content: this.contentToClaudeParts(m.content)
506
510
  }));
@@ -557,7 +561,7 @@ var ClaudeProvider = class extends BaseProvider {
557
561
  }
558
562
  }))
559
563
  );
560
- const baseMessages = request.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: this.contentToClaudeParts(m.content) }));
564
+ const baseMessages = request.messages.filter((m) => m.role !== "system" && m.role !== "tool" && !m.toolCalls).map((m) => ({ role: m.role, content: this.contentToClaudeParts(m.content) }));
561
565
  const extraMessages = request._extraMessages ?? [];
562
566
  const allMessages = [...baseMessages, ...extraMessages];
563
567
  const { thinking, temperature } = this.buildThinkingParams(request);
@@ -869,7 +873,7 @@ var GeminiProvider = class extends BaseProvider {
869
873
  return parts.length > 0 ? parts : [{ text: "" }];
870
874
  }
871
875
  toGeminiHistory(messages) {
872
- return messages.filter((m) => m.role !== "system").map((m) => ({
876
+ return messages.filter((m) => m.role !== "system" && m.role !== "tool" && !m.toolCalls).map((m) => ({
873
877
  role: m.role === "assistant" ? "model" : "user",
874
878
  parts: this.contentToGeminiParts(m.content)
875
879
  }));
@@ -2521,10 +2525,19 @@ var Session = class _Session {
2521
2525
  messageIndex: c.messageIndex,
2522
2526
  timestamp: c.timestamp.toISOString()
2523
2527
  })),
2524
- messages: this.messages.map((m) => ({
2525
- ...m,
2526
- timestamp: m.timestamp.toISOString()
2527
- }))
2528
+ messages: this.messages.map((m) => {
2529
+ const out = {
2530
+ role: m.role,
2531
+ content: m.content,
2532
+ timestamp: m.timestamp.toISOString()
2533
+ };
2534
+ if (m.toolCalls) out.toolCalls = m.toolCalls;
2535
+ if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
2536
+ if (m.toolCallId) out.toolCallId = m.toolCallId;
2537
+ if (m.toolName) out.toolName = m.toolName;
2538
+ if (m.isError !== void 0) out.isError = m.isError;
2539
+ return out;
2540
+ })
2528
2541
  };
2529
2542
  }
2530
2543
  /**
@@ -2594,11 +2607,17 @@ var Session = class _Session {
2594
2607
  }
2595
2608
  session.messages = d.messages.map((m) => {
2596
2609
  const ts = new Date(m.timestamp);
2597
- return {
2610
+ const msg = {
2598
2611
  role: m.role ?? "user",
2599
- content: m.content,
2612
+ content: Array.isArray(m.content) ? m.content : String(m.content ?? ""),
2600
2613
  timestamp: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
2601
2614
  };
2615
+ if (Array.isArray(m.toolCalls)) msg.toolCalls = m.toolCalls;
2616
+ if (typeof m.reasoningContent === "string") msg.reasoningContent = m.reasoningContent;
2617
+ if (typeof m.toolCallId === "string") msg.toolCallId = m.toolCallId;
2618
+ if (typeof m.toolName === "string") msg.toolName = m.toolName;
2619
+ if (typeof m.isError === "boolean") msg.isError = m.isError;
2620
+ return msg;
2602
2621
  });
2603
2622
  return session;
2604
2623
  }
@@ -3667,6 +3686,78 @@ function formatCost(amount) {
3667
3686
  return `$${amount.toFixed(2)}`;
3668
3687
  }
3669
3688
 
3689
+ // src/session/tool-history.ts
3690
+ function persistToolRound(session, toolCalls, toolResults, opts) {
3691
+ session.addMessage({
3692
+ role: "assistant",
3693
+ content: opts?.assistantContent ?? "",
3694
+ toolCalls,
3695
+ reasoningContent: opts?.reasoningContent,
3696
+ timestamp: /* @__PURE__ */ new Date()
3697
+ });
3698
+ for (let i = 0; i < toolCalls.length; i++) {
3699
+ const tc = toolCalls[i];
3700
+ const tr = toolResults[i];
3701
+ if (!tr) continue;
3702
+ session.addMessage({
3703
+ role: "tool",
3704
+ content: truncateForPersist(tr.content),
3705
+ toolCallId: tr.callId,
3706
+ toolName: tc.name,
3707
+ isError: tr.isError,
3708
+ timestamp: /* @__PURE__ */ new Date()
3709
+ });
3710
+ }
3711
+ }
3712
+ function isToolMessage(m) {
3713
+ return m.role === "tool" || !!(m.toolCalls && m.toolCalls.length > 0);
3714
+ }
3715
+ function extractToolHistory(messages) {
3716
+ const baseMessages = [];
3717
+ const toolHistory = [];
3718
+ for (const m of messages) {
3719
+ if (isToolMessage(m)) {
3720
+ toolHistory.push(m);
3721
+ } else {
3722
+ baseMessages.push(m);
3723
+ }
3724
+ }
3725
+ return { baseMessages, toolHistory };
3726
+ }
3727
+ function rebuildExtraMessages(provider, toolHistory) {
3728
+ if (toolHistory.length === 0) return [];
3729
+ const result = [];
3730
+ let i = 0;
3731
+ while (i < toolHistory.length) {
3732
+ const msg = toolHistory[i];
3733
+ if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length > 0) {
3734
+ const toolCalls = msg.toolCalls;
3735
+ const toolResults = [];
3736
+ let j = i + 1;
3737
+ while (j < toolHistory.length && toolHistory[j].role === "tool") {
3738
+ const tm = toolHistory[j];
3739
+ toolResults.push({
3740
+ callId: tm.toolCallId ?? "",
3741
+ content: typeof tm.content === "string" ? tm.content : getContentText(tm.content),
3742
+ isError: tm.isError ?? false
3743
+ });
3744
+ j++;
3745
+ }
3746
+ result.push(
3747
+ ...provider.buildToolResultMessages(toolCalls, toolResults, msg.reasoningContent)
3748
+ );
3749
+ i = j;
3750
+ } else {
3751
+ result.push({
3752
+ role: msg.role,
3753
+ content: typeof msg.content === "string" ? msg.content : getContentText(msg.content)
3754
+ });
3755
+ i++;
3756
+ }
3757
+ }
3758
+ return result;
3759
+ }
3760
+
3670
3761
  // src/repl/dev-state.ts
3671
3762
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "fs";
3672
3763
  import { join as join5 } from "path";
@@ -3777,6 +3868,9 @@ export {
3777
3868
  computeCost,
3778
3869
  formatCost,
3779
3870
  parseSimpleYaml,
3871
+ persistToolRound,
3872
+ extractToolHistory,
3873
+ rebuildExtraMessages,
3780
3874
  SNAPSHOT_PROMPT,
3781
3875
  sessionHasMeaningfulContent,
3782
3876
  saveDevState,
@@ -10,7 +10,7 @@ import {
10
10
  SUBAGENT_DEFAULT_MAX_ROUNDS,
11
11
  SUBAGENT_MAX_ROUNDS_LIMIT,
12
12
  runTestsTool
13
- } from "./chunk-7MQXQDVV.js";
13
+ } from "./chunk-2DWWB4KD.js";
14
14
 
15
15
  // src/tools/builtin/bash.ts
16
16
  import { execSync } from "child_process";
@@ -1098,6 +1098,20 @@ function snapToLineBoundary(content, target, direction) {
1098
1098
  return target;
1099
1099
  }
1100
1100
  }
1101
+ var PERSIST_MAX_CHARS = 8192;
1102
+ function truncateForPersist(content, maxChars = PERSIST_MAX_CHARS) {
1103
+ if (content.length <= maxChars) return content;
1104
+ const headSize = Math.floor(maxChars * 0.7);
1105
+ const tailSize = Math.floor(maxChars * 0.2);
1106
+ const head = content.slice(0, headSize);
1107
+ const tail = content.slice(-tailSize);
1108
+ const omitted = content.length - headSize - tailSize;
1109
+ return `${head}
1110
+
1111
+ [... ${omitted} chars omitted for storage ...]
1112
+
1113
+ ${tail}`;
1114
+ }
1101
1115
  function truncateOutput(content, toolName, maxChars) {
1102
1116
  const limit = maxChars ?? activeMaxChars;
1103
1117
  if (content.length <= limit) return content;
@@ -4208,6 +4222,7 @@ export {
4208
4222
  checkPermission,
4209
4223
  setMaxOutputCap,
4210
4224
  setContextWindow,
4225
+ truncateForPersist,
4211
4226
  truncateOutput,
4212
4227
  ToolExecutor,
4213
4228
  lastResponseStore,
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.58";
9
+ var VERSION = "0.4.60";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -385,7 +385,7 @@ ${content}`);
385
385
  }
386
386
  }
387
387
  async function runTaskMode(config, providers, configManager, topic) {
388
- const { TaskOrchestrator } = await import("./task-orchestrator-BWFYT4Q5.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-Z4IK3UEA.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  clearDevState,
14
14
  computeCost,
15
15
  detectsHallucinatedFileOp,
16
+ extractToolHistory,
16
17
  extractWrittenFilePaths,
17
18
  findPhantomClaims,
18
19
  formatCost,
@@ -24,10 +25,12 @@ import {
24
25
  hadPreviousWriteToolCalls,
25
26
  loadDevState,
26
27
  parseSimpleYaml,
28
+ persistToolRound,
29
+ rebuildExtraMessages,
27
30
  saveDevState,
28
31
  sessionHasMeaningfulContent,
29
32
  setupProxy
30
- } from "./chunk-ZHURWJEW.js";
33
+ } from "./chunk-3YVHYAXK.js";
31
34
  import {
32
35
  ToolExecutor,
33
36
  ToolRegistry,
@@ -41,7 +44,7 @@ import {
41
44
  spawnAgentContext,
42
45
  theme,
43
46
  undoStack
44
- } from "./chunk-HDNVCYD6.js";
47
+ } from "./chunk-C32FFHMY.js";
45
48
  import {
46
49
  fileCheckpoints
47
50
  } from "./chunk-4BKXL7SM.js";
@@ -66,7 +69,7 @@ import {
66
69
  SKILLS_DIR_NAME,
67
70
  VERSION,
68
71
  buildUserIdentityPrompt
69
- } from "./chunk-7MQXQDVV.js";
72
+ } from "./chunk-2DWWB4KD.js";
70
73
 
71
74
  // src/index.ts
72
75
  import { program } from "commander";
@@ -2161,7 +2164,7 @@ ${hint}` : "")
2161
2164
  usage: "/test [command|filter]",
2162
2165
  async execute(args, ctx) {
2163
2166
  try {
2164
- const { executeTests } = await import("./run-tests-ZC6WLEE4.js");
2167
+ const { executeTests } = await import("./run-tests-QGJHXL5Z.js");
2165
2168
  const argStr = args.join(" ").trim();
2166
2169
  let testArgs = {};
2167
2170
  if (argStr) {
@@ -4322,7 +4325,29 @@ Session '${this.resumeSessionId}' not found.
4322
4325
  sendNotification("ai-cli", `Task completed in ${Math.round(elapsed / 1e3)}s`);
4323
4326
  }
4324
4327
  } catch (err) {
4325
- this.renderer.renderError(err);
4328
+ const errMsg = err instanceof Error ? err.message : String(err);
4329
+ const isCtxLengthError = /maximum context length|context_length_exceeded|context window|too many tokens|reduce the length of the messages/i.test(errMsg);
4330
+ if (isCtxLengthError) {
4331
+ process.stderr.write(
4332
+ theme.error(`
4333
+ \u26A0 Context length exceeded \u2014 the conversation is too long for this model.
4334
+ `)
4335
+ );
4336
+ process.stderr.write(theme.dim(` Details: ${errMsg.split("\n")[0]}
4337
+ `));
4338
+ process.stderr.write(
4339
+ theme.dim(
4340
+ ` Recovery options:
4341
+ 1. Run /compact to summarize old messages and free context
4342
+ 2. Run /clear to start a fresh session (keeps this terminal)
4343
+ 3. Run /model to switch to a model with a larger context window
4344
+
4345
+ `
4346
+ )
4347
+ );
4348
+ } else {
4349
+ this.renderer.renderError(err);
4350
+ }
4326
4351
  }
4327
4352
  }
4328
4353
  /**
@@ -4397,6 +4422,23 @@ Session '${this.resumeSessionId}' not found.
4397
4422
  }
4398
4423
  return total;
4399
4424
  }
4425
+ /**
4426
+ * 估算 agentic 循环当前请求的 token 数(包含 session messages + extraMessages + system prompt)。
4427
+ * extraMessages 结构复杂(含 tool_calls、tool 结果等),这里用 JSON.stringify 后除以字符/token 比。
4428
+ * 用于 handleChatWithTools 循环内每轮发 API 前做上下文压力检查。
4429
+ */
4430
+ estimateRequestTokens(systemPrompt, extraMessages) {
4431
+ let total = this.estimateConversationTokens();
4432
+ if (extraMessages.length > 0) {
4433
+ try {
4434
+ const serialized = JSON.stringify(extraMessages);
4435
+ total += this.estimateTokens(serialized);
4436
+ } catch {
4437
+ }
4438
+ }
4439
+ void systemPrompt;
4440
+ return total;
4441
+ }
4400
4442
  /**
4401
4443
  * 获取当前模型的 context window 大小。
4402
4444
  */
@@ -4830,8 +4872,9 @@ Session '${this.resumeSessionId}' not found.
4830
4872
  if (this.blockedTools) {
4831
4873
  toolDefs = toolDefs.filter((t) => !this.blockedTools.has(t.name));
4832
4874
  }
4833
- const apiMessages = [...messages];
4834
- const extraMessages = [];
4875
+ const { baseMessages: cleanMessages, toolHistory } = extractToolHistory(messages);
4876
+ const apiMessages = [...cleanMessages];
4877
+ const extraMessages = toolHistory.length > 0 ? rebuildExtraMessages(provider, toolHistory) : [];
4835
4878
  const maxToolRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
4836
4879
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
4837
4880
  const autoPauseInterval = typeof autoPauseIntervalRaw === "number" ? autoPauseIntervalRaw : DEFAULT_AUTO_PAUSE_INTERVAL;
@@ -4865,6 +4908,7 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
4865
4908
  let lastToolCallSignature = "";
4866
4909
  let repeatedToolCallCount = 0;
4867
4910
  let emptyResponseRetries = 0;
4911
+ let warnedCtx80 = false;
4868
4912
  const roundToolHistory = [];
4869
4913
  this.setupInterjectionListener();
4870
4914
  try {
@@ -4923,6 +4967,52 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
4923
4967
  `));
4924
4968
  extraMessages.push({ role: "user", content: msg });
4925
4969
  }
4970
+ const ctxWindow = this.getContextWindowSize();
4971
+ if (ctxWindow > 0) {
4972
+ const reqTokens = this.estimateRequestTokens(systemPrompt, extraMessages);
4973
+ const reqRatio = reqTokens / ctxWindow;
4974
+ if (reqRatio >= 0.95) {
4975
+ spinner.stop();
4976
+ process.stderr.write(
4977
+ theme.error(
4978
+ `
4979
+ \u26A0 Context at ${Math.round(reqRatio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 aborting agentic loop before API rejection.
4980
+ `
4981
+ )
4982
+ );
4983
+ process.stderr.write(
4984
+ theme.dim(
4985
+ ` Too much tool output accumulated this turn. Your work so far is preserved.
4986
+ Recovery: run /compact to shrink history, then ask the AI to continue.
4987
+
4988
+ `
4989
+ )
4990
+ );
4991
+ if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
4992
+ this.addSessionUsage(roundUsage);
4993
+ session.addTokenUsage(roundUsage);
4994
+ if (this.shouldShowTokens()) {
4995
+ this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
4996
+ }
4997
+ }
4998
+ return;
4999
+ } else if (reqRatio >= 0.8 && !warnedCtx80) {
5000
+ warnedCtx80 = true;
5001
+ spinner.stop();
5002
+ process.stdout.write(
5003
+ theme.warning(
5004
+ `
5005
+ \u26A0 Context at ${Math.round(reqRatio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 asking AI to wrap up.
5006
+ `
5007
+ )
5008
+ );
5009
+ extraMessages.push({
5010
+ role: "user",
5011
+ content: `\u26A0\uFE0F Context pressure: ~${Math.round(reqRatio * 100)}% of the ${fmtTokens(ctxWindow)} context window is used. Avoid reading more files or running broad scans. Finish the current critical step, then produce a final summary. Every unnecessary tool call now risks breaking the conversation.`
5012
+ });
5013
+ spinner.start(`Thinking... (round ${round + 1}/${maxToolRounds})`);
5014
+ }
5015
+ }
4926
5016
  let result;
4927
5017
  let alreadyRendered = false;
4928
5018
  const chatRequest = {
@@ -5174,6 +5264,11 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
5174
5264
  const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
5175
5265
  const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults, reasoningContent);
5176
5266
  extraMessages.push(...newMsgs);
5267
+ const streamedContent = "content" in result ? result.content : void 0;
5268
+ persistToolRound(session, result.toolCalls, toolResults, {
5269
+ assistantContent: streamedContent,
5270
+ reasoningContent
5271
+ });
5177
5272
  const thisRoundHadWrite = result.toolCalls.some(
5178
5273
  (tc) => tc.name === "write_file" || tc.name === "edit_file"
5179
5274
  );
@@ -5609,7 +5704,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5609
5704
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5610
5705
  process.exit(1);
5611
5706
  }
5612
- const { startWebServer } = await import("./server-SFDOVFUN.js");
5707
+ const { startWebServer } = await import("./server-L2XJYXMB.js");
5613
5708
  await startWebServer({ port, host: options.host });
5614
5709
  });
5615
5710
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5842,7 +5937,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5842
5937
  }),
5843
5938
  config.get("customProviders")
5844
5939
  );
5845
- const { startHub } = await import("./hub-MRK53S5O.js");
5940
+ const { startHub } = await import("./hub-JTMNY7JR.js");
5846
5941
  await startHub(
5847
5942
  {
5848
5943
  topic: topic ?? "",
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-7MQXQDVV.js";
5
+ } from "./chunk-2DWWB4KD.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-5CQPX74I.js";
4
+ } from "./chunk-X4GL6D5L.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -9,6 +9,7 @@ import {
9
9
  TOOL_CALL_REMINDER,
10
10
  computeCost,
11
11
  detectsHallucinatedFileOp,
12
+ extractToolHistory,
12
13
  formatCost,
13
14
  formatGitContextForPrompt,
14
15
  getContentText,
@@ -16,8 +17,10 @@ import {
16
17
  getGitRoot,
17
18
  hadPreviousWriteToolCalls,
18
19
  loadDevState,
20
+ persistToolRound,
21
+ rebuildExtraMessages,
19
22
  setupProxy
20
- } from "./chunk-ZHURWJEW.js";
23
+ } from "./chunk-3YVHYAXK.js";
21
24
  import {
22
25
  AuthManager
23
26
  } from "./chunk-BYNY5JPB.js";
@@ -36,7 +39,7 @@ import {
36
39
  spawnAgentContext,
37
40
  truncateOutput,
38
41
  undoStack
39
- } from "./chunk-HDNVCYD6.js";
42
+ } from "./chunk-C32FFHMY.js";
40
43
  import "./chunk-4BKXL7SM.js";
41
44
  import {
42
45
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -56,7 +59,7 @@ import {
56
59
  SKILLS_DIR_NAME,
57
60
  VERSION,
58
61
  buildUserIdentityPrompt
59
- } from "./chunk-7MQXQDVV.js";
62
+ } from "./chunk-2DWWB4KD.js";
60
63
 
61
64
  // src/web/server.ts
62
65
  import express from "express";
@@ -642,6 +645,49 @@ var SessionHandler = class _SessionHandler {
642
645
  } catch {
643
646
  }
644
647
  }
648
+ /** 获取当前模型的 context window 大小(0 表示未知)*/
649
+ getContextWindowSize() {
650
+ try {
651
+ const provider = this.providers.get(this.currentProvider);
652
+ const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
653
+ return modelInfo?.contextWindow ?? 0;
654
+ } catch {
655
+ return 0;
656
+ }
657
+ }
658
+ /** 粗略估算文本 token 数(2.5 chars/token)*/
659
+ estTokens(text) {
660
+ return Math.ceil(text.length / 2.5);
661
+ }
662
+ /**
663
+ * 估算当前 agentic 请求总 token 数(session messages + extraMessages + system prompt)。
664
+ * 用于 handleChat 循环内每轮发 API 前的压力检查。
665
+ */
666
+ estimateRequestTokens(systemPrompt, extraMessages) {
667
+ let total = 0;
668
+ if (systemPrompt) total += this.estTokens(systemPrompt);
669
+ const session = this.sessions.current;
670
+ if (session) {
671
+ for (const msg of session.messages) {
672
+ if (typeof msg.content === "string") {
673
+ total += this.estTokens(msg.content);
674
+ } else if (Array.isArray(msg.content)) {
675
+ for (const part of msg.content) {
676
+ if (part.type === "text" && part.text) {
677
+ total += this.estTokens(part.text);
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+ if (extraMessages.length > 0) {
684
+ try {
685
+ total += this.estTokens(JSON.stringify(extraMessages));
686
+ } catch {
687
+ }
688
+ }
689
+ return total;
690
+ }
645
691
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
646
692
  saveIfNeeded() {
647
693
  if (this.sessions.current && this.sessions.current.messages.length > 0) {
@@ -771,8 +817,9 @@ var SessionHandler = class _SessionHandler {
771
817
  }
772
818
  async handleChatWithTools(provider, messages, toolDefs) {
773
819
  const session = this.sessions.current;
774
- const apiMessages = [...messages];
775
- const extraMessages = [];
820
+ const { baseMessages: cleanMessages, toolHistory } = extractToolHistory(messages);
821
+ const apiMessages = [...cleanMessages];
822
+ const extraMessages = toolHistory.length > 0 ? rebuildExtraMessages(provider, toolHistory) : [];
776
823
  const maxToolRounds = this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
777
824
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
778
825
  const autoPauseInterval = typeof autoPauseIntervalRaw === "number" ? autoPauseIntervalRaw : 50;
@@ -801,6 +848,7 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
801
848
  let warnedLow = false;
802
849
  let warnedCritical = false;
803
850
  let emptyResponseRetries = 0;
851
+ let warnedCtx80 = false;
804
852
  const ac = new AbortController();
805
853
  this.abortController = ac;
806
854
  try {
@@ -836,6 +884,38 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
836
884
  this.send({ type: "info", message: `\u26A1 Interjection: "${msg}"` });
837
885
  extraMessages.push({ role: "user", content: msg });
838
886
  }
887
+ const ctxWindow = this.getContextWindowSize();
888
+ if (ctxWindow > 0) {
889
+ const reqTokens = this.estimateRequestTokens(systemPrompt, extraMessages);
890
+ const reqRatio = reqTokens / ctxWindow;
891
+ if (reqRatio >= 0.95) {
892
+ this.send({
893
+ type: "response_done",
894
+ content: `\u26A0 Context at ${Math.round(reqRatio * 100)}% of ${ctxWindow.toLocaleString()} tokens \u2014 aborting before API rejection.
895
+
896
+ Too much tool output accumulated this turn. Your work so far is preserved.
897
+
898
+ **Recovery**:
899
+ 1. Run \`/compact\` to shrink history, then ask the AI to continue
900
+ 2. Run \`/clear\` to start fresh
901
+ 3. Switch to a larger-context model`,
902
+ usage: roundUsage
903
+ });
904
+ this.addWebSessionUsage(roundUsage);
905
+ session.addTokenUsage(roundUsage);
906
+ return;
907
+ } else if (reqRatio >= 0.8 && !warnedCtx80) {
908
+ warnedCtx80 = true;
909
+ this.send({
910
+ type: "info",
911
+ message: `\u26A0 Context at ${Math.round(reqRatio * 100)}% \u2014 asking AI to wrap up`
912
+ });
913
+ extraMessages.push({
914
+ role: "user",
915
+ content: `\u26A0\uFE0F Context pressure: ~${Math.round(reqRatio * 100)}% of the ${ctxWindow.toLocaleString()}-token context window is used. Avoid reading more files or running broad scans. Finish the current critical step, then produce a final summary. Every unnecessary tool call now risks breaking the conversation.`
916
+ });
917
+ }
918
+ }
839
919
  const chatRequest = {
840
920
  messages: apiMessages,
841
921
  model: this.currentModel,
@@ -850,11 +930,34 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
850
930
  ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
851
931
  };
852
932
  let result;
853
- if (supportsStreamingTools) {
854
- const streamGen = provider.chatWithToolsStream(chatRequest, toolDefs);
855
- result = await this.consumeToolStream(streamGen, ac);
856
- } else {
857
- result = await provider.chatWithTools(chatRequest, toolDefs);
933
+ try {
934
+ if (supportsStreamingTools) {
935
+ const streamGen = provider.chatWithToolsStream(chatRequest, toolDefs);
936
+ result = await this.consumeToolStream(streamGen, ac);
937
+ } else {
938
+ result = await provider.chatWithTools(chatRequest, toolDefs);
939
+ }
940
+ } catch (providerErr) {
941
+ const errMsg = providerErr instanceof Error ? providerErr.message : String(providerErr);
942
+ const isCtxLengthError = /maximum context length|context_length_exceeded|context window|too many tokens|reduce the length of the messages/i.test(errMsg);
943
+ if (isCtxLengthError) {
944
+ this.send({
945
+ type: "response_done",
946
+ content: `\u26A0 Context length exceeded \u2014 the conversation is too long for this model.
947
+
948
+ Details: ${errMsg.split("\n")[0]}
949
+
950
+ **Recovery options**:
951
+ 1. Run \`/compact\` to summarize old messages and free context
952
+ 2. Run \`/clear\` to start a fresh session
953
+ 3. Run \`/model\` to switch to a model with a larger context window`,
954
+ usage: roundUsage
955
+ });
956
+ this.addWebSessionUsage(roundUsage);
957
+ session.addTokenUsage(roundUsage);
958
+ return;
959
+ }
960
+ throw providerErr;
858
961
  }
859
962
  if (ac.signal.aborted) break;
860
963
  if (result.usage) {
@@ -921,6 +1024,10 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
921
1024
  const reasoningContent = result.reasoningContent;
922
1025
  const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults, reasoningContent);
923
1026
  extraMessages.push(...newMsgs);
1027
+ persistToolRound(session, result.toolCalls, toolResults, {
1028
+ assistantContent: result.content,
1029
+ reasoningContent
1030
+ });
924
1031
  const allFree = result.toolCalls.every((tc) => FREE_ROUND_TOOLS.has(tc.name));
925
1032
  if (allFree) {
926
1033
  consecutiveFreeRounds++;
@@ -1816,7 +1923,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1816
1923
  case "test": {
1817
1924
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1818
1925
  try {
1819
- const { executeTests } = await import("./run-tests-ZC6WLEE4.js");
1926
+ const { executeTests } = await import("./run-tests-QGJHXL5Z.js");
1820
1927
  const argStr = args.join(" ").trim();
1821
1928
  let testArgs = {};
1822
1929
  if (argStr) {
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-HDNVCYD6.js";
7
+ } from "./chunk-C32FFHMY.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-7MQXQDVV.js";
11
+ } from "./chunk-2DWWB4KD.js";
12
12
 
13
13
  // src/hub/task-orchestrator.ts
14
14
  import { createInterface } from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.58",
3
+ "version": "0.4.60",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",