@genesislcap/ai-assistant 15.19.6 → 15.20.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 (42) hide show
  1. package/dist/ai-assistant.api.json +605 -72
  2. package/dist/ai-assistant.d.ts +404 -25
  3. package/dist/chat-driver.cjs +341 -28
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +341 -28
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +630 -20
  8. package/dist/dts/components/ai-driver/ai-driver.d.ts +33 -7
  9. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -2
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -3
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  14. package/dist/dts/config/config.d.ts +44 -0
  15. package/dist/dts/config/config.d.ts.map +1 -1
  16. package/dist/dts/main/main.d.ts +187 -5
  17. package/dist/dts/main/main.d.ts.map +1 -1
  18. package/dist/dts/main/main.styles.d.ts.map +1 -1
  19. package/dist/dts/main/main.template.d.ts.map +1 -1
  20. package/dist/dts/utils/condense-history.d.ts.map +1 -1
  21. package/dist/dts/utils/context-tokens.d.ts +156 -0
  22. package/dist/dts/utils/context-tokens.d.ts.map +1 -0
  23. package/dist/dts/utils/history-transform.d.ts +76 -14
  24. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  25. package/dist/dts/utils/resolve-context-budget.d.ts +98 -0
  26. package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -0
  27. package/dist/esm/components/chat-driver/chat-driver.js +179 -34
  28. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +12 -4
  29. package/dist/esm/main/main.js +391 -21
  30. package/dist/esm/main/main.styles.js +128 -0
  31. package/dist/esm/main/main.template.js +64 -29
  32. package/dist/esm/state/debug-event-log.js +1 -1
  33. package/dist/esm/utils/condense-history.js +1 -5
  34. package/dist/esm/utils/context-tokens.js +339 -0
  35. package/dist/esm/utils/history-transform.js +101 -19
  36. package/dist/esm/utils/resolve-context-budget.js +84 -0
  37. package/package.json +16 -16
  38. package/sandbox/README.md +93 -4
  39. package/sandbox/controls.ts +77 -10
  40. package/sandbox/fixtures.ts +163 -6
  41. package/sandbox/sandbox.css +54 -1
  42. package/sandbox/sandbox.ts +384 -7
@@ -1486,6 +1486,61 @@ function abortableDelay(ms, signal) {
1486
1486
  });
1487
1487
  }
1488
1488
 
1489
+ // ../../foundation-ai/dist/esm/transports/context-overflow-error.js
1490
+ var CONTEXT_OVERFLOW_PHRASES = Object.freeze([
1491
+ /prompt is too long/i,
1492
+ /input token count .* exceeds/i,
1493
+ /maximum context length/i,
1494
+ /context[_ ]length[_ ]exceeded/i,
1495
+ /too many (input )?tokens/i
1496
+ ]);
1497
+ var TOKEN_FIGURES = /(\d[\d,]{2,})\D{0,40}?(?:>|exceeds)\D{0,80}?(\d[\d,]{2,})/i;
1498
+ var OVERFLOW_STATUS = 400;
1499
+ var DEFAULT_CONTEXT_OVERFLOW_MESSAGE = "This conversation has grown larger than the model can accept in one request. Compact it to free up room, then try again.";
1500
+ var ContextOverflowError = class extends Error {
1501
+ constructor(vendorLabel, promptTokens, limitTokens, detail) {
1502
+ super(`${vendorLabel} rejected the request \u2014 the prompt exceeds the model context window` + (promptTokens != null && limitTokens != null ? ` (${promptTokens} > ${limitTokens})` : "") + (detail ? `: ${detail}` : "") + ". Retrying will not clear this \u2014 the conversation must be compacted.");
1503
+ this.vendorLabel = vendorLabel;
1504
+ this.promptTokens = promptTokens;
1505
+ this.limitTokens = limitTokens;
1506
+ this.detail = detail;
1507
+ this.name = "ContextOverflowError";
1508
+ }
1509
+ };
1510
+ var MAX_ENVELOPE_DEPTH = 4;
1511
+ function messagesIn(payload) {
1512
+ if (!payload || typeof payload !== "object")
1513
+ return [];
1514
+ const found = [];
1515
+ const visit = (node, depth) => {
1516
+ if (depth > MAX_ENVELOPE_DEPTH || !node || typeof node !== "object")
1517
+ return;
1518
+ for (const value of Object.values(node)) {
1519
+ if (typeof value === "string")
1520
+ found.push(value);
1521
+ else if (typeof value === "object")
1522
+ visit(value, depth + 1);
1523
+ }
1524
+ };
1525
+ visit(payload, 0);
1526
+ return found;
1527
+ }
1528
+ function contextOverflowOf(_vendor, status, payload) {
1529
+ if (status !== OVERFLOW_STATUS)
1530
+ return void 0;
1531
+ for (const message of messagesIn(payload)) {
1532
+ if (!CONTEXT_OVERFLOW_PHRASES.some((phrase) => phrase.test(message)))
1533
+ continue;
1534
+ const figures = TOKEN_FIGURES.exec(message);
1535
+ return {
1536
+ promptTokens: figures ? Number(figures[1].replace(/,/g, "")) : void 0,
1537
+ limitTokens: figures ? Number(figures[2].replace(/,/g, "")) : void 0,
1538
+ message
1539
+ };
1540
+ }
1541
+ return void 0;
1542
+ }
1543
+
1489
1544
  // ../../foundation-ai/dist/esm/transports/ndjson-frames.js
1490
1545
  var MALFORMED_FRAME_PREVIEW_CHARS = 120;
1491
1546
  function readFramedBody(stream, onChunk, vendorLabel, stallTimeout) {
@@ -1614,7 +1669,7 @@ function serverBackoffSpentMs(resolvedMs, ladderMs) {
1614
1669
  var MAX_RETRIES = 5;
1615
1670
  function postWithRetry(options) {
1616
1671
  return __awaiter(this, void 0, void 0, function* () {
1617
- var _a, _b, _c, _d, _e, _f, _g;
1672
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1618
1673
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal } = options;
1619
1674
  const vendor = vendorOfTransportLabel(vendorLabel);
1620
1675
  let serverBackoffSpent = 0;
@@ -1667,6 +1722,10 @@ function postWithRetry(options) {
1667
1722
  if (refusal) {
1668
1723
  throw providerRefusedFrom(vendorLabel, refusal, response.status, parsed, errText || void 0);
1669
1724
  }
1725
+ const overflow = contextOverflowOf(vendor, response.status, parsed);
1726
+ if (overflow) {
1727
+ throw new ContextOverflowError(vendorLabel, overflow.promptTokens, overflow.limitTokens, (_d = overflow.message) !== null && _d !== void 0 ? _d : errText || void 0);
1728
+ }
1670
1729
  if (isBudgetRejection(response.status, codeOf(parsed))) {
1671
1730
  throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1672
1731
  }
@@ -1679,18 +1738,22 @@ function postWithRetry(options) {
1679
1738
  }
1680
1739
  throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1681
1740
  }
1682
- if (((_e = (_d = response.headers) === null || _d === void 0 ? void 0 : _d.get("content-type")) === null || _e === void 0 ? void 0 : _e.toLowerCase().includes("application/x-ndjson")) && response.body) {
1741
+ if (((_f = (_e = response.headers) === null || _e === void 0 ? void 0 : _e.get("content-type")) === null || _f === void 0 ? void 0 : _f.toLowerCase().includes("application/x-ndjson")) && response.body) {
1683
1742
  rearmStallTimer();
1684
1743
  const frame = yield readFramedBody(response.body, rearmStallTimer, vendorLabel, stallTimeout);
1685
1744
  if (frame.t === "ok")
1686
1745
  return frame.body;
1687
1746
  if (frame.code === "UPSTREAM_STALLED") {
1688
- throw new DOMException(`${vendorLabel} request stalled: ${(_f = frame.error) !== null && _f !== void 0 ? _f : "no upstream data"}`, "TimeoutError");
1747
+ throw new DOMException(`${vendorLabel} request stalled: ${(_g = frame.error) !== null && _g !== void 0 ? _g : "no upstream data"}`, "TimeoutError");
1689
1748
  }
1690
1749
  const framedRefusal = providerRefusalOf(vendor, frame.code, frame);
1691
1750
  if (framedRefusal) {
1692
1751
  throw providerRefusedFrom(vendorLabel, framedRefusal, frame.status, frame, frame.error);
1693
1752
  }
1753
+ const framedOverflow = contextOverflowOf(vendor, frame.status, frame);
1754
+ if (framedOverflow) {
1755
+ throw new ContextOverflowError(vendorLabel, framedOverflow.promptTokens, framedOverflow.limitTokens, (_h = framedOverflow.message) !== null && _h !== void 0 ? _h : frame.error);
1756
+ }
1694
1757
  if (isBudgetRejection(frame.status, frame.code)) {
1695
1758
  throw budgetExhaustedFrom(vendorLabel, frame);
1696
1759
  }
@@ -1700,7 +1763,7 @@ function postWithRetry(options) {
1700
1763
  yield backoff(attempt, retryInfoMs(frame));
1701
1764
  continue;
1702
1765
  }
1703
- throw new Error(`${vendorLabel} request error ${frame.status}: ${(_g = frame.error) !== null && _g !== void 0 ? _g : JSON.stringify(frame.details)}`);
1766
+ throw new Error(`${vendorLabel} request error ${frame.status}: ${(_j = frame.error) !== null && _j !== void 0 ? _j : JSON.stringify(frame.details)}`);
1704
1767
  }
1705
1768
  return yield response.json();
1706
1769
  } catch (e) {
@@ -3665,10 +3728,126 @@ function createInteractionContext(interactionId) {
3665
3728
  };
3666
3729
  }
3667
3730
 
3731
+ // src/utils/context-tokens.ts
3732
+ var APPROX_CHARS_PER_TOKEN = 4;
3733
+ var IMAGE_TOKEN_ESTIMATE = 1600;
3734
+ var PER_MESSAGE_FRAMING_TOKENS = 4;
3735
+ var COMPACTION_SUMMARY_TOKEN_ESTIMATE = 1200;
3736
+ function reachesProvider(m) {
3737
+ if (m.role === "system-event" || m.role === "synthetic-user") return false;
3738
+ if (m.category === "reasoning" || m.category === "narration") return false;
3739
+ return !m.thinking;
3740
+ }
3741
+ function imageCount(m) {
3742
+ const onMessage = m.attachments?.filter((a) => a.kind === "image").length ?? 0;
3743
+ const onResult = m.toolResult?.attachments?.filter((a) => a.kind === "image").length ?? 0;
3744
+ return onMessage + onResult;
3745
+ }
3746
+ var TEXT_ATTACHMENT_FRAMING_CHARS = 9;
3747
+ function wireChars(m) {
3748
+ let chars = m.content?.length ?? 0;
3749
+ for (const attachment of m.attachments ?? []) {
3750
+ if (attachment.kind === "image") continue;
3751
+ chars += TEXT_ATTACHMENT_FRAMING_CHARS + attachment.name.length + (attachment.content?.length ?? 0);
3752
+ }
3753
+ for (const tc of m.toolCalls ?? []) {
3754
+ chars += tc.name.length;
3755
+ try {
3756
+ chars += JSON.stringify(tc.args ?? {}).length;
3757
+ } catch {
3758
+ }
3759
+ }
3760
+ chars += m.toolResult?.content?.length ?? 0;
3761
+ return chars;
3762
+ }
3763
+ function baselineTokens(m) {
3764
+ if (!reachesProvider(m)) return 0;
3765
+ return Math.ceil(wireChars(m) / APPROX_CHARS_PER_TOKEN) + imageCount(m) * IMAGE_TOKEN_ESTIMATE + PER_MESSAGE_FRAMING_TOKENS;
3766
+ }
3767
+ function compactedAtOf(history) {
3768
+ return history.find((m) => m.role === "compacted-summary")?.compaction?.createdAt;
3769
+ }
3770
+ function usableAnchor(m, compactedAt) {
3771
+ if (m.inputTokens == null) return false;
3772
+ if (!compactedAt) return true;
3773
+ return !!m.timestamp && m.timestamp >= compactedAt;
3774
+ }
3775
+ function firstAnchor(history, compactedAt) {
3776
+ return history.findIndex((m) => usableAnchor(m, compactedAt));
3777
+ }
3778
+ function estimateMessageTokens(history) {
3779
+ const estimates = history.map(baselineTokens);
3780
+ const compactedAt = compactedAtOf(history);
3781
+ const anchors = [];
3782
+ history.forEach((m, i) => {
3783
+ if (usableAnchor(m, compactedAt)) anchors.push(i);
3784
+ });
3785
+ for (let a = 0; a < anchors.length - 1; a += 1) {
3786
+ const from = anchors[a];
3787
+ const to = anchors[a + 1];
3788
+ const measured = history[to].inputTokens - history[from].inputTokens;
3789
+ if (measured <= 0) continue;
3790
+ let baseline = 0;
3791
+ for (let i = from; i < to; i += 1) baseline += estimates[i];
3792
+ if (baseline <= 0) {
3793
+ estimates[from] = measured;
3794
+ continue;
3795
+ }
3796
+ const scale = measured / baseline;
3797
+ let assigned = 0;
3798
+ for (let i = from; i < to - 1; i += 1) {
3799
+ estimates[i] = Math.round(estimates[i] * scale);
3800
+ assigned += estimates[i];
3801
+ }
3802
+ estimates[to - 1] = measured - assigned;
3803
+ }
3804
+ return estimates;
3805
+ }
3806
+ function estimateSystemOverhead(history) {
3807
+ const first = firstAnchor(history, compactedAtOf(history));
3808
+ if (first < 0) return 0;
3809
+ const estimates = estimateMessageTokens(history);
3810
+ let messages = 0;
3811
+ for (let i = 0; i < first; i += 1) messages += estimates[i];
3812
+ return Math.max(0, history[first].inputTokens - messages);
3813
+ }
3814
+ function estimateContextTokens(history) {
3815
+ const estimates = estimateMessageTokens(history);
3816
+ let total = estimateSystemOverhead(history);
3817
+ for (const t of estimates) total += t;
3818
+ return total;
3819
+ }
3820
+ function estimateRequestTokens(storedHistory, requestHistory) {
3821
+ const overhead = estimateSystemOverhead(storedHistory);
3822
+ const calibratedMessages = Math.max(0, estimateContextTokens(storedHistory) - overhead);
3823
+ let storedBaseline = 0;
3824
+ for (const m of storedHistory) storedBaseline += baselineTokens(m);
3825
+ let requestBaseline = 0;
3826
+ for (const m of requestHistory) requestBaseline += baselineTokens(m);
3827
+ if (storedBaseline <= 0) return overhead + requestBaseline;
3828
+ return Math.round(overhead + calibratedMessages * (requestBaseline / storedBaseline));
3829
+ }
3830
+ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKEN_ESTIMATE) {
3831
+ const estimates = estimateMessageTokens(history);
3832
+ const overhead = estimateSystemOverhead(history);
3833
+ let tail = 0;
3834
+ for (let i = cut; i < estimates.length; i += 1) tail += estimates[i];
3835
+ let all = 0;
3836
+ for (const t of estimates) all += t;
3837
+ const tokensBefore = overhead + all;
3838
+ const tokensAfter = overhead + summaryTokens + tail;
3839
+ return {
3840
+ cut,
3841
+ compactedCount: cut,
3842
+ tokensBefore,
3843
+ tokensAfter,
3844
+ reclaimed: Math.max(0, tokensBefore - tokensAfter)
3845
+ };
3846
+ }
3847
+
3668
3848
  // src/utils/condense-history.ts
3669
3849
  var CONDENSE_MIN_CHARS = 1e3;
3670
3850
  var CONDENSED_ARGS_KEY = "condensed";
3671
- var APPROX_CHARS_PER_TOKEN = 4;
3672
3851
  function triggerReason(trigger) {
3673
3852
  switch (trigger.kind) {
3674
3853
  case "age":
@@ -3683,11 +3862,11 @@ function triggerReason(trigger) {
3683
3862
  return "superseded";
3684
3863
  }
3685
3864
  }
3686
- function condenseStub(target, tool, trigger, origLen, restorable, imageCount = 0) {
3865
+ function condenseStub(target, tool, trigger, origLen, restorable, imageCount2 = 0) {
3687
3866
  const what = target === "args" ? "args" : "result";
3688
3867
  const key = trigger.kind === "superseded" ? ` ${trigger.by}` : "";
3689
3868
  const restore = restorable ? "; re-call to restore" : "";
3690
- const images = imageCount > 0 ? ` + ${imageCount} image${imageCount === 1 ? "" : "s"}` : "";
3869
+ const images = imageCount2 > 0 ? ` + ${imageCount2} image${imageCount2 === 1 ? "" : "s"}` : "";
3691
3870
  return `[${tool}${key} \u2014 ${what} elided, ~${origLen} chars${images} (${triggerReason(trigger)})${restore}]`;
3692
3871
  }
3693
3872
  function triggerLabel(trigger) {
@@ -3839,20 +4018,43 @@ function applyHistoryCap(history, cap) {
3839
4018
  const cutoff = history.length - cap;
3840
4019
  return history.map((msg, i) => i < cutoff ? maskToolPayload(msg) : msg);
3841
4020
  }
3842
- var COMPACT_KEEP_RECENT_MESSAGES = 4;
3843
- var COMPACT_MIN_MESSAGES_TO_COMPACT = 4;
3844
- function findCompactionCut(history) {
3845
- if (history.length < COMPACT_KEEP_RECENT_MESSAGES + COMPACT_MIN_MESSAGES_TO_COMPACT) {
3846
- return null;
4021
+ var DEFAULT_TAIL_TOKEN_BUDGET = 3e4;
4022
+ var DEFAULT_MIN_RECLAIM_TOKENS = 5e3;
4023
+ function isSafeCompactionCut(history, i) {
4024
+ if (i <= 0 || i >= history.length) return false;
4025
+ for (let j = i; j < history.length; j += 1) {
4026
+ if (!reachesProvider(history[j])) continue;
4027
+ return history[j].role !== "tool";
3847
4028
  }
3848
- const target = history.length - COMPACT_KEEP_RECENT_MESSAGES;
3849
- for (let i = target; i > 0; i -= 1) {
3850
- if (history[i].role === "user") {
3851
- return i >= COMPACT_MIN_MESSAGES_TO_COMPACT ? i : null;
3852
- }
4029
+ return true;
4030
+ }
4031
+ function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET) {
4032
+ const estimates = estimateMessageTokens(history);
4033
+ let messagesTotal = 0;
4034
+ for (const t of estimates) messagesTotal += t;
4035
+ if (messagesTotal <= tailTokenBudget) return null;
4036
+ let tail = 0;
4037
+ let raw = history.length;
4038
+ for (let i = history.length - 1; i > 0; i -= 1) {
4039
+ if (tail + estimates[i] > tailTokenBudget) break;
4040
+ tail += estimates[i];
4041
+ raw = i;
4042
+ }
4043
+ for (let i = raw; i < history.length; i += 1) {
4044
+ if (isSafeCompactionCut(history, i)) return i;
3853
4045
  }
3854
4046
  return null;
3855
4047
  }
4048
+ function planCompaction(history, options = {}) {
4049
+ const {
4050
+ tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET,
4051
+ minReclaimTokens = DEFAULT_MIN_RECLAIM_TOKENS
4052
+ } = options;
4053
+ const cut = findCompactionCut(history, tailTokenBudget);
4054
+ if (cut == null) return null;
4055
+ const projection = projectCompaction(history, cut);
4056
+ return projection.reclaimed >= minReclaimTokens ? projection : null;
4057
+ }
3856
4058
  function renderMessageForSummary(m) {
3857
4059
  switch (m.role) {
3858
4060
  case "compacted-summary":
@@ -4582,6 +4784,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4582
4784
  const status = await this.resolveStatusForProvider(resolvedName, provider);
4583
4785
  this.lastResolvedModel = status.model;
4584
4786
  this.lastResolvedProvider = status.provider;
4787
+ this.lastResolvedContextLimit = status.contextLimit;
4585
4788
  if (resolvedName !== this.lastDispatchedProviderName) {
4586
4789
  this.lastDispatchedProviderName = resolvedName;
4587
4790
  recordMetaEvent(this.sessionKey, "provider.selected", {
@@ -4613,7 +4816,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4613
4816
  try {
4614
4817
  const resolved = await provider.getStatus?.();
4615
4818
  if (resolved) {
4616
- status = { model: resolved.model, provider: resolved.provider };
4819
+ status = {
4820
+ model: resolved.model,
4821
+ provider: resolved.provider,
4822
+ // Carried so the mid-loop guard can measure against the window of the
4823
+ // provider this call actually resolved to (GENC-1567).
4824
+ contextLimit: resolved.contextLimit
4825
+ };
4617
4826
  }
4618
4827
  } catch {
4619
4828
  status = {};
@@ -4770,9 +4979,48 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4770
4979
  return snapshot;
4771
4980
  }
4772
4981
  /**
4773
- * Optional transform applied to conversation history immediately before each LLM request.
4774
- * Cleared when `undefined`. Does not alter stored history.
4982
+ * Set the mid-loop context guard. Its margin is deliberately far smaller than
4983
+ * the reserve that blocks NEW turns: that reserve exists so an accepted turn can
4984
+ * spend it, and a guard set at the same line would kill every turn that used
4985
+ * the headroom it was given.
4986
+ */
4987
+ setContextGuard(policy) {
4988
+ this.contextGuard = policy && policy.marginTokens > 0 ? policy : void 0;
4989
+ }
4990
+ /**
4991
+ * Whether issuing `requestHistory` would run the context window out.
4992
+ *
4993
+ * Inert unless a margin has been set AND the resolved provider reports a
4994
+ * window — with no window there is nothing to measure against, and inventing
4995
+ * one to refuse a request is worse than letting the provider answer.
4996
+ *
4997
+ * Stopping here is strictly better than letting the request go: the provider
4998
+ * would reject an oversized prompt outright, leaving a transcript still too
4999
+ * large to retry and no explanation the user can act on, whereas ending the
5000
+ * turn keeps history intact so compaction is still available and the work so
5001
+ * far is not lost.
4775
5002
  */
5003
+ contextExhausted(requestHistory, pendingInput) {
5004
+ const guard = this.contextGuard;
5005
+ if (!guard) return void 0;
5006
+ const limit = this.lastResolvedContextLimit ?? guard.fallbackLimit;
5007
+ if (limit == null || limit <= 0) return void 0;
5008
+ const threshold = Math.max(0, limit - guard.marginTokens);
5009
+ const request = pendingInput && (pendingInput.content || pendingInput.attachments?.length) ? [
5010
+ ...requestHistory,
5011
+ {
5012
+ role: "user",
5013
+ content: pendingInput.content,
5014
+ attachments: pendingInput.attachments
5015
+ }
5016
+ ] : requestHistory;
5017
+ const estimated = estimateRequestTokens(this.history, request);
5018
+ if (estimated < threshold) return void 0;
5019
+ logger2.error(
5020
+ `ChatDriver: ending the turn \u2014 the request would fill the context window (~${estimated} of ${limit})`
5021
+ );
5022
+ return { estimated, threshold };
5023
+ }
4776
5024
  setProviderHistoryTransform(transform) {
4777
5025
  this.providerHistoryTransform = transform;
4778
5026
  }
@@ -4851,8 +5099,19 @@ Output format (strict):
4851
5099
  * turns exists behind a clean boundary. Uses the same `history` `compact()`
4852
5100
  * acts on, so the UI's gate can't disagree with the action (GENC-1351 follow-up).
4853
5101
  */
4854
- canCompact() {
4855
- return findCompactionCut(this.history) != null;
5102
+ canCompact(options) {
5103
+ return this.getCompactionPlan(options) != null;
5104
+ }
5105
+ /**
5106
+ * {@inheritDoc AiDriver.getCompactionPlan}
5107
+ *
5108
+ * Runs against the driver's own `history` — the exact list `compact()` acts on
5109
+ * — so a projection and the compaction it describes can never be computed from
5110
+ * different transcripts (the GENC-1351 follow-up that first made `canCompact`
5111
+ * read `history` rather than a mirrored copy).
5112
+ */
5113
+ getCompactionPlan(options) {
5114
+ return planCompaction(this.history, options);
4856
5115
  }
4857
5116
  /**
4858
5117
  * Destructively compact older turns into a single `compacted-summary` message
@@ -4863,7 +5122,7 @@ Output format (strict):
4863
5122
  * the new one. Returns the created summary message, or `null` when there is
4864
5123
  * nothing worth compacting or the default provider cannot summarize.
4865
5124
  */
4866
- async compact() {
5125
+ async compact(options) {
4867
5126
  if (this.busy) return null;
4868
5127
  const defaultProvider = this.providerRegistry.default();
4869
5128
  if (!defaultProvider.prompt) {
@@ -4871,7 +5130,7 @@ Output format (strict):
4871
5130
  return null;
4872
5131
  }
4873
5132
  const history = this.history;
4874
- const cut = findCompactionCut(history);
5133
+ const cut = planCompaction(history, options)?.cut;
4875
5134
  if (cut == null) return null;
4876
5135
  const toCompact = history.slice(0, cut);
4877
5136
  const tail = history.slice(cut);
@@ -5342,6 +5601,7 @@ Output format (strict):
5342
5601
  activityBus: this.activityBus
5343
5602
  });
5344
5603
  child.markAsSubAgent();
5604
+ child.setContextGuard(this.contextGuard);
5345
5605
  const disposeChild = () => child.dispose();
5346
5606
  if (this.lifecycleController.signal.aborted) {
5347
5607
  disposeChild();
@@ -5822,6 +6082,30 @@ ${tailBody}
5822
6082
  if (this.lastResolvedProvider !== void 0)
5823
6083
  turnSnapshot.provider = this.lastResolvedProvider;
5824
6084
  if (this.lastResolvedModel !== void 0) turnSnapshot.model = this.lastResolvedModel;
6085
+ const contextStop = this.contextExhausted(historyForCall, {
6086
+ content: userInputForCall,
6087
+ attachments: attachmentsForCall
6088
+ });
6089
+ if (contextStop) {
6090
+ recordTurnError(this.sessionKey, "context-exhausted", {
6091
+ agent: this.activeAgentName,
6092
+ provider: this.lastResolvedProviderName,
6093
+ contextTokens: contextStop.estimated,
6094
+ guard: contextStop.threshold,
6095
+ limit: this.lastResolvedContextLimit,
6096
+ iterations,
6097
+ isSubAgent: this.isSubAgent
6098
+ });
6099
+ if (this.isSubAgent) {
6100
+ this.failSubAgent("context_exhausted");
6101
+ } else {
6102
+ this.appendToHistory({
6103
+ role: "assistant",
6104
+ content: "I had to stop here \u2014 this conversation has filled the available context. Compact it to free up room, then ask me to continue."
6105
+ });
6106
+ }
6107
+ return this.turnDone("context-exhausted");
6108
+ }
5825
6109
  let response;
5826
6110
  try {
5827
6111
  response = await activeProvider.chat(historyForCall, userInputForCall, options);
@@ -5881,6 +6165,27 @@ ${tailBody}
5881
6165
  }
5882
6166
  return this.turnDone("response-truncated");
5883
6167
  }
6168
+ if (e instanceof ContextOverflowError) {
6169
+ logger2.error("ChatDriver: the prompt exceeded the model context window", e);
6170
+ recordTurnError(this.sessionKey, "context-exhausted", {
6171
+ agent: this.activeAgentName,
6172
+ provider: this.lastResolvedProviderName,
6173
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
6174
+ promptTokens: e.promptTokens,
6175
+ limitTokens: e.limitTokens,
6176
+ via: "provider",
6177
+ isSubAgent: this.isSubAgent
6178
+ });
6179
+ if (this.isSubAgent) {
6180
+ this.failSubAgent("context_exhausted");
6181
+ } else {
6182
+ this.appendToHistory({
6183
+ role: "assistant",
6184
+ content: DEFAULT_CONTEXT_OVERFLOW_MESSAGE
6185
+ });
6186
+ }
6187
+ return this.turnDone("context-exhausted");
6188
+ }
5884
6189
  if (e instanceof ProviderRefusedError) {
5885
6190
  if (this.isSubAgent) {
5886
6191
  logger2.error("ChatDriver: provider refused the request", e);
@@ -6552,13 +6857,21 @@ var OrchestratingDriver = class extends EventTarget {
6552
6857
  getRawHistory() {
6553
6858
  return this.chatDriver.getHistory();
6554
6859
  }
6860
+ /** {@inheritDoc AiDriver.setContextGuard} */
6861
+ setContextGuard(policy) {
6862
+ this.chatDriver.setContextGuard(policy);
6863
+ }
6555
6864
  /** {@inheritDoc AiDriver.compact} */
6556
- compact() {
6557
- return this.chatDriver.compact();
6865
+ compact(options) {
6866
+ return this.chatDriver.compact(options);
6558
6867
  }
6559
6868
  /** {@inheritDoc AiDriver.canCompact} */
6560
- canCompact() {
6561
- return this.chatDriver.canCompact();
6869
+ canCompact(options) {
6870
+ return this.chatDriver.canCompact(options);
6871
+ }
6872
+ /** {@inheritDoc AiDriver.getCompactionPlan} */
6873
+ getCompactionPlan(options) {
6874
+ return this.chatDriver.getCompactionPlan(options);
6562
6875
  }
6563
6876
  /** Delegates to the inner {@link ChatDriver} — turns are captured there. */
6564
6877
  getTurnSnapshots() {