@corbat-tech/coco 2.23.0 → 2.24.0

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/dist/cli/index.js CHANGED
@@ -1425,6 +1425,150 @@ var init_anthropic = __esm({
1425
1425
  };
1426
1426
  }
1427
1427
  });
1428
+ function getSingleBuilderKey(builders) {
1429
+ return builders.size === 1 ? Array.from(builders.keys())[0] ?? null : null;
1430
+ }
1431
+ function parseToolCallArguments(args, providerName) {
1432
+ try {
1433
+ return args ? JSON.parse(args) : {};
1434
+ } catch {
1435
+ try {
1436
+ if (args) {
1437
+ const repaired = jsonrepair(args);
1438
+ return JSON.parse(repaired);
1439
+ }
1440
+ } catch {
1441
+ console.error(`[${providerName}] Cannot parse tool arguments: ${args.slice(0, 200)}`);
1442
+ }
1443
+ return {};
1444
+ }
1445
+ }
1446
+ var ChatToolCallAssembler, ResponsesToolCallAssembler;
1447
+ var init_tool_call_normalizer = __esm({
1448
+ "src/providers/tool-call-normalizer.ts"() {
1449
+ ChatToolCallAssembler = class {
1450
+ builders = /* @__PURE__ */ new Map();
1451
+ lastBuilderKey = null;
1452
+ consume(delta) {
1453
+ const key = typeof delta.index === "number" ? `index:${delta.index}` : typeof delta.id === "string" && delta.id.length > 0 ? `id:${delta.id}` : getSingleBuilderKey(this.builders) ?? this.lastBuilderKey ?? `fallback:${this.builders.size}`;
1454
+ let started;
1455
+ if (!this.builders.has(key)) {
1456
+ const initialId = delta.id ?? "";
1457
+ const initialName = delta.function?.name ?? "";
1458
+ this.builders.set(key, { id: initialId, name: initialName, arguments: "" });
1459
+ started = {
1460
+ id: initialId || void 0,
1461
+ name: initialName || void 0
1462
+ };
1463
+ }
1464
+ const builder = this.builders.get(key);
1465
+ this.lastBuilderKey = key;
1466
+ if (delta.id) {
1467
+ builder.id = delta.id;
1468
+ }
1469
+ if (delta.function?.name) {
1470
+ builder.name = delta.function.name;
1471
+ }
1472
+ const text13 = delta.function?.arguments ?? "";
1473
+ if (!text13) return { started };
1474
+ builder.arguments += text13;
1475
+ return {
1476
+ started,
1477
+ argumentDelta: {
1478
+ id: builder.id,
1479
+ name: builder.name,
1480
+ text: text13
1481
+ }
1482
+ };
1483
+ }
1484
+ finalizeAll(providerName) {
1485
+ const result = [];
1486
+ for (const builder of this.builders.values()) {
1487
+ result.push({
1488
+ id: builder.id,
1489
+ name: builder.name,
1490
+ input: parseToolCallArguments(builder.arguments, providerName)
1491
+ });
1492
+ }
1493
+ this.builders.clear();
1494
+ this.lastBuilderKey = null;
1495
+ return result;
1496
+ }
1497
+ };
1498
+ ResponsesToolCallAssembler = class {
1499
+ builders = /* @__PURE__ */ new Map();
1500
+ outputIndexToBuilderKey = /* @__PURE__ */ new Map();
1501
+ onOutputItemAdded(event) {
1502
+ const item = event.item;
1503
+ if (!item || item.type !== "function_call") return null;
1504
+ const callId = item.call_id ?? "";
1505
+ const itemKey = item.id ?? callId;
1506
+ this.builders.set(itemKey, {
1507
+ callId,
1508
+ name: item.name ?? "",
1509
+ arguments: item.arguments ?? ""
1510
+ });
1511
+ if (typeof event.output_index === "number") {
1512
+ this.outputIndexToBuilderKey.set(event.output_index, itemKey);
1513
+ }
1514
+ return {
1515
+ id: callId,
1516
+ name: item.name ?? ""
1517
+ };
1518
+ }
1519
+ onArgumentsDelta(event) {
1520
+ const builderKey = this.resolveBuilderKey(event.item_id, event.output_index);
1521
+ if (!builderKey) return;
1522
+ const builder = this.builders.get(builderKey);
1523
+ if (!builder) return;
1524
+ builder.arguments += event.delta ?? "";
1525
+ }
1526
+ onArgumentsDone(event, providerName) {
1527
+ const builderKey = this.resolveBuilderKey(event.item_id, event.output_index);
1528
+ if (!builderKey) return null;
1529
+ const builder = this.builders.get(builderKey);
1530
+ if (!builder) return null;
1531
+ const toolCall = {
1532
+ id: builder.callId,
1533
+ name: builder.name,
1534
+ input: parseToolCallArguments(event.arguments ?? builder.arguments, providerName)
1535
+ };
1536
+ this.deleteBuilder(builderKey);
1537
+ return toolCall;
1538
+ }
1539
+ finalizeAll(providerName) {
1540
+ const calls = [];
1541
+ for (const builder of this.builders.values()) {
1542
+ calls.push({
1543
+ id: builder.callId,
1544
+ name: builder.name,
1545
+ input: parseToolCallArguments(builder.arguments, providerName)
1546
+ });
1547
+ }
1548
+ this.builders.clear();
1549
+ this.outputIndexToBuilderKey.clear();
1550
+ return calls;
1551
+ }
1552
+ resolveBuilderKey(itemId, outputIndex) {
1553
+ if (itemId && this.builders.has(itemId)) {
1554
+ return itemId;
1555
+ }
1556
+ if (typeof outputIndex === "number") {
1557
+ return this.outputIndexToBuilderKey.get(outputIndex) ?? null;
1558
+ }
1559
+ return getSingleBuilderKey(this.builders);
1560
+ }
1561
+ deleteBuilder(builderKey) {
1562
+ this.builders.delete(builderKey);
1563
+ for (const [idx, key] of this.outputIndexToBuilderKey.entries()) {
1564
+ if (key === builderKey) {
1565
+ this.outputIndexToBuilderKey.delete(idx);
1566
+ }
1567
+ }
1568
+ }
1569
+ };
1570
+ }
1571
+ });
1428
1572
  function needsResponsesApi(model) {
1429
1573
  return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model === "o3";
1430
1574
  }
@@ -1464,6 +1608,7 @@ var init_openai = __esm({
1464
1608
  "src/providers/openai.ts"() {
1465
1609
  init_errors();
1466
1610
  init_retry();
1611
+ init_tool_call_normalizer();
1467
1612
  DEFAULT_MODEL2 = "gpt-5.4-codex";
1468
1613
  CONTEXT_WINDOWS2 = {
1469
1614
  // OpenAI models
@@ -1791,7 +1936,7 @@ var init_openai = __esm({
1791
1936
  const stream = await this.client.chat.completions.create(
1792
1937
  requestParams
1793
1938
  );
1794
- const toolCallBuilders = /* @__PURE__ */ new Map();
1939
+ const toolCallAssembler = new ChatToolCallAssembler();
1795
1940
  const streamTimeout = this.config.timeout ?? 12e4;
1796
1941
  let lastActivityTime = Date.now();
1797
1942
  const timeoutController = new AbortController();
@@ -1805,30 +1950,6 @@ var init_openai = __esm({
1805
1950
  timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
1806
1951
  once: true
1807
1952
  });
1808
- const providerName = this.name;
1809
- const parseArguments2 = (builder) => {
1810
- let input = {};
1811
- try {
1812
- input = builder.arguments ? JSON.parse(builder.arguments) : {};
1813
- } catch (error) {
1814
- console.warn(
1815
- `[${providerName}] Failed to parse tool call arguments for ${builder.name}: ${builder.arguments?.slice(0, 300)}`
1816
- );
1817
- try {
1818
- if (builder.arguments) {
1819
- const repaired = jsonrepair(builder.arguments);
1820
- input = JSON.parse(repaired);
1821
- console.log(`[${providerName}] \u2713 Successfully repaired JSON for ${builder.name}`);
1822
- }
1823
- } catch {
1824
- console.error(
1825
- `[${providerName}] Cannot repair JSON for ${builder.name}, using empty object`
1826
- );
1827
- console.error(`[${providerName}] Original error:`, error);
1828
- }
1829
- }
1830
- return input;
1831
- };
1832
1953
  try {
1833
1954
  let streamStopReason;
1834
1955
  for await (const chunk of stream) {
@@ -1841,37 +1962,31 @@ var init_openai = __esm({
1841
1962
  }
1842
1963
  if (delta?.tool_calls) {
1843
1964
  for (const toolCallDelta of delta.tool_calls) {
1844
- const index = toolCallDelta.index ?? toolCallBuilders.size;
1845
- if (!toolCallBuilders.has(index)) {
1846
- toolCallBuilders.set(index, {
1847
- id: toolCallDelta.id ?? "",
1848
- name: toolCallDelta.function?.name ?? "",
1849
- arguments: ""
1850
- });
1965
+ const consumed = toolCallAssembler.consume({
1966
+ index: toolCallDelta.index,
1967
+ id: toolCallDelta.id ?? void 0,
1968
+ function: {
1969
+ name: toolCallDelta.function?.name ?? void 0,
1970
+ arguments: toolCallDelta.function?.arguments ?? void 0
1971
+ }
1972
+ });
1973
+ if (consumed.started) {
1851
1974
  yield {
1852
1975
  type: "tool_use_start",
1853
1976
  toolCall: {
1854
- id: toolCallDelta.id,
1855
- name: toolCallDelta.function?.name
1977
+ id: consumed.started.id,
1978
+ name: consumed.started.name
1856
1979
  }
1857
1980
  };
1858
1981
  }
1859
- const builder = toolCallBuilders.get(index);
1860
- if (toolCallDelta.id) {
1861
- builder.id = toolCallDelta.id;
1862
- }
1863
- if (toolCallDelta.function?.name) {
1864
- builder.name = toolCallDelta.function.name;
1865
- }
1866
- if (toolCallDelta.function?.arguments) {
1867
- builder.arguments += toolCallDelta.function.arguments;
1982
+ if (consumed.argumentDelta) {
1868
1983
  yield {
1869
1984
  type: "tool_use_delta",
1870
1985
  toolCall: {
1871
- id: builder.id,
1872
- name: builder.name
1986
+ id: consumed.argumentDelta.id,
1987
+ name: consumed.argumentDelta.name
1873
1988
  },
1874
- text: toolCallDelta.function.arguments
1989
+ text: consumed.argumentDelta.text
1875
1990
  };
1876
1991
  }
1877
1992
  }
@@ -1880,27 +1995,26 @@ var init_openai = __esm({
1880
1995
  if (finishReason) {
1881
1996
  streamStopReason = this.mapFinishReason(finishReason);
1882
1997
  }
1883
- if (finishReason && toolCallBuilders.size > 0) {
1884
- for (const [, builder] of toolCallBuilders) {
1998
+ if (finishReason) {
1999
+ for (const toolCall of toolCallAssembler.finalizeAll(this.name)) {
1885
2000
  yield {
1886
2001
  type: "tool_use_end",
1887
2002
  toolCall: {
1888
- id: builder.id,
1889
- name: builder.name,
1890
- input: parseArguments2(builder)
2003
+ id: toolCall.id,
2004
+ name: toolCall.name,
2005
+ input: toolCall.input
1891
2006
  }
1892
2007
  };
1893
2008
  }
1894
- toolCallBuilders.clear();
1895
2009
  }
1896
2010
  }
1897
- for (const [, builder] of toolCallBuilders) {
2011
+ for (const toolCall of toolCallAssembler.finalizeAll(this.name)) {
1898
2012
  yield {
1899
2013
  type: "tool_use_end",
1900
2014
  toolCall: {
1901
- id: builder.id,
1902
- name: builder.name,
1903
- input: parseArguments2(builder)
2015
+ id: toolCall.id,
2016
+ name: toolCall.name,
2017
+ input: toolCall.input
1904
2018
  }
1905
2019
  };
1906
2020
  }
@@ -2337,7 +2451,7 @@ var init_openai = __esm({
2337
2451
  toolCalls.push({
2338
2452
  id: item.call_id,
2339
2453
  name: item.name,
2340
- input: this.parseResponsesArguments(item.arguments)
2454
+ input: parseToolCallArguments(item.arguments, this.name)
2341
2455
  });
2342
2456
  }
2343
2457
  }
@@ -2443,7 +2557,7 @@ var init_openai = __esm({
2443
2557
  const stream = await this.client.responses.create(
2444
2558
  requestParams
2445
2559
  );
2446
- const fnCallBuilders = /* @__PURE__ */ new Map();
2560
+ const toolCallAssembler = new ResponsesToolCallAssembler();
2447
2561
  const streamTimeout = this.config.timeout ?? 12e4;
2448
2562
  let lastActivityTime = Date.now();
2449
2563
  const timeoutController = new AbortController();
@@ -2467,57 +2581,66 @@ var init_openai = __esm({
2467
2581
  yield { type: "text", text: event.delta };
2468
2582
  break;
2469
2583
  case "response.output_item.added":
2470
- if (event.item.type === "function_call") {
2471
- const fc = event.item;
2472
- const itemKey = fc.id ?? fc.call_id;
2473
- fnCallBuilders.set(itemKey, {
2474
- callId: fc.call_id,
2475
- name: fc.name,
2476
- arguments: ""
2584
+ {
2585
+ const item = event.item;
2586
+ const start = toolCallAssembler.onOutputItemAdded({
2587
+ output_index: event.output_index,
2588
+ item: {
2589
+ type: item.type,
2590
+ id: item.id,
2591
+ call_id: item.call_id,
2592
+ name: item.name,
2593
+ arguments: item.arguments
2594
+ }
2477
2595
  });
2596
+ if (!start) break;
2478
2597
  yield {
2479
2598
  type: "tool_use_start",
2480
- toolCall: { id: fc.call_id, name: fc.name }
2599
+ toolCall: { id: start.id, name: start.name }
2481
2600
  };
2482
2601
  }
2483
2602
  break;
2484
2603
  case "response.function_call_arguments.delta":
2485
- {
2486
- const builder = fnCallBuilders.get(event.item_id);
2487
- if (builder) {
2488
- builder.arguments += event.delta;
2489
- }
2490
- }
2604
+ toolCallAssembler.onArgumentsDelta({
2605
+ item_id: event.item_id,
2606
+ output_index: event.output_index,
2607
+ delta: event.delta
2608
+ });
2491
2609
  break;
2492
2610
  case "response.function_call_arguments.done":
2493
2611
  {
2494
- const builder = fnCallBuilders.get(event.item_id);
2495
- if (builder) {
2612
+ const toolCall = toolCallAssembler.onArgumentsDone(
2613
+ {
2614
+ item_id: event.item_id,
2615
+ output_index: event.output_index,
2616
+ arguments: event.arguments
2617
+ },
2618
+ this.name
2619
+ );
2620
+ if (toolCall) {
2496
2621
  yield {
2497
2622
  type: "tool_use_end",
2498
2623
  toolCall: {
2499
- id: builder.callId,
2500
- name: builder.name,
2501
- input: this.parseResponsesArguments(event.arguments)
2624
+ id: toolCall.id,
2625
+ name: toolCall.name,
2626
+ input: toolCall.input
2502
2627
  }
2503
2628
  };
2504
- fnCallBuilders.delete(event.item_id);
2505
2629
  }
2506
2630
  }
2507
2631
  break;
2508
2632
  case "response.completed":
2509
2633
  {
2510
- for (const [, builder] of fnCallBuilders) {
2634
+ for (const toolCall of toolCallAssembler.finalizeAll(this.name)) {
2511
2635
  yield {
2512
2636
  type: "tool_use_end",
2513
2637
  toolCall: {
2514
- id: builder.callId,
2515
- name: builder.name,
2516
- input: this.parseResponsesArguments(builder.arguments)
2638
+ id: toolCall.id,
2639
+ name: toolCall.name,
2640
+ input: toolCall.input
2517
2641
  }
2518
2642
  };
2519
2643
  }
2520
- fnCallBuilders.clear();
2521
2644
  const hasToolCalls = event.response.output.some(
2522
2645
  (i) => i.type === "function_call"
2523
2646
  );
@@ -2637,24 +2760,6 @@ var init_openai = __esm({
2637
2760
  strict: false
2638
2761
  }));
2639
2762
  }
2640
- /**
2641
- * Parse tool call arguments with jsonrepair fallback (Responses API)
2642
- */
2643
- parseResponsesArguments(args) {
2644
- try {
2645
- return args ? JSON.parse(args) : {};
2646
- } catch {
2647
- try {
2648
- if (args) {
2649
- const repaired = jsonrepair(args);
2650
- return JSON.parse(repaired);
2651
- }
2652
- } catch {
2653
- console.error(`[${this.name}] Cannot parse tool arguments: ${args.slice(0, 200)}`);
2654
- }
2655
- return {};
2656
- }
2657
- }
2658
2763
  };
2659
2764
  }
2660
2765
  });
@@ -4283,6 +4388,8 @@ var init_auth = __esm({
4283
4388
  init_gcloud();
4284
4389
  }
4285
4390
  });
4391
+
4392
+ // src/providers/codex.ts
4286
4393
  function parseJwtClaims(token) {
4287
4394
  const parts = token.split(".");
4288
4395
  if (parts.length !== 3 || !parts[1]) return void 0;
@@ -4298,21 +4405,6 @@ function extractAccountId(accessToken) {
4298
4405
  const auth = claims["https://api.openai.com/auth"];
4299
4406
  return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
4300
4407
  }
4301
- function parseArguments(args) {
4302
- try {
4303
- return args ? JSON.parse(args) : {};
4304
- } catch {
4305
- try {
4306
- if (args) {
4307
- const repaired = jsonrepair(args);
4308
- return JSON.parse(repaired);
4309
- }
4310
- } catch {
4311
- console.error(`[Codex] Cannot parse tool arguments: ${args.slice(0, 200)}`);
4312
- }
4313
- return {};
4314
- }
4315
- }
4316
4408
  function createCodexProvider(config) {
4317
4409
  const provider = new CodexProvider();
4318
4410
  if (config) {
@@ -4327,6 +4419,7 @@ var init_codex = __esm({
4327
4419
  init_errors();
4328
4420
  init_auth();
4329
4421
  init_retry();
4422
+ init_tool_call_normalizer();
4330
4423
  CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
4331
4424
  DEFAULT_MODEL3 = "gpt-5.4-codex";
4332
4425
  CONTEXT_WINDOWS3 = {
@@ -4639,7 +4732,7 @@ var init_codex = __esm({
4639
4732
  let inputTokens = 0;
4640
4733
  let outputTokens = 0;
4641
4734
  const toolCalls = [];
4642
- const fnCallBuilders = /* @__PURE__ */ new Map();
4735
+ const toolCallAssembler = new ResponsesToolCallAssembler();
4643
4736
  await this.readSSEStream(response, (event) => {
4644
4737
  if (event.id) responseId = event.id;
4645
4738
  switch (event.type) {
@@ -4650,31 +4743,35 @@ var init_codex = __esm({
4650
4743
  content = event.text ?? content;
4651
4744
  break;
4652
4745
  case "response.output_item.added": {
4653
- const item = event.item;
4654
- if (item.type === "function_call") {
4655
- const itemKey = item.id ?? item.call_id;
4656
- fnCallBuilders.set(itemKey, {
4657
- callId: item.call_id,
4658
- name: item.name,
4659
- arguments: ""
4660
- });
4661
- }
4746
+ toolCallAssembler.onOutputItemAdded({
4747
+ output_index: event.output_index,
4748
+ item: event.item
4749
+ });
4662
4750
  break;
4663
4751
  }
4664
4752
  case "response.function_call_arguments.delta": {
4665
- const builder = fnCallBuilders.get(event.item_id);
4666
- if (builder) builder.arguments += event.delta ?? "";
4753
+ toolCallAssembler.onArgumentsDelta({
4754
+ item_id: event.item_id,
4755
+ output_index: event.output_index,
4756
+ delta: event.delta
4757
+ });
4667
4758
  break;
4668
4759
  }
4669
4760
  case "response.function_call_arguments.done": {
4670
- const builder = fnCallBuilders.get(event.item_id);
4671
- if (builder) {
4761
+ const toolCall = toolCallAssembler.onArgumentsDone(
4762
+ {
4763
+ item_id: event.item_id,
4764
+ output_index: event.output_index,
4765
+ arguments: event.arguments
4766
+ },
4767
+ this.name
4768
+ );
4769
+ if (toolCall) {
4672
4770
  toolCalls.push({
4673
- id: builder.callId,
4674
- name: builder.name,
4675
- input: parseArguments(event.arguments)
4771
+ id: toolCall.id,
4772
+ name: toolCall.name,
4773
+ input: toolCall.input
4676
4774
  });
4677
- fnCallBuilders.delete(event.item_id);
4678
4775
  }
4679
4776
  break;
4680
4777
  }
@@ -4685,14 +4782,13 @@ var init_codex = __esm({
4685
4782
  inputTokens = usage.input_tokens ?? 0;
4686
4783
  outputTokens = usage.output_tokens ?? 0;
4687
4784
  }
4688
- for (const [, builder] of fnCallBuilders) {
4785
+ for (const toolCall of toolCallAssembler.finalizeAll(this.name)) {
4689
4786
  toolCalls.push({
4690
- id: builder.callId,
4691
- name: builder.name,
4692
- input: parseArguments(builder.arguments)
4787
+ id: toolCall.id,
4788
+ name: toolCall.name,
4789
+ input: toolCall.input
4693
4790
  });
4694
4791
  }
4695
- fnCallBuilders.clear();
4696
4792
  break;
4697
4793
  }
4698
4794
  }
@@ -4786,7 +4882,7 @@ var init_codex = __esm({
4786
4882
  const reader = response.body.getReader();
4787
4883
  const decoder = new TextDecoder();
4788
4884
  let buffer = "";
4789
- const fnCallBuilders = /* @__PURE__ */ new Map();
4885
+ const toolCallAssembler = new ResponsesToolCallAssembler();
4790
4886
  let lastActivityTime = Date.now();
4791
4887
  const timeoutController = new AbortController();
4792
4888
  const timeoutInterval = setInterval(() => {
@@ -4819,55 +4915,58 @@ var init_codex = __esm({
4819
4915
  yield { type: "text", text: event.delta ?? "" };
4820
4916
  break;
4821
4917
  case "response.output_item.added": {
4822
- const item = event.item;
4823
- if (item.type === "function_call") {
4824
- const itemKey = item.id ?? item.call_id;
4825
- fnCallBuilders.set(itemKey, {
4826
- callId: item.call_id,
4827
- name: item.name,
4828
- arguments: ""
4829
- });
4918
+ const start = toolCallAssembler.onOutputItemAdded({
4919
+ output_index: event.output_index,
4920
+ item: event.item
4921
+ });
4922
+ if (start) {
4830
4923
  yield {
4831
4924
  type: "tool_use_start",
4832
- toolCall: { id: item.call_id, name: item.name }
4925
+ toolCall: { id: start.id, name: start.name }
4833
4926
  };
4834
4927
  }
4835
4928
  break;
4836
4929
  }
4837
4930
  case "response.function_call_arguments.delta": {
4838
- const builder = fnCallBuilders.get(event.item_id);
4839
- if (builder) {
4840
- builder.arguments += event.delta ?? "";
4841
- }
4931
+ toolCallAssembler.onArgumentsDelta({
4932
+ item_id: event.item_id,
4933
+ output_index: event.output_index,
4934
+ delta: event.delta
4935
+ });
4842
4936
  break;
4843
4937
  }
4844
4938
  case "response.function_call_arguments.done": {
4845
- const builder = fnCallBuilders.get(event.item_id);
4846
- if (builder) {
4939
+ const toolCall = toolCallAssembler.onArgumentsDone(
4940
+ {
4941
+ item_id: event.item_id,
4942
+ output_index: event.output_index,
4943
+ arguments: event.arguments
4944
+ },
4945
+ this.name
4946
+ );
4947
+ if (toolCall) {
4847
4948
  yield {
4848
4949
  type: "tool_use_end",
4849
4950
  toolCall: {
4850
- id: builder.callId,
4851
- name: builder.name,
4852
- input: parseArguments(event.arguments ?? builder.arguments)
4951
+ id: toolCall.id,
4952
+ name: toolCall.name,
4953
+ input: toolCall.input
4853
4954
  }
4854
4955
  };
4855
- fnCallBuilders.delete(event.item_id);
4856
4956
  }
4857
4957
  break;
4858
4958
  }
4859
4959
  case "response.completed": {
4860
- for (const [, builder] of fnCallBuilders) {
4960
+ for (const toolCall of toolCallAssembler.finalizeAll(this.name)) {
4861
4961
  yield {
4862
4962
  type: "tool_use_end",
4863
4963
  toolCall: {
4864
- id: builder.callId,
4865
- name: builder.name,
4866
- input: parseArguments(builder.arguments)
4964
+ id: toolCall.id,
4965
+ name: toolCall.name,
4966
+ input: toolCall.input
4867
4967
  }
4868
4968
  };
4869
4969
  }
4870
- fnCallBuilders.clear();
4871
4970
  const resp = event.response;
4872
4971
  const output = resp?.output ?? [];
4873
4972
  const hasToolCalls = output.some((i) => i.type === "function_call");
@@ -5243,8 +5342,8 @@ var init_gemini = __esm({
5243
5342
  const { history, lastMessage } = this.convertMessages(messages);
5244
5343
  const chat = model.startChat({ history });
5245
5344
  const result = await chat.sendMessageStream(lastMessage);
5246
- const emittedToolCalls = /* @__PURE__ */ new Set();
5247
5345
  let streamStopReason;
5346
+ let streamToolCallCounter = 0;
5248
5347
  for await (const chunk of result.stream) {
5249
5348
  const text13 = chunk.text();
5250
5349
  if (text13) {
@@ -5259,30 +5358,23 @@ var init_gemini = __esm({
5259
5358
  for (const part of candidate.content.parts) {
5260
5359
  if ("functionCall" in part && part.functionCall) {
5261
5360
  const funcCall = part.functionCall;
5262
- const sortedArgs = funcCall.args ? Object.keys(funcCall.args).sort().map(
5263
- (k) => `${k}:${JSON.stringify(funcCall.args[k])}`
5264
- ).join(",") : "";
5265
- const callKey = `${funcCall.name}-${sortedArgs}`;
5266
- if (!emittedToolCalls.has(callKey)) {
5267
- emittedToolCalls.add(callKey);
5268
- const toolCall = {
5269
- id: funcCall.name,
5270
- // Gemini uses name as ID
5271
- name: funcCall.name,
5272
- input: funcCall.args ?? {}
5273
- };
5274
- yield {
5275
- type: "tool_use_start",
5276
- toolCall: {
5277
- id: toolCall.id,
5278
- name: toolCall.name
5279
- }
5280
- };
5281
- yield {
5282
- type: "tool_use_end",
5283
- toolCall
5284
- };
5285
- }
5361
+ streamToolCallCounter++;
5362
+ const toolCall = {
5363
+ id: `gemini_call_${streamToolCallCounter}`,
5364
+ name: funcCall.name,
5365
+ input: funcCall.args ?? {}
5366
+ };
5367
+ yield {
5368
+ type: "tool_use_start",
5369
+ toolCall: {
5370
+ id: toolCall.id,
5371
+ name: toolCall.name
5372
+ }
5373
+ };
5374
+ yield {
5375
+ type: "tool_use_end",
5376
+ toolCall
5377
+ };
5286
5378
  }
5287
5379
  }
5288
5380
  }
@@ -5357,13 +5449,13 @@ var init_gemini = __esm({
5357
5449
  * Convert messages to Gemini format
5358
5450
  */
5359
5451
  convertMessages(messages) {
5452
+ const toolNameByUseId = this.buildToolUseNameMap(messages);
5453
+ const conversation = messages.filter((m) => m.role !== "system");
5360
5454
  const history = [];
5361
5455
  let lastUserMessage = "";
5362
- for (const msg of messages) {
5363
- if (msg.role === "system") {
5364
- continue;
5365
- }
5366
- const parts = this.convertContent(msg.content);
5456
+ for (let i = 0; i < conversation.length; i++) {
5457
+ const msg = conversation[i];
5458
+ const isLastMessage = i === conversation.length - 1;
5367
5459
  if (msg.role === "user") {
5368
5460
  if (Array.isArray(msg.content) && msg.content[0]?.type === "tool_result") {
5369
5461
  const functionResponses = [];
@@ -5372,23 +5464,49 @@ var init_gemini = __esm({
5372
5464
  const toolResult = block;
5373
5465
  functionResponses.push({
5374
5466
  functionResponse: {
5375
- name: toolResult.tool_use_id,
5376
- // Gemini uses name, we store it in tool_use_id
5467
+ // Gemini expects the function name in functionResponse.name.
5468
+ // Recover it from prior assistant tool_use blocks when possible.
5469
+ name: toolNameByUseId.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
5377
5470
  response: { result: toolResult.content }
5378
5471
  }
5379
5472
  });
5380
5473
  }
5381
5474
  }
5382
- history.push({ role: "user", parts: functionResponses });
5475
+ if (isLastMessage) {
5476
+ lastUserMessage = functionResponses;
5477
+ } else {
5478
+ history.push({ role: "user", parts: functionResponses });
5479
+ }
5383
5480
  } else {
5384
- lastUserMessage = parts;
5481
+ const parts = this.convertContent(msg.content);
5482
+ if (isLastMessage) {
5483
+ lastUserMessage = parts;
5484
+ } else {
5485
+ history.push({ role: "user", parts });
5486
+ }
5385
5487
  }
5386
5488
  } else if (msg.role === "assistant") {
5489
+ const parts = this.convertContent(msg.content);
5387
5490
  history.push({ role: "model", parts });
5388
5491
  }
5389
5492
  }
5390
5493
  return { history, lastMessage: lastUserMessage };
5391
5494
  }
5495
+ /**
5496
+ * Build a map from tool_use IDs to function names from assistant history.
5497
+ */
5498
+ buildToolUseNameMap(messages) {
5499
+ const map = /* @__PURE__ */ new Map();
5500
+ for (const msg of messages) {
5501
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
5502
+ for (const block of msg.content) {
5503
+ if (block.type === "tool_use") {
5504
+ map.set(block.id, block.name);
5505
+ }
5506
+ }
5507
+ }
5508
+ return map;
5509
+ }
5392
5510
  /**
5393
5511
  * Convert content to Gemini parts
5394
5512
  */
@@ -5465,14 +5583,15 @@ var init_gemini = __esm({
5465
5583
  let textContent = "";
5466
5584
  const toolCalls = [];
5467
5585
  if (candidate?.content?.parts) {
5586
+ let toolIndex = 0;
5468
5587
  for (const part of candidate.content.parts) {
5469
5588
  if ("text" in part && part.text) {
5470
5589
  textContent += part.text;
5471
5590
  }
5472
5591
  if ("functionCall" in part && part.functionCall) {
5592
+ toolIndex++;
5473
5593
  toolCalls.push({
5474
- id: part.functionCall.name,
5475
- // Use name as ID for Gemini
5594
+ id: `gemini_call_${toolIndex}`,
5476
5595
  name: part.functionCall.name,
5477
5596
  input: part.functionCall.args ?? {}
5478
5597
  });
@@ -6068,6 +6187,154 @@ var init_fallback = __esm({
6068
6187
  }
6069
6188
  });
6070
6189
 
6190
+ // src/providers/resilient.ts
6191
+ function sleep2(ms) {
6192
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
6193
+ }
6194
+ function computeRetryDelay(attempt, config) {
6195
+ const exp = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
6196
+ const capped = Math.min(exp, config.maxDelayMs);
6197
+ const jitter = capped * config.jitterFactor * (Math.random() * 2 - 1);
6198
+ return Math.max(0, Math.min(capped + jitter, config.maxDelayMs));
6199
+ }
6200
+ function getDefaultResilienceConfig(providerId) {
6201
+ if (providerId === "ollama" || providerId === "lmstudio") {
6202
+ return {
6203
+ retry: {
6204
+ maxRetries: 1,
6205
+ initialDelayMs: 300,
6206
+ maxDelayMs: 1500
6207
+ },
6208
+ streamRetry: {
6209
+ maxRetries: 0
6210
+ },
6211
+ circuitBreaker: {
6212
+ failureThreshold: 3,
6213
+ resetTimeout: 1e4
6214
+ }
6215
+ };
6216
+ }
6217
+ return {
6218
+ retry: {
6219
+ maxRetries: 3,
6220
+ initialDelayMs: 1e3,
6221
+ maxDelayMs: 3e4
6222
+ },
6223
+ streamRetry: {
6224
+ maxRetries: 1,
6225
+ initialDelayMs: 500,
6226
+ maxDelayMs: 5e3
6227
+ },
6228
+ circuitBreaker: {
6229
+ failureThreshold: 5,
6230
+ resetTimeout: 3e4
6231
+ }
6232
+ };
6233
+ }
6234
+ function createResilientProvider(provider, config) {
6235
+ return new ResilientProvider(provider, config ?? getDefaultResilienceConfig(provider.id));
6236
+ }
6237
+ var DEFAULT_STREAM_RETRY, ResilientProvider;
6238
+ var init_resilient = __esm({
6239
+ "src/providers/resilient.ts"() {
6240
+ init_retry();
6241
+ init_circuit_breaker();
6242
+ DEFAULT_STREAM_RETRY = {
6243
+ maxRetries: 1,
6244
+ initialDelayMs: 500,
6245
+ maxDelayMs: 5e3,
6246
+ backoffMultiplier: 2,
6247
+ jitterFactor: 0.1
6248
+ };
6249
+ ResilientProvider = class {
6250
+ id;
6251
+ name;
6252
+ provider;
6253
+ breaker;
6254
+ retryConfig;
6255
+ streamRetryConfig;
6256
+ constructor(provider, config = {}) {
6257
+ this.provider = provider;
6258
+ this.id = provider.id;
6259
+ this.name = provider.name;
6260
+ this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retry };
6261
+ this.streamRetryConfig = { ...DEFAULT_STREAM_RETRY, ...config.streamRetry };
6262
+ this.breaker = new CircuitBreaker(
6263
+ { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config.circuitBreaker },
6264
+ provider.id
6265
+ );
6266
+ }
6267
+ async initialize(config) {
6268
+ await this.provider.initialize(config);
6269
+ }
6270
+ async chat(messages, options) {
6271
+ return this.breaker.execute(
6272
+ () => withRetry(() => this.provider.chat(messages, options), this.retryConfig)
6273
+ );
6274
+ }
6275
+ async chatWithTools(messages, options) {
6276
+ return this.breaker.execute(
6277
+ () => withRetry(() => this.provider.chatWithTools(messages, options), this.retryConfig)
6278
+ );
6279
+ }
6280
+ async *stream(messages, options) {
6281
+ yield* this.streamWithPolicy(() => this.provider.stream(messages, options));
6282
+ }
6283
+ async *streamWithTools(messages, options) {
6284
+ yield* this.streamWithPolicy(() => this.provider.streamWithTools(messages, options));
6285
+ }
6286
+ countTokens(text13) {
6287
+ return this.provider.countTokens(text13);
6288
+ }
6289
+ getContextWindow() {
6290
+ return this.provider.getContextWindow();
6291
+ }
6292
+ async isAvailable() {
6293
+ try {
6294
+ return await this.breaker.execute(() => this.provider.isAvailable());
6295
+ } catch (error) {
6296
+ if (error instanceof CircuitOpenError) {
6297
+ return false;
6298
+ }
6299
+ return false;
6300
+ }
6301
+ }
6302
+ getCircuitState() {
6303
+ return this.breaker.getState();
6304
+ }
6305
+ resetCircuit() {
6306
+ this.breaker.reset();
6307
+ }
6308
+ async *streamWithPolicy(createStream) {
6309
+ let attempt = 0;
6310
+ while (attempt <= this.streamRetryConfig.maxRetries) {
6311
+ if (this.breaker.isOpen()) {
6312
+ throw new CircuitOpenError(this.id, 0);
6313
+ }
6314
+ let emittedChunk = false;
6315
+ try {
6316
+ for await (const chunk of createStream()) {
6317
+ emittedChunk = true;
6318
+ yield chunk;
6319
+ }
6320
+ this.breaker.recordSuccess();
6321
+ return;
6322
+ } catch (error) {
6323
+ this.breaker.recordFailure();
6324
+ const shouldRetry = !emittedChunk && attempt < this.streamRetryConfig.maxRetries && isRetryableError(error);
6325
+ if (!shouldRetry) {
6326
+ throw error;
6327
+ }
6328
+ const delay = computeRetryDelay(attempt, this.streamRetryConfig);
6329
+ await sleep2(delay);
6330
+ attempt++;
6331
+ }
6332
+ }
6333
+ }
6334
+ };
6335
+ }
6336
+ });
6337
+
6071
6338
  // src/config/env.ts
6072
6339
  var env_exports = {};
6073
6340
  __export(env_exports, {
@@ -6471,6 +6738,7 @@ __export(providers_exports, {
6471
6738
  MODEL_PRICING: () => MODEL_PRICING,
6472
6739
  OpenAIProvider: () => OpenAIProvider,
6473
6740
  ProviderFallback: () => ProviderFallback,
6741
+ ResilientProvider: () => ResilientProvider,
6474
6742
  createAnthropicProvider: () => createAnthropicProvider,
6475
6743
  createCircuitBreaker: () => createCircuitBreaker,
6476
6744
  createCodexProvider: () => createCodexProvider,
@@ -6481,10 +6749,12 @@ __export(providers_exports, {
6481
6749
  createOpenAIProvider: () => createOpenAIProvider,
6482
6750
  createProvider: () => createProvider,
6483
6751
  createProviderFallback: () => createProviderFallback,
6752
+ createResilientProvider: () => createResilientProvider,
6484
6753
  createRetryableMethod: () => createRetryableMethod,
6485
6754
  estimateCost: () => estimateCost,
6486
6755
  formatCost: () => formatCost,
6487
6756
  getDefaultProvider: () => getDefaultProvider2,
6757
+ getDefaultResilienceConfig: () => getDefaultResilienceConfig,
6488
6758
  getModelPricing: () => getModelPricing,
6489
6759
  hasKnownPricing: () => hasKnownPricing,
6490
6760
  isRetryableError: () => isRetryableError,
@@ -6520,12 +6790,10 @@ async function createProvider(type, config = {}) {
6520
6790
  break;
6521
6791
  case "kimi":
6522
6792
  provider = createKimiProvider(mergedConfig);
6523
- await provider.initialize(mergedConfig);
6524
- return provider;
6793
+ break;
6525
6794
  case "kimi-code":
6526
6795
  provider = createKimiCodeProvider(mergedConfig);
6527
- await provider.initialize(mergedConfig);
6528
- return provider;
6796
+ break;
6529
6797
  case "lmstudio":
6530
6798
  provider = new OpenAIProvider("lmstudio", "LM Studio");
6531
6799
  mergedConfig.baseUrl = mergedConfig.baseUrl ?? "http://localhost:1234/v1";
@@ -6570,7 +6838,10 @@ async function createProvider(type, config = {}) {
6570
6838
  });
6571
6839
  }
6572
6840
  await provider.initialize(mergedConfig);
6573
- return provider;
6841
+ const resilienceEnabled = !["0", "false", "off"].includes(
6842
+ (process.env["COCO_PROVIDER_RESILIENCE"] ?? "1").toLowerCase()
6843
+ );
6844
+ return resilienceEnabled ? createResilientProvider(provider) : provider;
6574
6845
  }
6575
6846
  async function getDefaultProvider2(config = {}) {
6576
6847
  const { getDefaultProvider: getEnvProvider } = await Promise.resolve().then(() => (init_env(), env_exports));
@@ -6624,6 +6895,7 @@ var init_providers = __esm({
6624
6895
  init_pricing();
6625
6896
  init_circuit_breaker();
6626
6897
  init_fallback();
6898
+ init_resilient();
6627
6899
  init_copilot();
6628
6900
  init_anthropic();
6629
6901
  init_openai();
@@ -6632,6 +6904,7 @@ var init_providers = __esm({
6632
6904
  init_copilot2();
6633
6905
  init_errors();
6634
6906
  init_env();
6907
+ init_resilient();
6635
6908
  }
6636
6909
  });
6637
6910
 
@@ -9394,7 +9667,7 @@ async function checkAndCompactContext(session, provider, signal, toolRegistry) {
9394
9667
  session.contextManager.setUsedTokens(result.compactedTokens);
9395
9668
  }
9396
9669
  return result;
9397
- } catch (error) {
9670
+ } catch {
9398
9671
  return null;
9399
9672
  }
9400
9673
  }
@@ -10498,6 +10771,34 @@ function isAbortError(error, signal) {
10498
10771
  if (error.message.endsWith("Request was aborted.")) return true;
10499
10772
  return false;
10500
10773
  }
10774
+ function classifyAgentLoopError(error, signal) {
10775
+ if (isAbortError(error, signal)) {
10776
+ return {
10777
+ kind: "abort",
10778
+ message: "Request was aborted.",
10779
+ original: error
10780
+ };
10781
+ }
10782
+ if (isNonRetryableProviderError(error)) {
10783
+ return {
10784
+ kind: "provider_non_retryable",
10785
+ message: error instanceof Error ? error.message : String(error),
10786
+ original: error
10787
+ };
10788
+ }
10789
+ if (error instanceof ProviderError) {
10790
+ return {
10791
+ kind: "provider_retryable",
10792
+ message: error.message,
10793
+ original: error
10794
+ };
10795
+ }
10796
+ return {
10797
+ kind: "unexpected",
10798
+ message: error instanceof Error ? error.message : String(error),
10799
+ original: error
10800
+ };
10801
+ }
10501
10802
  function isNonRetryableProviderError(error) {
10502
10803
  if (error instanceof ProviderError) {
10503
10804
  const code = error.statusCode;
@@ -19048,7 +19349,7 @@ async function runCIChecks(ctx) {
19048
19349
  cwd: ctx.cwd
19049
19350
  });
19050
19351
  if (result.checks.length === 0) {
19051
- await sleep2(pollMs);
19352
+ await sleep3(pollMs);
19052
19353
  continue;
19053
19354
  }
19054
19355
  if (result.allPassed) {
@@ -19098,7 +19399,7 @@ async function runCIChecks(ctx) {
19098
19399
  spinner18.message(`CI: ${passed}/${result.checks.length} passed, ${pending} pending...`);
19099
19400
  } catch {
19100
19401
  }
19101
- await sleep2(pollMs);
19402
+ await sleep3(pollMs);
19102
19403
  }
19103
19404
  spinner18.stop("CI check timeout");
19104
19405
  const action = await p26.select({
@@ -19123,7 +19424,7 @@ async function runCIChecks(ctx) {
19123
19424
  durationMs: performance.now() - start
19124
19425
  };
19125
19426
  }
19126
- function sleep2(ms) {
19427
+ function sleep3(ms) {
19127
19428
  return new Promise((resolve4) => setTimeout(resolve4, ms));
19128
19429
  }
19129
19430
  var init_ci_checks = __esm({
@@ -31546,6 +31847,15 @@ async function selectModelInteractively(models, currentModelId) {
31546
31847
  renderMenu();
31547
31848
  });
31548
31849
  }
31850
+ async function persistModelPreference(provider, model) {
31851
+ try {
31852
+ await saveProviderPreference(provider, model);
31853
+ } catch (error) {
31854
+ const reason = error instanceof Error ? error.message : String(error);
31855
+ console.log(chalk2.yellow(`\u26A0 Could not persist model preference: ${reason}`));
31856
+ console.log(chalk2.dim(" Model changed for this session only.\n"));
31857
+ }
31858
+ }
31549
31859
  var modelCommand = {
31550
31860
  name: "model",
31551
31861
  aliases: ["m"],
@@ -31594,7 +31904,7 @@ var modelCommand = {
31594
31904
  return false;
31595
31905
  }
31596
31906
  session.config.provider.model = selectedModel;
31597
- await saveProviderPreference(currentProvider, selectedModel);
31907
+ await persistModelPreference(currentProvider, selectedModel);
31598
31908
  const modelInfo2 = providerDef.models.find((m) => m.id === selectedModel);
31599
31909
  console.log(chalk2.green(`\u2713 Switched to ${modelInfo2?.name ?? selectedModel}
31600
31910
  `));
@@ -31616,7 +31926,7 @@ var modelCommand = {
31616
31926
  if (!foundInProvider) {
31617
31927
  console.log(chalk2.yellow(`Model "${newModel}" not in known list, setting anyway...`));
31618
31928
  session.config.provider.model = newModel;
31619
- await saveProviderPreference(currentProvider, newModel);
31929
+ await persistModelPreference(currentProvider, newModel);
31620
31930
  console.log(chalk2.green(`\u2713 Model set to: ${newModel}
31621
31931
  `));
31622
31932
  return false;
@@ -31631,7 +31941,7 @@ var modelCommand = {
31631
31941
  return false;
31632
31942
  }
31633
31943
  session.config.provider.model = newModel;
31634
- await saveProviderPreference(currentProvider, newModel);
31944
+ await persistModelPreference(currentProvider, newModel);
31635
31945
  const modelInfo = providerDef.models.find((m) => m.id === newModel);
31636
31946
  console.log(chalk2.green(`\u2713 Switched to ${modelInfo?.name ?? newModel}
31637
31947
  `));
@@ -50052,6 +50362,60 @@ function extractDeniedPath(error) {
50052
50362
  // src/cli/repl/agent-loop.ts
50053
50363
  init_allow_path_prompt();
50054
50364
  init_error_resilience();
50365
+
50366
+ // src/cli/repl/turn-quality.ts
50367
+ function clamp(value, min, max) {
50368
+ return Math.max(min, Math.min(max, value));
50369
+ }
50370
+ function computeTurnQualityMetrics(input) {
50371
+ const executedToolCalls = input.executedTools.length;
50372
+ const successfulToolCalls = input.executedTools.filter((t) => t.result.success).length;
50373
+ const failedToolCalls = executedToolCalls - successfulToolCalls;
50374
+ let score = 100;
50375
+ if (input.hadError) score -= 25;
50376
+ if (executedToolCalls > 0) {
50377
+ const failureRatio = failedToolCalls / executedToolCalls;
50378
+ score -= Math.round(failureRatio * 35);
50379
+ }
50380
+ if (input.maxIterations > 0) {
50381
+ const iterationRatio = input.iterationsUsed / input.maxIterations;
50382
+ if (iterationRatio > 0.8) score -= 10;
50383
+ if (iterationRatio >= 1) score -= 10;
50384
+ }
50385
+ score += Math.min(input.repeatedOutputsSuppressed * 2, 8);
50386
+ return {
50387
+ score: clamp(score, 0, 100),
50388
+ iterationsUsed: input.iterationsUsed,
50389
+ maxIterations: input.maxIterations,
50390
+ executedToolCalls,
50391
+ successfulToolCalls,
50392
+ failedToolCalls,
50393
+ hadError: input.hadError,
50394
+ repeatedOutputsSuppressed: input.repeatedOutputsSuppressed
50395
+ };
50396
+ }
50397
+ var RepeatedOutputSuppressor = class {
50398
+ seen = /* @__PURE__ */ new Map();
50399
+ transform(toolName, content) {
50400
+ const fingerprint = this.fingerprint(toolName, content);
50401
+ const count = this.seen.get(fingerprint) ?? 0;
50402
+ this.seen.set(fingerprint, count + 1);
50403
+ if (count === 0) {
50404
+ return { content, suppressed: false };
50405
+ }
50406
+ return {
50407
+ content: `[Repeated tool output suppressed: '${toolName}' produced the same output as before (occurrence ${count + 1}). If needed, re-run with different inputs.]`,
50408
+ suppressed: true
50409
+ };
50410
+ }
50411
+ fingerprint(toolName, content) {
50412
+ const head = content.slice(0, 200);
50413
+ const tail = content.slice(-200);
50414
+ return `${toolName}|${content.length}|${head}|${tail}`;
50415
+ }
50416
+ };
50417
+
50418
+ // src/cli/repl/agent-loop.ts
50055
50419
  async function executeAgentTurn(session, userMessage, provider, toolRegistry, options = {}) {
50056
50420
  resetLineBuffer();
50057
50421
  const messageSnapshot = session.messages.length;
@@ -50060,12 +50424,23 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
50060
50424
  let totalInputTokens = 0;
50061
50425
  let totalOutputTokens = 0;
50062
50426
  let finalContent = "";
50427
+ let hadTurnError = false;
50428
+ let repeatedOutputsSuppressed = 0;
50429
+ const repeatedOutputSuppressor = new RepeatedOutputSuppressor();
50430
+ const buildQualityMetrics = () => computeTurnQualityMetrics({
50431
+ iterationsUsed: iteration,
50432
+ maxIterations,
50433
+ executedTools,
50434
+ hadError: hadTurnError,
50435
+ repeatedOutputsSuppressed
50436
+ });
50063
50437
  const abortReturn = () => {
50064
50438
  session.messages.length = messageSnapshot;
50065
50439
  return {
50066
50440
  content: finalContent,
50067
50441
  toolCalls: executedTools,
50068
50442
  usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
50443
+ quality: buildQualityMetrics(),
50069
50444
  aborted: true,
50070
50445
  partialContent: finalContent || void 0,
50071
50446
  abortReason: "user_cancel"
@@ -50179,13 +50554,15 @@ ${tail}`;
50179
50554
  options.onThinkingEnd?.();
50180
50555
  thinkingEnded = true;
50181
50556
  }
50182
- if (isAbortError(streamError, options.signal)) {
50557
+ const classification = classifyAgentLoopError(streamError, options.signal);
50558
+ if (classification.kind === "abort") {
50183
50559
  return abortReturn();
50184
50560
  }
50185
- if (isNonRetryableProviderError(streamError)) {
50186
- throw streamError;
50561
+ if (classification.kind === "provider_non_retryable") {
50562
+ throw classification.original;
50187
50563
  }
50188
- const errorMsg = streamError instanceof Error ? streamError.message : String(streamError);
50564
+ hadTurnError = true;
50565
+ const errorMsg = classification.message;
50189
50566
  addMessage(session, {
50190
50567
  role: "assistant",
50191
50568
  content: `[Error during streaming: ${errorMsg}]`
@@ -50194,6 +50571,7 @@ ${tail}`;
50194
50571
  content: finalContent || `[Error: ${errorMsg}]`,
50195
50572
  toolCalls: executedTools,
50196
50573
  usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
50574
+ quality: buildQualityMetrics(),
50197
50575
  aborted: false,
50198
50576
  partialContent: finalContent || void 0,
50199
50577
  error: errorMsg
@@ -50399,10 +50777,15 @@ ${tail}`;
50399
50777
  }
50400
50778
  const executedCall = executedTools.find((e) => e.id === toolCall.id);
50401
50779
  if (executedCall) {
50780
+ const truncatedOutput = truncateInlineResult(executedCall.result.output, toolCall.name);
50781
+ const transformedOutput = repeatedOutputSuppressor.transform(toolCall.name, truncatedOutput);
50782
+ if (transformedOutput.suppressed) {
50783
+ repeatedOutputsSuppressed++;
50784
+ }
50402
50785
  toolResults.push({
50403
50786
  type: "tool_result",
50404
50787
  tool_use_id: toolCall.id,
50405
- content: truncateInlineResult(executedCall.result.output, toolCall.name),
50788
+ content: transformedOutput.content,
50406
50789
  is_error: !executedCall.result.success
50407
50790
  });
50408
50791
  } else {
@@ -50550,6 +50933,7 @@ I have reached the maximum iteration limit (${maxIterations}). The task may be i
50550
50933
  content: finalContent,
50551
50934
  toolCalls: executedTools,
50552
50935
  usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
50936
+ quality: buildQualityMetrics(),
50553
50937
  aborted: false
50554
50938
  };
50555
50939
  }