@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.
- package/dist/ai-assistant.api.json +605 -72
- package/dist/ai-assistant.d.ts +404 -25
- package/dist/chat-driver.cjs +341 -28
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +341 -28
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +630 -20
- package/dist/dts/components/ai-driver/ai-driver.d.ts +33 -7
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -2
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -3
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +44 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +187 -5
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/utils/condense-history.d.ts.map +1 -1
- package/dist/dts/utils/context-tokens.d.ts +156 -0
- package/dist/dts/utils/context-tokens.d.ts.map +1 -0
- package/dist/dts/utils/history-transform.d.ts +76 -14
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/dts/utils/resolve-context-budget.d.ts +98 -0
- package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +179 -34
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +12 -4
- package/dist/esm/main/main.js +391 -21
- package/dist/esm/main/main.styles.js +128 -0
- package/dist/esm/main/main.template.js +64 -29
- package/dist/esm/state/debug-event-log.js +1 -1
- package/dist/esm/utils/condense-history.js +1 -5
- package/dist/esm/utils/context-tokens.js +339 -0
- package/dist/esm/utils/history-transform.js +101 -19
- package/dist/esm/utils/resolve-context-budget.js +84 -0
- package/package.json +16 -16
- package/sandbox/README.md +93 -4
- package/sandbox/controls.ts +77 -10
- package/sandbox/fixtures.ts +163 -6
- package/sandbox/sandbox.css +54 -1
- package/sandbox/sandbox.ts +384 -7
|
@@ -592,6 +592,72 @@ const baseStyles = css `
|
|
|
592
592
|
opacity: 45%;
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
/* Context-gate states on the session-menu pill (GENC-1567).
|
|
596
|
+
|
|
597
|
+
ONE accent across both levels rather than amber escalating to red. The
|
|
598
|
+
escalation is carried by things that survive without colour vision and read
|
|
599
|
+
on a screen reader: the label changes wording ("Approaching" → "At"), the
|
|
600
|
+
composer gains an outline, and the composer disables with the remedy in its
|
|
601
|
+
placeholder. A colour change would have been the one signal adding nothing
|
|
602
|
+
the others do not already say, while making the composer shout in two hues
|
|
603
|
+
at once.
|
|
604
|
+
|
|
605
|
+
Colour is never the signal on its own here either — the pill swaps ⋮ for a
|
|
606
|
+
warning triangle and grows a label, and its title and aria-label carry the
|
|
607
|
+
full reason. */
|
|
608
|
+
|
|
609
|
+
/* The pill's own label is the loudest part, so it carries the weight the glyph
|
|
610
|
+
alone would not. max-width is widened from the agent pin's 45%: this label
|
|
611
|
+
is a fixed phrase rather than a user-supplied agent name, and ellipsing
|
|
612
|
+
"Approaching context limit" to "Approaching cont…" would lose the noun that
|
|
613
|
+
makes it mean anything. */
|
|
614
|
+
.session-menu-button.context-warn,
|
|
615
|
+
.session-menu-button.context-blocked {
|
|
616
|
+
max-width: 65%;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.session-menu-button.context-warn,
|
|
620
|
+
.session-menu-button.context-warn:hover {
|
|
621
|
+
color: var(--warning-color);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.session-menu-button.context-blocked,
|
|
625
|
+
.session-menu-button.context-blocked:hover {
|
|
626
|
+
color: var(--error-color);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/* The gate disables this button during a turn like any other composer control.
|
|
630
|
+
Left at the shared 45% it reads as "inactive" at exactly the moment the
|
|
631
|
+
conversation is running away, so it keeps more of its presence — still
|
|
632
|
+
clearly not clickable, but not faded out of notice. */
|
|
633
|
+
.session-menu-button.context-warn[disabled],
|
|
634
|
+
.session-menu-button.context-blocked[disabled] {
|
|
635
|
+
opacity: 70%;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* The composer outline (GENC-1567). Drawn from the WARNING onwards, not just at
|
|
639
|
+
the block: the composer is the control the condition is about, and a user who
|
|
640
|
+
only ever sees it appear at the moment sending stops has had no warning where
|
|
641
|
+
they were looking. It escalates amber → red with the state rather than
|
|
642
|
+
appearing from nothing.
|
|
643
|
+
|
|
644
|
+
A border-COLOUR change only: the composer already declares a transparent
|
|
645
|
+
1px border on all four sides so its box metrics are identical in both the
|
|
646
|
+
flush and floating layouts, which means this cannot reflow anything or shift
|
|
647
|
+
the composer as the state flips. */
|
|
648
|
+
.composer.context-warn,
|
|
649
|
+
.composer.context-blocked {
|
|
650
|
+
border-radius: calc(var(--control-corner-radius) * 1px);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.composer.context-warn {
|
|
654
|
+
border-color: var(--warning-color);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.composer.context-blocked {
|
|
658
|
+
border-color: var(--error-color);
|
|
659
|
+
}
|
|
660
|
+
|
|
595
661
|
.composer-pill::part(control) {
|
|
596
662
|
min-width: 0;
|
|
597
663
|
padding: 0;
|
|
@@ -834,6 +900,26 @@ const baseStyles = css `
|
|
|
834
900
|
border-color: var(--neutral-stroke-rest);
|
|
835
901
|
}
|
|
836
902
|
|
|
903
|
+
/* Announced, not drawn (GENC-1567). The context gate has three visible
|
|
904
|
+
expressions already; this keeps the role=status region mounted and
|
|
905
|
+
mutating so the condition is still spoken, which nothing else can do once
|
|
906
|
+
the composer disables and drops out of the tab order.
|
|
907
|
+
|
|
908
|
+
Clipped rather than display:none or visibility:hidden — both of those
|
|
909
|
+
remove the node from the accessibility tree, which would silence the very
|
|
910
|
+
announcement this exists for. */
|
|
911
|
+
.blocked-banner.is-sr-only {
|
|
912
|
+
position: absolute;
|
|
913
|
+
width: 1px;
|
|
914
|
+
height: 1px;
|
|
915
|
+
margin: -1px;
|
|
916
|
+
padding: 0;
|
|
917
|
+
border: 0;
|
|
918
|
+
overflow: hidden;
|
|
919
|
+
clip-path: inset(50%);
|
|
920
|
+
white-space: nowrap;
|
|
921
|
+
}
|
|
922
|
+
|
|
837
923
|
.blocked-banner-text {
|
|
838
924
|
font-size: 0.9em;
|
|
839
925
|
line-height: 1.4;
|
|
@@ -868,6 +954,48 @@ const baseStyles = css `
|
|
|
868
954
|
box-shadow: none;
|
|
869
955
|
}
|
|
870
956
|
|
|
957
|
+
/* At the context limit the placeholder stops being a hint and becomes the only
|
|
958
|
+
visible statement of what is wrong and how to clear it — so it has to be
|
|
959
|
+
readable. The design system dims a disabled control's placeholder towards
|
|
960
|
+
the point of being skipped over, which is right for "Message assistant..."
|
|
961
|
+
and wrong for a sentence the user has to act on. Restored to the normal
|
|
962
|
+
foreground, and left un-dimmed by the disabled treatment. */
|
|
963
|
+
.composer.context-blocked .chat-input::part(control)::placeholder {
|
|
964
|
+
color: var(--neutral-foreground-rest);
|
|
965
|
+
opacity: 100%;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/* And tall enough to actually show it. The placeholder here is two sentences,
|
|
969
|
+
and the input's resting height clipped them — losing the end, which is the
|
|
970
|
+
half that says what to do about it.
|
|
971
|
+
|
|
972
|
+
min-height rather than height, deliberately: it outranks the inline height
|
|
973
|
+
the drag-to-resize handle writes, so a user who had shrunk the composer
|
|
974
|
+
earlier still sees the whole message.
|
|
975
|
+
|
|
976
|
+
Sized from measurement across the width range rather than from one look at
|
|
977
|
+
one panel: the wrap count changes with the container, and a value picked at
|
|
978
|
+
a comfortable width silently clips at a narrow one. Measured need is 76px
|
|
979
|
+
from 400px up to ~560px, and 56px beyond that.
|
|
980
|
+
|
|
981
|
+
**400px is taken as the narrowest supported composer.** Below it the message
|
|
982
|
+
wraps further and this floor stops being enough — it degrades to a clipped
|
|
983
|
+
sentence rather than anything broken, but it is not a width the copy is sized
|
|
984
|
+
for.
|
|
985
|
+
|
|
986
|
+
Past 600px the message fits the composer's natural resting height, so the
|
|
987
|
+
floor is simply removed rather than set to a value it already exceeds. What
|
|
988
|
+
remains is a floor either way: the input still auto-grows past it on input. */
|
|
989
|
+
.composer.context-blocked .chat-input {
|
|
990
|
+
min-height: calc(var(--design-unit) * 20px);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
@container (width > 600px) {
|
|
994
|
+
.composer.context-blocked .chat-input {
|
|
995
|
+
min-height: 0;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
871
999
|
/* Full-width grab bar above the input row; drag up/down to resize the composer.
|
|
872
1000
|
Sits above the row so it never overlaps the send/stop controls. */
|
|
873
1001
|
.composer-resize-handle {
|
|
@@ -268,8 +268,13 @@ const restoringTemplate = html `
|
|
|
268
268
|
*/
|
|
269
269
|
export const blockedBannerClasses = (x) => [
|
|
270
270
|
'blocked-banner',
|
|
271
|
+
// Only a budget wall is DRAWN here. The context gate states itself through
|
|
272
|
+
// the pill label, the composer outline and the placeholder, so a strip of
|
|
273
|
+
// prose on top would be a fourth statement of one condition — but it still
|
|
274
|
+
// has to be announced, hence `is-sr-only` rather than dropping the region.
|
|
271
275
|
x.bannerVisible ? 'is-visible' : '',
|
|
272
276
|
x.bannerVisible && !x.blocked ? 'is-partial' : '',
|
|
277
|
+
x.composerNoticeSrOnly ? 'is-sr-only' : '',
|
|
273
278
|
]
|
|
274
279
|
.filter(Boolean)
|
|
275
280
|
.join(' ');
|
|
@@ -305,7 +310,7 @@ export const BLOCKED_BANNER_ID = 'blocked-banner';
|
|
|
305
310
|
*
|
|
306
311
|
* @internal
|
|
307
312
|
*/
|
|
308
|
-
export const composerDescribedBy = (x) => x.
|
|
313
|
+
export const composerDescribedBy = (x) => x.composerNoticeVisible ? BLOCKED_BANNER_ID : null;
|
|
309
314
|
/**
|
|
310
315
|
* Persistent banner shown above the composer while a backend condition blocks
|
|
311
316
|
* the assistant — today an exhausted AI budget (GENC-1464).
|
|
@@ -345,8 +350,8 @@ const blockedBannerTemplate = html `
|
|
|
345
350
|
part="blocked-banner"
|
|
346
351
|
role="status"
|
|
347
352
|
>
|
|
348
|
-
${when((x) => x.
|
|
349
|
-
<span class="blocked-banner-text">${(x) => x.
|
|
353
|
+
${when((x) => x.composerNoticeVisible, html `
|
|
354
|
+
<span class="blocked-banner-text">${(x) => x.composerNoticeText}</span>
|
|
350
355
|
`)}
|
|
351
356
|
</div>
|
|
352
357
|
`;
|
|
@@ -560,23 +565,38 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
560
565
|
appearance="stealth"
|
|
561
566
|
title=${(x) => { var _a; return `Attach file (${(_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles})`; }}
|
|
562
567
|
aria-label="Attach file"
|
|
563
|
-
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
|
|
568
|
+
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked || x.contextGate === 'blocked'}
|
|
564
569
|
aria-describedby=${composerDescribedBy}
|
|
565
570
|
@click=${(x) => x.triggerFileInput()}
|
|
566
571
|
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
567
572
|
`;
|
|
573
|
+
// The same control carries the context-gate signal (GENC-1567): ⋮ becomes a
|
|
574
|
+
// warning triangle as the window fills, amber at the warning and red once
|
|
575
|
+
// sends are blocked. Deliberately this button and not a new one — it is
|
|
576
|
+
// already the way to reach Compact, and it stays mounted (disabled, not
|
|
577
|
+
// removed) during a turn, so the colour change is visible through exactly the
|
|
578
|
+
// runaway tool loop that causes the problem.
|
|
579
|
+
//
|
|
580
|
+
// While a turn runs the button is disabled, so the notice copy above must not
|
|
581
|
+
// point at it as if it were clickable — see `contextGateReason`.
|
|
568
582
|
const sessionMenuButtonTemplate = html `
|
|
569
583
|
<${buttonTag}
|
|
570
|
-
class="composer-pill session-menu-button
|
|
584
|
+
class="${(x) => classNames('composer-pill', 'session-menu-button',
|
|
585
|
+
// Borrows the agent pin's labelled shape only while the gate is active,
|
|
586
|
+
// so the composer's resting state is unchanged — three equal circular
|
|
587
|
+
// pills — and the label's arrival is itself part of the signal.
|
|
588
|
+
['composer-pill-labelled', x.contextGate !== 'ok'], ['context-warn', x.contextGate === 'warn'], ['context-blocked', x.contextGate === 'blocked'])}"
|
|
571
589
|
part="session-menu-button"
|
|
572
590
|
appearance="stealth"
|
|
573
|
-
title="Session options"
|
|
574
|
-
aria-label="Session options"
|
|
591
|
+
title="${(x) => (x.contextGate === 'ok' ? 'Session options' : x.contextGateReason)}"
|
|
592
|
+
aria-label="${(x) => x.contextGate === 'ok' ? 'Session options' : `Session options — ${x.contextGateReason}`}"
|
|
575
593
|
aria-haspopup="menu"
|
|
576
594
|
aria-expanded=${(x) => (x.sessionMenuOpen ? 'true' : 'false')}
|
|
577
595
|
?disabled=${(x) => x.busy || x.compacting || x.restoring}
|
|
578
596
|
@click=${(x) => x.toggleSessionMenu()}
|
|
579
|
-
|
|
597
|
+
>${when((x) => x.contextGate === 'ok', html `<${iconTag} name="ellipsis"></${iconTag}>`)}${when((x) => x.contextGate !== 'ok', html `<span class="pill-glyph"
|
|
598
|
+
><${iconTag} name="triangle-exclamation"></${iconTag}></span
|
|
599
|
+
><span class="pill-label">${(x) => x.contextGateLabel}</span>`)}</${buttonTag}>
|
|
580
600
|
`;
|
|
581
601
|
// The agent control is LAST in the cluster because it is the only one whose
|
|
582
602
|
// width changes: pinning an agent swaps `AUTO` for a name, and with it in
|
|
@@ -616,7 +636,12 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
616
636
|
<${buttonTag}
|
|
617
637
|
class="send-button"
|
|
618
638
|
part="send-button"
|
|
619
|
-
?disabled=${(x) => x.busy ||
|
|
639
|
+
?disabled=${(x) => x.busy ||
|
|
640
|
+
x.compacting ||
|
|
641
|
+
x.restoring ||
|
|
642
|
+
x.blocked ||
|
|
643
|
+
x.contextGate === 'blocked' ||
|
|
644
|
+
(!x.inputValue.trim() && !x.attachments.length)}
|
|
620
645
|
aria-describedby=${composerDescribedBy}
|
|
621
646
|
@click=${(x) => x.handleSendClick()}
|
|
622
647
|
>Send</${buttonTag}>
|
|
@@ -655,29 +680,32 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
655
680
|
role="menuitem"
|
|
656
681
|
type="button"
|
|
657
682
|
?disabled=${(x) => !x.compactable}
|
|
658
|
-
title=${(x) => x.
|
|
659
|
-
? 'Summarize older messages to free up context'
|
|
660
|
-
: 'Not enough conversation yet to be worth compacting'}
|
|
683
|
+
title=${(x) => x.compactHint}
|
|
661
684
|
@click=${(x) => (x.confirmingCompact = true)}
|
|
662
685
|
>
|
|
663
686
|
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="compress"></${iconTag}></span>
|
|
664
687
|
<span class="menu-item-body">
|
|
665
688
|
<span class="menu-item-name">Compact</span>
|
|
666
|
-
<span class="menu-item-hint"
|
|
667
|
-
</span>
|
|
668
|
-
</button>
|
|
669
|
-
<button
|
|
670
|
-
class="menu-item"
|
|
671
|
-
role="menuitem"
|
|
672
|
-
type="button"
|
|
673
|
-
@click=${(x) => (x.confirmingClear = true)}
|
|
674
|
-
>
|
|
675
|
-
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="trash"></${iconTag}></span>
|
|
676
|
-
<span class="menu-item-body">
|
|
677
|
-
<span class="menu-item-name">Clear</span>
|
|
678
|
-
<span class="menu-item-hint">Start a new conversation</span>
|
|
689
|
+
<span class="menu-item-hint">${(x) => x.compactHint}</span>
|
|
679
690
|
</span>
|
|
680
691
|
</button>
|
|
692
|
+
${when(
|
|
693
|
+
// Clear is withheld from a menu the context gate forced open — but only
|
|
694
|
+
// while Compact is a real escape. See `clearWithheldByContext`.
|
|
695
|
+
(x) => !x.clearWithheldByContext, html `
|
|
696
|
+
<button
|
|
697
|
+
class="menu-item"
|
|
698
|
+
role="menuitem"
|
|
699
|
+
type="button"
|
|
700
|
+
@click=${(x) => (x.confirmingClear = true)}
|
|
701
|
+
>
|
|
702
|
+
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="trash"></${iconTag}></span>
|
|
703
|
+
<span class="menu-item-body">
|
|
704
|
+
<span class="menu-item-name">Clear</span>
|
|
705
|
+
<span class="menu-item-hint">Start a new conversation</span>
|
|
706
|
+
</span>
|
|
707
|
+
</button>
|
|
708
|
+
`)}
|
|
681
709
|
`;
|
|
682
710
|
// The hint is not decoration. Every other row in this menu has one, and the
|
|
683
711
|
// panel is anchored by its BOTTOM edge to the button: a hintless Cancel makes
|
|
@@ -891,7 +919,10 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
891
919
|
@pointercancel=${(x, c) => x.onComposerResizeEnd(c.event)}
|
|
892
920
|
></div>
|
|
893
921
|
`)}
|
|
894
|
-
<div
|
|
922
|
+
<div
|
|
923
|
+
class="${(x) => classNames('composer', ['context-warn', x.contextGate === 'warn'], ['context-blocked', x.contextGate === 'blocked'])}"
|
|
924
|
+
part="composer"
|
|
925
|
+
>
|
|
895
926
|
<${textareaTag}
|
|
896
927
|
${ref('chatInputEl')}
|
|
897
928
|
class="chat-input"
|
|
@@ -899,9 +930,13 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
899
930
|
style=${(x) => x.composerHeightStyle}
|
|
900
931
|
placeholder=${(x) => x.effectivePlaceholder}
|
|
901
932
|
:value=${(x) => x.inputValue}
|
|
902
|
-
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
|
|
903
|
-
aria-disabled=${(x) => (x.blocked ? 'true' : null)}
|
|
904
|
-
aria-label=${(x) =>
|
|
933
|
+
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked || x.contextGate === 'blocked'}
|
|
934
|
+
aria-disabled=${(x) => (x.blocked || x.contextGate === 'blocked' ? 'true' : null)}
|
|
935
|
+
aria-label=${(x) => x.blocked
|
|
936
|
+
? x.effectiveBlockedReason
|
|
937
|
+
: x.contextGate === 'blocked'
|
|
938
|
+
? x.contextGateReason
|
|
939
|
+
: null}
|
|
905
940
|
aria-describedby=${composerDescribedBy}
|
|
906
941
|
@input=${(x, c) => (x.inputValue = c.event.target.value)}
|
|
907
942
|
@keydown=${(x, c) => {
|
|
@@ -219,7 +219,7 @@ export const DEBUG_LOG_README = [
|
|
|
219
219
|
"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
220
220
|
"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|
|
221
221
|
"Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
|
|
222
|
-
'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused; budget_exhausted and
|
|
222
|
+
'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused/context-exhausted, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, for context-exhausted the estimated contextTokens against the guard that stopped the turn plus the iterations reached — the driver ending a runaway tool loop at the edge of the context window rather than issuing a request the provider would reject, so the transcript survives intact and can still be compacted, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused/context_exhausted; budget_exhausted, provider_refused and context_exhausted are all terminal for the PARENT turn too — the parent stops rather than calling the model again into the same wall), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (the resolved provider for the upcoming turns — detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
|
|
223
223
|
'Sub-agent meta events: a sub-agent\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `"<parent> › <sub-agent>"` breadcrumb that composes when nested (e.g. `"UI Builder › Planner › Grounding"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\'s span.',
|
|
224
224
|
"`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, contextTokens/contextLimit/contextUsagePercent for the last call, and the session totals — sessionCostUsd, sessionTokensConsumed, and the four-bucket sessionUsage lifted to the top of this log), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
|
|
225
225
|
'Note the two different scopes in `meta.context`: `contextTokens` is the prompt size of the LAST call (against `contextLimit`, the model context window), while `sessionUsage`/`sessionTokensConsumed` are cumulative BILLED throughput. Every turn resends the conversation, so the cumulative figure counts each turn’s prompt again in the next turn’s and is expected to dwarf the context size — that is not double-counting.',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { __rest } from "tslib";
|
|
2
|
+
import { APPROX_CHARS_PER_TOKEN } from './context-tokens';
|
|
2
3
|
/**
|
|
3
4
|
* Tool-context condensation: collapse stale tool payloads out of the history
|
|
4
5
|
* sent to the model, while leaving stored history untouched.
|
|
@@ -21,11 +22,6 @@ import { __rest } from "tslib";
|
|
|
21
22
|
export const CONDENSE_MIN_CHARS = 1000;
|
|
22
23
|
/** Key the collapsed args are stored under — a tool-call's args must stay a Record. */
|
|
23
24
|
export const CONDENSED_ARGS_KEY = 'condensed';
|
|
24
|
-
/**
|
|
25
|
-
* Rough chars-per-token divisor for the `tokensSaved` estimate. No tokenizer is
|
|
26
|
-
* available in this stack; ~4 chars/token is the usual English approximation.
|
|
27
|
-
*/
|
|
28
|
-
const APPROX_CHARS_PER_TOKEN = 4;
|
|
29
25
|
/** Short human-readable reason for the stub text, per fired trigger. */
|
|
30
26
|
function triggerReason(trigger) {
|
|
31
27
|
switch (trigger.kind) {
|