@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
@@ -1553,6 +1553,61 @@ function abortableDelay(ms, signal) {
1553
1553
  });
1554
1554
  }
1555
1555
 
1556
+ // ../../foundation-ai/dist/esm/transports/context-overflow-error.js
1557
+ var CONTEXT_OVERFLOW_PHRASES = Object.freeze([
1558
+ /prompt is too long/i,
1559
+ /input token count .* exceeds/i,
1560
+ /maximum context length/i,
1561
+ /context[_ ]length[_ ]exceeded/i,
1562
+ /too many (input )?tokens/i
1563
+ ]);
1564
+ var TOKEN_FIGURES = /(\d[\d,]{2,})\D{0,40}?(?:>|exceeds)\D{0,80}?(\d[\d,]{2,})/i;
1565
+ var OVERFLOW_STATUS = 400;
1566
+ 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.";
1567
+ var ContextOverflowError = class extends Error {
1568
+ constructor(vendorLabel, promptTokens, limitTokens, detail) {
1569
+ 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.");
1570
+ this.vendorLabel = vendorLabel;
1571
+ this.promptTokens = promptTokens;
1572
+ this.limitTokens = limitTokens;
1573
+ this.detail = detail;
1574
+ this.name = "ContextOverflowError";
1575
+ }
1576
+ };
1577
+ var MAX_ENVELOPE_DEPTH = 4;
1578
+ function messagesIn(payload) {
1579
+ if (!payload || typeof payload !== "object")
1580
+ return [];
1581
+ const found = [];
1582
+ const visit = (node, depth) => {
1583
+ if (depth > MAX_ENVELOPE_DEPTH || !node || typeof node !== "object")
1584
+ return;
1585
+ for (const value of Object.values(node)) {
1586
+ if (typeof value === "string")
1587
+ found.push(value);
1588
+ else if (typeof value === "object")
1589
+ visit(value, depth + 1);
1590
+ }
1591
+ };
1592
+ visit(payload, 0);
1593
+ return found;
1594
+ }
1595
+ function contextOverflowOf(_vendor, status, payload) {
1596
+ if (status !== OVERFLOW_STATUS)
1597
+ return void 0;
1598
+ for (const message of messagesIn(payload)) {
1599
+ if (!CONTEXT_OVERFLOW_PHRASES.some((phrase) => phrase.test(message)))
1600
+ continue;
1601
+ const figures = TOKEN_FIGURES.exec(message);
1602
+ return {
1603
+ promptTokens: figures ? Number(figures[1].replace(/,/g, "")) : void 0,
1604
+ limitTokens: figures ? Number(figures[2].replace(/,/g, "")) : void 0,
1605
+ message
1606
+ };
1607
+ }
1608
+ return void 0;
1609
+ }
1610
+
1556
1611
  // ../../foundation-ai/dist/esm/transports/ndjson-frames.js
1557
1612
  var MALFORMED_FRAME_PREVIEW_CHARS = 120;
1558
1613
  function readFramedBody(stream, onChunk, vendorLabel, stallTimeout) {
@@ -1681,7 +1736,7 @@ function serverBackoffSpentMs(resolvedMs, ladderMs) {
1681
1736
  var MAX_RETRIES = 5;
1682
1737
  function postWithRetry(options) {
1683
1738
  return __awaiter(this, void 0, void 0, function* () {
1684
- var _a, _b, _c, _d, _e, _f, _g;
1739
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1685
1740
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal } = options;
1686
1741
  const vendor = vendorOfTransportLabel(vendorLabel);
1687
1742
  let serverBackoffSpent = 0;
@@ -1734,6 +1789,10 @@ function postWithRetry(options) {
1734
1789
  if (refusal) {
1735
1790
  throw providerRefusedFrom(vendorLabel, refusal, response.status, parsed, errText || void 0);
1736
1791
  }
1792
+ const overflow = contextOverflowOf(vendor, response.status, parsed);
1793
+ if (overflow) {
1794
+ throw new ContextOverflowError(vendorLabel, overflow.promptTokens, overflow.limitTokens, (_d = overflow.message) !== null && _d !== void 0 ? _d : errText || void 0);
1795
+ }
1737
1796
  if (isBudgetRejection(response.status, codeOf(parsed))) {
1738
1797
  throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1739
1798
  }
@@ -1746,18 +1805,22 @@ function postWithRetry(options) {
1746
1805
  }
1747
1806
  throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1748
1807
  }
1749
- 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) {
1808
+ 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) {
1750
1809
  rearmStallTimer();
1751
1810
  const frame = yield readFramedBody(response.body, rearmStallTimer, vendorLabel, stallTimeout);
1752
1811
  if (frame.t === "ok")
1753
1812
  return frame.body;
1754
1813
  if (frame.code === "UPSTREAM_STALLED") {
1755
- throw new DOMException(`${vendorLabel} request stalled: ${(_f = frame.error) !== null && _f !== void 0 ? _f : "no upstream data"}`, "TimeoutError");
1814
+ throw new DOMException(`${vendorLabel} request stalled: ${(_g = frame.error) !== null && _g !== void 0 ? _g : "no upstream data"}`, "TimeoutError");
1756
1815
  }
1757
1816
  const framedRefusal = providerRefusalOf(vendor, frame.code, frame);
1758
1817
  if (framedRefusal) {
1759
1818
  throw providerRefusedFrom(vendorLabel, framedRefusal, frame.status, frame, frame.error);
1760
1819
  }
1820
+ const framedOverflow = contextOverflowOf(vendor, frame.status, frame);
1821
+ if (framedOverflow) {
1822
+ throw new ContextOverflowError(vendorLabel, framedOverflow.promptTokens, framedOverflow.limitTokens, (_h = framedOverflow.message) !== null && _h !== void 0 ? _h : frame.error);
1823
+ }
1761
1824
  if (isBudgetRejection(frame.status, frame.code)) {
1762
1825
  throw budgetExhaustedFrom(vendorLabel, frame);
1763
1826
  }
@@ -1767,7 +1830,7 @@ function postWithRetry(options) {
1767
1830
  yield backoff(attempt, retryInfoMs(frame));
1768
1831
  continue;
1769
1832
  }
1770
- throw new Error(`${vendorLabel} request error ${frame.status}: ${(_g = frame.error) !== null && _g !== void 0 ? _g : JSON.stringify(frame.details)}`);
1833
+ throw new Error(`${vendorLabel} request error ${frame.status}: ${(_j = frame.error) !== null && _j !== void 0 ? _j : JSON.stringify(frame.details)}`);
1771
1834
  }
1772
1835
  return yield response.json();
1773
1836
  } catch (e) {
@@ -3732,10 +3795,126 @@ function createInteractionContext(interactionId) {
3732
3795
  };
3733
3796
  }
3734
3797
 
3798
+ // src/utils/context-tokens.ts
3799
+ var APPROX_CHARS_PER_TOKEN = 4;
3800
+ var IMAGE_TOKEN_ESTIMATE = 1600;
3801
+ var PER_MESSAGE_FRAMING_TOKENS = 4;
3802
+ var COMPACTION_SUMMARY_TOKEN_ESTIMATE = 1200;
3803
+ function reachesProvider(m) {
3804
+ if (m.role === "system-event" || m.role === "synthetic-user") return false;
3805
+ if (m.category === "reasoning" || m.category === "narration") return false;
3806
+ return !m.thinking;
3807
+ }
3808
+ function imageCount(m) {
3809
+ const onMessage = m.attachments?.filter((a) => a.kind === "image").length ?? 0;
3810
+ const onResult = m.toolResult?.attachments?.filter((a) => a.kind === "image").length ?? 0;
3811
+ return onMessage + onResult;
3812
+ }
3813
+ var TEXT_ATTACHMENT_FRAMING_CHARS = 9;
3814
+ function wireChars(m) {
3815
+ let chars = m.content?.length ?? 0;
3816
+ for (const attachment of m.attachments ?? []) {
3817
+ if (attachment.kind === "image") continue;
3818
+ chars += TEXT_ATTACHMENT_FRAMING_CHARS + attachment.name.length + (attachment.content?.length ?? 0);
3819
+ }
3820
+ for (const tc of m.toolCalls ?? []) {
3821
+ chars += tc.name.length;
3822
+ try {
3823
+ chars += JSON.stringify(tc.args ?? {}).length;
3824
+ } catch {
3825
+ }
3826
+ }
3827
+ chars += m.toolResult?.content?.length ?? 0;
3828
+ return chars;
3829
+ }
3830
+ function baselineTokens(m) {
3831
+ if (!reachesProvider(m)) return 0;
3832
+ return Math.ceil(wireChars(m) / APPROX_CHARS_PER_TOKEN) + imageCount(m) * IMAGE_TOKEN_ESTIMATE + PER_MESSAGE_FRAMING_TOKENS;
3833
+ }
3834
+ function compactedAtOf(history) {
3835
+ return history.find((m) => m.role === "compacted-summary")?.compaction?.createdAt;
3836
+ }
3837
+ function usableAnchor(m, compactedAt) {
3838
+ if (m.inputTokens == null) return false;
3839
+ if (!compactedAt) return true;
3840
+ return !!m.timestamp && m.timestamp >= compactedAt;
3841
+ }
3842
+ function firstAnchor(history, compactedAt) {
3843
+ return history.findIndex((m) => usableAnchor(m, compactedAt));
3844
+ }
3845
+ function estimateMessageTokens(history) {
3846
+ const estimates = history.map(baselineTokens);
3847
+ const compactedAt = compactedAtOf(history);
3848
+ const anchors = [];
3849
+ history.forEach((m, i) => {
3850
+ if (usableAnchor(m, compactedAt)) anchors.push(i);
3851
+ });
3852
+ for (let a = 0; a < anchors.length - 1; a += 1) {
3853
+ const from = anchors[a];
3854
+ const to = anchors[a + 1];
3855
+ const measured = history[to].inputTokens - history[from].inputTokens;
3856
+ if (measured <= 0) continue;
3857
+ let baseline = 0;
3858
+ for (let i = from; i < to; i += 1) baseline += estimates[i];
3859
+ if (baseline <= 0) {
3860
+ estimates[from] = measured;
3861
+ continue;
3862
+ }
3863
+ const scale = measured / baseline;
3864
+ let assigned = 0;
3865
+ for (let i = from; i < to - 1; i += 1) {
3866
+ estimates[i] = Math.round(estimates[i] * scale);
3867
+ assigned += estimates[i];
3868
+ }
3869
+ estimates[to - 1] = measured - assigned;
3870
+ }
3871
+ return estimates;
3872
+ }
3873
+ function estimateSystemOverhead(history) {
3874
+ const first = firstAnchor(history, compactedAtOf(history));
3875
+ if (first < 0) return 0;
3876
+ const estimates = estimateMessageTokens(history);
3877
+ let messages = 0;
3878
+ for (let i = 0; i < first; i += 1) messages += estimates[i];
3879
+ return Math.max(0, history[first].inputTokens - messages);
3880
+ }
3881
+ function estimateContextTokens(history) {
3882
+ const estimates = estimateMessageTokens(history);
3883
+ let total = estimateSystemOverhead(history);
3884
+ for (const t of estimates) total += t;
3885
+ return total;
3886
+ }
3887
+ function estimateRequestTokens(storedHistory, requestHistory) {
3888
+ const overhead = estimateSystemOverhead(storedHistory);
3889
+ const calibratedMessages = Math.max(0, estimateContextTokens(storedHistory) - overhead);
3890
+ let storedBaseline = 0;
3891
+ for (const m of storedHistory) storedBaseline += baselineTokens(m);
3892
+ let requestBaseline = 0;
3893
+ for (const m of requestHistory) requestBaseline += baselineTokens(m);
3894
+ if (storedBaseline <= 0) return overhead + requestBaseline;
3895
+ return Math.round(overhead + calibratedMessages * (requestBaseline / storedBaseline));
3896
+ }
3897
+ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKEN_ESTIMATE) {
3898
+ const estimates = estimateMessageTokens(history);
3899
+ const overhead = estimateSystemOverhead(history);
3900
+ let tail = 0;
3901
+ for (let i = cut; i < estimates.length; i += 1) tail += estimates[i];
3902
+ let all = 0;
3903
+ for (const t of estimates) all += t;
3904
+ const tokensBefore = overhead + all;
3905
+ const tokensAfter = overhead + summaryTokens + tail;
3906
+ return {
3907
+ cut,
3908
+ compactedCount: cut,
3909
+ tokensBefore,
3910
+ tokensAfter,
3911
+ reclaimed: Math.max(0, tokensBefore - tokensAfter)
3912
+ };
3913
+ }
3914
+
3735
3915
  // src/utils/condense-history.ts
3736
3916
  var CONDENSE_MIN_CHARS = 1e3;
3737
3917
  var CONDENSED_ARGS_KEY = "condensed";
3738
- var APPROX_CHARS_PER_TOKEN = 4;
3739
3918
  function triggerReason(trigger) {
3740
3919
  switch (trigger.kind) {
3741
3920
  case "age":
@@ -3750,11 +3929,11 @@ function triggerReason(trigger) {
3750
3929
  return "superseded";
3751
3930
  }
3752
3931
  }
3753
- function condenseStub(target, tool, trigger, origLen, restorable, imageCount = 0) {
3932
+ function condenseStub(target, tool, trigger, origLen, restorable, imageCount2 = 0) {
3754
3933
  const what = target === "args" ? "args" : "result";
3755
3934
  const key = trigger.kind === "superseded" ? ` ${trigger.by}` : "";
3756
3935
  const restore = restorable ? "; re-call to restore" : "";
3757
- const images = imageCount > 0 ? ` + ${imageCount} image${imageCount === 1 ? "" : "s"}` : "";
3936
+ const images = imageCount2 > 0 ? ` + ${imageCount2} image${imageCount2 === 1 ? "" : "s"}` : "";
3758
3937
  return `[${tool}${key} \u2014 ${what} elided, ~${origLen} chars${images} (${triggerReason(trigger)})${restore}]`;
3759
3938
  }
3760
3939
  function triggerLabel(trigger) {
@@ -3906,20 +4085,43 @@ function applyHistoryCap(history, cap) {
3906
4085
  const cutoff = history.length - cap;
3907
4086
  return history.map((msg, i) => i < cutoff ? maskToolPayload(msg) : msg);
3908
4087
  }
3909
- var COMPACT_KEEP_RECENT_MESSAGES = 4;
3910
- var COMPACT_MIN_MESSAGES_TO_COMPACT = 4;
3911
- function findCompactionCut(history) {
3912
- if (history.length < COMPACT_KEEP_RECENT_MESSAGES + COMPACT_MIN_MESSAGES_TO_COMPACT) {
3913
- return null;
4088
+ var DEFAULT_TAIL_TOKEN_BUDGET = 3e4;
4089
+ var DEFAULT_MIN_RECLAIM_TOKENS = 5e3;
4090
+ function isSafeCompactionCut(history, i) {
4091
+ if (i <= 0 || i >= history.length) return false;
4092
+ for (let j = i; j < history.length; j += 1) {
4093
+ if (!reachesProvider(history[j])) continue;
4094
+ return history[j].role !== "tool";
3914
4095
  }
3915
- const target = history.length - COMPACT_KEEP_RECENT_MESSAGES;
3916
- for (let i = target; i > 0; i -= 1) {
3917
- if (history[i].role === "user") {
3918
- return i >= COMPACT_MIN_MESSAGES_TO_COMPACT ? i : null;
3919
- }
4096
+ return true;
4097
+ }
4098
+ function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET) {
4099
+ const estimates = estimateMessageTokens(history);
4100
+ let messagesTotal = 0;
4101
+ for (const t of estimates) messagesTotal += t;
4102
+ if (messagesTotal <= tailTokenBudget) return null;
4103
+ let tail = 0;
4104
+ let raw = history.length;
4105
+ for (let i = history.length - 1; i > 0; i -= 1) {
4106
+ if (tail + estimates[i] > tailTokenBudget) break;
4107
+ tail += estimates[i];
4108
+ raw = i;
4109
+ }
4110
+ for (let i = raw; i < history.length; i += 1) {
4111
+ if (isSafeCompactionCut(history, i)) return i;
3920
4112
  }
3921
4113
  return null;
3922
4114
  }
4115
+ function planCompaction(history, options = {}) {
4116
+ const {
4117
+ tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET,
4118
+ minReclaimTokens = DEFAULT_MIN_RECLAIM_TOKENS
4119
+ } = options;
4120
+ const cut = findCompactionCut(history, tailTokenBudget);
4121
+ if (cut == null) return null;
4122
+ const projection = projectCompaction(history, cut);
4123
+ return projection.reclaimed >= minReclaimTokens ? projection : null;
4124
+ }
3923
4125
  function renderMessageForSummary(m) {
3924
4126
  switch (m.role) {
3925
4127
  case "compacted-summary":
@@ -4649,6 +4851,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4649
4851
  const status = await this.resolveStatusForProvider(resolvedName, provider);
4650
4852
  this.lastResolvedModel = status.model;
4651
4853
  this.lastResolvedProvider = status.provider;
4854
+ this.lastResolvedContextLimit = status.contextLimit;
4652
4855
  if (resolvedName !== this.lastDispatchedProviderName) {
4653
4856
  this.lastDispatchedProviderName = resolvedName;
4654
4857
  recordMetaEvent(this.sessionKey, "provider.selected", {
@@ -4680,7 +4883,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4680
4883
  try {
4681
4884
  const resolved = await provider.getStatus?.();
4682
4885
  if (resolved) {
4683
- status = { model: resolved.model, provider: resolved.provider };
4886
+ status = {
4887
+ model: resolved.model,
4888
+ provider: resolved.provider,
4889
+ // Carried so the mid-loop guard can measure against the window of the
4890
+ // provider this call actually resolved to (GENC-1567).
4891
+ contextLimit: resolved.contextLimit
4892
+ };
4684
4893
  }
4685
4894
  } catch {
4686
4895
  status = {};
@@ -4837,9 +5046,48 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4837
5046
  return snapshot;
4838
5047
  }
4839
5048
  /**
4840
- * Optional transform applied to conversation history immediately before each LLM request.
4841
- * Cleared when `undefined`. Does not alter stored history.
5049
+ * Set the mid-loop context guard. Its margin is deliberately far smaller than
5050
+ * the reserve that blocks NEW turns: that reserve exists so an accepted turn can
5051
+ * spend it, and a guard set at the same line would kill every turn that used
5052
+ * the headroom it was given.
5053
+ */
5054
+ setContextGuard(policy) {
5055
+ this.contextGuard = policy && policy.marginTokens > 0 ? policy : void 0;
5056
+ }
5057
+ /**
5058
+ * Whether issuing `requestHistory` would run the context window out.
5059
+ *
5060
+ * Inert unless a margin has been set AND the resolved provider reports a
5061
+ * window — with no window there is nothing to measure against, and inventing
5062
+ * one to refuse a request is worse than letting the provider answer.
5063
+ *
5064
+ * Stopping here is strictly better than letting the request go: the provider
5065
+ * would reject an oversized prompt outright, leaving a transcript still too
5066
+ * large to retry and no explanation the user can act on, whereas ending the
5067
+ * turn keeps history intact so compaction is still available and the work so
5068
+ * far is not lost.
4842
5069
  */
5070
+ contextExhausted(requestHistory, pendingInput) {
5071
+ const guard = this.contextGuard;
5072
+ if (!guard) return void 0;
5073
+ const limit = this.lastResolvedContextLimit ?? guard.fallbackLimit;
5074
+ if (limit == null || limit <= 0) return void 0;
5075
+ const threshold = Math.max(0, limit - guard.marginTokens);
5076
+ const request = pendingInput && (pendingInput.content || pendingInput.attachments?.length) ? [
5077
+ ...requestHistory,
5078
+ {
5079
+ role: "user",
5080
+ content: pendingInput.content,
5081
+ attachments: pendingInput.attachments
5082
+ }
5083
+ ] : requestHistory;
5084
+ const estimated = estimateRequestTokens(this.history, request);
5085
+ if (estimated < threshold) return void 0;
5086
+ logger2.error(
5087
+ `ChatDriver: ending the turn \u2014 the request would fill the context window (~${estimated} of ${limit})`
5088
+ );
5089
+ return { estimated, threshold };
5090
+ }
4843
5091
  setProviderHistoryTransform(transform) {
4844
5092
  this.providerHistoryTransform = transform;
4845
5093
  }
@@ -4918,8 +5166,19 @@ Output format (strict):
4918
5166
  * turns exists behind a clean boundary. Uses the same `history` `compact()`
4919
5167
  * acts on, so the UI's gate can't disagree with the action (GENC-1351 follow-up).
4920
5168
  */
4921
- canCompact() {
4922
- return findCompactionCut(this.history) != null;
5169
+ canCompact(options) {
5170
+ return this.getCompactionPlan(options) != null;
5171
+ }
5172
+ /**
5173
+ * {@inheritDoc AiDriver.getCompactionPlan}
5174
+ *
5175
+ * Runs against the driver's own `history` — the exact list `compact()` acts on
5176
+ * — so a projection and the compaction it describes can never be computed from
5177
+ * different transcripts (the GENC-1351 follow-up that first made `canCompact`
5178
+ * read `history` rather than a mirrored copy).
5179
+ */
5180
+ getCompactionPlan(options) {
5181
+ return planCompaction(this.history, options);
4923
5182
  }
4924
5183
  /**
4925
5184
  * Destructively compact older turns into a single `compacted-summary` message
@@ -4930,7 +5189,7 @@ Output format (strict):
4930
5189
  * the new one. Returns the created summary message, or `null` when there is
4931
5190
  * nothing worth compacting or the default provider cannot summarize.
4932
5191
  */
4933
- async compact() {
5192
+ async compact(options) {
4934
5193
  if (this.busy) return null;
4935
5194
  const defaultProvider = this.providerRegistry.default();
4936
5195
  if (!defaultProvider.prompt) {
@@ -4938,7 +5197,7 @@ Output format (strict):
4938
5197
  return null;
4939
5198
  }
4940
5199
  const history = this.history;
4941
- const cut = findCompactionCut(history);
5200
+ const cut = planCompaction(history, options)?.cut;
4942
5201
  if (cut == null) return null;
4943
5202
  const toCompact = history.slice(0, cut);
4944
5203
  const tail = history.slice(cut);
@@ -5409,6 +5668,7 @@ Output format (strict):
5409
5668
  activityBus: this.activityBus
5410
5669
  });
5411
5670
  child.markAsSubAgent();
5671
+ child.setContextGuard(this.contextGuard);
5412
5672
  const disposeChild = () => child.dispose();
5413
5673
  if (this.lifecycleController.signal.aborted) {
5414
5674
  disposeChild();
@@ -5889,6 +6149,30 @@ ${tailBody}
5889
6149
  if (this.lastResolvedProvider !== void 0)
5890
6150
  turnSnapshot.provider = this.lastResolvedProvider;
5891
6151
  if (this.lastResolvedModel !== void 0) turnSnapshot.model = this.lastResolvedModel;
6152
+ const contextStop = this.contextExhausted(historyForCall, {
6153
+ content: userInputForCall,
6154
+ attachments: attachmentsForCall
6155
+ });
6156
+ if (contextStop) {
6157
+ recordTurnError(this.sessionKey, "context-exhausted", {
6158
+ agent: this.activeAgentName,
6159
+ provider: this.lastResolvedProviderName,
6160
+ contextTokens: contextStop.estimated,
6161
+ guard: contextStop.threshold,
6162
+ limit: this.lastResolvedContextLimit,
6163
+ iterations,
6164
+ isSubAgent: this.isSubAgent
6165
+ });
6166
+ if (this.isSubAgent) {
6167
+ this.failSubAgent("context_exhausted");
6168
+ } else {
6169
+ this.appendToHistory({
6170
+ role: "assistant",
6171
+ 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."
6172
+ });
6173
+ }
6174
+ return this.turnDone("context-exhausted");
6175
+ }
5892
6176
  let response;
5893
6177
  try {
5894
6178
  response = await activeProvider.chat(historyForCall, userInputForCall, options);
@@ -5948,6 +6232,27 @@ ${tailBody}
5948
6232
  }
5949
6233
  return this.turnDone("response-truncated");
5950
6234
  }
6235
+ if (e instanceof ContextOverflowError) {
6236
+ logger2.error("ChatDriver: the prompt exceeded the model context window", e);
6237
+ recordTurnError(this.sessionKey, "context-exhausted", {
6238
+ agent: this.activeAgentName,
6239
+ provider: this.lastResolvedProviderName,
6240
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
6241
+ promptTokens: e.promptTokens,
6242
+ limitTokens: e.limitTokens,
6243
+ via: "provider",
6244
+ isSubAgent: this.isSubAgent
6245
+ });
6246
+ if (this.isSubAgent) {
6247
+ this.failSubAgent("context_exhausted");
6248
+ } else {
6249
+ this.appendToHistory({
6250
+ role: "assistant",
6251
+ content: DEFAULT_CONTEXT_OVERFLOW_MESSAGE
6252
+ });
6253
+ }
6254
+ return this.turnDone("context-exhausted");
6255
+ }
5951
6256
  if (e instanceof ProviderRefusedError) {
5952
6257
  if (this.isSubAgent) {
5953
6258
  logger2.error("ChatDriver: provider refused the request", e);
@@ -6619,13 +6924,21 @@ var OrchestratingDriver = class extends EventTarget {
6619
6924
  getRawHistory() {
6620
6925
  return this.chatDriver.getHistory();
6621
6926
  }
6927
+ /** {@inheritDoc AiDriver.setContextGuard} */
6928
+ setContextGuard(policy) {
6929
+ this.chatDriver.setContextGuard(policy);
6930
+ }
6622
6931
  /** {@inheritDoc AiDriver.compact} */
6623
- compact() {
6624
- return this.chatDriver.compact();
6932
+ compact(options) {
6933
+ return this.chatDriver.compact(options);
6625
6934
  }
6626
6935
  /** {@inheritDoc AiDriver.canCompact} */
6627
- canCompact() {
6628
- return this.chatDriver.canCompact();
6936
+ canCompact(options) {
6937
+ return this.chatDriver.canCompact(options);
6938
+ }
6939
+ /** {@inheritDoc AiDriver.getCompactionPlan} */
6940
+ getCompactionPlan(options) {
6941
+ return this.chatDriver.getCompactionPlan(options);
6629
6942
  }
6630
6943
  /** Delegates to the inner {@link ChatDriver} — turns are captured there. */
6631
6944
  getTurnSnapshots() {