@yourgpt/copilot-sdk 2.1.3 → 2.1.5-alpha.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.
Files changed (103) hide show
  1. package/dist/MessageTree-CoIt_4nB.d.cts +161 -0
  2. package/dist/MessageTree-CzaN9Eul.d.ts +161 -0
  3. package/dist/{ThreadManager-Dkp_eLty.d.ts → ThreadManager-BEAECB7Y.d.ts} +1 -1
  4. package/dist/{ThreadManager-LfFRhr4e.d.cts → ThreadManager-Cw5fwyCN.d.cts} +1 -1
  5. package/dist/{chunk-POZNNKNJ.cjs → chunk-246B6X5D.cjs} +8 -2
  6. package/dist/chunk-246B6X5D.cjs.map +1 -0
  7. package/dist/{chunk-QLH6TSCC.js → chunk-4QXY2PBG.js} +8 -2
  8. package/dist/chunk-4QXY2PBG.js.map +1 -0
  9. package/dist/{chunk-DMBFN7KO.js → chunk-6BMQZIS3.js} +3105 -355
  10. package/dist/chunk-6BMQZIS3.js.map +1 -0
  11. package/dist/{chunk-R6LKHKAI.cjs → chunk-76RE7AJE.cjs} +3277 -509
  12. package/dist/chunk-76RE7AJE.cjs.map +1 -0
  13. package/dist/chunk-BJYA5NDL.cjs +96 -0
  14. package/dist/chunk-BJYA5NDL.cjs.map +1 -0
  15. package/dist/{chunk-LZMBBGWH.js → chunk-ENFWM3EY.js} +4 -4
  16. package/dist/{chunk-LZMBBGWH.js.map → chunk-ENFWM3EY.js.map} +1 -1
  17. package/dist/{chunk-WQSK3Z4K.cjs → chunk-I3SQUNTT.cjs} +81 -31
  18. package/dist/chunk-I3SQUNTT.cjs.map +1 -0
  19. package/dist/{chunk-WAPGTQDR.cjs → chunk-JKGFQUHJ.cjs} +10 -10
  20. package/dist/{chunk-WAPGTQDR.cjs.map → chunk-JKGFQUHJ.cjs.map} +1 -1
  21. package/dist/{chunk-XGITAEXU.js → chunk-LLM7AHMO.js} +2 -2
  22. package/dist/{chunk-XGITAEXU.js.map → chunk-LLM7AHMO.js.map} +1 -1
  23. package/dist/{chunk-ASV6JLYG.cjs → chunk-NUXLAZOE.cjs} +2 -2
  24. package/dist/{chunk-ASV6JLYG.cjs.map → chunk-NUXLAZOE.cjs.map} +1 -1
  25. package/dist/{chunk-VFV5FVVI.js → chunk-UXJ6LIZB.js} +61 -13
  26. package/dist/chunk-UXJ6LIZB.js.map +1 -0
  27. package/dist/chunk-VNLLW3ZI.js +94 -0
  28. package/dist/chunk-VNLLW3ZI.js.map +1 -0
  29. package/dist/core/index.cjs +99 -91
  30. package/dist/core/index.d.cts +21 -10
  31. package/dist/core/index.d.ts +21 -10
  32. package/dist/core/index.js +5 -5
  33. package/dist/{index-BHkRA0mM.d.cts → index-CiExk87c.d.cts} +1 -1
  34. package/dist/{index-tB0qI8my.d.ts → index-Dwrcf-CP.d.ts} +1 -1
  35. package/dist/mcp/index.d.cts +3 -3
  36. package/dist/mcp/index.d.ts +3 -3
  37. package/dist/react/index.cjs +113 -52
  38. package/dist/react/index.d.cts +703 -90
  39. package/dist/react/index.d.ts +703 -90
  40. package/dist/react/index.js +7 -6
  41. package/dist/server/index.cjs +339 -0
  42. package/dist/server/index.cjs.map +1 -0
  43. package/dist/server/index.d.cts +171 -0
  44. package/dist/server/index.d.ts +171 -0
  45. package/dist/server/index.js +332 -0
  46. package/dist/server/index.js.map +1 -0
  47. package/dist/tools/anthropic/index.cjs +3 -3
  48. package/dist/tools/anthropic/index.d.cts +1 -1
  49. package/dist/tools/anthropic/index.d.ts +1 -1
  50. package/dist/tools/anthropic/index.js +3 -3
  51. package/dist/tools/brave/index.cjs +6 -6
  52. package/dist/tools/brave/index.d.cts +1 -1
  53. package/dist/tools/brave/index.d.ts +1 -1
  54. package/dist/tools/brave/index.js +3 -3
  55. package/dist/tools/exa/index.cjs +6 -6
  56. package/dist/tools/exa/index.d.cts +1 -1
  57. package/dist/tools/exa/index.d.ts +1 -1
  58. package/dist/tools/exa/index.js +3 -3
  59. package/dist/tools/google/index.cjs +6 -6
  60. package/dist/tools/google/index.d.cts +1 -1
  61. package/dist/tools/google/index.d.ts +1 -1
  62. package/dist/tools/google/index.js +3 -3
  63. package/dist/tools/openai/index.cjs +6 -6
  64. package/dist/tools/openai/index.d.cts +1 -1
  65. package/dist/tools/openai/index.d.ts +1 -1
  66. package/dist/tools/openai/index.js +3 -3
  67. package/dist/tools/searxng/index.cjs +6 -6
  68. package/dist/tools/searxng/index.d.cts +1 -1
  69. package/dist/tools/searxng/index.d.ts +1 -1
  70. package/dist/tools/searxng/index.js +3 -3
  71. package/dist/tools/serper/index.cjs +6 -6
  72. package/dist/tools/serper/index.d.cts +1 -1
  73. package/dist/tools/serper/index.d.ts +1 -1
  74. package/dist/tools/serper/index.js +3 -3
  75. package/dist/tools/tavily/index.cjs +6 -6
  76. package/dist/tools/tavily/index.d.cts +1 -1
  77. package/dist/tools/tavily/index.d.ts +1 -1
  78. package/dist/tools/tavily/index.js +3 -3
  79. package/dist/tools/web-search/index.cjs +7 -7
  80. package/dist/tools/web-search/index.d.cts +2 -2
  81. package/dist/tools/web-search/index.d.ts +2 -2
  82. package/dist/tools/web-search/index.js +4 -4
  83. package/dist/{tools-coIcskZ4.d.ts → tools-DHZhF5km.d.cts} +161 -1
  84. package/dist/{tools-coIcskZ4.d.cts → tools-DHZhF5km.d.ts} +161 -1
  85. package/dist/{types-rjaSVmEF.d.ts → types-BTyJu0WD.d.ts} +1 -1
  86. package/dist/types-BckL3hiw.d.cts +93 -0
  87. package/dist/types-BckL3hiw.d.ts +93 -0
  88. package/dist/{types-C8t4Ut8f.d.cts → types-BdX7uPj0.d.cts} +1 -1
  89. package/dist/{types-DG2ya08y.d.ts → types-BeFBBZ5i.d.cts} +64 -1
  90. package/dist/{types-DG2ya08y.d.cts → types-BeFBBZ5i.d.ts} +64 -1
  91. package/dist/ui/index.cjs +468 -163
  92. package/dist/ui/index.cjs.map +1 -1
  93. package/dist/ui/index.d.cts +81 -4
  94. package/dist/ui/index.d.ts +81 -4
  95. package/dist/ui/index.js +416 -112
  96. package/dist/ui/index.js.map +1 -1
  97. package/package.json +6 -1
  98. package/dist/chunk-DMBFN7KO.js.map +0 -1
  99. package/dist/chunk-POZNNKNJ.cjs.map +0 -1
  100. package/dist/chunk-QLH6TSCC.js.map +0 -1
  101. package/dist/chunk-R6LKHKAI.cjs.map +0 -1
  102. package/dist/chunk-VFV5FVVI.js.map +0 -1
  103. package/dist/chunk-WQSK3Z4K.cjs.map +0 -1
@@ -1,6 +1,7 @@
1
- import { ThreadManager, isConsoleCaptureActive, startConsoleCapture, isNetworkCaptureActive, startNetworkCapture, stopConsoleCapture, stopNetworkCapture, isScreenshotSupported, captureScreenshot, getConsoleLogs, getNetworkRequests, clearConsoleLogs, clearNetworkRequests, formatLogsForAI, formatRequestsForAI, detectIntent, streamSSE, zodObjectToInputSchema } from './chunk-VFV5FVVI.js';
1
+ import { ThreadManager, createLogger, zodToJsonSchema, isConsoleCaptureActive, startConsoleCapture, isNetworkCaptureActive, startNetworkCapture, stopConsoleCapture, stopNetworkCapture, isScreenshotSupported, captureScreenshot, getConsoleLogs, getNetworkRequests, clearConsoleLogs, clearNetworkRequests, formatLogsForAI, formatRequestsForAI, detectIntent, streamSSE, zodObjectToInputSchema } from './chunk-UXJ6LIZB.js';
2
2
  import { createMCPClient, MCPToolAdapter } from './chunk-EWVQWTNV.js';
3
- import { createContext, useRef, useState, useCallback, useEffect, useMemo, useContext, useSyncExternalStore } from 'react';
3
+ import { SkillRegistry } from './chunk-VNLLW3ZI.js';
4
+ import React2, { createContext, useRef, useState, useCallback, useEffect, useMemo, useContext, useSyncExternalStore } from 'react';
4
5
  import { jsxs, jsx } from 'react/jsx-runtime';
5
6
  import * as z from 'zod';
6
7
 
@@ -119,15 +120,6 @@ function parseSSELine(line) {
119
120
  function isStreamDone(chunk) {
120
121
  return chunk.type === "done" || chunk.type === "error";
121
122
  }
122
- function requiresToolExecution(chunk) {
123
- if (chunk.type === "done" && chunk.requiresAction) {
124
- return true;
125
- }
126
- if (chunk.type === "tool_calls") {
127
- return true;
128
- }
129
- return false;
130
- }
131
123
 
132
124
  // src/chat/functions/stream/processChunk.ts
133
125
  function processStreamChunk(chunk, state) {
@@ -262,13 +254,14 @@ function createStreamState(messageId) {
262
254
  function generateMessageId() {
263
255
  return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
264
256
  }
265
- function createUserMessage(content, attachments) {
257
+ function createUserMessage(content, attachments, options) {
266
258
  return {
267
259
  id: generateMessageId(),
268
260
  role: "user",
269
261
  content,
270
262
  attachments,
271
- createdAt: /* @__PURE__ */ new Date()
263
+ createdAt: /* @__PURE__ */ new Date(),
264
+ ...options?.parentId !== void 0 ? { parentId: options.parentId } : {}
272
265
  };
273
266
  }
274
267
  function streamStateToMessage(state) {
@@ -307,12 +300,13 @@ function streamStateToMessage(state) {
307
300
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0
308
301
  };
309
302
  }
310
- function createEmptyAssistantMessage(id) {
303
+ function createEmptyAssistantMessage(id, options) {
311
304
  return {
312
305
  id: generateMessageId(),
313
306
  role: "assistant",
314
307
  content: "",
315
- createdAt: /* @__PURE__ */ new Date()
308
+ createdAt: /* @__PURE__ */ new Date(),
309
+ ...options?.parentId !== void 0 ? { parentId: options.parentId } : {}
316
310
  };
317
311
  }
318
312
 
@@ -381,6 +375,7 @@ var HttpTransport = class {
381
375
  tools: request.tools,
382
376
  actions: request.actions,
383
377
  streaming: this.config.streaming,
378
+ __skills: request.__skills,
384
379
  ...resolved.configBody,
385
380
  ...request.body
386
381
  }),
@@ -521,17 +516,443 @@ var HttpTransport = class {
521
516
  }
522
517
  };
523
518
 
524
- // src/chat/classes/AbstractChat.ts
525
- function buildToolResultContentForAI(result, tool2, args) {
526
- if (typeof result === "string") return result;
527
- const typedResult = result;
519
+ // src/chat/optimizations.ts
520
+ var DEFAULT_CHARS_PER_TOKEN = 4;
521
+ var DEFAULT_SAFETY_MARGIN = 1.2;
522
+ var DEFAULT_INPUT_HEADROOM_RATIO = 0.75;
523
+ var DEFAULT_SYSTEM_PROMPT_SHARE = 0.15;
524
+ var DEFAULT_HISTORY_SHARE = 0.5;
525
+ var DEFAULT_TOOL_RESULTS_SHARE = 0.3;
526
+ var DEFAULT_TOOL_DEFINITIONS_SHARE = 0.05;
527
+ var DEFAULT_MAX_TOOL_RESULT_CONTEXT_SHARE = 0.3;
528
+ var DEFAULT_TOOL_RESULT_HARD_MAX_CHARS = 4e5;
529
+ var DEFAULT_TOOL_RESULT_MIN_KEEP_CHARS = 2e3;
530
+ var DEFAULT_TOOL_RESULT_STRATEGY = "head-tail";
531
+ var DEFAULT_RECENT_HISTORY_PRESERVE = 6;
532
+ var TOOL_RESULT_TRUNCATION_NOTICE = "\n\n[tool result truncated to fit prompt budget]";
533
+ var TOOL_RESULT_COMPACTION_NOTICE = "[tool result compacted to preserve context budget]";
534
+ var SYSTEM_PROMPT_TRUNCATION_NOTICE = "\n\n[system prompt truncated to fit prompt budget]";
535
+ var HISTORY_SUMMARY_HEADER = "Conversation summary of earlier context:";
536
+ var HISTORY_SUMMARY_COMPACTION_NOTICE = "\n\n[summary compacted to preserve context continuity]";
537
+ var DEFAULT_SUMMARY_TRIGGER = 12;
538
+ var DEFAULT_SUMMARY_CHUNK_SIZE = 10;
539
+ var DEFAULT_SUMMARY_MAX_CHARS = 1600;
540
+ var SUMMARY_STOP_WORDS = /* @__PURE__ */ new Set([
541
+ "about",
542
+ "after",
543
+ "again",
544
+ "also",
545
+ "because",
546
+ "been",
547
+ "before",
548
+ "being",
549
+ "could",
550
+ "from",
551
+ "have",
552
+ "into",
553
+ "just",
554
+ "more",
555
+ "need",
556
+ "only",
557
+ "over",
558
+ "same",
559
+ "some",
560
+ "than",
561
+ "that",
562
+ "their",
563
+ "them",
564
+ "then",
565
+ "there",
566
+ "these",
567
+ "they",
568
+ "this",
569
+ "those",
570
+ "through",
571
+ "under",
572
+ "very",
573
+ "want",
574
+ "were",
575
+ "what",
576
+ "when",
577
+ "where",
578
+ "which",
579
+ "while",
580
+ "with",
581
+ "would",
582
+ "your"
583
+ ]);
584
+ function clampRatio(value, fallback) {
585
+ if (!Number.isFinite(value)) {
586
+ return fallback;
587
+ }
588
+ return Math.min(1, Math.max(0, value));
589
+ }
590
+ function unique(values) {
591
+ return [...new Set(values)];
592
+ }
593
+ function stringifyContent(value) {
594
+ if (typeof value === "string") {
595
+ return value;
596
+ }
597
+ if (value == null) {
598
+ return "";
599
+ }
600
+ try {
601
+ return JSON.stringify(value);
602
+ } catch {
603
+ return String(value);
604
+ }
605
+ }
606
+ function normalizeWhitespace(text) {
607
+ return text.replace(/\s+/g, " ").trim();
608
+ }
609
+ function abbreviateText(text, maxChars = 220) {
610
+ const normalized = normalizeWhitespace(text);
611
+ if (!normalized) {
612
+ return "";
613
+ }
614
+ if (normalized.length <= maxChars) {
615
+ return normalized;
616
+ }
617
+ return `${normalized.slice(0, Math.max(1, maxChars - 3)).trimEnd()}...`;
618
+ }
619
+ function tokenize(text) {
620
+ return text.toLowerCase().replace(/[^a-z0-9_\s-]/g, " ").split(/\s+/).filter((token) => token.length > 1);
621
+ }
622
+ function estimateTokens(text, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
623
+ if (!text) {
624
+ return 0;
625
+ }
626
+ return Math.ceil(text.length / Math.max(1, charsPerToken));
627
+ }
628
+ function estimateMessageTokens(message, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
629
+ const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "");
630
+ const toolCalls = message.tool_calls ? JSON.stringify(message.tool_calls) : "";
631
+ const attachments = message.attachments ? JSON.stringify(message.attachments) : "";
632
+ return estimateTokens(
633
+ `${message.role}
634
+ ${content}
635
+ ${toolCalls}
636
+ ${attachments}`,
637
+ charsPerToken
638
+ );
639
+ }
640
+ function estimateToolTokens(tool2, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
641
+ return estimateTokens(JSON.stringify(tool2), charsPerToken);
642
+ }
643
+ function buildToolQuery(messages) {
644
+ return messages.filter(
645
+ (message) => message.role === "user" || message.role === "assistant"
646
+ ).slice(-3).map((message) => message.content).filter(Boolean).join(" ");
647
+ }
648
+ function matchesSelector(tool2, selector, activeProfile) {
649
+ const normalized = selector.trim().toLowerCase();
650
+ if (!normalized) {
651
+ return false;
652
+ }
653
+ if (normalized === "*" || normalized === "all") {
654
+ return true;
655
+ }
656
+ if (normalized === tool2.name.toLowerCase()) {
657
+ return true;
658
+ }
659
+ if (normalized.startsWith("group:")) {
660
+ return (tool2.group ?? "").toLowerCase() === normalized.slice(6);
661
+ }
662
+ if (normalized.startsWith("category:")) {
663
+ return (tool2.category ?? "").toLowerCase() === normalized.slice(9);
664
+ }
665
+ if (normalized.startsWith("profile:")) {
666
+ return (tool2.profiles ?? []).map((value) => value.toLowerCase()).includes(normalized.slice(8));
667
+ }
668
+ if (activeProfile && normalized === activeProfile.toLowerCase()) {
669
+ return (tool2.profiles ?? []).map((value) => value.toLowerCase()).includes(normalized);
670
+ }
671
+ return false;
672
+ }
673
+ function scoreTool(tool2, queryTokens, activeProfile) {
674
+ const haystack = [
675
+ tool2.name,
676
+ tool2.description,
677
+ tool2.category,
678
+ tool2.group,
679
+ ...tool2.profiles ?? [],
680
+ ...tool2.searchKeywords ?? []
681
+ ].filter(Boolean).join(" ").toLowerCase();
682
+ let score = tool2.deferLoading ? 0 : 2;
683
+ if (activeProfile && tool2.profiles?.includes(activeProfile)) {
684
+ score += 2;
685
+ }
686
+ for (const token of queryTokens) {
687
+ if (tool2.name.toLowerCase() === token) {
688
+ score += 6;
689
+ } else if (tool2.name.toLowerCase().includes(token)) {
690
+ score += 4;
691
+ } else if (haystack.includes(token)) {
692
+ score += 2;
693
+ }
694
+ }
695
+ return score;
696
+ }
697
+ function truncateText(text, maxChars, strategy, notice = TOOL_RESULT_TRUNCATION_NOTICE) {
698
+ if (text.length <= maxChars) {
699
+ return text;
700
+ }
701
+ const bodyBudget = Math.max(1, maxChars - notice.length);
702
+ if (strategy === "head") {
703
+ return text.slice(0, bodyBudget) + notice;
704
+ }
705
+ if (strategy === "head-tail" || strategy === "smart") {
706
+ const tailLooksImportant = strategy === "smart" ? /\b(error|exception|failed|traceback|summary|result|done|complete)\b/i.test(
707
+ text.slice(-2e3)
708
+ ) : true;
709
+ if (tailLooksImportant && bodyBudget > 32) {
710
+ const tailBudget = Math.min(Math.floor(bodyBudget * 0.3), 4e3);
711
+ const headBudget = Math.max(1, bodyBudget - tailBudget - 32);
712
+ return text.slice(0, headBudget) + "\n\n[... omitted ...]\n\n" + text.slice(-tailBudget) + notice;
713
+ }
714
+ }
715
+ return text.slice(0, bodyBudget) + notice;
716
+ }
717
+ function isHistorySummaryMessage(message) {
718
+ return message.role === "system" && typeof message.content === "string" && message.content.startsWith(HISTORY_SUMMARY_HEADER);
719
+ }
720
+ function collectTopKeywords(messages) {
721
+ const counts = /* @__PURE__ */ new Map();
722
+ for (const message of messages) {
723
+ if (message.role !== "user") {
724
+ continue;
725
+ }
726
+ for (const token of tokenize(stringifyContent(message.content))) {
727
+ if (token.length < 3 || SUMMARY_STOP_WORDS.has(token)) {
728
+ continue;
729
+ }
730
+ counts.set(token, (counts.get(token) ?? 0) + 1);
731
+ }
732
+ }
733
+ return [...counts.entries()].sort((left, right) => {
734
+ const countDiff = right[1] - left[1];
735
+ if (countDiff !== 0) {
736
+ return countDiff;
737
+ }
738
+ return left[0].localeCompare(right[0]);
739
+ }).slice(0, 5).map(([token]) => token);
740
+ }
741
+ function collectToolCallNames(messages) {
742
+ const toolCallNames = /* @__PURE__ */ new Map();
743
+ for (const message of messages) {
744
+ if (message.role !== "assistant" || !message.tool_calls?.length) {
745
+ continue;
746
+ }
747
+ for (const toolCall of message.tool_calls) {
748
+ const parsedToolCall = toolCall;
749
+ if (parsedToolCall.id && parsedToolCall.function?.name) {
750
+ toolCallNames.set(parsedToolCall.id, parsedToolCall.function.name);
751
+ }
752
+ }
753
+ }
754
+ return toolCallNames;
755
+ }
756
+ function compressSummaryContent(content, maxChars, fallbackBehavior) {
757
+ if (content.length <= maxChars) {
758
+ return content;
759
+ }
760
+ if (fallbackBehavior === "error") {
761
+ throw new Error("History summary exceeded configured continuity budget.");
762
+ }
763
+ if (fallbackBehavior === "statistical") {
764
+ const lines = content.split("\n");
765
+ const retained = lines.filter(
766
+ (line) => /^(Conversation summary|Stats:|- Messages compacted:|- User turns compacted:|- Assistant turns compacted:|- Tool results compacted:|- Latest user request before preserved window:|- Latest assistant response before preserved window:)/.test(
767
+ line
768
+ )
769
+ );
770
+ const statistical = retained.join("\n");
771
+ if (statistical.length <= maxChars) {
772
+ return statistical;
773
+ }
774
+ }
775
+ return truncateText(
776
+ content,
777
+ maxChars,
778
+ "head",
779
+ HISTORY_SUMMARY_COMPACTION_NOTICE
780
+ );
781
+ }
782
+ function buildHistorySummary(messages, summarization, maxChars = DEFAULT_SUMMARY_MAX_CHARS) {
783
+ if (messages.length === 0) {
784
+ return null;
785
+ }
786
+ if (summarization?.enabled === false) {
787
+ return null;
788
+ }
789
+ const previousSummaries = messages.filter(isHistorySummaryMessage);
790
+ const rawMessages = messages.filter(
791
+ (message) => !isHistorySummaryMessage(message)
792
+ );
793
+ const focusWindowSize = Math.max(
794
+ 1,
795
+ summarization?.chunkSize ?? DEFAULT_SUMMARY_CHUNK_SIZE
796
+ );
797
+ const detailedThreshold = Math.max(
798
+ 1,
799
+ summarization?.triggerAt ?? DEFAULT_SUMMARY_TRIGGER
800
+ );
801
+ const focusMessages = rawMessages.slice(-focusWindowSize);
802
+ const userMessages = rawMessages.filter((message) => message.role === "user");
803
+ const assistantMessages = rawMessages.filter(
804
+ (message) => message.role === "assistant"
805
+ );
806
+ const toolMessages = rawMessages.filter((message) => message.role === "tool");
807
+ const recentUser = abbreviateText(stringifyContent(userMessages.at(-1)?.content), 240) || "n/a";
808
+ const recentAssistant = abbreviateText(stringifyContent(assistantMessages.at(-1)?.content), 240) || "n/a";
809
+ const recentUserGoals = userMessages.slice(-3).map((message) => abbreviateText(stringifyContent(message.content), 180)).filter(Boolean);
810
+ const recentAssistantNotes = assistantMessages.map((message) => abbreviateText(stringifyContent(message.content), 180)).filter(Boolean).slice(-2);
811
+ const toolCallNames = collectToolCallNames(rawMessages);
812
+ const toolActivity = unique([
813
+ ...focusMessages.filter((message) => message.role === "assistant").flatMap(
814
+ (message) => (message.tool_calls ?? []).map((toolCall) => {
815
+ const parsedToolCall = toolCall;
816
+ return parsedToolCall.function?.name;
817
+ }).filter((name) => Boolean(name))
818
+ ),
819
+ ...toolMessages.slice(-2).map((message) => {
820
+ const toolName = message.tool_call_id ? toolCallNames.get(message.tool_call_id) : void 0;
821
+ const snippet = abbreviateText(stringifyContent(message.content), 120);
822
+ return toolName && snippet ? `${toolName}: ${snippet}` : toolName ?? snippet;
823
+ }).filter(Boolean)
824
+ ]).slice(-4);
825
+ const priorSummaryCarryForward = previousSummaries.map(
826
+ (message) => abbreviateText(
827
+ stringifyContent(message.content).replace(
828
+ `${HISTORY_SUMMARY_HEADER}
829
+ `,
830
+ ""
831
+ ),
832
+ 180
833
+ )
834
+ ).filter(Boolean).slice(-2);
835
+ const topKeywords = rawMessages.length >= detailedThreshold ? collectTopKeywords(
836
+ focusMessages.length > 0 ? focusMessages : rawMessages
837
+ ) : [];
838
+ const lines = [
839
+ HISTORY_SUMMARY_HEADER,
840
+ "Stats:",
841
+ `- Messages compacted: ${messages.length}`,
842
+ `- User turns compacted: ${userMessages.length}`,
843
+ `- Assistant turns compacted: ${assistantMessages.length}`,
844
+ `- Tool results compacted: ${toolMessages.length}`
845
+ ];
846
+ if (previousSummaries.length > 0) {
847
+ lines.push(`- Previous summaries merged: ${previousSummaries.length}`);
848
+ }
849
+ if (rawMessages.length > focusMessages.length) {
850
+ lines.push(
851
+ `- Older compacted messages outside the detailed window: ${rawMessages.length - focusMessages.length}`
852
+ );
853
+ }
854
+ if (topKeywords.length > 0) {
855
+ lines.push(`- Recurring user topics: ${topKeywords.join(", ")}`);
856
+ }
857
+ if (recentUser !== "n/a") {
858
+ lines.push(`- Latest user request before preserved window: ${recentUser}`);
859
+ }
860
+ if (recentAssistant !== "n/a") {
861
+ lines.push(
862
+ `- Latest assistant response before preserved window: ${recentAssistant}`
863
+ );
864
+ }
865
+ if (recentUserGoals.length > 0) {
866
+ lines.push("Carry forward user goals:");
867
+ for (const goal of recentUserGoals) {
868
+ lines.push(`- ${goal}`);
869
+ }
870
+ }
871
+ if (recentAssistantNotes.length > 0) {
872
+ lines.push("Carry forward assistant commitments:");
873
+ for (const note of recentAssistantNotes) {
874
+ lines.push(`- ${note}`);
875
+ }
876
+ }
877
+ if (toolActivity.length > 0) {
878
+ lines.push("Recent tool activity:");
879
+ for (const item of toolActivity) {
880
+ lines.push(`- ${item}`);
881
+ }
882
+ }
883
+ if (priorSummaryCarryForward.length > 0) {
884
+ lines.push("Earlier carried-forward context:");
885
+ for (const item of priorSummaryCarryForward) {
886
+ lines.push(`- ${item}`);
887
+ }
888
+ }
889
+ const content = compressSummaryContent(
890
+ lines.join("\n"),
891
+ maxChars,
892
+ summarization?.fallbackBehavior
893
+ );
894
+ return {
895
+ role: "system",
896
+ content
897
+ };
898
+ }
899
+ function buildToolDefinitions(selectedTools) {
900
+ if (selectedTools.length === 0) {
901
+ return void 0;
902
+ }
903
+ return selectedTools.map((tool2) => ({
904
+ name: tool2.name,
905
+ description: tool2.description,
906
+ category: tool2.category,
907
+ group: tool2.group,
908
+ deferLoading: tool2.deferLoading,
909
+ profiles: tool2.profiles,
910
+ searchKeywords: tool2.searchKeywords,
911
+ inputSchema: tool2.inputSchema
912
+ }));
913
+ }
914
+ function resolveTruncationConfig(params) {
915
+ const charsPerToken = params.config?.contextManagement?.tokenEstimation?.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
916
+ const contextWindowTokens = params.config?.contextBudget?.budget?.contextWindowTokens;
917
+ const globalConfig = params.config?.toolResultConfig?.truncation;
918
+ const perToolConfig = params.tool?.resultConfig?.truncation;
919
+ const merged = { ...globalConfig, ...perToolConfig };
920
+ const hardMaxChars = merged.hardMaxChars ?? (contextWindowTokens ? Math.floor(
921
+ contextWindowTokens * clampRatio(
922
+ merged.maxContextShare,
923
+ DEFAULT_MAX_TOOL_RESULT_CONTEXT_SHARE
924
+ ) * charsPerToken
925
+ ) : DEFAULT_TOOL_RESULT_HARD_MAX_CHARS);
926
+ return {
927
+ enabled: merged.enabled ?? true,
928
+ maxContextShare: clampRatio(
929
+ merged.maxContextShare,
930
+ DEFAULT_MAX_TOOL_RESULT_CONTEXT_SHARE
931
+ ),
932
+ hardMaxChars: Math.max(1, hardMaxChars),
933
+ minKeepChars: Math.max(
934
+ 256,
935
+ merged.minKeepChars ?? DEFAULT_TOOL_RESULT_MIN_KEEP_CHARS
936
+ ),
937
+ strategy: merged.strategy ?? DEFAULT_TOOL_RESULT_STRATEGY,
938
+ preserveErrors: merged.preserveErrors ?? true
939
+ };
940
+ }
941
+ function buildToolResultContent(result, tool2, args) {
942
+ if (typeof result === "string") {
943
+ return result;
944
+ }
945
+ const typedResult = result ?? null;
528
946
  const responseMode = typedResult?._aiResponseMode ?? tool2?.aiResponseMode ?? "full";
529
947
  if (typedResult?._aiContent) {
530
948
  return JSON.stringify(typedResult._aiContent);
531
949
  }
532
950
  let aiContext = typedResult?._aiContext;
533
951
  if (!aiContext && tool2?.aiContext) {
534
- aiContext = typeof tool2.aiContext === "function" ? tool2.aiContext(typedResult, args ?? {}) : tool2.aiContext;
952
+ aiContext = typeof tool2.aiContext === "function" ? tool2.aiContext(
953
+ typedResult ?? { success: true },
954
+ args ?? {}
955
+ ) : tool2.aiContext;
535
956
  }
536
957
  switch (responseMode) {
537
958
  case "none":
@@ -539,7 +960,7 @@ function buildToolResultContentForAI(result, tool2, args) {
539
960
  case "brief":
540
961
  return aiContext ?? "[Tool executed successfully]";
541
962
  case "full":
542
- default:
963
+ default: {
543
964
  if (aiContext) {
544
965
  const {
545
966
  _aiResponseMode,
@@ -557,18 +978,526 @@ Full data: ${JSON.stringify(dataOnly)}`;
557
978
  return JSON.stringify(dataOnly);
558
979
  }
559
980
  return JSON.stringify(result);
981
+ }
982
+ }
983
+ }
984
+ function buildToolResultContentForPrompt(result, tool2, args, config) {
985
+ const text = buildToolResultContent(result, tool2, args);
986
+ const truncation = resolveTruncationConfig({ tool: tool2, config });
987
+ if (!truncation.enabled) {
988
+ return text;
989
+ }
990
+ if (truncation.preserveErrors && typeof result === "object" && result !== null && "error" in result && typeof result.error === "string") {
991
+ return text;
992
+ }
993
+ const maxChars = Math.max(truncation.minKeepChars, truncation.hardMaxChars);
994
+ return truncateText(text, maxChars, truncation.strategy);
995
+ }
996
+ function sliceHistoryToMaxMessages(params) {
997
+ const { historyMessages, maxMessages, pruneStrategy, summarization } = params;
998
+ if (!maxMessages || historyMessages.length <= maxMessages) {
999
+ return historyMessages;
1000
+ }
1001
+ const dropped = historyMessages.slice(
1002
+ 0,
1003
+ historyMessages.length - maxMessages
1004
+ );
1005
+ const kept = historyMessages.slice(-maxMessages);
1006
+ if (pruneStrategy === "summarize") {
1007
+ const summary = buildHistorySummary(dropped, summarization);
1008
+ return summary ? [summary, ...kept] : kept;
1009
+ }
1010
+ return kept;
1011
+ }
1012
+ function compactHistoryToTokenBudget(params) {
1013
+ const {
1014
+ maxTokens,
1015
+ preserveRecent,
1016
+ charsPerToken,
1017
+ pruneStrategy,
1018
+ summarization
1019
+ } = params;
1020
+ let historyMessages = params.historyMessages;
1021
+ if (!maxTokens) {
1022
+ return historyMessages;
1023
+ }
1024
+ const getHistoryTokens = () => historyMessages.reduce(
1025
+ (sum, message) => sum + estimateMessageTokens(message, charsPerToken),
1026
+ 0
1027
+ );
1028
+ while (historyMessages.length > 1 && getHistoryTokens() > maxTokens) {
1029
+ const prunableCount = Math.max(0, historyMessages.length - preserveRecent);
1030
+ if (prunableCount <= 0) {
1031
+ const firstMessage = historyMessages[0];
1032
+ if (isHistorySummaryMessage(firstMessage) && typeof firstMessage.content === "string") {
1033
+ const compactedSummary = compressSummaryContent(
1034
+ firstMessage.content,
1035
+ Math.max(400, Math.floor(maxTokens * charsPerToken * 0.25)),
1036
+ summarization?.fallbackBehavior
1037
+ );
1038
+ if (compactedSummary !== firstMessage.content) {
1039
+ historyMessages = [
1040
+ { ...firstMessage, content: compactedSummary },
1041
+ ...historyMessages.slice(1)
1042
+ ];
1043
+ continue;
1044
+ }
1045
+ }
1046
+ historyMessages = historyMessages.slice(1);
1047
+ continue;
1048
+ }
1049
+ const pruned = historyMessages.slice(0, prunableCount);
1050
+ const kept = historyMessages.slice(prunableCount);
1051
+ if (pruneStrategy === "summarize") {
1052
+ const summary = buildHistorySummary(
1053
+ pruned,
1054
+ summarization,
1055
+ Math.max(500, Math.floor(maxTokens * charsPerToken * 0.35))
1056
+ );
1057
+ historyMessages = summary ? [summary, ...kept] : kept;
1058
+ } else {
1059
+ historyMessages = kept;
1060
+ }
1061
+ }
1062
+ return historyMessages;
1063
+ }
1064
+ function compactToolResultsToBudget(params) {
1065
+ let toolResultMessages = params.toolResultMessages;
1066
+ if (!params.maxTokens) {
1067
+ return toolResultMessages;
1068
+ }
1069
+ const getToolResultTokens = () => toolResultMessages.reduce(
1070
+ (sum, message) => sum + estimateMessageTokens(message, params.charsPerToken),
1071
+ 0
1072
+ );
1073
+ while (toolResultMessages.length > 0 && getToolResultTokens() > params.maxTokens) {
1074
+ const index = toolResultMessages.findIndex(
1075
+ (message) => message.content !== TOOL_RESULT_COMPACTION_NOTICE
1076
+ );
1077
+ if (index === -1) {
1078
+ break;
1079
+ }
1080
+ toolResultMessages = toolResultMessages.map(
1081
+ (message, currentIndex) => currentIndex === index ? { ...message, content: TOOL_RESULT_COMPACTION_NOTICE } : message
1082
+ );
1083
+ }
1084
+ return toolResultMessages;
1085
+ }
1086
+ function fitToolsToBudget(params) {
1087
+ let tools = params.tools;
1088
+ if (!tools?.length || !params.maxTokens) {
1089
+ return tools;
1090
+ }
1091
+ const getToolTokens = () => tools.reduce(
1092
+ (sum, tool2) => sum + estimateToolTokens(tool2, params.charsPerToken),
1093
+ 0
1094
+ );
1095
+ while (tools.length > 0 && getToolTokens() > params.maxTokens) {
1096
+ tools = tools.slice(0, -1);
560
1097
  }
1098
+ return tools;
1099
+ }
1100
+ function truncateSystemPromptToBudget(params) {
1101
+ const { systemPrompt, maxTokens, charsPerToken } = params;
1102
+ if (!systemPrompt || !maxTokens) {
1103
+ return systemPrompt;
1104
+ }
1105
+ const maxChars = maxTokens * charsPerToken;
1106
+ if (systemPrompt.length <= maxChars) {
1107
+ return systemPrompt;
1108
+ }
1109
+ return truncateText(
1110
+ systemPrompt,
1111
+ maxChars,
1112
+ "head",
1113
+ SYSTEM_PROMPT_TRUNCATION_NOTICE
1114
+ );
1115
+ }
1116
+ function calculateBuckets(params) {
1117
+ const systemPromptTokens = estimateTokens(
1118
+ params.systemPrompt ?? "",
1119
+ params.charsPerToken
1120
+ );
1121
+ const historyTokens = params.historyMessages.reduce(
1122
+ (sum, message) => sum + estimateMessageTokens(message, params.charsPerToken),
1123
+ 0
1124
+ );
1125
+ const toolResultsTokens = params.toolResultMessages.reduce(
1126
+ (sum, message) => sum + estimateMessageTokens(message, params.charsPerToken),
1127
+ 0
1128
+ );
1129
+ const toolDefinitionTokens = (params.requestTools ?? []).reduce(
1130
+ (sum, tool2) => sum + estimateToolTokens(tool2, params.charsPerToken),
1131
+ 0
1132
+ );
1133
+ const total = systemPromptTokens + historyTokens + toolResultsTokens + toolDefinitionTokens;
1134
+ const budget = Number.isFinite(params.availableBudget) ? params.availableBudget : total;
1135
+ const toPart = (tokens) => ({
1136
+ tokens,
1137
+ percent: budget > 0 ? Number((tokens / budget * 100).toFixed(2)) : 0
1138
+ });
1139
+ return {
1140
+ total: toPart(total),
1141
+ breakdown: {
1142
+ systemPrompt: toPart(systemPromptTokens),
1143
+ history: toPart(historyTokens),
1144
+ toolResults: toPart(toolResultsTokens),
1145
+ tools: toPart(toolDefinitionTokens)
1146
+ },
1147
+ budget: {
1148
+ available: budget,
1149
+ remaining: Math.max(0, budget - total)
1150
+ },
1151
+ warnings: unique(params.warnings)
1152
+ };
1153
+ }
1154
+ function mergeBucketsInOriginalOrder(params) {
1155
+ const historyQueue = [...params.historyMessages];
1156
+ const toolQueue = [...params.toolResultMessages];
1157
+ return params.transformedMessages.flatMap((message) => {
1158
+ if (message.role === "tool") {
1159
+ const nextTool = toolQueue.shift();
1160
+ return nextTool ? [nextTool] : [];
1161
+ }
1162
+ const nextHistory = historyQueue.shift();
1163
+ return nextHistory ? [nextHistory] : [];
1164
+ });
561
1165
  }
1166
+ var ChatContextOptimizer = class {
1167
+ constructor(config) {
1168
+ this.lastContextUsage = null;
1169
+ this.config = config;
1170
+ this.activeProfile = config?.toolProfiles?.defaultProfile;
1171
+ }
1172
+ updateConfig(config) {
1173
+ this.config = config;
1174
+ if (!this.activeProfile) {
1175
+ this.activeProfile = config?.toolProfiles?.defaultProfile;
1176
+ }
1177
+ }
1178
+ setActiveProfile(profile) {
1179
+ this.activeProfile = profile?.trim() || void 0;
1180
+ }
1181
+ getContextUsage() {
1182
+ return this.lastContextUsage;
1183
+ }
1184
+ prepare(params) {
1185
+ const charsPerToken = this.config?.contextManagement?.tokenEstimation?.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
1186
+ const safetyMargin = this.config?.contextManagement?.tokenEstimation?.safetyMargin ?? DEFAULT_SAFETY_MARGIN;
1187
+ const warnings = [];
1188
+ const contextManagement = this.config?.contextManagement;
1189
+ const contextBudget = this.config?.contextBudget;
1190
+ const allTools = params.tools ?? [];
1191
+ const selectedTools = this.selectTools(allTools, params.messages);
1192
+ const transformedMessages = this.transformMessages(
1193
+ params.messages,
1194
+ allTools
1195
+ );
1196
+ const preserveRecent = contextManagement?.summarization?.preserveRecent ?? DEFAULT_RECENT_HISTORY_PRESERVE;
1197
+ let buckets = {
1198
+ systemPrompt: params.systemPrompt,
1199
+ transformedMessages,
1200
+ historyMessages: transformedMessages.filter(
1201
+ (message) => message.role !== "tool"
1202
+ ),
1203
+ toolResultMessages: transformedMessages.filter(
1204
+ (message) => message.role === "tool"
1205
+ ),
1206
+ requestTools: buildToolDefinitions(selectedTools)
1207
+ };
1208
+ if (contextManagement?.enabled) {
1209
+ buckets.historyMessages = sliceHistoryToMaxMessages({
1210
+ historyMessages: buckets.historyMessages,
1211
+ maxMessages: contextManagement.history?.maxMessages,
1212
+ pruneStrategy: contextManagement.history?.pruneStrategy,
1213
+ summarization: contextManagement?.summarization
1214
+ });
1215
+ }
1216
+ const budgetConfig = contextBudget?.budget;
1217
+ const contextWindowTokens = budgetConfig?.contextWindowTokens;
1218
+ const inputHeadroomRatio = clampRatio(
1219
+ budgetConfig?.inputHeadroomRatio,
1220
+ DEFAULT_INPUT_HEADROOM_RATIO
1221
+ );
1222
+ const availableBudget = contextWindowTokens ? Math.max(1, Math.floor(contextWindowTokens * inputHeadroomRatio)) : Number.POSITIVE_INFINITY;
1223
+ const sharedBudget = Number.isFinite(availableBudget) ? availableBudget : void 0;
1224
+ const systemPromptBudget = sharedBudget ? Math.max(
1225
+ 1,
1226
+ Math.floor(
1227
+ sharedBudget * clampRatio(
1228
+ budgetConfig?.systemPromptShare,
1229
+ DEFAULT_SYSTEM_PROMPT_SHARE
1230
+ )
1231
+ )
1232
+ ) : void 0;
1233
+ const historyBudgetByShare = sharedBudget ? Math.max(
1234
+ 1,
1235
+ Math.floor(
1236
+ sharedBudget * clampRatio(budgetConfig?.historyShare, DEFAULT_HISTORY_SHARE)
1237
+ )
1238
+ ) : void 0;
1239
+ const historyBudgetByConfig = contextManagement?.enabled && contextManagement.history?.maxTokens ? Math.floor(contextManagement.history.maxTokens / safetyMargin) : void 0;
1240
+ const historyBudget = historyBudgetByShare && historyBudgetByConfig ? Math.min(historyBudgetByShare, historyBudgetByConfig) : historyBudgetByShare ?? historyBudgetByConfig;
1241
+ const toolResultsBudget = sharedBudget ? Math.max(
1242
+ 1,
1243
+ Math.floor(
1244
+ sharedBudget * clampRatio(
1245
+ budgetConfig?.toolResultsShare,
1246
+ DEFAULT_TOOL_RESULTS_SHARE
1247
+ )
1248
+ )
1249
+ ) : void 0;
1250
+ const toolDefinitionsBudget = sharedBudget ? Math.max(
1251
+ 1,
1252
+ Math.floor(
1253
+ sharedBudget * clampRatio(
1254
+ budgetConfig?.toolDefinitionsShare,
1255
+ DEFAULT_TOOL_DEFINITIONS_SHARE
1256
+ )
1257
+ )
1258
+ ) : void 0;
1259
+ if (contextBudget?.enabled) {
1260
+ buckets.systemPrompt = truncateSystemPromptToBudget({
1261
+ systemPrompt: buckets.systemPrompt,
1262
+ maxTokens: systemPromptBudget,
1263
+ charsPerToken
1264
+ });
1265
+ }
1266
+ buckets.historyMessages = compactHistoryToTokenBudget({
1267
+ historyMessages: buckets.historyMessages,
1268
+ maxTokens: historyBudget,
1269
+ preserveRecent,
1270
+ charsPerToken,
1271
+ pruneStrategy: contextManagement?.history?.pruneStrategy,
1272
+ summarization: contextManagement?.summarization
1273
+ });
1274
+ buckets.toolResultMessages = compactToolResultsToBudget({
1275
+ toolResultMessages: buckets.toolResultMessages,
1276
+ maxTokens: toolResultsBudget,
1277
+ charsPerToken
1278
+ });
1279
+ buckets.requestTools = fitToolsToBudget({
1280
+ tools: buckets.requestTools,
1281
+ maxTokens: toolDefinitionsBudget,
1282
+ charsPerToken
1283
+ });
1284
+ let usage = calculateBuckets({
1285
+ ...buckets,
1286
+ charsPerToken,
1287
+ availableBudget,
1288
+ warnings
1289
+ });
1290
+ if (Number.isFinite(availableBudget) && usage.total.tokens > availableBudget) {
1291
+ buckets.toolResultMessages = compactToolResultsToBudget({
1292
+ toolResultMessages: buckets.toolResultMessages,
1293
+ maxTokens: Math.max(
1294
+ 1,
1295
+ usage.breakdown.toolResults.tokens - usage.total.tokens + availableBudget
1296
+ ),
1297
+ charsPerToken
1298
+ });
1299
+ usage = calculateBuckets({
1300
+ ...buckets,
1301
+ charsPerToken,
1302
+ availableBudget,
1303
+ warnings
1304
+ });
1305
+ if (usage.total.tokens > availableBudget) {
1306
+ const overflow = usage.total.tokens - availableBudget;
1307
+ buckets.historyMessages = compactHistoryToTokenBudget({
1308
+ historyMessages: buckets.historyMessages,
1309
+ maxTokens: Math.max(1, usage.breakdown.history.tokens - overflow),
1310
+ preserveRecent,
1311
+ charsPerToken,
1312
+ pruneStrategy: contextManagement?.history?.pruneStrategy
1313
+ });
1314
+ usage = calculateBuckets({
1315
+ ...buckets,
1316
+ charsPerToken,
1317
+ availableBudget,
1318
+ warnings
1319
+ });
1320
+ }
1321
+ if (usage.total.tokens > availableBudget) {
1322
+ buckets.requestTools = fitToolsToBudget({
1323
+ tools: buckets.requestTools,
1324
+ maxTokens: Math.max(
1325
+ 1,
1326
+ usage.breakdown.tools.tokens - (usage.total.tokens - availableBudget)
1327
+ ),
1328
+ charsPerToken
1329
+ });
1330
+ usage = calculateBuckets({
1331
+ ...buckets,
1332
+ charsPerToken,
1333
+ availableBudget,
1334
+ warnings
1335
+ });
1336
+ }
1337
+ }
1338
+ if (Number.isFinite(availableBudget) && usage.total.tokens > availableBudget) {
1339
+ warnings.push(
1340
+ `Prompt budget exceeded: using ${usage.total.tokens} tokens of ${availableBudget}.`
1341
+ );
1342
+ usage = {
1343
+ ...usage,
1344
+ warnings: unique(warnings)
1345
+ };
1346
+ if (contextBudget?.enforcement?.mode === "error") {
1347
+ throw new Error(warnings[warnings.length - 1]);
1348
+ }
1349
+ contextBudget?.enforcement?.onBudgetExceeded?.(usage);
1350
+ } else {
1351
+ usage = {
1352
+ ...usage,
1353
+ warnings: unique(warnings)
1354
+ };
1355
+ }
1356
+ contextBudget?.monitoring?.onUsageUpdate?.(usage);
1357
+ this.lastContextUsage = usage;
1358
+ return {
1359
+ messages: mergeBucketsInOriginalOrder(buckets),
1360
+ tools: buckets.requestTools,
1361
+ contextUsage: usage,
1362
+ warnings: usage.warnings
1363
+ };
1364
+ }
1365
+ selectTools(tools, messages) {
1366
+ if (!tools.length) {
1367
+ return [];
1368
+ }
1369
+ const available = tools.filter((tool2) => tool2.available !== false);
1370
+ const profileConfig = this.config?.toolProfiles;
1371
+ if (!profileConfig?.enabled) {
1372
+ return available;
1373
+ }
1374
+ const activeProfile = this.activeProfile ?? profileConfig.defaultProfile;
1375
+ const includeUnprofiled = profileConfig.includeUnprofiled ?? true;
1376
+ const profile = activeProfile ? profileConfig.profiles?.[activeProfile] : void 0;
1377
+ let filtered = available;
1378
+ if (profile?.include?.length) {
1379
+ filtered = filtered.filter(
1380
+ (tool2) => profile.include.some(
1381
+ (selector) => matchesSelector(tool2, selector, activeProfile)
1382
+ ) || !!activeProfile && tool2.profiles?.includes(activeProfile)
1383
+ );
1384
+ } else if (activeProfile) {
1385
+ filtered = filtered.filter((tool2) => {
1386
+ if (tool2.profiles?.length) {
1387
+ return tool2.profiles.includes(activeProfile);
1388
+ }
1389
+ return includeUnprofiled;
1390
+ });
1391
+ }
1392
+ if (profile?.exclude?.length) {
1393
+ filtered = filtered.filter(
1394
+ (tool2) => !profile.exclude.some(
1395
+ (selector) => matchesSelector(tool2, selector, activeProfile)
1396
+ )
1397
+ );
1398
+ }
1399
+ if (!profileConfig.dynamicSelection?.enabled) {
1400
+ return filtered;
1401
+ }
1402
+ const maxTools = Math.max(
1403
+ 1,
1404
+ Math.min(
1405
+ profileConfig.dynamicSelection.maxTools ?? filtered.length,
1406
+ filtered.length
1407
+ )
1408
+ );
1409
+ const queryTokens = tokenize(buildToolQuery(messages));
1410
+ return [...filtered].sort((left, right) => {
1411
+ const scoreDiff = scoreTool(right, queryTokens, activeProfile) - scoreTool(left, queryTokens, activeProfile);
1412
+ if (scoreDiff !== 0) {
1413
+ return scoreDiff;
1414
+ }
1415
+ return left.name.localeCompare(right.name);
1416
+ }).slice(0, maxTools);
1417
+ }
1418
+ transformMessages(messages, allTools) {
1419
+ const toolCallMap = /* @__PURE__ */ new Map();
1420
+ for (const message of messages) {
1421
+ if (message.role !== "assistant" || !message.toolCalls?.length) {
1422
+ continue;
1423
+ }
1424
+ for (const toolCall of message.toolCalls) {
1425
+ try {
1426
+ toolCallMap.set(toolCall.id, {
1427
+ toolName: toolCall.function.name,
1428
+ args: JSON.parse(toolCall.function.arguments)
1429
+ });
1430
+ } catch {
1431
+ toolCallMap.set(toolCall.id, {
1432
+ toolName: toolCall.function.name,
1433
+ args: {}
1434
+ });
1435
+ }
1436
+ }
1437
+ }
1438
+ const toolDefMap = new Map(
1439
+ allTools.map((tool2) => [tool2.name, tool2])
1440
+ );
1441
+ return messages.map((message) => {
1442
+ if (message.role !== "tool") {
1443
+ return {
1444
+ role: message.role,
1445
+ content: message.content,
1446
+ tool_calls: message.toolCalls,
1447
+ tool_call_id: message.toolCallId,
1448
+ attachments: message.attachments
1449
+ };
1450
+ }
1451
+ const toolCall = message.toolCallId ? toolCallMap.get(message.toolCallId) : void 0;
1452
+ const tool2 = toolCall ? toolDefMap.get(toolCall.toolName) : void 0;
1453
+ let content = message.content;
1454
+ try {
1455
+ const parsed = JSON.parse(message.content);
1456
+ content = buildToolResultContentForPrompt(
1457
+ parsed,
1458
+ tool2,
1459
+ toolCall?.args ?? {},
1460
+ this.config
1461
+ );
1462
+ } catch {
1463
+ content = buildToolResultContentForPrompt(
1464
+ message.content,
1465
+ tool2,
1466
+ toolCall?.args ?? {},
1467
+ this.config
1468
+ );
1469
+ }
1470
+ return {
1471
+ role: message.role,
1472
+ content,
1473
+ tool_call_id: message.toolCallId
1474
+ };
1475
+ });
1476
+ }
1477
+ };
1478
+
1479
+ // src/chat/classes/AbstractChat.ts
562
1480
  var AbstractChat = class {
563
1481
  constructor(init) {
1482
+ this.lastContextUsage = null;
564
1483
  // Event handlers
565
1484
  this.eventHandlers = /* @__PURE__ */ new Map();
566
1485
  // Current streaming state
567
1486
  this.streamState = null;
1487
+ /**
1488
+ * Inline skills from the client (sent on every request for server to merge)
1489
+ */
1490
+ this.inlineSkills = [];
568
1491
  /**
569
1492
  * Dynamic context from useAIContext hook
570
1493
  */
571
1494
  this.dynamicContext = "";
1495
+ /**
1496
+ * Optional transform applied to messages just before building the HTTP request.
1497
+ * Used by the message-history / compaction system to send a pruned message list
1498
+ * without mutating the in-memory store (which keeps the full history for display).
1499
+ */
1500
+ this.requestMessageTransform = null;
572
1501
  this._isDisposed = false;
573
1502
  this.config = {
574
1503
  runtimeUrl: init.runtimeUrl,
@@ -578,7 +1507,8 @@ var AbstractChat = class {
578
1507
  headers: init.headers,
579
1508
  body: init.body,
580
1509
  threadId: init.threadId,
581
- debug: init.debug
1510
+ debug: init.debug,
1511
+ optimization: init.optimization
582
1512
  };
583
1513
  this.state = init.state ?? new SimpleChatState();
584
1514
  this.transport = init.transport ?? new HttpTransport({
@@ -588,6 +1518,7 @@ var AbstractChat = class {
588
1518
  streaming: init.streaming ?? true
589
1519
  });
590
1520
  this.callbacks = init.callbacks ?? {};
1521
+ this.optimizer = new ChatContextOptimizer(init.optimization);
591
1522
  if (init.initialMessages?.length) {
592
1523
  this.state.setMessages(init.initialMessages);
593
1524
  }
@@ -619,20 +1550,42 @@ var AbstractChat = class {
619
1550
  /**
620
1551
  * Send a message
621
1552
  * Returns false if a request is already in progress
1553
+ *
1554
+ * @param content - Message content
1555
+ * @param attachments - Optional attachments
1556
+ * @param options - Optional branching options
1557
+ * @param options.editMessageId - Edit flow: new message branches from the
1558
+ * same parent as this message ID, creating a parallel conversation path
622
1559
  */
623
- async sendMessage(content, attachments) {
1560
+ async sendMessage(content, attachments, options) {
624
1561
  if (this.isBusy) {
625
1562
  this.debug("sendMessage", "Blocked - request already in progress");
626
1563
  return false;
627
1564
  }
628
- this.debug("sendMessage", { content, attachments });
1565
+ this.debug("sendMessage", { content, attachments, options });
629
1566
  try {
630
1567
  this.resolveUnresolvedToolCalls();
631
- const userMessage = createUserMessage(content, attachments);
1568
+ let newParentId;
1569
+ const visibleMessages = this.state.messages;
1570
+ if (options?.editMessageId && this.state.setCurrentLeaf) {
1571
+ const allMessages = this.state.getAllMessages?.() ?? this.state.messages;
1572
+ const target = allMessages.find((m) => m.id === options.editMessageId);
1573
+ if (target && target.parentId !== void 0) {
1574
+ newParentId = target.parentId;
1575
+ this.state.setCurrentLeaf(
1576
+ typeof target.parentId === "string" ? target.parentId : null
1577
+ );
1578
+ }
1579
+ } else if (visibleMessages.length > 0) {
1580
+ newParentId = visibleMessages[visibleMessages.length - 1].id;
1581
+ }
1582
+ const userMessage = createUserMessage(content, attachments, {
1583
+ parentId: newParentId
1584
+ });
632
1585
  this.state.pushMessage(userMessage);
633
1586
  this.state.status = "submitted";
634
1587
  this.state.error = void 0;
635
- this.callbacks.onMessagesChange?.(this.state.messages);
1588
+ this.callbacks.onMessagesChange?.(this._allMessages());
636
1589
  this.callbacks.onStatusChange?.("submitted");
637
1590
  await Promise.resolve();
638
1591
  await this.processRequest();
@@ -682,7 +1635,7 @@ var AbstractChat = class {
682
1635
  };
683
1636
  this.state.pushMessage(toolMessage);
684
1637
  }
685
- this.callbacks.onMessagesChange?.(this.state.messages);
1638
+ this.callbacks.onMessagesChange?.(this._allMessages());
686
1639
  }
687
1640
  }
688
1641
  /**
@@ -736,9 +1689,9 @@ var AbstractChat = class {
736
1689
  this.state.pushMessage(userMessage);
737
1690
  }
738
1691
  this.state.status = "submitted";
739
- this.callbacks.onMessagesChange?.(this.state.messages);
1692
+ this.callbacks.onMessagesChange?.(this._allMessages());
740
1693
  this.callbacks.onStatusChange?.("submitted");
741
- await Promise.resolve();
1694
+ await new Promise((resolve) => setTimeout(resolve, 0));
742
1695
  await this.processRequest();
743
1696
  } catch (error) {
744
1697
  this.handleError(error);
@@ -767,30 +1720,58 @@ var AbstractChat = class {
767
1720
  this.callbacks.onMessagesChange?.(messages);
768
1721
  }
769
1722
  /**
770
- * Regenerate last response
1723
+ * Regenerate last response.
1724
+ *
1725
+ * Branch-aware: when the state supports branching (setCurrentLeaf is available),
1726
+ * regenerate creates a new sibling response instead of destroying the original.
1727
+ * The old response is preserved and navigable via switchBranch().
1728
+ *
1729
+ * Legacy fallback: when branching is not available, uses old slice() behavior.
771
1730
  */
772
1731
  async regenerate(messageId) {
1732
+ if (this.isBusy) return;
773
1733
  const messages = this.state.messages;
774
- let targetIndex = messages.length - 1;
1734
+ let targetMessage;
775
1735
  if (messageId) {
776
- targetIndex = messages.findIndex((m) => m.id === messageId);
1736
+ targetMessage = messages.find((m) => m.id === messageId);
1737
+ if (!targetMessage) {
1738
+ targetMessage = this.state.getAllMessages?.().find((m) => m.id === messageId);
1739
+ }
777
1740
  } else {
778
1741
  for (let i = messages.length - 1; i >= 0; i--) {
779
1742
  if (messages[i].role === "assistant") {
780
- targetIndex = i;
1743
+ targetMessage = messages[i];
781
1744
  break;
782
1745
  }
783
1746
  }
784
1747
  }
1748
+ if (!targetMessage) return;
1749
+ if (targetMessage.parentId !== void 0 && this.state.setCurrentLeaf) {
1750
+ this.state.setCurrentLeaf(targetMessage.parentId ?? null);
1751
+ this.callbacks.onMessagesChange?.(this._allMessages());
1752
+ this.state.status = "submitted";
1753
+ await Promise.resolve();
1754
+ await this.processRequest();
1755
+ return;
1756
+ }
1757
+ const targetIndex = messages.indexOf(targetMessage);
785
1758
  if (targetIndex > 0) {
786
1759
  this.state.setMessages(messages.slice(0, targetIndex));
787
- this.callbacks.onMessagesChange?.(this.state.messages);
1760
+ this.callbacks.onMessagesChange?.(this._allMessages());
788
1761
  await this.processRequest();
789
1762
  }
790
1763
  }
791
1764
  // ============================================
792
1765
  // Event Handling
793
1766
  // ============================================
1767
+ /**
1768
+ * Returns all messages across all branches when the state supports it
1769
+ * (branch-aware), otherwise returns the visible path.
1770
+ * Use this whenever firing onMessagesChange so inactive branches are not lost.
1771
+ */
1772
+ _allMessages() {
1773
+ return this.state.getAllMessages?.() ?? this.state.messages;
1774
+ }
794
1775
  /**
795
1776
  * Subscribe to events
796
1777
  */
@@ -822,10 +1803,32 @@ var AbstractChat = class {
822
1803
  */
823
1804
  async processRequest() {
824
1805
  const request = this.buildRequest();
1806
+ let preCreatedMessageId;
1807
+ if (this.config.streaming !== false) {
1808
+ const visibleMessages = this.state.messages;
1809
+ const currentLeafId = visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1].id : void 0;
1810
+ const preMsg = createEmptyAssistantMessage(void 0, {
1811
+ parentId: currentLeafId
1812
+ });
1813
+ this.state.pushMessage(preMsg);
1814
+ this.callbacks.onMessagesChange?.(this._allMessages());
1815
+ preCreatedMessageId = preMsg.id;
1816
+ }
825
1817
  const response = await this.transport.send(request);
826
1818
  if (this.isAsyncIterable(response)) {
827
- await this.handleStreamResponse(response);
1819
+ await this.handleStreamResponse(response, preCreatedMessageId);
828
1820
  } else {
1821
+ if (preCreatedMessageId) {
1822
+ const id = preCreatedMessageId;
1823
+ const visibleMsgs = this.state.messages;
1824
+ const placeholderIdx = visibleMsgs.findIndex((m) => m.id === id);
1825
+ const intendedLeafId = placeholderIdx > 0 ? visibleMsgs[placeholderIdx - 1].id : null;
1826
+ const allMsgs = this.state.getAllMessages?.() ?? this.state.messages;
1827
+ this.state.setMessages(allMsgs.filter((m) => m.id !== id));
1828
+ if (intendedLeafId && this.state.setCurrentLeaf) {
1829
+ this.state.setCurrentLeaf(intendedLeafId);
1830
+ }
1831
+ }
829
1832
  this.handleJsonResponse(response);
830
1833
  }
831
1834
  }
@@ -836,30 +1839,60 @@ var AbstractChat = class {
836
1839
  this.config.tools = tools;
837
1840
  }
838
1841
  /**
839
- * Set dynamic context (appended to system prompt)
1842
+ * Update prompt/tool optimization behavior.
840
1843
  */
841
- setContext(context) {
842
- this.dynamicContext = context;
843
- this.debug("Context updated", { length: context.length });
1844
+ setOptimizationConfig(config) {
1845
+ this.config.optimization = config;
1846
+ this.optimizer.updateConfig(config);
844
1847
  }
845
1848
  /**
846
- * Set system prompt dynamically
847
- * This allows updating the system prompt after initialization
1849
+ * Select the active tool profile for future requests.
848
1850
  */
849
- setSystemPrompt(prompt) {
850
- this.config.systemPrompt = prompt;
851
- this.debug("System prompt updated", { length: prompt.length });
1851
+ setToolProfile(profile) {
1852
+ this.optimizer.setActiveProfile(profile);
852
1853
  }
853
1854
  /**
854
- * Set headers configuration
855
- * Can be static headers or a getter function for dynamic resolution
1855
+ * Get the most recent prompt context usage snapshot.
856
1856
  */
857
- setHeaders(headers) {
858
- this.config.headers = headers;
859
- if (this.transport.setHeaders && headers !== void 0) {
1857
+ getContextUsage() {
1858
+ return this.lastContextUsage;
1859
+ }
1860
+ /**
1861
+ * Set inline skills (called by SkillProvider via React layer)
1862
+ */
1863
+ setInlineSkills(skills) {
1864
+ this.inlineSkills = skills;
1865
+ this.debug("Inline skills updated", { count: skills.length });
1866
+ }
1867
+ /**
1868
+ * Set (or clear) the per-request message transform.
1869
+ * Pass null to disable.
1870
+ */
1871
+ setRequestMessageTransform(fn) {
1872
+ this.requestMessageTransform = fn;
1873
+ }
1874
+ /**
1875
+ * Set dynamic context (appended to system prompt)
1876
+ */
1877
+ setContext(context) {
1878
+ this.dynamicContext = context;
1879
+ }
1880
+ /**
1881
+ * Set system prompt dynamically
1882
+ * This allows updating the system prompt after initialization
1883
+ */
1884
+ setSystemPrompt(prompt) {
1885
+ this.config.systemPrompt = prompt;
1886
+ }
1887
+ /**
1888
+ * Set headers configuration
1889
+ * Can be static headers or a getter function for dynamic resolution
1890
+ */
1891
+ setHeaders(headers) {
1892
+ this.config.headers = headers;
1893
+ if (this.transport.setHeaders && headers !== void 0) {
860
1894
  this.transport.setHeaders(headers);
861
1895
  }
862
- this.debug("Headers config updated");
863
1896
  }
864
1897
  /**
865
1898
  * Set URL configuration
@@ -870,7 +1903,6 @@ var AbstractChat = class {
870
1903
  if (this.transport.setUrl) {
871
1904
  this.transport.setUrl(url);
872
1905
  }
873
- this.debug("URL config updated");
874
1906
  }
875
1907
  /**
876
1908
  * Set body configuration
@@ -881,110 +1913,88 @@ var AbstractChat = class {
881
1913
  if (this.transport.setBody && body !== void 0) {
882
1914
  this.transport.setBody(body);
883
1915
  }
884
- this.debug("Body config updated");
885
1916
  }
886
1917
  /**
887
1918
  * Build the request payload
888
1919
  */
889
1920
  buildRequest() {
890
- const tools = this.config.tools?.filter((tool2) => tool2.available !== false).map((tool2) => ({
891
- name: tool2.name,
892
- description: tool2.description,
893
- inputSchema: tool2.inputSchema
894
- }));
895
- const toolCallMap = /* @__PURE__ */ new Map();
896
- for (const msg of this.state.messages) {
897
- if (msg.role === "assistant" && msg.toolCalls) {
898
- for (const tc of msg.toolCalls) {
899
- try {
900
- const args = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
901
- toolCallMap.set(tc.id, { toolName: tc.function.name, args });
902
- } catch {
903
- toolCallMap.set(tc.id, { toolName: tc.function.name, args: {} });
904
- }
905
- }
906
- }
907
- }
908
- const toolDefMap = /* @__PURE__ */ new Map();
909
- if (this.config.tools) {
910
- for (const tool2 of this.config.tools) {
911
- toolDefMap.set(tool2.name, {
912
- name: tool2.name,
913
- aiResponseMode: tool2.aiResponseMode,
914
- aiContext: tool2.aiContext
915
- });
916
- }
917
- }
918
- return {
919
- messages: this.state.messages.map((m) => {
920
- if (m.role === "tool" && m.content && m.toolCallId) {
921
- try {
922
- const fullResult = JSON.parse(m.content);
923
- const toolCallInfo = toolCallMap.get(m.toolCallId);
924
- const toolDef = toolCallInfo ? toolDefMap.get(toolCallInfo.toolName) : void 0;
925
- const toolArgs = toolCallInfo?.args;
926
- const transformedContent = buildToolResultContentForAI(
927
- fullResult,
928
- toolDef,
929
- toolArgs
930
- );
931
- return {
932
- role: m.role,
933
- content: transformedContent,
934
- tool_call_id: m.toolCallId
935
- };
936
- } catch (e) {
937
- this.debug("Failed to parse tool message JSON", {
938
- content: m.content?.slice(0, 100),
939
- error: e instanceof Error ? e.message : String(e)
940
- });
941
- return {
942
- role: m.role,
943
- content: m.content,
944
- tool_call_id: m.toolCallId
945
- };
946
- }
947
- }
948
- return {
949
- role: m.role,
950
- content: m.content,
951
- tool_calls: m.toolCalls,
952
- tool_call_id: m.toolCallId,
953
- attachments: m.attachments
954
- };
955
- }),
956
- threadId: this.config.threadId,
957
- systemPrompt: this.dynamicContext ? `${this.config.systemPrompt || ""}
1921
+ const systemPrompt = this.dynamicContext ? `${this.config.systemPrompt || ""}
958
1922
 
959
1923
  ## Current App Context:
960
- ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1924
+ ${this.dynamicContext}`.trim() : this.config.systemPrompt;
1925
+ const rawMessages = this.requestMessageTransform ? this.requestMessageTransform(
1926
+ this.state.messages
1927
+ ) : this.state.messages;
1928
+ const optimized = this.optimizer.prepare({
1929
+ messages: rawMessages,
1930
+ tools: this.config.tools,
1931
+ systemPrompt
1932
+ });
1933
+ this.lastContextUsage = optimized.contextUsage;
1934
+ this.callbacks.onContextUsageChange?.(optimized.contextUsage);
1935
+ return {
1936
+ messages: optimized.messages,
1937
+ threadId: this.config.threadId,
1938
+ systemPrompt,
961
1939
  llm: this.config.llm,
962
- tools: tools?.length ? tools : void 0
1940
+ tools: this.config.tools?.length ? this.config.tools.map((tool2) => ({
1941
+ name: tool2.name,
1942
+ description: tool2.description,
1943
+ category: tool2.category,
1944
+ group: tool2.group,
1945
+ deferLoading: tool2.deferLoading,
1946
+ profiles: tool2.profiles,
1947
+ searchKeywords: tool2.searchKeywords,
1948
+ inputSchema: tool2.inputSchema
1949
+ })) : void 0,
1950
+ __skills: this.inlineSkills.length ? this.inlineSkills : void 0
963
1951
  };
964
1952
  }
965
1953
  /**
966
1954
  * Handle streaming response
967
1955
  */
968
- async handleStreamResponse(stream) {
1956
+ async handleStreamResponse(stream, preCreatedMessageId) {
969
1957
  this.state.status = "streaming";
970
1958
  this.callbacks.onStatusChange?.("streaming");
971
- const assistantMessage = createEmptyAssistantMessage();
972
- this.state.pushMessage(assistantMessage);
1959
+ let assistantMessage;
1960
+ if (preCreatedMessageId) {
1961
+ const existing = this.state.messages.find(
1962
+ (m) => m.id === preCreatedMessageId
1963
+ );
1964
+ if (existing) {
1965
+ assistantMessage = existing;
1966
+ } else {
1967
+ assistantMessage = createEmptyAssistantMessage();
1968
+ this.state.pushMessage(assistantMessage);
1969
+ }
1970
+ } else {
1971
+ assistantMessage = createEmptyAssistantMessage();
1972
+ this.state.pushMessage(assistantMessage);
1973
+ }
973
1974
  this.streamState = createStreamState(assistantMessage.id);
974
1975
  this.callbacks.onMessageStart?.(assistantMessage.id);
975
- this.debug("handleStreamResponse", "Starting to process stream");
1976
+ this.debugGroup("handleStreamResponse");
1977
+ this.debug("Starting to process stream");
976
1978
  let chunkCount = 0;
977
1979
  let toolCallsEmitted = false;
1980
+ let pendingClientToolCalls;
978
1981
  for await (const chunk of stream) {
979
1982
  chunkCount++;
980
- this.debug("chunk", { count: chunkCount, type: chunk.type });
1983
+ if (chunk.type !== "message:delta") {
1984
+ this.debug("chunk", { count: chunkCount, type: chunk.type });
1985
+ }
981
1986
  if (chunk.type === "error") {
982
1987
  const error = new Error(chunk.message || "Stream error");
983
1988
  this.handleError(error);
984
1989
  return;
985
1990
  }
986
1991
  if (chunk.type === "message:end" && this.streamState?.content) {
987
- this.debug("message:end mid-stream - finalizing current turn");
1992
+ this.debug("message:end mid-stream", {
1993
+ messageId: this.streamState.messageId,
1994
+ contentLength: this.streamState.content.length,
1995
+ toolCallsInState: this.streamState.toolCalls?.length ?? 0,
1996
+ chunkCount
1997
+ });
988
1998
  const turnMessage = streamStateToMessage(this.streamState);
989
1999
  const toolCallsHidden = {};
990
2000
  for (const [id, result] of this.streamState.toolResults) {
@@ -1000,7 +2010,11 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1000
2010
  }
1001
2011
  this.state.updateMessageById(
1002
2012
  this.streamState.messageId,
1003
- () => turnMessage
2013
+ (existing) => ({
2014
+ ...turnMessage,
2015
+ ...existing.parentId !== void 0 ? { parentId: existing.parentId } : {},
2016
+ ...existing.childrenIds !== void 0 ? { childrenIds: existing.childrenIds } : {}
2017
+ })
1004
2018
  );
1005
2019
  this.callbacks.onMessageFinish?.(turnMessage);
1006
2020
  this.streamState = null;
@@ -1015,6 +2029,93 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1015
2029
  continue;
1016
2030
  }
1017
2031
  if (!this.streamState) {
2032
+ if (chunk.type === "tool_calls") {
2033
+ pendingClientToolCalls = chunk.toolCalls;
2034
+ this.debug("tool_calls (post-message:end, stored as pending)", {
2035
+ count: pendingClientToolCalls?.length,
2036
+ ids: pendingClientToolCalls?.map((tc) => tc.id)
2037
+ });
2038
+ continue;
2039
+ }
2040
+ if (chunk.type === "done") {
2041
+ this.debug("done (post-message:end)", {
2042
+ hasPendingToolCalls: !!pendingClientToolCalls?.length,
2043
+ pendingCount: pendingClientToolCalls?.length ?? 0,
2044
+ doneMessagesCount: chunk.messages?.length ?? 0,
2045
+ requiresAction: chunk.requiresAction,
2046
+ toolCallsEmitted
2047
+ });
2048
+ if (chunk.messages?.length) {
2049
+ const pendingIds = new Set(
2050
+ (pendingClientToolCalls ?? []).filter((tc) => tc?.id).map((tc) => tc.id)
2051
+ );
2052
+ const messagesToInsert = [];
2053
+ let clientAssistantToolCalls;
2054
+ for (const msg of chunk.messages) {
2055
+ if (msg.role === "assistant" && msg.tool_calls?.length && pendingIds.size > 0 && msg.tool_calls.every(
2056
+ (tc) => pendingIds.has(tc?.id ?? "")
2057
+ )) {
2058
+ clientAssistantToolCalls = msg.tool_calls;
2059
+ continue;
2060
+ }
2061
+ if (msg.role === "assistant" && !msg.tool_calls?.length) continue;
2062
+ messagesToInsert.push({
2063
+ id: generateMessageId(),
2064
+ role: msg.role,
2065
+ content: msg.content ?? "",
2066
+ toolCalls: msg.tool_calls,
2067
+ toolCallId: msg.tool_call_id,
2068
+ createdAt: /* @__PURE__ */ new Date()
2069
+ });
2070
+ }
2071
+ if (clientAssistantToolCalls) {
2072
+ const currentMessages = this.state.messages;
2073
+ for (let i = currentMessages.length - 1; i >= 0; i--) {
2074
+ if (currentMessages[i].role === "assistant") {
2075
+ this.state.updateMessageById(
2076
+ currentMessages[i].id,
2077
+ (m) => ({
2078
+ ...m,
2079
+ toolCalls: clientAssistantToolCalls
2080
+ })
2081
+ );
2082
+ break;
2083
+ }
2084
+ }
2085
+ }
2086
+ if (messagesToInsert.length > 0) {
2087
+ const currentMessages = this.state.messages;
2088
+ let insertIdx = currentMessages.length;
2089
+ for (let i = currentMessages.length - 1; i >= 0; i--) {
2090
+ if (currentMessages[i].role === "assistant") {
2091
+ insertIdx = i;
2092
+ break;
2093
+ }
2094
+ }
2095
+ this.state.setMessages([
2096
+ ...currentMessages.slice(0, insertIdx),
2097
+ ...messagesToInsert,
2098
+ ...currentMessages.slice(insertIdx)
2099
+ ]);
2100
+ }
2101
+ }
2102
+ if (!toolCallsEmitted && pendingClientToolCalls?.length) {
2103
+ toolCallsEmitted = true;
2104
+ this.debug("emit toolCalls (post-message:end path)", {
2105
+ count: pendingClientToolCalls.length,
2106
+ names: pendingClientToolCalls.map(
2107
+ (tc) => tc.function?.name ?? tc.name
2108
+ )
2109
+ });
2110
+ this.emit("toolCalls", { toolCalls: pendingClientToolCalls });
2111
+ } else {
2112
+ this.debug("skip emit toolCalls (post-message:end path)", {
2113
+ toolCallsEmitted,
2114
+ hasPending: !!pendingClientToolCalls?.length
2115
+ });
2116
+ }
2117
+ continue;
2118
+ }
1018
2119
  this.debug("warning", "streamState is null, skipping chunk");
1019
2120
  continue;
1020
2121
  }
@@ -1050,22 +2151,38 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1050
2151
  const updatedMessage = streamStateToMessage(this.streamState);
1051
2152
  this.state.updateMessageById(
1052
2153
  this.streamState.messageId,
1053
- () => updatedMessage
2154
+ // Preserve parentId/childrenIds from the existing placeholder so the
2155
+ // branch tree structure (activeChildMap) is not corrupted when
2156
+ // setCurrentLeaf() walks up the chain later.
2157
+ (existing) => ({
2158
+ ...updatedMessage,
2159
+ ...existing.parentId !== void 0 ? { parentId: existing.parentId } : {},
2160
+ ...existing.childrenIds !== void 0 ? { childrenIds: existing.childrenIds } : {}
2161
+ })
1054
2162
  );
1055
2163
  if (chunk.type === "message:delta") {
1056
2164
  this.callbacks.onMessageDelta?.(assistantMessage.id, chunk.content);
1057
2165
  }
1058
- if (requiresToolExecution(chunk) && !toolCallsEmitted) {
1059
- toolCallsEmitted = true;
1060
- this.debug("toolCalls", { toolCalls: updatedMessage.toolCalls });
1061
- this.emit("toolCalls", { toolCalls: updatedMessage.toolCalls });
1062
- }
1063
2166
  if (isStreamDone(chunk)) {
1064
- this.debug("streamDone", { chunk });
2167
+ this.debug("streamDone", {
2168
+ chunkType: chunk.type,
2169
+ requiresAction: chunk.requiresAction,
2170
+ doneMessagesCount: chunk.messages?.length ?? 0,
2171
+ streamToolCallsCount: this.streamState?.toolCalls?.length ?? 0,
2172
+ toolCallsEmitted,
2173
+ chunkCount
2174
+ });
1065
2175
  if (chunk.type === "done" && chunk.messages?.length) {
1066
2176
  this.debug("processDoneMessages", {
1067
- count: chunk.messages.length
2177
+ count: chunk.messages.length,
2178
+ roles: chunk.messages.map(
2179
+ (m) => `${m.role}${m.tool_calls?.length ? `[${m.tool_calls.length}tc]` : ""}`
2180
+ )
1068
2181
  });
2182
+ const currentStreamToolCallIds = new Set(
2183
+ this.streamState?.toolCalls?.map((toolCall) => toolCall.id) ?? []
2184
+ );
2185
+ const messagesToInsert = [];
1069
2186
  const toolCallsHidden = {};
1070
2187
  if (this.streamState?.toolResults) {
1071
2188
  for (const [id, result] of this.streamState.toolResults) {
@@ -1075,7 +2192,12 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1075
2192
  }
1076
2193
  }
1077
2194
  for (const msg of chunk.messages) {
1078
- if (msg.role === "assistant") {
2195
+ if (msg.role === "assistant" && !msg.tool_calls?.length) {
2196
+ continue;
2197
+ }
2198
+ if (msg.role === "assistant" && msg.tool_calls?.length && msg.tool_calls.every(
2199
+ (toolCall) => currentStreamToolCallIds.has(toolCall.id)
2200
+ )) {
1079
2201
  continue;
1080
2202
  }
1081
2203
  let metadata;
@@ -1091,7 +2213,60 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1091
2213
  createdAt: /* @__PURE__ */ new Date(),
1092
2214
  metadata
1093
2215
  };
1094
- this.state.pushMessage(message);
2216
+ messagesToInsert.push(message);
2217
+ }
2218
+ if (messagesToInsert.length > 0) {
2219
+ const currentMessages = this.state.messages;
2220
+ const currentStreamIndex = this.streamState ? currentMessages.findIndex(
2221
+ (message) => message.id === this.streamState.messageId
2222
+ ) : -1;
2223
+ if (currentStreamIndex === -1) {
2224
+ this.state.setMessages([...currentMessages, ...messagesToInsert]);
2225
+ } else {
2226
+ this.state.setMessages([
2227
+ ...currentMessages.slice(0, currentStreamIndex),
2228
+ ...messagesToInsert,
2229
+ ...currentMessages.slice(currentStreamIndex)
2230
+ ]);
2231
+ }
2232
+ }
2233
+ this.debug("requiresAction check", {
2234
+ requiresAction: chunk.requiresAction,
2235
+ toolCallsEmitted,
2236
+ updatedMessageToolCallsCount: updatedMessage.toolCalls?.length ?? 0,
2237
+ messagesToInsertCount: messagesToInsert.length
2238
+ });
2239
+ if (chunk.requiresAction && !toolCallsEmitted) {
2240
+ let clientToolCalls = updatedMessage.toolCalls;
2241
+ if (!clientToolCalls?.length && messagesToInsert.length > 0) {
2242
+ for (let i = messagesToInsert.length - 1; i >= 0; i--) {
2243
+ const m = messagesToInsert[i];
2244
+ if (m.role === "assistant" && m.toolCalls?.length) {
2245
+ clientToolCalls = m.toolCalls;
2246
+ this.debug("clientToolCalls from messagesToInsert", {
2247
+ index: i,
2248
+ count: clientToolCalls?.length
2249
+ });
2250
+ break;
2251
+ }
2252
+ }
2253
+ }
2254
+ if (clientToolCalls?.length) {
2255
+ toolCallsEmitted = true;
2256
+ this.debug("emit toolCalls (normal done path)", {
2257
+ count: clientToolCalls.length,
2258
+ names: clientToolCalls.map((tc) => tc.function?.name ?? tc.name)
2259
+ });
2260
+ this.emit("toolCalls", { toolCalls: clientToolCalls });
2261
+ } else {
2262
+ this.debug("requiresAction=true but no clientToolCalls found", {
2263
+ updatedMessageToolCalls: updatedMessage.toolCalls,
2264
+ messagesToInsert: messagesToInsert.map((m) => ({
2265
+ role: m.role,
2266
+ hasToolCalls: !!m.toolCalls?.length
2267
+ }))
2268
+ });
2269
+ }
1095
2270
  }
1096
2271
  }
1097
2272
  break;
@@ -1116,15 +2291,22 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1116
2291
  toolCallsHidden
1117
2292
  };
1118
2293
  }
1119
- this.state.updateMessageById(
1120
- this.streamState.messageId,
1121
- () => finalMessage
1122
- );
2294
+ this.state.updateMessageById(this.streamState.messageId, (existing) => ({
2295
+ ...finalMessage,
2296
+ ...existing.parentId !== void 0 ? { parentId: existing.parentId } : {},
2297
+ ...existing.childrenIds !== void 0 ? { childrenIds: existing.childrenIds } : {}
2298
+ }));
1123
2299
  if (!finalMessage.content && (!finalMessage.toolCalls || finalMessage.toolCalls.length === 0)) {
1124
2300
  this.debug("warning", "Empty response - no content and no tool calls");
1125
2301
  }
1126
2302
  }
1127
- this.callbacks.onMessagesChange?.(this.state.messages);
2303
+ this.callbacks.onMessagesChange?.(this._allMessages());
2304
+ this.debugGroupEnd();
2305
+ this.debug("stream end", {
2306
+ toolCallsEmitted,
2307
+ totalChunks: chunkCount,
2308
+ messagesInState: this.state.messages.length
2309
+ });
1128
2310
  if (!toolCallsEmitted) {
1129
2311
  this.state.status = "ready";
1130
2312
  this.callbacks.onStatusChange?.("ready");
@@ -1145,6 +2327,7 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1145
2327
  }
1146
2328
  }
1147
2329
  }
2330
+ let currentParentId = this.state.messages.length > 0 ? this.state.messages[this.state.messages.length - 1].id : void 0;
1148
2331
  for (const msg of response.messages ?? []) {
1149
2332
  let metadata;
1150
2333
  if (msg.role === "assistant" && msg.tool_calls && toolCallHiddenMap.size > 0) {
@@ -1167,11 +2350,15 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1167
2350
  // CRITICAL: Preserve toolCallId for tool messages (fixes Anthropic API errors)
1168
2351
  toolCallId: msg.tool_call_id,
1169
2352
  createdAt: /* @__PURE__ */ new Date(),
1170
- metadata
2353
+ metadata,
2354
+ // Preserve branch tree structure: each message is a child of the
2355
+ // current leaf so the tree is not corrupted for non-streaming mode.
2356
+ ...currentParentId !== void 0 ? { parentId: currentParentId } : {}
1171
2357
  };
1172
2358
  this.state.pushMessage(message);
2359
+ currentParentId = message.id;
1173
2360
  }
1174
- this.callbacks.onMessagesChange?.(this.state.messages);
2361
+ this.callbacks.onMessagesChange?.(this._allMessages());
1175
2362
  const hasToolCalls = response.requiresAction && this.state.messages.length > 0 && this.state.messages[this.state.messages.length - 1]?.toolCalls?.length;
1176
2363
  if (hasToolCalls) {
1177
2364
  const lastMessage = this.state.messages[this.state.messages.length - 1];
@@ -1194,14 +2381,25 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt,
1194
2381
  this.callbacks.onStatusChange?.("error");
1195
2382
  this.emit("error", { error });
1196
2383
  }
1197
- /**
1198
- * Debug logging
1199
- */
2384
+ get log() {
2385
+ if (!this._log) {
2386
+ this._log = createLogger("streaming", () => this.config.debug ?? false);
2387
+ }
2388
+ return this._log;
2389
+ }
1200
2390
  debug(action, data) {
1201
- if (this.config.debug) {
1202
- console.log(`[AbstractChat] ${action}`, data);
2391
+ this.log(action, data);
2392
+ }
2393
+ debugGroup(label, collapsed = true) {
2394
+ if (collapsed) {
2395
+ this.log.groupCollapsed(label);
2396
+ } else {
2397
+ this.log.group(label);
1203
2398
  }
1204
2399
  }
2400
+ debugGroupEnd() {
2401
+ this.log.groupEnd();
2402
+ }
1205
2403
  /**
1206
2404
  * Type guard for async iterable
1207
2405
  */
@@ -1412,19 +2610,18 @@ var AbstractAgentLoop = class {
1412
2610
  this._isCancelled = false;
1413
2611
  this._isProcessing = true;
1414
2612
  this.setIteration(this._iteration + 1);
1415
- const results = [];
1416
- for (const toolCall of toolCalls) {
1417
- if (this._isCancelled || this.abortController.signal.aborted) {
1418
- results.push({
1419
- toolCallId: toolCall.id,
1420
- success: false,
1421
- error: "Tool execution cancelled"
1422
- });
1423
- continue;
1424
- }
1425
- const result = await this.executeSingleTool(toolCall);
1426
- results.push(result);
1427
- }
2613
+ const results = await Promise.all(
2614
+ toolCalls.map((toolCall) => {
2615
+ if (this._isCancelled || this.abortController.signal.aborted) {
2616
+ return Promise.resolve({
2617
+ toolCallId: toolCall.id,
2618
+ success: false,
2619
+ error: "Tool execution cancelled"
2620
+ });
2621
+ }
2622
+ return this.executeSingleTool(toolCall);
2623
+ })
2624
+ );
1428
2625
  this._isProcessing = false;
1429
2626
  return results;
1430
2627
  }
@@ -1742,6 +2939,7 @@ var ChatWithTools = class {
1742
2939
  streaming: config.streaming,
1743
2940
  headers: config.headers,
1744
2941
  body: config.body,
2942
+ optimization: config.optimization,
1745
2943
  threadId: config.threadId,
1746
2944
  debug: config.debug,
1747
2945
  initialMessages: config.initialMessages,
@@ -1756,6 +2954,7 @@ var ChatWithTools = class {
1756
2954
  onMessageFinish: callbacks.onMessageFinish,
1757
2955
  onToolCalls: callbacks.onToolCalls,
1758
2956
  onFinish: callbacks.onFinish,
2957
+ onContextUsageChange: callbacks.onContextUsageChange,
1759
2958
  // Server-side tool callbacks - track in agentLoop for UI display
1760
2959
  // IMPORTANT: Only track tools that are NOT registered client-side
1761
2960
  // Client-side tools are tracked via executeToolCalls() path
@@ -1923,14 +3122,17 @@ var ChatWithTools = class {
1923
3122
  /**
1924
3123
  * Send a message
1925
3124
  * Returns false if a request is already in progress
3125
+ *
3126
+ * @param options.editMessageId - Edit flow: new message branches from the
3127
+ * same parent as this message ID
1926
3128
  */
1927
- async sendMessage(content, attachments) {
3129
+ async sendMessage(content, attachments, options) {
1928
3130
  if (this.isLoading) {
1929
3131
  this.debug("sendMessage blocked - request already in progress");
1930
3132
  return false;
1931
3133
  }
1932
3134
  this.agentLoop.resetIterations();
1933
- return await this.chat.sendMessage(content, attachments);
3135
+ return await this.chat.sendMessage(content, attachments, options);
1934
3136
  }
1935
3137
  /**
1936
3138
  * Stop generation and cancel any running tools
@@ -1965,6 +3167,25 @@ var ChatWithTools = class {
1965
3167
  setTools(tools) {
1966
3168
  this.chat.setTools(tools);
1967
3169
  }
3170
+ /**
3171
+ * Update prompt/tool optimization controls.
3172
+ */
3173
+ setOptimizationConfig(config) {
3174
+ this.config.optimization = config;
3175
+ this.chat.setOptimizationConfig(config);
3176
+ }
3177
+ /**
3178
+ * Set the active tool profile used for request-time tool selection.
3179
+ */
3180
+ setToolProfile(profile) {
3181
+ this.chat.setToolProfile(profile);
3182
+ }
3183
+ /**
3184
+ * Get the most recent prompt context usage snapshot.
3185
+ */
3186
+ getContextUsage() {
3187
+ return this.chat.getContextUsage();
3188
+ }
1968
3189
  /**
1969
3190
  * Set dynamic context (from useAIContext hook)
1970
3191
  */
@@ -1998,6 +3219,15 @@ var ChatWithTools = class {
1998
3219
  setBody(body) {
1999
3220
  this.chat.setBody(body);
2000
3221
  }
3222
+ setRequestMessageTransform(fn) {
3223
+ this.chat.setRequestMessageTransform(fn);
3224
+ }
3225
+ /**
3226
+ * Set inline skills (forwarded to underlying chat instance)
3227
+ */
3228
+ setInlineSkills(skills) {
3229
+ this.chat.setInlineSkills(skills);
3230
+ }
2001
3231
  // ============================================
2002
3232
  // Tool Registration
2003
3233
  // ============================================
@@ -2074,16 +3304,307 @@ var ChatWithTools = class {
2074
3304
  // Private
2075
3305
  // ============================================
2076
3306
  debug(message, ...args) {
2077
- if (this.config.debug) {
2078
- console.log(`[ChatWithTools] ${message}`, ...args);
3307
+ createLogger("tools", () => this.config.debug ?? false)(
3308
+ message,
3309
+ args.length === 1 ? args[0] : args.length > 1 ? args : void 0
3310
+ );
3311
+ }
3312
+ };
3313
+
3314
+ // src/chat/branching/MessageTree.ts
3315
+ var _MessageTree = class _MessageTree {
3316
+ constructor(messages) {
3317
+ /** All messages by ID */
3318
+ this.nodeMap = /* @__PURE__ */ new Map();
3319
+ /** parentKey → ordered list of child IDs (insertion order = oldest-first) */
3320
+ this.childrenOf = /* @__PURE__ */ new Map();
3321
+ /** parentKey → currently-active child ID */
3322
+ this.activeChildMap = /* @__PURE__ */ new Map();
3323
+ /** Current leaf message ID (tip of the active path) */
3324
+ this._currentLeafId = null;
3325
+ /** Cached visible messages — invalidated on every mutation */
3326
+ this._visibleCache = null;
3327
+ if (messages?.length) {
3328
+ this._buildFromMessages(messages);
3329
+ }
3330
+ }
3331
+ // ============================================
3332
+ // Static Migration Helpers
3333
+ // ============================================
3334
+ /**
3335
+ * Convert a legacy flat array (no parentId) to a tree-linked array.
3336
+ *
3337
+ * Rules:
3338
+ * - Tool messages get parentId = the owning assistant message's id
3339
+ * (matched via toolCallId → toolCall.id).
3340
+ * - All other messages get parentId of the previous non-tool message
3341
+ * (or null for the first message).
3342
+ *
3343
+ * Returns a new array with parentId/childrenIds filled in.
3344
+ * Does NOT mutate the original messages.
3345
+ */
3346
+ static fromFlatArray(messages) {
3347
+ if (messages.length === 0) return messages;
3348
+ const alreadyLinked = messages.some((m) => m.parentId !== void 0);
3349
+ if (alreadyLinked) return messages;
3350
+ const result = [];
3351
+ let prevNonToolId = null;
3352
+ const assistantById = /* @__PURE__ */ new Map();
3353
+ for (const msg of messages) {
3354
+ if (msg.role === "assistant") {
3355
+ assistantById.set(msg.id, msg);
3356
+ }
3357
+ }
3358
+ for (const msg of messages) {
3359
+ if (msg.role === "tool" && msg.toolCallId) {
3360
+ let ownerAssistantId = null;
3361
+ for (const [, assistant] of assistantById) {
3362
+ if (assistant.toolCalls?.some((tc) => tc.id === msg.toolCallId)) {
3363
+ ownerAssistantId = assistant.id;
3364
+ break;
3365
+ }
3366
+ }
3367
+ result.push({
3368
+ ...msg,
3369
+ parentId: ownerAssistantId ?? prevNonToolId,
3370
+ childrenIds: []
3371
+ });
3372
+ } else {
3373
+ result.push({
3374
+ ...msg,
3375
+ parentId: prevNonToolId,
3376
+ childrenIds: []
3377
+ });
3378
+ prevNonToolId = msg.id;
3379
+ }
3380
+ }
3381
+ const childrenMap = /* @__PURE__ */ new Map();
3382
+ for (const msg of result) {
3383
+ const parentKey = msg.parentId == null ? _MessageTree.ROOT_KEY : msg.parentId;
3384
+ if (!childrenMap.has(parentKey)) {
3385
+ childrenMap.set(parentKey, []);
3386
+ }
3387
+ childrenMap.get(parentKey).push(msg.id);
3388
+ }
3389
+ return result.map((msg) => ({
3390
+ ...msg,
3391
+ childrenIds: childrenMap.get(msg.id) ?? []
3392
+ }));
3393
+ }
3394
+ // ============================================
3395
+ // Core Queries
3396
+ // ============================================
3397
+ /**
3398
+ * Returns the visible path (root → current leaf) — what the UI renders
3399
+ * and what gets sent to the API.
3400
+ *
3401
+ * Backward-compat: if NO message has parentId set (all undefined),
3402
+ * falls back to insertion order (legacy linear mode).
3403
+ */
3404
+ getVisibleMessages() {
3405
+ if (this._visibleCache !== null) return this._visibleCache;
3406
+ if (this.nodeMap.size === 0) {
3407
+ this._visibleCache = [];
3408
+ return this._visibleCache;
3409
+ }
3410
+ const hasTreeStructure = Array.from(this.nodeMap.values()).some(
3411
+ (m) => m.parentId !== void 0
3412
+ );
3413
+ this._visibleCache = hasTreeStructure ? this._getActivePath().map((id) => this.nodeMap.get(id)) : Array.from(this.nodeMap.values());
3414
+ return this._visibleCache;
3415
+ }
3416
+ _invalidateCache() {
3417
+ this._visibleCache = null;
3418
+ }
3419
+ /**
3420
+ * Returns ALL messages across every branch (for persistence / ThreadManager).
3421
+ */
3422
+ getAllMessages() {
3423
+ return Array.from(this.nodeMap.values());
3424
+ }
3425
+ /**
3426
+ * Branch navigation info for the UI navigator.
3427
+ * Returns null if the message has no siblings (only child).
3428
+ */
3429
+ getBranchInfo(messageId) {
3430
+ const msg = this.nodeMap.get(messageId);
3431
+ if (!msg) return null;
3432
+ const parentKey = this._parentKey(msg.parentId);
3433
+ const siblings = this.childrenOf.get(parentKey) ?? [];
3434
+ if (siblings.length <= 1) return null;
3435
+ const siblingIndex = siblings.indexOf(messageId);
3436
+ return {
3437
+ siblingIndex,
3438
+ totalSiblings: siblings.length,
3439
+ siblingIds: [...siblings],
3440
+ hasPrevious: siblingIndex > 0,
3441
+ hasNext: siblingIndex < siblings.length - 1
3442
+ };
3443
+ }
3444
+ get currentLeafId() {
3445
+ return this._currentLeafId;
3446
+ }
3447
+ get hasBranches() {
3448
+ for (const children of this.childrenOf.values()) {
3449
+ if (children.length > 1) return true;
3450
+ }
3451
+ return false;
3452
+ }
3453
+ // ============================================
3454
+ // Mutations
3455
+ // ============================================
3456
+ /**
3457
+ * Insert a new message.
3458
+ * - Updates childrenOf and nodeMap.
3459
+ * - New branch becomes active (activeChildMap updated).
3460
+ * - Updates current leaf.
3461
+ */
3462
+ addMessage(message) {
3463
+ this.nodeMap.set(message.id, message);
3464
+ const parentKey = this._parentKey(message.parentId);
3465
+ if (!this.childrenOf.has(parentKey)) {
3466
+ this.childrenOf.set(parentKey, []);
3467
+ }
3468
+ const siblings = this.childrenOf.get(parentKey);
3469
+ if (!siblings.includes(message.id)) {
3470
+ siblings.push(message.id);
3471
+ }
3472
+ this.activeChildMap.set(parentKey, message.id);
3473
+ this._currentLeafId = this._walkToLeaf(message.id);
3474
+ this._invalidateCache();
3475
+ return message;
3476
+ }
3477
+ /**
3478
+ * Navigate: make messageId the active child at its parent fork,
3479
+ * then walk to its leaf and update currentLeafId.
3480
+ */
3481
+ switchBranch(messageId) {
3482
+ const msg = this.nodeMap.get(messageId);
3483
+ if (!msg) return;
3484
+ const parentKey = this._parentKey(msg.parentId);
3485
+ this.activeChildMap.set(parentKey, messageId);
3486
+ this._currentLeafId = this._walkToLeaf(messageId);
3487
+ this._invalidateCache();
3488
+ }
3489
+ /**
3490
+ * Update message content in-place (streaming updates).
3491
+ * No tree structure change.
3492
+ */
3493
+ updateMessage(id, updater) {
3494
+ const existing = this.nodeMap.get(id);
3495
+ if (!existing) return false;
3496
+ this.nodeMap.set(id, updater(existing));
3497
+ this._invalidateCache();
3498
+ return true;
3499
+ }
3500
+ /**
3501
+ * Set current leaf explicitly.
3502
+ * Used by regenerate() to rewind the active path before pushing a new message.
3503
+ */
3504
+ setCurrentLeaf(leafId) {
3505
+ this._currentLeafId = leafId;
3506
+ if (leafId === null) return;
3507
+ const msg = this.nodeMap.get(leafId);
3508
+ if (!msg) return;
3509
+ let current = msg;
3510
+ while (current) {
3511
+ const parentKey = this._parentKey(current.parentId);
3512
+ this.activeChildMap.set(parentKey, current.id);
3513
+ if (current.parentId == null || current.parentId === void 0) break;
3514
+ current = this.nodeMap.get(current.parentId);
3515
+ }
3516
+ this._invalidateCache();
3517
+ }
3518
+ /**
3519
+ * Rebuild entire tree from a message array.
3520
+ * Used by setMessages().
3521
+ */
3522
+ reset(messages) {
3523
+ this.nodeMap.clear();
3524
+ this.childrenOf.clear();
3525
+ this.activeChildMap.clear();
3526
+ this._currentLeafId = null;
3527
+ this._invalidateCache();
3528
+ if (messages.length > 0) {
3529
+ this._buildFromMessages(messages);
3530
+ }
3531
+ }
3532
+ // ============================================
3533
+ // Private Helpers
3534
+ // ============================================
3535
+ _buildFromMessages(messages) {
3536
+ const linked = messages.some((m) => m.parentId !== void 0) ? messages : _MessageTree.fromFlatArray(messages);
3537
+ for (const msg of linked) {
3538
+ this.nodeMap.set(msg.id, msg);
3539
+ const parentKey = this._parentKey(msg.parentId);
3540
+ if (!this.childrenOf.has(parentKey)) {
3541
+ this.childrenOf.set(parentKey, []);
3542
+ }
3543
+ const siblings = this.childrenOf.get(parentKey);
3544
+ if (!siblings.includes(msg.id)) {
3545
+ siblings.push(msg.id);
3546
+ }
3547
+ }
3548
+ for (const [parentKey, children] of this.childrenOf) {
3549
+ if (children.length > 0) {
3550
+ this.activeChildMap.set(parentKey, children[children.length - 1]);
3551
+ }
3552
+ }
3553
+ const path = this._getActivePath();
3554
+ this._currentLeafId = path.length > 0 ? path[path.length - 1] : null;
3555
+ }
3556
+ _parentKey(parentId) {
3557
+ if (parentId == null || parentId === void 0) {
3558
+ return _MessageTree.ROOT_KEY;
3559
+ }
3560
+ return parentId;
3561
+ }
3562
+ /**
3563
+ * Walk forward from a message along active children to find the leaf.
3564
+ */
3565
+ _walkToLeaf(fromId) {
3566
+ let current = fromId;
3567
+ while (true) {
3568
+ const children = this.childrenOf.get(current);
3569
+ if (!children || children.length === 0) break;
3570
+ const activeChild = this.activeChildMap.get(current);
3571
+ if (!activeChild) break;
3572
+ if (!this.nodeMap.has(activeChild)) break;
3573
+ current = activeChild;
3574
+ }
3575
+ return current;
3576
+ }
3577
+ /**
3578
+ * Walk the active path from root to the current leaf.
3579
+ */
3580
+ _getActivePath() {
3581
+ const path = [];
3582
+ const visited = /* @__PURE__ */ new Set();
3583
+ const rootChildren = this.childrenOf.get(_MessageTree.ROOT_KEY) ?? [];
3584
+ if (rootChildren.length === 0) return path;
3585
+ let activeId = this.activeChildMap.get(_MessageTree.ROOT_KEY);
3586
+ if (!activeId) {
3587
+ activeId = rootChildren[rootChildren.length - 1];
3588
+ }
3589
+ let current = activeId;
3590
+ while (current && !visited.has(current)) {
3591
+ if (!this.nodeMap.has(current)) break;
3592
+ visited.add(current);
3593
+ path.push(current);
3594
+ const activeChild = this.activeChildMap.get(current);
3595
+ if (!activeChild || !this.nodeMap.has(activeChild)) break;
3596
+ current = activeChild;
2079
3597
  }
3598
+ return path;
2080
3599
  }
2081
3600
  };
3601
+ /** Sentinel key used for root-level messages (parentId === null) */
3602
+ _MessageTree.ROOT_KEY = "__root__";
3603
+ var MessageTree = _MessageTree;
2082
3604
 
2083
3605
  // src/react/internal/ReactChatState.ts
2084
3606
  var ReactChatState = class {
2085
3607
  constructor(initialMessages) {
2086
- this._messages = [];
2087
3608
  this._status = "ready";
2088
3609
  this._error = void 0;
2089
3610
  // Callbacks for React subscriptions (useSyncExternalStore)
@@ -2109,15 +3630,19 @@ var ReactChatState = class {
2109
3630
  this.subscribers.delete(callback);
2110
3631
  };
2111
3632
  };
2112
- if (initialMessages) {
2113
- this._messages = initialMessages;
2114
- }
3633
+ this.tree = new MessageTree(initialMessages);
2115
3634
  }
2116
3635
  // ============================================
2117
- // Getters
3636
+ // Getters — visible path only
2118
3637
  // ============================================
3638
+ /**
3639
+ * Returns the VISIBLE PATH (active branch) — what the UI renders
3640
+ * and what gets sent to the API.
3641
+ *
3642
+ * For all messages across all branches, use getAllMessages().
3643
+ */
2119
3644
  get messages() {
2120
- return this._messages;
3645
+ return this.tree.getVisibleMessages();
2121
3646
  }
2122
3647
  get status() {
2123
3648
  return this._status;
@@ -2129,7 +3654,7 @@ var ReactChatState = class {
2129
3654
  // Setters (trigger reactivity)
2130
3655
  // ============================================
2131
3656
  set messages(value) {
2132
- this._messages = value;
3657
+ this.tree.reset(value);
2133
3658
  this.notify();
2134
3659
  }
2135
3660
  set status(value) {
@@ -2144,61 +3669,100 @@ var ReactChatState = class {
2144
3669
  // Mutations
2145
3670
  // ============================================
2146
3671
  pushMessage(message) {
2147
- this._messages = [...this._messages, message];
3672
+ this.tree.addMessage(message);
2148
3673
  this.notify();
2149
3674
  }
2150
3675
  popMessage() {
2151
- this._messages = this._messages.slice(0, -1);
3676
+ const leafId = this.tree.currentLeafId;
3677
+ if (!leafId) return;
3678
+ const allMessages = this.tree.getAllMessages().filter((m) => m.id !== leafId);
3679
+ const leaf = this.tree.getAllMessages().find((m) => m.id === leafId);
3680
+ const newLeafId = leaf && leaf.parentId !== void 0 && leaf.parentId !== null ? leaf.parentId : null;
3681
+ this.tree.reset(allMessages);
3682
+ if (newLeafId) {
3683
+ this.tree.setCurrentLeaf(newLeafId);
3684
+ }
2152
3685
  this.notify();
2153
3686
  }
2154
3687
  replaceMessage(index, message) {
2155
- this._messages = this._messages.map((m, i) => i === index ? message : m);
3688
+ const visible = this.tree.getVisibleMessages();
3689
+ const target = visible[index];
3690
+ if (!target) return;
3691
+ this.tree.updateMessage(target.id, () => message);
2156
3692
  this.notify();
2157
3693
  }
2158
3694
  updateLastMessage(updater) {
2159
- if (this._messages.length === 0) return;
2160
- const lastIndex = this._messages.length - 1;
2161
- const lastMessage = this._messages[lastIndex];
2162
- this._messages = [
2163
- ...this._messages.slice(0, lastIndex),
2164
- updater(lastMessage)
2165
- ];
3695
+ const leafId = this.tree.currentLeafId;
3696
+ if (!leafId) return;
3697
+ this.tree.updateMessage(leafId, updater);
2166
3698
  this.notify();
2167
3699
  }
2168
3700
  updateMessageById(id, updater) {
2169
- const index = this._messages.findIndex((m) => m.id === id);
2170
- if (index === -1) return false;
2171
- this._messages = this._messages.map(
2172
- (m, i) => i === index ? updater(m) : m
2173
- );
2174
- this.notify();
2175
- return true;
3701
+ const updated = this.tree.updateMessage(id, updater);
3702
+ if (updated) this.notify();
3703
+ return updated;
2176
3704
  }
2177
3705
  setMessages(messages) {
2178
- this._messages = messages;
3706
+ this.tree.reset(messages);
2179
3707
  this.notify();
2180
3708
  }
2181
3709
  clearMessages() {
2182
- this._messages = [];
3710
+ this.tree.reset([]);
2183
3711
  this.notify();
2184
3712
  }
2185
3713
  // ============================================
2186
- // Private Methods
3714
+ // Branching API
2187
3715
  // ============================================
2188
- notify() {
2189
- this.subscribers.forEach((cb) => cb());
2190
- }
2191
3716
  /**
2192
- * Cleanup subscriptions
3717
+ * Returns ALL messages across all branches.
3718
+ * Use this for persistence (ThreadManager save).
2193
3719
  */
2194
- dispose() {
2195
- this.subscribers.clear();
3720
+ getAllMessages() {
3721
+ return this.tree.getAllMessages();
2196
3722
  }
2197
3723
  /**
2198
- * Revive after dispose (for React StrictMode compatibility)
2199
- * Subscribers will be re-added automatically via useSyncExternalStore
3724
+ * Get branch navigation info for a message.
3725
+ * Returns null if the message has no siblings.
2200
3726
  */
2201
- revive() {
3727
+ getBranchInfo(messageId) {
3728
+ return this.tree.getBranchInfo(messageId);
3729
+ }
3730
+ /**
3731
+ * Navigate to a sibling branch.
3732
+ * Triggers re-render via notify().
3733
+ */
3734
+ switchBranch(messageId) {
3735
+ this.tree.switchBranch(messageId);
3736
+ this.notify();
3737
+ }
3738
+ /**
3739
+ * Set the current leaf (used by regenerate() to rewind active path).
3740
+ * Triggers re-render via notify().
3741
+ */
3742
+ setCurrentLeaf(leafId) {
3743
+ this.tree.setCurrentLeaf(leafId);
3744
+ this.notify();
3745
+ }
3746
+ get hasBranches() {
3747
+ return this.tree.hasBranches;
3748
+ }
3749
+ // ============================================
3750
+ // Private Methods
3751
+ // ============================================
3752
+ notify() {
3753
+ this.subscribers.forEach((cb) => cb());
3754
+ }
3755
+ /**
3756
+ * Cleanup subscriptions
3757
+ */
3758
+ dispose() {
3759
+ this.subscribers.clear();
3760
+ }
3761
+ /**
3762
+ * Revive after dispose (for React StrictMode compatibility)
3763
+ * Subscribers will be re-added automatically via useSyncExternalStore
3764
+ */
3765
+ revive() {
2202
3766
  }
2203
3767
  };
2204
3768
  function createReactChatState(initialMessages) {
@@ -2218,6 +3782,33 @@ var ReactChatWithTools = class extends ChatWithTools {
2218
3782
  };
2219
3783
  this.reactState = reactState;
2220
3784
  }
3785
+ // ============================================
3786
+ // Branching API — pass-throughs to ReactChatState
3787
+ // ============================================
3788
+ /**
3789
+ * Navigate to a sibling branch.
3790
+ */
3791
+ switchBranch(messageId) {
3792
+ this.reactState.switchBranch(messageId);
3793
+ }
3794
+ /**
3795
+ * Get branch navigation info for a message.
3796
+ */
3797
+ getBranchInfo(messageId) {
3798
+ return this.reactState.getBranchInfo(messageId);
3799
+ }
3800
+ /**
3801
+ * Get all messages across all branches (for persistence).
3802
+ */
3803
+ getAllMessages() {
3804
+ return this.reactState.getAllMessages();
3805
+ }
3806
+ /**
3807
+ * Whether any message has siblings (branching has occurred).
3808
+ */
3809
+ get hasBranches() {
3810
+ return this.reactState.hasBranches;
3811
+ }
2221
3812
  /**
2222
3813
  * Dispose and cleanup
2223
3814
  */
@@ -2494,36 +4085,933 @@ function useMCPTools(config) {
2494
4085
  source
2495
4086
  ]);
2496
4087
  useEffect(() => {
2497
- if (!autoRegister) {
2498
- return;
2499
- }
2500
- for (const toolName of registeredToolsRef.current) {
2501
- unregisterTool(toolName);
2502
- }
2503
- registeredToolsRef.current = [];
2504
- if (mcpClient.isConnected && toolDefinitions.length > 0) {
2505
- for (const tool2 of toolDefinitions) {
2506
- registerTool(tool2);
2507
- registeredToolsRef.current.push(tool2.name);
4088
+ if (!autoRegister) {
4089
+ return;
4090
+ }
4091
+ for (const toolName of registeredToolsRef.current) {
4092
+ unregisterTool(toolName);
4093
+ }
4094
+ registeredToolsRef.current = [];
4095
+ if (mcpClient.isConnected && toolDefinitions.length > 0) {
4096
+ for (const tool2 of toolDefinitions) {
4097
+ registerTool(tool2);
4098
+ registeredToolsRef.current.push(tool2.name);
4099
+ }
4100
+ }
4101
+ return () => {
4102
+ for (const toolName of registeredToolsRef.current) {
4103
+ unregisterTool(toolName);
4104
+ }
4105
+ registeredToolsRef.current = [];
4106
+ };
4107
+ }, [
4108
+ autoRegister,
4109
+ mcpClient.isConnected,
4110
+ toolDefinitions,
4111
+ registerTool,
4112
+ unregisterTool
4113
+ ]);
4114
+ return {
4115
+ ...mcpClient,
4116
+ toolDefinitions
4117
+ };
4118
+ }
4119
+ var defaultTokenUsage = {
4120
+ current: 0,
4121
+ max: 128e3,
4122
+ percentage: 0,
4123
+ isApproaching: false
4124
+ };
4125
+ var defaultCompactionState = {
4126
+ rollingSummary: null,
4127
+ lastCompactionAt: null,
4128
+ compactionCount: 0,
4129
+ totalTokensSaved: 0,
4130
+ workingMemory: [],
4131
+ displayMessageCount: 0,
4132
+ llmMessageCount: 0
4133
+ };
4134
+ var defaultMessageHistoryConfig = {
4135
+ strategy: "none",
4136
+ maxContextTokens: 128e3,
4137
+ reserveForResponse: 4096,
4138
+ compactionThreshold: 0.75,
4139
+ recentBuffer: 10,
4140
+ toolResultMaxChars: 1e4,
4141
+ persistSession: false,
4142
+ storageKey: "copilot-session"
4143
+ };
4144
+ var MessageHistoryContext = createContext({
4145
+ config: defaultMessageHistoryConfig,
4146
+ tokenUsage: defaultTokenUsage,
4147
+ compactionState: defaultCompactionState
4148
+ });
4149
+ function useMessageHistoryContext() {
4150
+ return useContext(MessageHistoryContext);
4151
+ }
4152
+
4153
+ // src/react/message-history/message-utils.ts
4154
+ function toDisplayMessage(msg) {
4155
+ return {
4156
+ ...msg,
4157
+ timestamp: msg.createdAt instanceof Date ? msg.createdAt.getTime() : Date.now()
4158
+ };
4159
+ }
4160
+ function toLLMMessage(msg) {
4161
+ if (isCompactionMarker(msg)) {
4162
+ return {
4163
+ role: "system",
4164
+ content: `[Previous conversation summary]
4165
+ ${msg.content}`
4166
+ };
4167
+ }
4168
+ const base = {
4169
+ role: msg.role,
4170
+ content: msg.content
4171
+ };
4172
+ if (msg.toolCalls?.length) {
4173
+ base.tool_calls = msg.toolCalls;
4174
+ }
4175
+ if (msg.toolCallId) {
4176
+ base.tool_call_id = msg.toolCallId;
4177
+ }
4178
+ return base;
4179
+ }
4180
+ function toLLMMessages(messages) {
4181
+ return messages.map(toLLMMessage);
4182
+ }
4183
+ function keepToolPairsAtomic(messages) {
4184
+ if (messages.length === 0) return messages;
4185
+ const firstIdx = messages.findIndex((msg, i) => {
4186
+ if (msg.role !== "assistant" || !msg.toolCalls?.length) return false;
4187
+ const toolCallIds = new Set(msg.toolCalls.map((tc) => tc.id));
4188
+ const resultIds = new Set(
4189
+ messages.slice(i + 1).filter((m) => m.role === "tool" && m.toolCallId).map((m) => m.toolCallId)
4190
+ );
4191
+ return [...toolCallIds].some((id) => !resultIds.has(id));
4192
+ });
4193
+ if (firstIdx === -1) return messages;
4194
+ return messages.slice(firstIdx);
4195
+ }
4196
+ function findSafeWindowStart(messages, desiredStart) {
4197
+ for (let i = desiredStart; i < messages.length; i++) {
4198
+ const msg = messages[i];
4199
+ if (msg.role === "user" || msg.role === "assistant" && !msg.toolCalls?.length) {
4200
+ return i;
4201
+ }
4202
+ }
4203
+ return desiredStart;
4204
+ }
4205
+ function isCompactionMarker(msg) {
4206
+ return msg.role === "system" && msg.type === "compaction-marker";
4207
+ }
4208
+
4209
+ // src/react/message-history/token-counter.ts
4210
+ function estimateMessageTokens2(msg) {
4211
+ let chars = msg.content?.length ?? 0;
4212
+ if (msg.tool_calls?.length) {
4213
+ for (const tc of msg.tool_calls) {
4214
+ chars += JSON.stringify(tc).length;
4215
+ }
4216
+ }
4217
+ return Math.ceil(chars / 3.5) + 4;
4218
+ }
4219
+ function estimateMessagesTokens(messages) {
4220
+ return messages.reduce((sum, msg) => sum + estimateMessageTokens2(msg), 0);
4221
+ }
4222
+ function estimateTokens2(messages, mode = "fast") {
4223
+ if (mode === "off") return 0;
4224
+ return estimateMessagesTokens(messages);
4225
+ }
4226
+
4227
+ // src/react/message-history/strategies/sliding-window.ts
4228
+ function applySlidingWindow(messages, options) {
4229
+ const { tokenBudget, recentBuffer } = options;
4230
+ if (messages.length === 0) return messages;
4231
+ const systemMessages = messages.filter(
4232
+ (m) => m.role === "system" || isCompactionMarker(m)
4233
+ );
4234
+ const conversationMessages = messages.filter(
4235
+ (m) => m.role !== "system" && !isCompactionMarker(m)
4236
+ );
4237
+ const systemTokens = estimateMessagesTokens(toLLMMessages(systemMessages));
4238
+ const remainingBudget = tokenBudget - systemTokens;
4239
+ if (conversationMessages.length === 0) return systemMessages;
4240
+ const recentStart = Math.max(0, conversationMessages.length - recentBuffer);
4241
+ const recent = conversationMessages.slice(recentStart);
4242
+ const older = conversationMessages.slice(0, recentStart);
4243
+ const allTokens = estimateMessagesTokens(toLLMMessages(conversationMessages));
4244
+ if (allTokens <= remainingBudget) {
4245
+ return messages;
4246
+ }
4247
+ const recentTokens = estimateMessagesTokens(toLLMMessages(recent));
4248
+ let available = remainingBudget - recentTokens;
4249
+ const included = [];
4250
+ for (let i = older.length - 1; i >= 0; i--) {
4251
+ const msgTokens = estimateMessagesTokens(toLLMMessages([older[i]]));
4252
+ if (available - msgTokens < 0) break;
4253
+ included.unshift(older[i]);
4254
+ available -= msgTokens;
4255
+ }
4256
+ const combined = [...included, ...recent];
4257
+ const safeStart = findSafeWindowStart(combined, 0);
4258
+ const safeWindow = combined.slice(safeStart);
4259
+ return [...systemMessages, ...safeWindow];
4260
+ }
4261
+ function truncateToolResults(messages, maxChars) {
4262
+ if (maxChars === 0) return messages;
4263
+ return messages.map((msg) => {
4264
+ if (msg.role !== "tool") return msg;
4265
+ if (!msg.content || msg.content.length <= maxChars) return msg;
4266
+ return {
4267
+ ...msg,
4268
+ content: msg.content.slice(0, maxChars) + `
4269
+ [truncated \u2014 original ${msg.content.length} chars, limit ${maxChars}]`
4270
+ };
4271
+ });
4272
+ }
4273
+
4274
+ // src/react/message-history/strategies/selective-prune.ts
4275
+ function applySelectivePrune(displayMessages, recentBuffer, options = {}) {
4276
+ const {
4277
+ toolResultAgeTurns = 3,
4278
+ stripOldReasoning = true,
4279
+ deduplicateSkills = true
4280
+ } = options;
4281
+ const cutoff = Math.max(0, displayMessages.length - recentBuffer);
4282
+ const seenSkillContent = /* @__PURE__ */ new Set();
4283
+ return displayMessages.map((msg, idx) => {
4284
+ const llm = toLLMMessage(msg);
4285
+ const isOld = idx < cutoff;
4286
+ if (deduplicateSkills && msg.role === "system" && llm.content) {
4287
+ const key = llm.content.slice(0, 100);
4288
+ if (seenSkillContent.has(key)) {
4289
+ return { ...llm, content: "[skill instruction \u2014 deduplicated]" };
4290
+ }
4291
+ seenSkillContent.add(key);
4292
+ }
4293
+ if (!isOld) return llm;
4294
+ if (stripOldReasoning && msg.role === "assistant" && msg.thinking) {
4295
+ llm.content = llm.content;
4296
+ }
4297
+ if (msg.role === "tool" && llm.content) {
4298
+ const originalSize = llm.content.length;
4299
+ if (originalSize > 500) {
4300
+ const stub = buildToolResultStub(msg, llm.content);
4301
+ return {
4302
+ role: "tool",
4303
+ tool_call_id: llm.tool_call_id,
4304
+ content: JSON.stringify(stub)
4305
+ };
4306
+ }
4307
+ }
4308
+ return llm;
4309
+ });
4310
+ }
4311
+ function buildToolResultStub(msg, content) {
4312
+ return {
4313
+ type: "compacted-tool-result",
4314
+ toolName: msg.metadata?.toolName ?? "tool",
4315
+ toolCallId: msg.toolCallId ?? "",
4316
+ args: msg.metadata?.toolArgs ?? {},
4317
+ executedAt: msg.timestamp,
4318
+ status: content.includes('"error"') ? "error" : "success",
4319
+ originalSize: content.length,
4320
+ summary: buildSummary(content),
4321
+ extract: content.slice(0, 200)
4322
+ };
4323
+ }
4324
+ function buildSummary(content) {
4325
+ try {
4326
+ const parsed = JSON.parse(content);
4327
+ if (parsed?.message) return String(parsed.message).slice(0, 120);
4328
+ if (parsed?.error) return `Error: ${String(parsed.error).slice(0, 100)}`;
4329
+ if (Array.isArray(parsed)) return `Array result \u2014 ${parsed.length} items`;
4330
+ const keys = Object.keys(parsed).slice(0, 3).join(", ");
4331
+ return `Object result \u2014 keys: ${keys}`;
4332
+ } catch {
4333
+ return content.slice(0, 120);
4334
+ }
4335
+ }
4336
+
4337
+ // src/react/message-history/strategies/summary-buffer.ts
4338
+ function buildSummaryBufferContext(displayMessages, compactionState, options) {
4339
+ const { recentBuffer } = options;
4340
+ const systemMessages = displayMessages.filter(
4341
+ (m) => m.role === "system" || isCompactionMarker(m)
4342
+ );
4343
+ const conversationMessages = displayMessages.filter(
4344
+ (m) => m.role !== "system" && !isCompactionMarker(m)
4345
+ );
4346
+ const recentStart = Math.max(0, conversationMessages.length - recentBuffer);
4347
+ const recentMessages = conversationMessages.slice(recentStart);
4348
+ const result = [];
4349
+ if (compactionState.workingMemory.length > 0) {
4350
+ result.push({
4351
+ role: "system",
4352
+ content: `[Working memory \u2014 always active]
4353
+ ${compactionState.workingMemory.join("\n")}`
4354
+ });
4355
+ }
4356
+ if (compactionState.rollingSummary) {
4357
+ result.push({
4358
+ role: "system",
4359
+ content: `[Previous conversation summary]
4360
+ ${compactionState.rollingSummary}`
4361
+ });
4362
+ }
4363
+ result.push(...toLLMMessages(systemMessages));
4364
+ result.push(...toLLMMessages(recentMessages));
4365
+ return result;
4366
+ }
4367
+ async function runCompaction(displayMessages, compactionState, options) {
4368
+ const { recentBuffer, compactionUrl, summarizer } = options;
4369
+ const conversationMessages = displayMessages.filter(
4370
+ (m) => m.role !== "system" && !isCompactionMarker(m)
4371
+ );
4372
+ const cutoff = Math.max(0, conversationMessages.length - recentBuffer);
4373
+ const toSummarize = conversationMessages.slice(0, cutoff);
4374
+ if (toSummarize.length === 0) {
4375
+ return { llmMessages: toLLMMessages(displayMessages) };
4376
+ }
4377
+ const llmToSummarize = toLLMMessages(toSummarize);
4378
+ const originalTokens = estimateMessagesTokens(llmToSummarize);
4379
+ let newSummary;
4380
+ if (summarizer) {
4381
+ newSummary = await summarizer(llmToSummarize);
4382
+ } else if (compactionUrl) {
4383
+ newSummary = await fetchSummary(compactionUrl, {
4384
+ messages: llmToSummarize,
4385
+ existingSummary: compactionState.rollingSummary,
4386
+ workingMemory: compactionState.workingMemory
4387
+ });
4388
+ } else {
4389
+ newSummary = buildFallbackSummary(
4390
+ llmToSummarize,
4391
+ compactionState.rollingSummary
4392
+ );
4393
+ }
4394
+ const summaryTokens = Math.ceil(newSummary.length / 3.5);
4395
+ const tokensSaved = Math.max(0, originalTokens - summaryTokens);
4396
+ return {
4397
+ llmMessages: buildSummaryBufferContext(
4398
+ displayMessages,
4399
+ { ...compactionState, rollingSummary: newSummary },
4400
+ options
4401
+ ),
4402
+ newSummary,
4403
+ tokensSaved,
4404
+ messagesSummarized: toSummarize.length
4405
+ };
4406
+ }
4407
+ async function fetchSummary(url, body) {
4408
+ const res = await fetch(url, {
4409
+ method: "POST",
4410
+ headers: { "Content-Type": "application/json" },
4411
+ body: JSON.stringify(body)
4412
+ });
4413
+ if (!res.ok) {
4414
+ throw new Error(`Compaction endpoint returned ${res.status}`);
4415
+ }
4416
+ const data = await res.json();
4417
+ if (!data?.summary) {
4418
+ throw new Error("Compaction endpoint did not return { summary: string }");
4419
+ }
4420
+ return data.summary;
4421
+ }
4422
+ function buildFallbackSummary(messages, existingSummary) {
4423
+ const lines = messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => `${m.role}: ${(m.content ?? "").slice(0, 200)}`).join("\n");
4424
+ return existingSummary ? `${existingSummary}
4425
+
4426
+ [Additional context]
4427
+ ${lines}` : lines;
4428
+ }
4429
+
4430
+ // src/react/message-history/session-persistence.ts
4431
+ var IDB_DB_NAME = "copilot-sdk";
4432
+ var IDB_STORE = "sessions";
4433
+ var IDB_VERSION = 1;
4434
+ function saveCompactionState(storageKey, state) {
4435
+ try {
4436
+ localStorage.setItem(
4437
+ `${storageKey}-state`,
4438
+ JSON.stringify({ ...state, _savedAt: Date.now() })
4439
+ );
4440
+ } catch {
4441
+ }
4442
+ }
4443
+ function loadCompactionState(storageKey) {
4444
+ try {
4445
+ const raw = localStorage.getItem(`${storageKey}-state`);
4446
+ if (!raw) return null;
4447
+ const parsed = JSON.parse(raw);
4448
+ delete parsed._savedAt;
4449
+ return parsed;
4450
+ } catch {
4451
+ return null;
4452
+ }
4453
+ }
4454
+ function clearCompactionState(storageKey) {
4455
+ try {
4456
+ localStorage.removeItem(`${storageKey}-state`);
4457
+ } catch {
4458
+ }
4459
+ }
4460
+ function openDB() {
4461
+ return new Promise((resolve, reject) => {
4462
+ const req = indexedDB.open(IDB_DB_NAME, IDB_VERSION);
4463
+ req.onupgradeneeded = () => {
4464
+ req.result.createObjectStore(IDB_STORE, { keyPath: "sessionId" });
4465
+ };
4466
+ req.onsuccess = () => resolve(req.result);
4467
+ req.onerror = () => reject(req.error);
4468
+ });
4469
+ }
4470
+ async function saveDisplayMessages(storageKey, messages) {
4471
+ try {
4472
+ const db = await openDB();
4473
+ const tx = db.transaction(IDB_STORE, "readwrite");
4474
+ tx.objectStore(IDB_STORE).put({
4475
+ sessionId: storageKey,
4476
+ messages,
4477
+ savedAt: Date.now()
4478
+ });
4479
+ await new Promise((res, rej) => {
4480
+ tx.oncomplete = () => res();
4481
+ tx.onerror = () => rej(tx.error);
4482
+ });
4483
+ db.close();
4484
+ } catch {
4485
+ }
4486
+ }
4487
+ async function loadDisplayMessages(storageKey) {
4488
+ try {
4489
+ const db = await openDB();
4490
+ const tx = db.transaction(IDB_STORE, "readonly");
4491
+ const req = tx.objectStore(IDB_STORE).get(storageKey);
4492
+ const result = await new Promise((res, rej) => {
4493
+ req.onsuccess = () => res(req.result);
4494
+ req.onerror = () => rej(req.error);
4495
+ });
4496
+ db.close();
4497
+ return result?.messages ?? null;
4498
+ } catch {
4499
+ return null;
4500
+ }
4501
+ }
4502
+ async function clearDisplayMessages(storageKey) {
4503
+ try {
4504
+ const db = await openDB();
4505
+ const tx = db.transaction(IDB_STORE, "readwrite");
4506
+ tx.objectStore(IDB_STORE).delete(storageKey);
4507
+ await new Promise((res, rej) => {
4508
+ tx.oncomplete = () => res();
4509
+ tx.onerror = () => rej(tx.error);
4510
+ });
4511
+ db.close();
4512
+ } catch {
4513
+ }
4514
+ }
4515
+ async function clearSession(storageKey) {
4516
+ clearCompactionState(storageKey);
4517
+ await clearDisplayMessages(storageKey);
4518
+ }
4519
+
4520
+ // src/react/message-history/useMessageHistory.ts
4521
+ var DEFAULT_COMPACTION_STATE = {
4522
+ rollingSummary: null,
4523
+ lastCompactionAt: null,
4524
+ compactionCount: 0,
4525
+ totalTokensSaved: 0,
4526
+ workingMemory: [],
4527
+ displayMessageCount: 0,
4528
+ llmMessageCount: 0
4529
+ };
4530
+ function useMessageHistory(options = {}) {
4531
+ const { messages } = useCopilot();
4532
+ const ctx = useMessageHistoryContext();
4533
+ const config = useMemo(
4534
+ () => ({ ...defaultMessageHistoryConfig, ...ctx.config, ...options }),
4535
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4536
+ [
4537
+ ctx.config,
4538
+ options.strategy,
4539
+ options.maxContextTokens,
4540
+ options.recentBuffer,
4541
+ options.compactionThreshold
4542
+ ]
4543
+ );
4544
+ const storageKey = config.storageKey ?? "copilot-session";
4545
+ const strategy = options.skipCompaction ? "none" : config.strategy ?? "none";
4546
+ const [compactionState, setCompactionState] = useState(() => {
4547
+ if (config.persistSession) {
4548
+ return loadCompactionState(storageKey) ?? DEFAULT_COMPACTION_STATE;
4549
+ }
4550
+ return DEFAULT_COMPACTION_STATE;
4551
+ });
4552
+ const displayMessages = useMemo(
4553
+ () => messages.map(toDisplayMessage),
4554
+ [messages]
4555
+ );
4556
+ const restoredRef = useRef(false);
4557
+ useEffect(() => {
4558
+ if (!config.persistSession || restoredRef.current) return;
4559
+ restoredRef.current = true;
4560
+ loadDisplayMessages(storageKey).then((saved) => {
4561
+ if (saved?.length && messages.length === 0) ;
4562
+ });
4563
+ }, [config.persistSession, storageKey, messages.length]);
4564
+ useEffect(() => {
4565
+ if (!config.persistSession || displayMessages.length === 0) return;
4566
+ saveDisplayMessages(storageKey, displayMessages);
4567
+ }, [config.persistSession, storageKey, displayMessages]);
4568
+ const llmMessages = useMemo(() => {
4569
+ const maxTokens = config.maxContextTokens ?? 128e3;
4570
+ const reserve = config.reserveForResponse ?? 4096;
4571
+ const tokenBudget = maxTokens - reserve;
4572
+ const recentBuffer = config.recentBuffer ?? 10;
4573
+ const maxChars = config.toolResultMaxChars ?? 1e4;
4574
+ let result;
4575
+ switch (strategy) {
4576
+ case "sliding-window": {
4577
+ const windowed = applySlidingWindow(displayMessages, {
4578
+ tokenBudget,
4579
+ recentBuffer
4580
+ });
4581
+ result = truncateToolResults(toLLMMessages(windowed), maxChars);
4582
+ break;
4583
+ }
4584
+ case "selective-prune": {
4585
+ result = truncateToolResults(
4586
+ applySelectivePrune(displayMessages, recentBuffer),
4587
+ maxChars
4588
+ );
4589
+ break;
4590
+ }
4591
+ case "summary-buffer": {
4592
+ result = truncateToolResults(
4593
+ buildSummaryBufferContext(displayMessages, compactionState, {
4594
+ recentBuffer,
4595
+ compactionThreshold: config.compactionThreshold ?? 0.75,
4596
+ compactionUrl: config.compactionUrl,
4597
+ summarizer: options.summarizer
4598
+ }),
4599
+ maxChars
4600
+ );
4601
+ break;
4602
+ }
4603
+ default:
4604
+ result = truncateToolResults(toLLMMessages(displayMessages), maxChars);
4605
+ }
4606
+ return result;
4607
+ }, [displayMessages, compactionState, strategy, config, options.summarizer]);
4608
+ const tokenUsage = useMemo(() => {
4609
+ const mode = options.tokenEstimation ?? "fast";
4610
+ const current = estimateTokens2(toLLMMessages(displayMessages), mode);
4611
+ const max = config.maxContextTokens ?? 128e3;
4612
+ const threshold = config.compactionThreshold ?? 0.75;
4613
+ const percentage = current / max;
4614
+ return { current, max, percentage, isApproaching: percentage >= threshold };
4615
+ }, [
4616
+ displayMessages,
4617
+ config.maxContextTokens,
4618
+ config.compactionThreshold,
4619
+ options.tokenEstimation
4620
+ ]);
4621
+ useEffect(() => {
4622
+ if (config.onTokenUsage && tokenUsage.current > 0) {
4623
+ config.onTokenUsage(tokenUsage);
4624
+ }
4625
+ }, [tokenUsage, config.onTokenUsage]);
4626
+ useEffect(() => {
4627
+ if (config.persistSession) {
4628
+ saveCompactionState(storageKey, {
4629
+ ...compactionState,
4630
+ displayMessageCount: displayMessages.length,
4631
+ llmMessageCount: llmMessages.length
4632
+ });
4633
+ }
4634
+ }, [
4635
+ config.persistSession,
4636
+ storageKey,
4637
+ compactionState,
4638
+ displayMessages.length,
4639
+ llmMessages.length
4640
+ ]);
4641
+ const isCompactingRef = useRef(false);
4642
+ const [isCompacting, setIsCompacting] = useState(false);
4643
+ useEffect(() => {
4644
+ if (strategy !== "summary-buffer" || options.skipCompaction || isCompactingRef.current || !tokenUsage.isApproaching)
4645
+ return;
4646
+ isCompactingRef.current = true;
4647
+ setIsCompacting(true);
4648
+ runCompaction(displayMessages, compactionState, {
4649
+ recentBuffer: config.recentBuffer ?? 10,
4650
+ tokenBudget: (config.maxContextTokens ?? 128e3) - (config.reserveForResponse ?? 4096),
4651
+ compactionThreshold: config.compactionThreshold ?? 0.75,
4652
+ compactionUrl: config.compactionUrl,
4653
+ summarizer: options.summarizer
4654
+ }).then((result) => {
4655
+ if (result.newSummary) {
4656
+ const event = {
4657
+ type: "auto",
4658
+ compactionCount: compactionState.compactionCount + 1,
4659
+ messagesSummarized: result.messagesSummarized ?? 0,
4660
+ tokensSaved: result.tokensSaved ?? 0,
4661
+ timestamp: Date.now()
4662
+ };
4663
+ setCompactionState((prev) => ({
4664
+ ...prev,
4665
+ rollingSummary: result.newSummary,
4666
+ lastCompactionAt: Date.now(),
4667
+ compactionCount: prev.compactionCount + 1,
4668
+ totalTokensSaved: prev.totalTokensSaved + (result.tokensSaved ?? 0)
4669
+ }));
4670
+ config.onCompaction?.(event);
4671
+ }
4672
+ }).finally(() => {
4673
+ isCompactingRef.current = false;
4674
+ setIsCompacting(false);
4675
+ });
4676
+ }, [tokenUsage.isApproaching, strategy]);
4677
+ const compactSession = useCallback(
4678
+ async (instructions) => {
4679
+ if (strategy !== "summary-buffer") return;
4680
+ const result = await runCompaction(displayMessages, compactionState, {
4681
+ recentBuffer: config.recentBuffer ?? 10,
4682
+ tokenBudget: (config.maxContextTokens ?? 128e3) - (config.reserveForResponse ?? 4096),
4683
+ compactionThreshold: config.compactionThreshold ?? 0.75,
4684
+ compactionUrl: config.compactionUrl,
4685
+ summarizer: options.summarizer ? (msgs) => options.summarizer(msgs) : instructions ? (msgs) => fetchWithInstructions(config.compactionUrl, msgs, instructions) : void 0
4686
+ });
4687
+ if (result.newSummary) {
4688
+ const event = {
4689
+ type: "manual",
4690
+ compactionCount: compactionState.compactionCount + 1,
4691
+ messagesSummarized: result.messagesSummarized ?? 0,
4692
+ tokensSaved: result.tokensSaved ?? 0,
4693
+ timestamp: Date.now()
4694
+ };
4695
+ setCompactionState((prev) => ({
4696
+ ...prev,
4697
+ rollingSummary: result.newSummary,
4698
+ lastCompactionAt: Date.now(),
4699
+ compactionCount: prev.compactionCount + 1,
4700
+ totalTokensSaved: prev.totalTokensSaved + (result.tokensSaved ?? 0)
4701
+ }));
4702
+ config.onCompaction?.(event);
4703
+ }
4704
+ },
4705
+ [displayMessages, compactionState, config, strategy, options.summarizer]
4706
+ );
4707
+ const addToWorkingMemory = useCallback((fact) => {
4708
+ setCompactionState((prev) => ({
4709
+ ...prev,
4710
+ workingMemory: [...prev.workingMemory, fact]
4711
+ }));
4712
+ }, []);
4713
+ const clearWorkingMemory = useCallback(() => {
4714
+ setCompactionState((prev) => ({ ...prev, workingMemory: [] }));
4715
+ }, []);
4716
+ const resetSession = useCallback(async () => {
4717
+ setCompactionState(DEFAULT_COMPACTION_STATE);
4718
+ if (config.persistSession) {
4719
+ await clearSession(storageKey);
4720
+ }
4721
+ }, [config.persistSession, storageKey]);
4722
+ return {
4723
+ displayMessages,
4724
+ llmMessages,
4725
+ tokenUsage,
4726
+ isCompacting,
4727
+ compactionState: {
4728
+ ...compactionState,
4729
+ displayMessageCount: displayMessages.length,
4730
+ llmMessageCount: llmMessages.length
4731
+ },
4732
+ compactSession,
4733
+ addToWorkingMemory,
4734
+ clearWorkingMemory,
4735
+ resetSession
4736
+ };
4737
+ }
4738
+ async function fetchWithInstructions(url, messages, instructions) {
4739
+ const res = await fetch(url, {
4740
+ method: "POST",
4741
+ headers: { "Content-Type": "application/json" },
4742
+ body: JSON.stringify({ messages, instructions })
4743
+ });
4744
+ const data = await res.json();
4745
+ return data.summary;
4746
+ }
4747
+ var SkillContext = createContext(null);
4748
+ function useSkillContext() {
4749
+ const ctx = useContext(SkillContext);
4750
+ if (!ctx) {
4751
+ throw new Error("useSkillContext must be used within <SkillProvider>");
4752
+ }
4753
+ return ctx;
4754
+ }
4755
+ function useAIContext(item) {
4756
+ const { addContext, removeContext } = useCopilot();
4757
+ const contextIdRef = useRef(null);
4758
+ const serializedData = typeof item.data === "string" ? item.data : JSON.stringify(item.data);
4759
+ useEffect(() => {
4760
+ const formattedValue = typeof item.data === "string" ? item.data : JSON.stringify(item.data, null, 2);
4761
+ const contextString = item.description ? `${item.description}:
4762
+ ${formattedValue}` : `${item.key}:
4763
+ ${formattedValue}`;
4764
+ contextIdRef.current = addContext(contextString, item.parentId);
4765
+ return () => {
4766
+ if (contextIdRef.current) {
4767
+ removeContext(contextIdRef.current);
4768
+ contextIdRef.current = null;
4769
+ }
4770
+ };
4771
+ }, [
4772
+ item.key,
4773
+ serializedData,
4774
+ item.description,
4775
+ item.parentId,
4776
+ addContext,
4777
+ removeContext
4778
+ ]);
4779
+ return contextIdRef.current ?? void 0;
4780
+ }
4781
+ function useAIContexts(items) {
4782
+ const { addContext, removeContext } = useCopilot();
4783
+ const contextIdsRef = useRef([]);
4784
+ const serializedItems = JSON.stringify(
4785
+ items.map((item) => ({
4786
+ key: item.key,
4787
+ data: item.data,
4788
+ description: item.description,
4789
+ parentId: item.parentId
4790
+ }))
4791
+ );
4792
+ useEffect(() => {
4793
+ contextIdsRef.current.forEach((id) => removeContext(id));
4794
+ contextIdsRef.current = [];
4795
+ const parsedItems = JSON.parse(serializedItems);
4796
+ for (const item of parsedItems) {
4797
+ const formattedValue = typeof item.data === "string" ? item.data : JSON.stringify(item.data, null, 2);
4798
+ const contextString = item.description ? `${item.description}:
4799
+ ${formattedValue}` : `${item.key}:
4800
+ ${formattedValue}`;
4801
+ const id = addContext(contextString, item.parentId);
4802
+ contextIdsRef.current.push(id);
4803
+ }
4804
+ return () => {
4805
+ contextIdsRef.current.forEach((id) => removeContext(id));
4806
+ contextIdsRef.current = [];
4807
+ };
4808
+ }, [serializedItems, addContext, removeContext]);
4809
+ }
4810
+ function isZodSchema(value) {
4811
+ if (value === null || typeof value !== "object") return false;
4812
+ const obj = value;
4813
+ return "_def" in obj && typeof obj._def === "object" || "_zod" in obj && typeof obj._zod === "object" || "~standard" in obj;
4814
+ }
4815
+ function useTool(config, dependencies = []) {
4816
+ const { registerTool, unregisterTool } = useCopilot();
4817
+ const configRef = useRef(config);
4818
+ configRef.current = config;
4819
+ const inputSchema = useMemo(() => {
4820
+ if (isZodSchema(config.inputSchema)) {
4821
+ return zodToJsonSchema(config.inputSchema);
4822
+ }
4823
+ return config.inputSchema;
4824
+ }, [config.inputSchema]);
4825
+ useEffect(() => {
4826
+ const tool2 = {
4827
+ name: config.name,
4828
+ description: config.description,
4829
+ location: "client",
4830
+ inputSchema,
4831
+ handler: async (params, context) => {
4832
+ return configRef.current.handler(params, context);
4833
+ },
4834
+ render: config.render,
4835
+ available: config.available ?? true,
4836
+ needsApproval: config.needsApproval,
4837
+ approvalMessage: config.approvalMessage,
4838
+ hidden: config.hidden,
4839
+ deferLoading: config.deferLoading,
4840
+ profiles: config.profiles,
4841
+ searchKeywords: config.searchKeywords,
4842
+ group: config.group,
4843
+ category: config.category,
4844
+ resultConfig: config.resultConfig,
4845
+ title: config.title,
4846
+ executingTitle: config.executingTitle,
4847
+ completedTitle: config.completedTitle,
4848
+ aiResponseMode: config.aiResponseMode,
4849
+ aiContext: config.aiContext
4850
+ };
4851
+ registerTool(tool2);
4852
+ return () => {
4853
+ unregisterTool(config.name);
4854
+ };
4855
+ }, [config.name, inputSchema, ...dependencies]);
4856
+ }
4857
+ function useTools(tools) {
4858
+ const { registerTool, unregisterTool } = useCopilot();
4859
+ const registeredToolsRef = useRef([]);
4860
+ const toolsRef = useRef(tools);
4861
+ toolsRef.current = tools;
4862
+ const toolsKey = Object.keys(tools).sort().join(",");
4863
+ useEffect(() => {
4864
+ const currentTools = toolsRef.current;
4865
+ const toolNames = [];
4866
+ for (const [name, toolDef] of Object.entries(currentTools)) {
4867
+ const fullTool = {
4868
+ ...toolDef,
4869
+ name
4870
+ // Use the key as the name
4871
+ };
4872
+ registerTool(fullTool);
4873
+ toolNames.push(name);
4874
+ }
4875
+ registeredToolsRef.current = toolNames;
4876
+ return () => {
4877
+ for (const name of registeredToolsRef.current) {
4878
+ unregisterTool(name);
4879
+ }
4880
+ registeredToolsRef.current = [];
4881
+ };
4882
+ }, [toolsKey]);
4883
+ }
4884
+ function SkillContextInjector({
4885
+ registry,
4886
+ skills
4887
+ }) {
4888
+ const catalog = useMemo(() => registry.buildCatalog(), [skills]);
4889
+ const eagerContent = useMemo(() => registry.buildEagerContent(), [skills]);
4890
+ useAIContext({
4891
+ key: "__skill_catalog__",
4892
+ description: "Skills the AI can load on demand",
4893
+ data: catalog ? `You have access to specialized skills. Call load_skill({ name }) when relevant.
4894
+
4895
+ ${catalog}` : ""
4896
+ });
4897
+ useAIContext({
4898
+ key: "__skill_eager__",
4899
+ description: "Always-active skill instructions",
4900
+ data: eagerContent
4901
+ });
4902
+ return null;
4903
+ }
4904
+ function SkillRequestSync({ skills }) {
4905
+ const { setInlineSkills } = useCopilot();
4906
+ useEffect(() => {
4907
+ const inlineSkills = skills.filter((s) => s.source.type === "inline").map((s) => ({
4908
+ name: s.name,
4909
+ description: s.description,
4910
+ content: s.content,
4911
+ strategy: s.strategy
4912
+ }));
4913
+ setInlineSkills(inlineSkills);
4914
+ }, [skills, setInlineSkills]);
4915
+ return null;
4916
+ }
4917
+ function SkillToolRegistrar({
4918
+ registry,
4919
+ skills
4920
+ }) {
4921
+ useTool(
4922
+ {
4923
+ name: "load_skill",
4924
+ description: "Load a skill by name to get full instructions for a specialized task.",
4925
+ inputSchema: {
4926
+ type: "object",
4927
+ properties: {
4928
+ name: {
4929
+ type: "string",
4930
+ description: "The name of the skill to load."
4931
+ }
4932
+ },
4933
+ required: ["name"]
4934
+ },
4935
+ handler: async ({ name }) => {
4936
+ const skill = registry.get(name);
4937
+ if (!skill) {
4938
+ const available = registry.getAuto().map((s) => s.name).join(", ") || "none";
4939
+ return {
4940
+ success: false,
4941
+ error: `Skill "${name}" not found. Available skills: ${available}`
4942
+ };
4943
+ }
4944
+ const sourceTypeMap = {
4945
+ inline: "client-inline",
4946
+ url: "remote-url",
4947
+ file: "server-dir"
4948
+ };
4949
+ return {
4950
+ success: true,
4951
+ name: skill.name,
4952
+ description: skill.description,
4953
+ strategy: skill.strategy ?? "auto",
4954
+ content: skill.content,
4955
+ source: sourceTypeMap[skill.source.type]
4956
+ };
4957
+ }
4958
+ },
4959
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4960
+ [skills]
4961
+ );
4962
+ return null;
4963
+ }
4964
+ function SkillProvider({
4965
+ children,
4966
+ skills: skillsProp
4967
+ }) {
4968
+ const registryRef = useRef(null);
4969
+ if (registryRef.current === null) {
4970
+ registryRef.current = new SkillRegistry();
4971
+ }
4972
+ const registry = registryRef.current;
4973
+ const [skills, setSkills] = useState([]);
4974
+ useEffect(() => {
4975
+ if (!skillsProp?.length) return;
4976
+ for (const def of skillsProp) {
4977
+ if (def.source.type !== "inline") {
4978
+ console.warn(
4979
+ `[copilot-sdk/skills] Client-side SkillProvider only supports inline skills. Skill "${def.name}" has source type "${def.source.type}" and will be skipped. Use loadSkills() on the server for file/url skills.`
4980
+ );
4981
+ continue;
2508
4982
  }
4983
+ const resolved = {
4984
+ ...def,
4985
+ content: def.source.content
4986
+ };
4987
+ registry.register(resolved);
2509
4988
  }
4989
+ setSkills(registry.getAll());
2510
4990
  return () => {
2511
- for (const toolName of registeredToolsRef.current) {
2512
- unregisterTool(toolName);
4991
+ for (const def of skillsProp ?? []) {
4992
+ registry.unregister(def.name);
2513
4993
  }
2514
- registeredToolsRef.current = [];
4994
+ setSkills(registry.getAll());
2515
4995
  };
2516
- }, [
2517
- autoRegister,
2518
- mcpClient.isConnected,
2519
- toolDefinitions,
2520
- registerTool,
2521
- unregisterTool
2522
- ]);
2523
- return {
2524
- ...mcpClient,
2525
- toolDefinitions
2526
- };
4996
+ }, [skillsProp]);
4997
+ const register = useCallback((skill) => {
4998
+ registry.register(skill);
4999
+ setSkills(registry.getAll());
5000
+ }, []);
5001
+ const unregister = useCallback((name) => {
5002
+ registry.unregister(name);
5003
+ setSkills(registry.getAll());
5004
+ }, []);
5005
+ const contextValue = useMemo(
5006
+ () => ({ registry, register, unregister, skills }),
5007
+ [register, unregister, skills]
5008
+ );
5009
+ return /* @__PURE__ */ jsxs(SkillContext.Provider, { value: contextValue, children: [
5010
+ /* @__PURE__ */ jsx(SkillContextInjector, { registry, skills }),
5011
+ /* @__PURE__ */ jsx(SkillToolRegistrar, { registry, skills }),
5012
+ /* @__PURE__ */ jsx(SkillRequestSync, { skills }),
5013
+ children
5014
+ ] });
2527
5015
  }
2528
5016
  function MCPConnection({ config }) {
2529
5017
  useMCPTools({
@@ -2537,6 +5025,123 @@ function MCPConnection({ config }) {
2537
5025
  });
2538
5026
  return null;
2539
5027
  }
5028
+ var COMPACTING_MARKER_ID = "__compacting-in-progress__";
5029
+ function MessageHistoryBridge({
5030
+ chatRef
5031
+ }) {
5032
+ const { compactionState, tokenUsage } = useMessageHistory();
5033
+ const ctx = useMessageHistoryContext();
5034
+ const loaderAddedRef = useRef(false);
5035
+ const prevCompactionCountRef = useRef(compactionState.compactionCount);
5036
+ useEffect(() => {
5037
+ if (!tokenUsage.isApproaching) {
5038
+ loaderAddedRef.current = false;
5039
+ return;
5040
+ }
5041
+ if (loaderAddedRef.current) return;
5042
+ const chat = chatRef.current;
5043
+ if (!chat) return;
5044
+ const alreadyAdded = chat.messages.some(
5045
+ (m) => m.id === COMPACTING_MARKER_ID
5046
+ );
5047
+ if (alreadyAdded) return;
5048
+ loaderAddedRef.current = true;
5049
+ const loading = {
5050
+ id: COMPACTING_MARKER_ID,
5051
+ role: "system",
5052
+ content: "Compacting conversation\u2026",
5053
+ createdAt: /* @__PURE__ */ new Date(),
5054
+ metadata: { type: "compaction-marker", compacting: true }
5055
+ };
5056
+ chat.setMessages([...chat.messages, loading]);
5057
+ }, [tokenUsage.isApproaching]);
5058
+ useEffect(() => {
5059
+ if (compactionState.compactionCount <= prevCompactionCountRef.current)
5060
+ return;
5061
+ prevCompactionCountRef.current = compactionState.compactionCount;
5062
+ loaderAddedRef.current = false;
5063
+ const chat = chatRef.current;
5064
+ if (!chat) return;
5065
+ const hasLoader = chat.messages.some((m) => m.id === COMPACTING_MARKER_ID);
5066
+ const base = hasLoader ? chat.messages.map(
5067
+ (m) => m.id === COMPACTING_MARKER_ID ? {
5068
+ ...m,
5069
+ id: `compaction-marker-${compactionState.compactionCount}`,
5070
+ content: `Conversation compacted \u2014 context window refreshed`,
5071
+ metadata: { type: "compaction-marker", compacting: false }
5072
+ } : m
5073
+ ) : [
5074
+ ...chat.messages,
5075
+ {
5076
+ id: `compaction-marker-${compactionState.compactionCount}`,
5077
+ role: "system",
5078
+ content: `Conversation compacted \u2014 context window refreshed`,
5079
+ createdAt: /* @__PURE__ */ new Date(),
5080
+ metadata: { type: "compaction-marker", compacting: false }
5081
+ }
5082
+ ];
5083
+ chat.setMessages(base);
5084
+ }, [compactionState.compactionCount]);
5085
+ const compactionStateRef = useRef(compactionState);
5086
+ compactionStateRef.current = compactionState;
5087
+ const configRef = useRef(ctx.config);
5088
+ configRef.current = ctx.config;
5089
+ useEffect(() => {
5090
+ const chat = chatRef.current;
5091
+ if (!chat) return;
5092
+ chat.setRequestMessageTransform((allMessages) => {
5093
+ if (allMessages.length === 0) return allMessages;
5094
+ let lastUserIdx = -1;
5095
+ for (let i = allMessages.length - 1; i >= 0; i--) {
5096
+ if (allMessages[i].role === "user") {
5097
+ lastUserIdx = i;
5098
+ break;
5099
+ }
5100
+ }
5101
+ if (lastUserIdx === -1) return allMessages;
5102
+ const historyMessages = allMessages.slice(0, lastUserIdx);
5103
+ const currentTurn = allMessages.slice(lastUserIdx);
5104
+ if (historyMessages.length === 0) return allMessages;
5105
+ const cfg = configRef.current;
5106
+ const cs = compactionStateRef.current;
5107
+ const recentBuffer = cfg.recentBuffer ?? 10;
5108
+ const isCompactionMsg = (m) => m.metadata?.["type"] === "compaction-marker";
5109
+ const windowedHistory = [];
5110
+ if (cs.workingMemory.length > 0) {
5111
+ windowedHistory.push({
5112
+ id: "working-memory",
5113
+ role: "system",
5114
+ content: `[Working memory \u2014 always active]
5115
+ ${cs.workingMemory.join("\n")}`,
5116
+ createdAt: /* @__PURE__ */ new Date()
5117
+ });
5118
+ }
5119
+ if (cs.rollingSummary) {
5120
+ windowedHistory.push({
5121
+ id: "rolling-summary",
5122
+ role: "system",
5123
+ content: `[Previous conversation summary]
5124
+ ${cs.rollingSummary}`,
5125
+ createdAt: /* @__PURE__ */ new Date()
5126
+ });
5127
+ }
5128
+ const systemMsgs = historyMessages.filter(
5129
+ (m) => m.role === "system" && !isCompactionMsg(m)
5130
+ );
5131
+ windowedHistory.push(...systemMsgs);
5132
+ const conversationMsgs = historyMessages.filter(
5133
+ (m) => m.role !== "system"
5134
+ );
5135
+ const recentStart = Math.max(0, conversationMsgs.length - recentBuffer);
5136
+ windowedHistory.push(...conversationMsgs.slice(recentStart));
5137
+ return [...windowedHistory, ...currentTurn];
5138
+ });
5139
+ return () => {
5140
+ chatRef.current?.setRequestMessageTransform(null);
5141
+ };
5142
+ }, []);
5143
+ return null;
5144
+ }
2540
5145
  var CopilotContext = createContext(null);
2541
5146
  function useCopilot() {
2542
5147
  const context = useContext(CopilotContext);
@@ -2560,11 +5165,14 @@ function CopilotProvider({
2560
5165
  debug = false,
2561
5166
  maxIterations,
2562
5167
  maxIterationsMessage,
2563
- mcpServers
5168
+ mcpServers,
5169
+ optimization,
5170
+ messageHistory,
5171
+ skills
2564
5172
  }) {
2565
5173
  const debugLog = useCallback(
2566
- (...args) => {
2567
- if (debug) console.log("[Copilot SDK]", ...args);
5174
+ (action, data) => {
5175
+ createLogger("provider", () => debug ?? false)(action, data);
2568
5176
  },
2569
5177
  [debug]
2570
5178
  );
@@ -2604,7 +5212,8 @@ function CopilotProvider({
2604
5212
  body,
2605
5213
  debug,
2606
5214
  maxIterations,
2607
- maxIterationsMessage
5215
+ maxIterationsMessage,
5216
+ optimization
2608
5217
  },
2609
5218
  {
2610
5219
  onToolExecutionsChange: (executions) => {
@@ -2614,6 +5223,9 @@ function CopilotProvider({
2614
5223
  onApprovalRequired: (execution) => {
2615
5224
  debugLog("Tool approval required:", execution.name);
2616
5225
  },
5226
+ onContextUsageChange: (usage) => {
5227
+ setContextUsage(usage);
5228
+ },
2617
5229
  onError: (error2) => {
2618
5230
  if (error2) onError?.(error2);
2619
5231
  }
@@ -2644,19 +5256,27 @@ function CopilotProvider({
2644
5256
  debugLog("URL config updated from prop");
2645
5257
  }
2646
5258
  }, [runtimeUrl, debugLog]);
5259
+ const EMPTY_MESSAGES = useRef([]);
5260
+ const getMessagesSnapshot = useCallback(() => chatRef.current.messages, []);
5261
+ const getServerMessagesSnapshot = useCallback(
5262
+ () => EMPTY_MESSAGES.current,
5263
+ []
5264
+ );
5265
+ const getStatusSnapshot = useCallback(() => chatRef.current.status, []);
5266
+ const getErrorSnapshot = useCallback(() => chatRef.current.error, []);
2647
5267
  const messages = useSyncExternalStore(
2648
5268
  chatRef.current.subscribe,
2649
- () => chatRef.current.messages,
2650
- () => chatRef.current.messages
5269
+ getMessagesSnapshot,
5270
+ getServerMessagesSnapshot
2651
5271
  );
2652
5272
  const status = useSyncExternalStore(
2653
5273
  chatRef.current.subscribe,
2654
- () => chatRef.current.status,
5274
+ getStatusSnapshot,
2655
5275
  () => "ready"
2656
5276
  );
2657
5277
  const errorFromChat = useSyncExternalStore(
2658
5278
  chatRef.current.subscribe,
2659
- () => chatRef.current.error,
5279
+ getErrorSnapshot,
2660
5280
  () => void 0
2661
5281
  );
2662
5282
  const error = errorFromChat ?? null;
@@ -2679,9 +5299,15 @@ function CopilotProvider({
2679
5299
  },
2680
5300
  []
2681
5301
  );
2682
- const registeredTools = chatRef.current?.tools ?? [];
2683
- const pendingApprovals = toolExecutions.filter(
2684
- (e) => e.approvalStatus === "required"
5302
+ const registeredTools = useMemo(
5303
+ () => chatRef.current?.tools ?? [],
5304
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5305
+ [toolExecutions]
5306
+ // re-derive when tool executions change (tools change alongside)
5307
+ );
5308
+ const pendingApprovals = useMemo(
5309
+ () => toolExecutions.filter((e) => e.approvalStatus === "required"),
5310
+ [toolExecutions]
2685
5311
  );
2686
5312
  const actionsRef = useRef(/* @__PURE__ */ new Map());
2687
5313
  const [actionsVersion, setActionsVersion] = useState(0);
@@ -2699,6 +5325,8 @@ function CopilotProvider({
2699
5325
  );
2700
5326
  const contextTreeRef = useRef([]);
2701
5327
  const contextIdCounter = useRef(0);
5328
+ const [contextChars, setContextChars] = useState(0);
5329
+ const [contextUsage, setContextUsage] = useState(null);
2702
5330
  const addContext = useCallback(
2703
5331
  (context, parentId) => {
2704
5332
  const id = `ctx-${++contextIdCounter.current}`;
@@ -2709,6 +5337,7 @@ function CopilotProvider({
2709
5337
  );
2710
5338
  const contextString = printTree(contextTreeRef.current);
2711
5339
  chatRef.current?.setContext(contextString);
5340
+ setContextChars(contextString.length);
2712
5341
  debugLog("Context added:", id);
2713
5342
  return id;
2714
5343
  },
@@ -2719,6 +5348,7 @@ function CopilotProvider({
2719
5348
  contextTreeRef.current = removeNode(contextTreeRef.current, id);
2720
5349
  const contextString = printTree(contextTreeRef.current);
2721
5350
  chatRef.current?.setContext(contextString);
5351
+ setContextChars(contextString.length);
2722
5352
  debugLog("Context removed:", id);
2723
5353
  },
2724
5354
  [debugLog]
@@ -2730,6 +5360,13 @@ function CopilotProvider({
2730
5360
  },
2731
5361
  [debugLog]
2732
5362
  );
5363
+ const setInlineSkills = useCallback(
5364
+ (skills2) => {
5365
+ chatRef.current?.setInlineSkills(skills2);
5366
+ debugLog("Inline skills updated", { count: skills2.length });
5367
+ },
5368
+ [debugLog]
5369
+ );
2733
5370
  const sendMessage = useCallback(
2734
5371
  async (content, attachments) => {
2735
5372
  debugLog("Sending message:", content);
@@ -2749,15 +5386,46 @@ function CopilotProvider({
2749
5386
  const regenerate = useCallback(async (messageId) => {
2750
5387
  await chatRef.current?.regenerate(messageId);
2751
5388
  }, []);
5389
+ const switchBranch = useCallback((messageId) => {
5390
+ chatRef.current?.switchBranch(messageId);
5391
+ }, []);
5392
+ const getBranchInfo = useCallback(
5393
+ (messageId) => chatRef.current?.getBranchInfo(messageId) ?? null,
5394
+ []
5395
+ );
5396
+ const editMessage = useCallback(
5397
+ async (messageId, newContent) => {
5398
+ await chatRef.current?.sendMessage(newContent, void 0, {
5399
+ editMessageId: messageId
5400
+ });
5401
+ },
5402
+ []
5403
+ );
5404
+ const getHasBranchesSnapshot = useCallback(
5405
+ () => chatRef.current.hasBranches,
5406
+ []
5407
+ );
5408
+ const hasBranches = useSyncExternalStore(
5409
+ chatRef.current.subscribe,
5410
+ getHasBranchesSnapshot,
5411
+ () => false
5412
+ );
5413
+ const getAllMessages = useCallback(
5414
+ () => chatRef.current?.getAllMessages?.() ?? [],
5415
+ []
5416
+ );
2752
5417
  useEffect(() => {
2753
5418
  if (onMessagesChange && messages.length > 0) {
2754
- const coreMessages = messages.map((m) => ({
5419
+ const allUIMessages = chatRef.current?.getAllMessages?.() ?? messages;
5420
+ const coreMessages = allUIMessages.map((m) => ({
2755
5421
  id: m.id,
2756
5422
  role: m.role,
2757
5423
  content: m.content,
2758
5424
  created_at: m.createdAt,
2759
5425
  tool_calls: m.toolCalls,
2760
5426
  tool_call_id: m.toolCallId,
5427
+ parent_id: m.parentId,
5428
+ children_ids: m.childrenIds,
2761
5429
  metadata: {
2762
5430
  attachments: m.attachments,
2763
5431
  thinking: m.thinking
@@ -2789,6 +5457,12 @@ function CopilotProvider({
2789
5457
  clearMessages,
2790
5458
  setMessages,
2791
5459
  regenerate,
5460
+ // Branching
5461
+ switchBranch,
5462
+ getBranchInfo,
5463
+ editMessage,
5464
+ hasBranches,
5465
+ getAllMessages,
2792
5466
  // Tool execution
2793
5467
  registerTool,
2794
5468
  unregisterTool,
@@ -2804,8 +5478,12 @@ function CopilotProvider({
2804
5478
  // AI Context
2805
5479
  addContext,
2806
5480
  removeContext,
5481
+ contextChars,
5482
+ contextUsage,
2807
5483
  // System Prompt
2808
5484
  setSystemPrompt,
5485
+ // Skills
5486
+ setInlineSkills,
2809
5487
  // Config
2810
5488
  threadId,
2811
5489
  runtimeUrl,
@@ -2821,6 +5499,11 @@ function CopilotProvider({
2821
5499
  clearMessages,
2822
5500
  setMessages,
2823
5501
  regenerate,
5502
+ switchBranch,
5503
+ getBranchInfo,
5504
+ editMessage,
5505
+ hasBranches,
5506
+ getAllMessages,
2824
5507
  registerTool,
2825
5508
  unregisterTool,
2826
5509
  registeredTools,
@@ -2833,16 +5516,41 @@ function CopilotProvider({
2833
5516
  registeredActions,
2834
5517
  addContext,
2835
5518
  removeContext,
5519
+ contextChars,
5520
+ contextUsage,
2836
5521
  setSystemPrompt,
5522
+ setInlineSkills,
2837
5523
  threadId,
2838
5524
  runtimeUrl,
2839
5525
  toolsConfig
2840
5526
  ]
2841
5527
  );
2842
- return /* @__PURE__ */ jsxs(CopilotContext.Provider, { value: contextValue, children: [
5528
+ const messageHistoryContextValue = React2.useMemo(
5529
+ () => ({
5530
+ config: { ...defaultMessageHistoryConfig, ...messageHistory },
5531
+ tokenUsage: {
5532
+ current: 0,
5533
+ max: messageHistory?.maxContextTokens ?? 128e3,
5534
+ percentage: 0,
5535
+ isApproaching: false
5536
+ },
5537
+ compactionState: {
5538
+ rollingSummary: null,
5539
+ lastCompactionAt: null,
5540
+ compactionCount: 0,
5541
+ totalTokensSaved: 0,
5542
+ workingMemory: [],
5543
+ displayMessageCount: 0,
5544
+ llmMessageCount: 0
5545
+ }
5546
+ }),
5547
+ [messageHistory]
5548
+ );
5549
+ return /* @__PURE__ */ jsx(MessageHistoryContext.Provider, { value: messageHistoryContextValue, children: /* @__PURE__ */ jsxs(CopilotContext.Provider, { value: contextValue, children: [
2843
5550
  mcpServers?.map((config) => /* @__PURE__ */ jsx(MCPConnection, { config }, config.name)),
2844
- children
2845
- ] });
5551
+ messageHistory?.strategy && messageHistory.strategy !== "none" && /* @__PURE__ */ jsx(MessageHistoryBridge, { chatRef }),
5552
+ skills ? /* @__PURE__ */ jsx(SkillProvider, { skills, children }) : children
5553
+ ] }) });
2846
5554
  }
2847
5555
  function useAIActions(actions) {
2848
5556
  const { registerAction, unregisterAction } = useCopilot();
@@ -2860,61 +5568,6 @@ function useAIActions(actions) {
2860
5568
  function useAIAction(action) {
2861
5569
  useAIActions([action]);
2862
5570
  }
2863
- function useAIContext(item) {
2864
- const { addContext, removeContext } = useCopilot();
2865
- const contextIdRef = useRef(null);
2866
- const serializedData = typeof item.data === "string" ? item.data : JSON.stringify(item.data);
2867
- useEffect(() => {
2868
- const formattedValue = typeof item.data === "string" ? item.data : JSON.stringify(item.data, null, 2);
2869
- const contextString = item.description ? `${item.description}:
2870
- ${formattedValue}` : `${item.key}:
2871
- ${formattedValue}`;
2872
- contextIdRef.current = addContext(contextString, item.parentId);
2873
- return () => {
2874
- if (contextIdRef.current) {
2875
- removeContext(contextIdRef.current);
2876
- contextIdRef.current = null;
2877
- }
2878
- };
2879
- }, [
2880
- item.key,
2881
- serializedData,
2882
- item.description,
2883
- item.parentId,
2884
- addContext,
2885
- removeContext
2886
- ]);
2887
- return contextIdRef.current ?? void 0;
2888
- }
2889
- function useAIContexts(items) {
2890
- const { addContext, removeContext } = useCopilot();
2891
- const contextIdsRef = useRef([]);
2892
- const serializedItems = JSON.stringify(
2893
- items.map((item) => ({
2894
- key: item.key,
2895
- data: item.data,
2896
- description: item.description,
2897
- parentId: item.parentId
2898
- }))
2899
- );
2900
- useEffect(() => {
2901
- contextIdsRef.current.forEach((id) => removeContext(id));
2902
- contextIdsRef.current = [];
2903
- const parsedItems = JSON.parse(serializedItems);
2904
- for (const item of parsedItems) {
2905
- const formattedValue = typeof item.data === "string" ? item.data : JSON.stringify(item.data, null, 2);
2906
- const contextString = item.description ? `${item.description}:
2907
- ${formattedValue}` : `${item.key}:
2908
- ${formattedValue}`;
2909
- const id = addContext(contextString, item.parentId);
2910
- contextIdsRef.current.push(id);
2911
- }
2912
- return () => {
2913
- contextIdsRef.current.forEach((id) => removeContext(id));
2914
- contextIdsRef.current = [];
2915
- };
2916
- }, [serializedItems, addContext, removeContext]);
2917
- }
2918
5571
  function useAITools(options = {}) {
2919
5572
  const {
2920
5573
  screenshot = false,
@@ -3159,58 +5812,6 @@ function useAITools(options = {}) {
3159
5812
  ]
3160
5813
  );
3161
5814
  }
3162
- function useTool(config, dependencies = []) {
3163
- const { registerTool, unregisterTool } = useCopilot();
3164
- const configRef = useRef(config);
3165
- configRef.current = config;
3166
- useEffect(() => {
3167
- const tool2 = {
3168
- name: config.name,
3169
- description: config.description,
3170
- location: "client",
3171
- inputSchema: config.inputSchema,
3172
- handler: async (params, context) => {
3173
- return configRef.current.handler(params, context);
3174
- },
3175
- render: config.render,
3176
- available: config.available ?? true,
3177
- needsApproval: config.needsApproval,
3178
- approvalMessage: config.approvalMessage,
3179
- hidden: config.hidden
3180
- };
3181
- registerTool(tool2);
3182
- return () => {
3183
- unregisterTool(config.name);
3184
- };
3185
- }, [config.name, ...dependencies]);
3186
- }
3187
- function useTools(tools) {
3188
- const { registerTool, unregisterTool } = useCopilot();
3189
- const registeredToolsRef = useRef([]);
3190
- const toolsRef = useRef(tools);
3191
- toolsRef.current = tools;
3192
- const toolsKey = Object.keys(tools).sort().join(",");
3193
- useEffect(() => {
3194
- const currentTools = toolsRef.current;
3195
- const toolNames = [];
3196
- for (const [name, toolDef] of Object.entries(currentTools)) {
3197
- const fullTool = {
3198
- ...toolDef,
3199
- name
3200
- // Use the key as the name
3201
- };
3202
- registerTool(fullTool);
3203
- toolNames.push(name);
3204
- }
3205
- registeredToolsRef.current = toolNames;
3206
- return () => {
3207
- for (const name of registeredToolsRef.current) {
3208
- unregisterTool(name);
3209
- }
3210
- registeredToolsRef.current = [];
3211
- };
3212
- }, [toolsKey]);
3213
- }
3214
5815
  function convertZodSchema(schema, _toolName) {
3215
5816
  try {
3216
5817
  const zodWithJsonSchema = z;
@@ -4398,6 +6999,94 @@ function createToolIntentHandler(callTool) {
4398
6999
  }
4399
7000
  };
4400
7001
  }
7002
+ function getLastResponseUsage(messages) {
7003
+ for (let i = messages.length - 1; i >= 0; i--) {
7004
+ const msg = messages[i];
7005
+ if (msg.role === "assistant" && msg.metadata?.usage) {
7006
+ const u = msg.metadata.usage;
7007
+ const prompt = u.prompt_tokens ?? 0;
7008
+ const completion = u.completion_tokens ?? 0;
7009
+ return {
7010
+ prompt_tokens: prompt,
7011
+ completion_tokens: completion,
7012
+ total_tokens: u.total_tokens ?? prompt + completion
7013
+ };
7014
+ }
7015
+ }
7016
+ return null;
7017
+ }
7018
+ function useContextStats() {
7019
+ const { contextChars, contextUsage, registeredTools, messages } = useCopilot();
7020
+ const toolCount = useMemo(() => registeredTools.length, [registeredTools]);
7021
+ const messageCount = useMemo(
7022
+ () => messages.filter((m) => m.role !== "system").length,
7023
+ [messages]
7024
+ );
7025
+ const totalTokens = useMemo(() => {
7026
+ if (contextUsage) return contextUsage.total.tokens;
7027
+ return Math.ceil(contextChars / 3.5);
7028
+ }, [contextUsage, contextChars]);
7029
+ const usagePercent = useMemo(() => {
7030
+ if (contextUsage) return contextUsage.total.percent;
7031
+ return 0;
7032
+ }, [contextUsage]);
7033
+ const lastResponseUsage = useMemo(
7034
+ () => getLastResponseUsage(messages),
7035
+ [messages]
7036
+ );
7037
+ return {
7038
+ contextUsage,
7039
+ totalTokens,
7040
+ usagePercent,
7041
+ contextChars,
7042
+ toolCount,
7043
+ messageCount,
7044
+ lastResponseUsage
7045
+ };
7046
+ }
7047
+ var DEV_CONTENT_WARN_THRESHOLD = 2e3;
7048
+ function useSkill(skill) {
7049
+ const { register, unregister } = useSkillContext();
7050
+ if (process.env.NODE_ENV !== "production" && skill.source.type === "inline" && skill.source.content.length > DEV_CONTENT_WARN_THRESHOLD) {
7051
+ console.warn(
7052
+ `[copilot-sdk/skills] Inline skill "${skill.name}" has ${skill.source.content.length} characters. Inline skills are sent on every request \u2014 keep them under ${DEV_CONTENT_WARN_THRESHOLD} characters. Consider using a file or URL skill instead.`
7053
+ );
7054
+ }
7055
+ useEffect(() => {
7056
+ if (skill.source.type !== "inline") {
7057
+ console.warn(
7058
+ `[copilot-sdk/skills] useSkill only supports inline skills client-side. Skill "${skill.name}" has source type "${skill.source.type}" and will be skipped.`
7059
+ );
7060
+ return;
7061
+ }
7062
+ const resolved = {
7063
+ ...skill,
7064
+ content: skill.source.content
7065
+ };
7066
+ register(resolved);
7067
+ return () => {
7068
+ unregister(skill.name);
7069
+ };
7070
+ }, [
7071
+ skill.name,
7072
+ skill.source.type === "inline" ? skill.source.content : "",
7073
+ skill.strategy,
7074
+ skill.description
7075
+ ]);
7076
+ }
7077
+ function useSkillStatus() {
7078
+ const { skills, registry } = useSkillContext();
7079
+ const has = useCallback(
7080
+ (name) => registry.has(name),
7081
+ // eslint-disable-next-line react-hooks/exhaustive-deps
7082
+ [skills]
7083
+ );
7084
+ return {
7085
+ skills,
7086
+ count: skills.length,
7087
+ has
7088
+ };
7089
+ }
4401
7090
 
4402
7091
  // src/react/utils/permission-storage.ts
4403
7092
  var DEFAULT_KEY_PREFIX = "yourgpt-permissions";
@@ -4550,6 +7239,34 @@ var ReactChat = class extends AbstractChat {
4550
7239
  return this.on("error", handler);
4551
7240
  }
4552
7241
  // ============================================
7242
+ // Branching API — pass-throughs to ReactChatState
7243
+ // ============================================
7244
+ /**
7245
+ * Navigate to a sibling branch (makes it the active path).
7246
+ */
7247
+ switchBranch(messageId) {
7248
+ this.reactState.switchBranch(messageId);
7249
+ }
7250
+ /**
7251
+ * Get branch navigation info for a message.
7252
+ * Returns null if the message has no siblings.
7253
+ */
7254
+ getBranchInfo(messageId) {
7255
+ return this.reactState.getBranchInfo(messageId);
7256
+ }
7257
+ /**
7258
+ * Get all messages across all branches (for persistence).
7259
+ */
7260
+ getAllMessages() {
7261
+ return this.reactState.getAllMessages();
7262
+ }
7263
+ /**
7264
+ * Whether any message has siblings (branching has occurred).
7265
+ */
7266
+ get hasBranches() {
7267
+ return this.reactState.hasBranches;
7268
+ }
7269
+ // ============================================
4553
7270
  // Override dispose to clean up state
4554
7271
  // ============================================
4555
7272
  dispose() {
@@ -4609,6 +7326,11 @@ function useChat(config) {
4609
7326
  () => void 0
4610
7327
  // Server snapshot
4611
7328
  );
7329
+ const hasBranches = useSyncExternalStore(
7330
+ chatRef.current.subscribe,
7331
+ () => chatRef.current.hasBranches,
7332
+ () => false
7333
+ );
4612
7334
  const isLoading = status === "streaming" || status === "submitted";
4613
7335
  const sendMessage = useCallback(
4614
7336
  async (content, attachments) => {
@@ -4635,6 +7357,24 @@ function useChat(config) {
4635
7357
  },
4636
7358
  []
4637
7359
  );
7360
+ const switchBranch = useCallback((messageId) => {
7361
+ chatRef.current?.switchBranch(messageId);
7362
+ }, []);
7363
+ const getBranchInfo = useCallback(
7364
+ (messageId) => {
7365
+ return chatRef.current?.getBranchInfo(messageId) ?? null;
7366
+ },
7367
+ []
7368
+ );
7369
+ const editMessage = useCallback(
7370
+ async (messageId, newContent) => {
7371
+ await chatRef.current?.sendMessage(newContent, void 0, {
7372
+ editMessageId: messageId
7373
+ });
7374
+ setInput("");
7375
+ },
7376
+ []
7377
+ );
4638
7378
  useEffect(() => {
4639
7379
  return () => {
4640
7380
  chatRef.current?.dispose();
@@ -4653,10 +7393,20 @@ function useChat(config) {
4653
7393
  setMessages,
4654
7394
  regenerate,
4655
7395
  continueWithToolResults,
4656
- chatRef
7396
+ chatRef,
7397
+ // Branching
7398
+ switchBranch,
7399
+ getBranchInfo,
7400
+ editMessage,
7401
+ hasBranches
4657
7402
  };
4658
7403
  }
4659
7404
 
4660
- export { AbstractAgentLoop, AbstractChat, CopilotProvider, ReactChat, ReactChatState, ReactThreadManager, ReactThreadManagerState, createMessageIntentHandler, createPermissionStorage, createReactChat, createReactChatState, createReactThreadManager, createReactThreadManagerState, createSessionPermissionCache, createToolIntentHandler, formatKnowledgeResultsForAI, initialAgentLoopState, searchKnowledgeBase, useAIAction, useAIActions, useAIContext, useAIContexts, useAITools, useAgent, useCapabilities, useChat, useCopilot, useDevLogger, useFeatureSupport, useKnowledgeBase, useMCPClient, useMCPTools, useMCPUIIntents, useSuggestions, useSupportedMediaTypes, useThreadManager, useTool, useToolExecutor, useToolWithSchema, useTools, useToolsWithSchema };
4661
- //# sourceMappingURL=chunk-DMBFN7KO.js.map
4662
- //# sourceMappingURL=chunk-DMBFN7KO.js.map
7405
+ // src/react/skill/define-skill.ts
7406
+ function defineSkill(def) {
7407
+ return def;
7408
+ }
7409
+
7410
+ export { AbstractAgentLoop, AbstractChat, CopilotProvider, MessageHistoryContext, MessageTree, ReactChat, ReactChatState, ReactThreadManager, ReactThreadManagerState, SkillProvider, createMessageIntentHandler, createPermissionStorage, createReactChat, createReactChatState, createReactThreadManager, createReactThreadManagerState, createSessionPermissionCache, createToolIntentHandler, defaultMessageHistoryConfig, defineSkill, formatKnowledgeResultsForAI, initialAgentLoopState, isCompactionMarker, keepToolPairsAtomic, searchKnowledgeBase, toDisplayMessage, toLLMMessage, toLLMMessages, useAIAction, useAIActions, useAIContext, useAIContexts, useAITools, useAgent, useCapabilities, useChat, useContextStats, useCopilot, useDevLogger, useFeatureSupport, useKnowledgeBase, useMCPClient, useMCPTools, useMCPUIIntents, useMessageHistory, useMessageHistoryContext, useSkill, useSkillStatus, useSuggestions, useSupportedMediaTypes, useThreadManager, useTool, useToolExecutor, useToolWithSchema, useTools, useToolsWithSchema };
7411
+ //# sourceMappingURL=chunk-6BMQZIS3.js.map
7412
+ //# sourceMappingURL=chunk-6BMQZIS3.js.map