@genesislcap/ai-assistant 15.19.0 → 15.19.1
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/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core';
|
|
2
|
-
import {
|
|
3
|
-
AI_COLOUR_AMBER,
|
|
4
|
-
AI_COLOUR_CYAN,
|
|
5
|
-
AI_COLOUR_PINK,
|
|
6
|
-
AI_COLOUR_VIOLET,
|
|
7
|
-
} from '../styles/ai-colours';
|
|
8
|
-
|
|
9
|
-
const HALO_DEFAULT_SPEED = 1.5;
|
|
10
|
-
const HALO_DEFAULT_BORDER_SIZE = 3;
|
|
11
|
-
const HALO_DEFAULT_GLOW_OPACITY = 0.35;
|
|
12
|
-
const HALO_DEFAULT_GLOW_SPREAD = 70;
|
|
13
|
-
/** Milliseconds in one second. */
|
|
14
|
-
const MS_PER_SECOND = 1000;
|
|
15
|
-
/** Baseline frame rate (fps) the `speed` (degrees/frame) is calibrated against. */
|
|
16
|
-
const BASELINE_FPS = 60;
|
|
17
|
-
/**
|
|
18
|
-
* Frame duration (ms) at the baseline rate. Rotation advances by the elapsed
|
|
19
|
-
* wall-clock time divided by this — not a fixed step per rAF callback — so it
|
|
20
|
-
* holds a constant real-world speed regardless of the display's refresh rate or
|
|
21
|
-
* dropped frames.
|
|
22
|
-
*/
|
|
23
|
-
const FRAME_MS = MS_PER_SECOND / BASELINE_FPS;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Animated halo overlay — rotating conic-gradient border with an inward glow.
|
|
27
|
-
*
|
|
28
|
-
* Drop inside any `position: relative; overflow: hidden` container and toggle
|
|
29
|
-
* the `active` attribute to show/hide the effect.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```html
|
|
33
|
-
* <ai-halo-overlay ?active="${(x) => x.isLoading}"></ai-halo-overlay>
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* @beta
|
|
37
|
-
*/
|
|
38
|
-
@customElement({
|
|
39
|
-
name: 'ai-halo-overlay',
|
|
40
|
-
template: html`
|
|
41
|
-
<slot></slot>
|
|
42
|
-
`,
|
|
43
|
-
styles: css`
|
|
44
|
-
:host {
|
|
45
|
-
display: block;
|
|
46
|
-
position: absolute;
|
|
47
|
-
inset: 0;
|
|
48
|
-
border-radius: inherit;
|
|
49
|
-
pointer-events: none;
|
|
50
|
-
z-index: 1000;
|
|
51
|
-
opacity: 0;
|
|
52
|
-
transition: opacity 0.4s ease;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
:host([active]) {
|
|
56
|
-
opacity: 1;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* Rotating solid border */
|
|
60
|
-
:host::before {
|
|
61
|
-
content: '';
|
|
62
|
-
position: absolute;
|
|
63
|
-
inset: 0;
|
|
64
|
-
border-radius: inherit;
|
|
65
|
-
padding: var(--halo-border-size, 3px);
|
|
66
|
-
background: repeating-conic-gradient(
|
|
67
|
-
from var(--halo-angle, 0deg),
|
|
68
|
-
${AI_COLOUR_AMBER},
|
|
69
|
-
${AI_COLOUR_PINK},
|
|
70
|
-
${AI_COLOUR_CYAN},
|
|
71
|
-
${AI_COLOUR_VIOLET},
|
|
72
|
-
${AI_COLOUR_AMBER}
|
|
73
|
-
);
|
|
74
|
-
-webkit-mask:
|
|
75
|
-
linear-gradient(#000 0 0) content-box,
|
|
76
|
-
linear-gradient(#000 0 0);
|
|
77
|
-
mask:
|
|
78
|
-
linear-gradient(#000 0 0) content-box,
|
|
79
|
-
linear-gradient(#000 0 0);
|
|
80
|
-
-webkit-mask-composite: destination-out;
|
|
81
|
-
mask-composite: exclude;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* Inward glow — radial mask makes centre transparent, edges coloured */
|
|
85
|
-
:host::after {
|
|
86
|
-
content: '';
|
|
87
|
-
position: absolute;
|
|
88
|
-
inset: 0;
|
|
89
|
-
border-radius: inherit;
|
|
90
|
-
background: repeating-conic-gradient(
|
|
91
|
-
from var(--halo-angle, 0deg),
|
|
92
|
-
${AI_COLOUR_AMBER},
|
|
93
|
-
${AI_COLOUR_PINK},
|
|
94
|
-
${AI_COLOUR_CYAN},
|
|
95
|
-
${AI_COLOUR_VIOLET},
|
|
96
|
-
${AI_COLOUR_AMBER}
|
|
97
|
-
);
|
|
98
|
-
opacity: var(--halo-glow-opacity, 0.35);
|
|
99
|
-
-webkit-mask-image: radial-gradient(
|
|
100
|
-
ellipse at center,
|
|
101
|
-
transparent var(--halo-glow-spread, 70%),
|
|
102
|
-
black 100%
|
|
103
|
-
);
|
|
104
|
-
mask-image: radial-gradient(
|
|
105
|
-
ellipse at center,
|
|
106
|
-
transparent var(--halo-glow-spread, 70%),
|
|
107
|
-
black 100%
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
`,
|
|
111
|
-
})
|
|
112
|
-
export class AiHaloOverlay extends GenesisElement {
|
|
113
|
-
@attr({ mode: 'boolean' }) active: boolean = false;
|
|
114
|
-
/** Rotation speed in degrees per frame. Default: 1.5 (≈ 4 s per full revolution at 60 fps). */
|
|
115
|
-
@attr({ converter: { fromView: Number, toView: String } }) speed: number = HALO_DEFAULT_SPEED;
|
|
116
|
-
/** Rotation direction. Default: 'cw' (clockwise). */
|
|
117
|
-
@attr direction: 'cw' | 'ccw' = 'cw';
|
|
118
|
-
/** Border thickness in px. Default: 3. */
|
|
119
|
-
@attr({ attribute: 'border-size', converter: { fromView: Number, toView: String } })
|
|
120
|
-
borderSize: number = HALO_DEFAULT_BORDER_SIZE;
|
|
121
|
-
/** Glow layer opacity (0–1). Default: 0.35. */
|
|
122
|
-
@attr({ attribute: 'glow-opacity', converter: { fromView: Number, toView: String } })
|
|
123
|
-
glowOpacity: number = HALO_DEFAULT_GLOW_OPACITY;
|
|
124
|
-
/** Transparent stop of the radial glow mask as a percentage (0–100). Higher = less spread. Default: 70. */
|
|
125
|
-
@attr({ attribute: 'glow-spread', converter: { fromView: Number, toView: String } })
|
|
126
|
-
glowSpread: number = HALO_DEFAULT_GLOW_SPREAD;
|
|
127
|
-
|
|
128
|
-
borderSizeChanged() {
|
|
129
|
-
this.style.setProperty('--halo-border-size', `${this.borderSize}px`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
glowOpacityChanged() {
|
|
133
|
-
this.style.setProperty('--halo-glow-opacity', String(this.glowOpacity));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
glowSpreadChanged() {
|
|
137
|
-
this.style.setProperty('--halo-glow-spread', `${this.glowSpread}%`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// TODO: The rAF loop is fine for demos but has two drawbacks vs a pure CSS @property animation:
|
|
141
|
-
// 1. It runs on the main thread, so heavy JS work can cause the animation to stutter.
|
|
142
|
-
// 2. The loop ticks even when `active` is false (idle cost is negligible but non-zero).
|
|
143
|
-
// Once @property inside Shadow DOM has solid cross-browser support, consider switching back
|
|
144
|
-
// to a CSS @keyframes approach and removing connectedCallback/disconnectedCallback/tick().
|
|
145
|
-
|
|
146
|
-
private static readonly FULL_ROTATION_DEG = 360;
|
|
147
|
-
|
|
148
|
-
private angle = 0;
|
|
149
|
-
private lastTime?: number;
|
|
150
|
-
private animFrame?: number;
|
|
151
|
-
|
|
152
|
-
connectedCallback() {
|
|
153
|
-
super.connectedCallback();
|
|
154
|
-
this.tick();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
disconnectedCallback() {
|
|
158
|
-
super.disconnectedCallback();
|
|
159
|
-
if (this.animFrame !== undefined) {
|
|
160
|
-
cancelAnimationFrame(this.animFrame);
|
|
161
|
-
this.animFrame = undefined;
|
|
162
|
-
}
|
|
163
|
-
// Reset the clock so a later reconnect doesn't apply one huge time delta.
|
|
164
|
-
this.lastTime = undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private tick(now?: number) {
|
|
168
|
-
// Advance by elapsed wall-clock time (expressed in 60fps-equivalent frames)
|
|
169
|
-
// rather than a fixed step per callback, so the rotation runs at a constant
|
|
170
|
-
// real-world speed regardless of the display's refresh rate or dropped
|
|
171
|
-
// frames. The first tick just seeds the clock (no delta yet).
|
|
172
|
-
if (this.lastTime !== undefined && now !== undefined) {
|
|
173
|
-
const frames = (now - this.lastTime) / FRAME_MS;
|
|
174
|
-
const step = this.direction === 'ccw' ? -this.speed : this.speed;
|
|
175
|
-
const raw = this.angle + step * frames;
|
|
176
|
-
this.angle =
|
|
177
|
-
((raw % AiHaloOverlay.FULL_ROTATION_DEG) + AiHaloOverlay.FULL_ROTATION_DEG) %
|
|
178
|
-
AiHaloOverlay.FULL_ROTATION_DEG;
|
|
179
|
-
this.style.setProperty('--halo-angle', `${this.angle}deg`);
|
|
180
|
-
}
|
|
181
|
-
this.lastTime = now;
|
|
182
|
-
this.animFrame = requestAnimationFrame((t) => this.tick(t));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { OrchestratingDriver } from './orchestrating-driver';
|
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AIProvider,
|
|
3
|
-
AIProviderRegistry,
|
|
4
|
-
ChatDriverResult,
|
|
5
|
-
ChatMessage,
|
|
6
|
-
ChatRequestOptions,
|
|
7
|
-
} from '@genesislcap/foundation-ai';
|
|
8
|
-
import { BudgetExhaustedError } from '@genesislcap/foundation-ai';
|
|
9
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
10
|
-
import { AgenticActivityBus } from '../../channel/ai-activity-bus';
|
|
11
|
-
import type { AgentConfig } from '../../config/config';
|
|
12
|
-
// Side-effect import — MUST precede `./orchestrating-driver` so the driver
|
|
13
|
-
// subclasses jsdom's EventTarget (CustomEvent dispatch then works in node).
|
|
14
|
-
import '../chat-driver/align-event-globals';
|
|
15
|
-
import { OrchestratingDriver } from './orchestrating-driver';
|
|
16
|
-
|
|
17
|
-
// GENC-1464 — the orchestrated turn's OUTCOME.
|
|
18
|
-
//
|
|
19
|
-
// `createDriver` returns an `OrchestratingDriver` for every agents-configured
|
|
20
|
-
// host, and its `sendMessage` used to end with a hardcoded `return { reason:
|
|
21
|
-
// 'done' }`. That made every typed failure unreachable for those hosts —
|
|
22
|
-
// including `'budget-exhausted'`, which the assistant element latches its blocked
|
|
23
|
-
// state off. A 402 therefore produced an apology over a still-enabled composer
|
|
24
|
-
// for exactly the configurations this workstream exists to fix.
|
|
25
|
-
//
|
|
26
|
-
// The classifier compounds it: `classify()` is a SECOND retry ladder stacked on
|
|
27
|
-
// the transport's, so an untyped catch re-issued the 402 `classifierRetries + 1`
|
|
28
|
-
// times and then ran a fallback turn into the same wall.
|
|
29
|
-
|
|
30
|
-
const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
|
|
31
|
-
get: () => provider,
|
|
32
|
-
default: () => provider,
|
|
33
|
-
defaultName: () => 'test',
|
|
34
|
-
names: () => ['test'],
|
|
35
|
-
getStatus: async () => null,
|
|
36
|
-
listStatuses: async () => [],
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
|
|
40
|
-
({ description: 'test agent', ...overrides }) as AgentConfig;
|
|
41
|
-
|
|
42
|
-
/** A provider that ends every turn immediately with a plain-text reply (no tools). */
|
|
43
|
-
const okProvider = (): AIProvider =>
|
|
44
|
-
({
|
|
45
|
-
chat: async (): Promise<ChatMessage> => ({ role: 'assistant', content: 'ok' }),
|
|
46
|
-
}) as AIProvider;
|
|
47
|
-
|
|
48
|
-
/** A provider that counts calls and always refuses for budget. */
|
|
49
|
-
const walledProvider = (
|
|
50
|
-
vendorLabel = 'Anthropic',
|
|
51
|
-
): { calls: () => number; provider: AIProvider } => {
|
|
52
|
-
let n = 0;
|
|
53
|
-
return {
|
|
54
|
-
calls: () => n,
|
|
55
|
-
provider: {
|
|
56
|
-
chat: async (): Promise<ChatMessage> => {
|
|
57
|
-
n += 1;
|
|
58
|
-
throw new BudgetExhaustedError(vendorLabel, 25, 25.4);
|
|
59
|
-
},
|
|
60
|
-
} as AIProvider,
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const failureReasonOf = (r: ChatDriverResult) =>
|
|
65
|
-
r.reason === 'done' ? r.failureReason : undefined;
|
|
66
|
-
|
|
67
|
-
const Suite = createLogicSuite('orchestrating-driver budget wall');
|
|
68
|
-
|
|
69
|
-
// ── 1. The showstopper: a walled AGENT turn must reach the caller ───────────
|
|
70
|
-
|
|
71
|
-
Suite('a budget wall on the agent turn surfaces as the orchestrated result', async () => {
|
|
72
|
-
// ONE specialist and no fallback, so `classify` short-circuits without calling
|
|
73
|
-
// the provider — the wall is hit by the AGENT turn, inside a real tool loop.
|
|
74
|
-
// Against the old hardcoded return this deep-equal fails.
|
|
75
|
-
const { provider } = walledProvider();
|
|
76
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
77
|
-
sessionKey: 'budget-1',
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const result = await driver.sendMessage('go');
|
|
81
|
-
|
|
82
|
-
assert.equal(result, {
|
|
83
|
-
reason: 'done',
|
|
84
|
-
failureReason: 'budget-exhausted',
|
|
85
|
-
budget: { budgetUsd: 25, spentUsd: 25.4, vendorLabel: 'Anthropic', vendor: 'anthropic' },
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
Suite("the vendor is the refusing transport's, not a hardcoded default", async () => {
|
|
90
|
-
// The label rides the contract; `vendor` is its normalised key. Without a
|
|
91
|
-
// second vendor exercised here, a constant would pass every other assertion in
|
|
92
|
-
// this file — and per-vendor blocking would wall the wrong vendor forever.
|
|
93
|
-
const { provider } = walledProvider('Gemini');
|
|
94
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
95
|
-
sessionKey: 'budget-gemini',
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const result = await driver.sendMessage('go');
|
|
99
|
-
|
|
100
|
-
assert.equal(result.reason === 'done' ? result.budget : undefined, {
|
|
101
|
-
budgetUsd: 25,
|
|
102
|
-
spentUsd: 25.4,
|
|
103
|
-
vendorLabel: 'Gemini',
|
|
104
|
-
vendor: 'gemini',
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
Suite('an unrecognised vendor label carries no typed vendor at all', async () => {
|
|
109
|
-
// Attribution degrades to "unattributable" rather than to a wrong vendor —
|
|
110
|
-
// which makes the element fall back to its vendor-agnostic block. Safe.
|
|
111
|
-
const { provider } = walledProvider('Acme AI');
|
|
112
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
113
|
-
sessionKey: 'budget-unknown-vendor',
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const result = await driver.sendMessage('go');
|
|
117
|
-
|
|
118
|
-
assert.equal(result.reason === 'done' ? result.budget : undefined, {
|
|
119
|
-
budgetUsd: 25,
|
|
120
|
-
spentUsd: 25.4,
|
|
121
|
-
vendorLabel: 'Acme AI',
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
Suite('the wall also reaches the tool-loop-end bus detail through the orchestrator', async () => {
|
|
126
|
-
const bus = new AgenticActivityBus();
|
|
127
|
-
const seen: Array<{ failureReason?: string } | undefined> = [];
|
|
128
|
-
const stop = bus.subscribe('tool-loop-end', (d) => seen.push(d));
|
|
129
|
-
const { provider } = walledProvider();
|
|
130
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
131
|
-
sessionKey: 'budget-2',
|
|
132
|
-
activityBus: bus,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
await driver.sendMessage('go');
|
|
136
|
-
|
|
137
|
-
assert.is(seen.length, 1, 'exactly one tool-loop-end for the one agent turn');
|
|
138
|
-
assert.is(seen[0]?.failureReason, 'budget-exhausted');
|
|
139
|
-
stop();
|
|
140
|
-
bus.close();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
Suite('the bus detail carries the proxy figures, not just the failure reason', async () => {
|
|
144
|
-
// The element latches off THIS event for an in-loop wall — the publish sits in
|
|
145
|
-
// sendMessage's `finally`, so it lands before the return-value seam and the
|
|
146
|
-
// idempotent latch makes the later one a no-op. If the figures rode only the
|
|
147
|
-
// return value they would be unreachable in the common case and the banner would
|
|
148
|
-
// show the generic copy in exactly the path they were added for.
|
|
149
|
-
const bus = new AgenticActivityBus();
|
|
150
|
-
const seen: Array<
|
|
151
|
-
| {
|
|
152
|
-
failureReason?: string;
|
|
153
|
-
vendor?: string;
|
|
154
|
-
budget?: {
|
|
155
|
-
budgetUsd?: number;
|
|
156
|
-
spentUsd?: number;
|
|
157
|
-
vendorLabel: string;
|
|
158
|
-
vendor?: string;
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
| undefined
|
|
162
|
-
> = [];
|
|
163
|
-
const stop = bus.subscribe('tool-loop-end', (d) => seen.push(d));
|
|
164
|
-
const { provider } = walledProvider();
|
|
165
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
166
|
-
sessionKey: 'budget-figures',
|
|
167
|
-
activityBus: bus,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const result = await driver.sendMessage('go');
|
|
171
|
-
|
|
172
|
-
assert.equal(
|
|
173
|
-
seen[0]?.budget,
|
|
174
|
-
{ budgetUsd: 25, spentUsd: 25.4, vendorLabel: 'Anthropic', vendor: 'anthropic' },
|
|
175
|
-
'the event carries the same figures as the return value',
|
|
176
|
-
);
|
|
177
|
-
assert.is(
|
|
178
|
-
seen[0]?.vendor,
|
|
179
|
-
'anthropic',
|
|
180
|
-
'and the top-level vendor agrees with the refusing transport, not with a stale last-resolved provider',
|
|
181
|
-
);
|
|
182
|
-
assert.equal(
|
|
183
|
-
result.reason === 'done' ? result.budget : undefined,
|
|
184
|
-
seen[0]?.budget,
|
|
185
|
-
'event and return value agree — neither is the sole carrier',
|
|
186
|
-
);
|
|
187
|
-
stop();
|
|
188
|
-
bus.close();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
Suite('a clean turn publishes no budget key at all', async () => {
|
|
192
|
-
const bus = new AgenticActivityBus();
|
|
193
|
-
const seen: Array<{ budget?: unknown } | undefined> = [];
|
|
194
|
-
const stop = bus.subscribe('tool-loop-end', (d) => seen.push(d));
|
|
195
|
-
const provider: AIProvider = {
|
|
196
|
-
chat: async () => ({ role: 'assistant', content: 'ok' }) as ChatMessage,
|
|
197
|
-
} as unknown as AIProvider;
|
|
198
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
199
|
-
sessionKey: 'budget-clean',
|
|
200
|
-
activityBus: bus,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
await driver.sendMessage('go');
|
|
204
|
-
|
|
205
|
-
assert.ok(
|
|
206
|
-
seen.every((d) => d === undefined || !('budget' in d)),
|
|
207
|
-
'no budget key on a non-budget turn — the historical detail shape is unchanged',
|
|
208
|
-
);
|
|
209
|
-
stop();
|
|
210
|
-
bus.close();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ── 2. No regression on the happy / handoff paths ───────────────────────────
|
|
214
|
-
|
|
215
|
-
Suite('a clean turn still returns the bare legacy shape', async () => {
|
|
216
|
-
const driver = new OrchestratingDriver(makeRegistry(okProvider()), [agent({ name: 'Solo' })], {
|
|
217
|
-
sessionKey: 'budget-3',
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const result = await driver.sendMessage('go');
|
|
221
|
-
|
|
222
|
-
assert.equal(result, { reason: 'done' }, 'no failureReason on a clean turn');
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
Suite('a handoff-capped turn never leaks agent-handoff to the caller', async () => {
|
|
226
|
-
// `'agent-handoff'` is an internal routing protocol. The two breaks that hold a
|
|
227
|
-
// non-'done' inner result both carry it, so the collapse rule in `sendMessage`
|
|
228
|
-
// is what keeps "report the turn that ended the loop" from changing the
|
|
229
|
-
// discriminant a host matches on.
|
|
230
|
-
let n = 0;
|
|
231
|
-
const nextId = () => {
|
|
232
|
-
n += 1;
|
|
233
|
-
return n;
|
|
234
|
-
};
|
|
235
|
-
const handoffProvider: AIProvider = {
|
|
236
|
-
chat: async (
|
|
237
|
-
_h: ChatMessage[],
|
|
238
|
-
_u: string,
|
|
239
|
-
options?: ChatRequestOptions,
|
|
240
|
-
): Promise<ChatMessage> => {
|
|
241
|
-
// The classifier call carries the routing tool; answer it with index 0.
|
|
242
|
-
if (options?.tools?.some((t) => t.name === 'select_agent')) {
|
|
243
|
-
return {
|
|
244
|
-
role: 'assistant',
|
|
245
|
-
content: '',
|
|
246
|
-
toolCalls: [{ id: `c-${nextId()}`, name: 'select_agent', args: { agent_index: 0 } }],
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
// Every agent turn asks to hand off, so the handoff cap is what ends it.
|
|
250
|
-
return {
|
|
251
|
-
role: 'assistant',
|
|
252
|
-
content: '',
|
|
253
|
-
toolCalls: [
|
|
254
|
-
{
|
|
255
|
-
id: `h-${nextId()}`,
|
|
256
|
-
name: 'request_continuation',
|
|
257
|
-
args: { summary: 's', remaining_task: 'more' },
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
};
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
const driver = new OrchestratingDriver(
|
|
264
|
-
makeRegistry(handoffProvider),
|
|
265
|
-
[agent({ name: 'A' }), agent({ name: 'B' })],
|
|
266
|
-
{ sessionKey: 'budget-4', maxHandoffs: 1 },
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
const result = await driver.sendMessage('go');
|
|
270
|
-
|
|
271
|
-
assert.is(result.reason, 'done', 'the internal handoff protocol never escapes');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
Suite('a wall on a post-handoff classify does not duplicate the user message', async () => {
|
|
275
|
-
// `classify()` runs at TWO seams. Before the first agent turn the user's
|
|
276
|
-
// message is unappended (only the optimistic `history-updated` echo exists),
|
|
277
|
-
// so the wall-catch must append it. At the END of a handoff iteration
|
|
278
|
-
// `chatDriver.sendMessage` has already appended it — passing it again
|
|
279
|
-
// produced ['user','assistant','tool','user','assistant'] with 'go' twice,
|
|
280
|
-
// persisting into the snapshot and into the history sent to the model once
|
|
281
|
-
// the budget was raised. The single-specialist narrowness guard below cannot
|
|
282
|
-
// catch this: with one specialist, classify short-circuits and the handoff
|
|
283
|
-
// seam never runs.
|
|
284
|
-
let n = 0;
|
|
285
|
-
const provider: AIProvider = {
|
|
286
|
-
chat: async (
|
|
287
|
-
_h: ChatMessage[],
|
|
288
|
-
_u: string,
|
|
289
|
-
options?: ChatRequestOptions,
|
|
290
|
-
): Promise<ChatMessage> => {
|
|
291
|
-
n += 1;
|
|
292
|
-
if (options?.tools?.some((t) => t.name === 'select_agent')) {
|
|
293
|
-
// First classify routes; the post-handoff classify hits the wall.
|
|
294
|
-
if (n > 2) throw new BudgetExhaustedError('Anthropic', 25, 25.4);
|
|
295
|
-
return {
|
|
296
|
-
role: 'assistant',
|
|
297
|
-
content: '',
|
|
298
|
-
toolCalls: [{ id: `c-${n}`, name: 'select_agent', args: { agent_index: 0 } }],
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
// The agent turn asks to hand off, forcing the second classify.
|
|
302
|
-
return {
|
|
303
|
-
role: 'assistant',
|
|
304
|
-
content: '',
|
|
305
|
-
toolCalls: [
|
|
306
|
-
{
|
|
307
|
-
id: `h-${n}`,
|
|
308
|
-
name: 'request_continuation',
|
|
309
|
-
args: { summary: 's', remaining_task: 'more' },
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
};
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
const driver = new OrchestratingDriver(
|
|
316
|
-
makeRegistry(provider),
|
|
317
|
-
[agent({ name: 'A' }), agent({ name: 'B' })],
|
|
318
|
-
{ sessionKey: 'budget-4b' },
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const result = await driver.sendMessage('go');
|
|
322
|
-
|
|
323
|
-
assert.is(failureReasonOf(result), 'budget-exhausted');
|
|
324
|
-
const history = driver.getRawHistory() as ChatMessage[];
|
|
325
|
-
const userMessages = history.filter((m) => m.role === 'user' && m.content === 'go');
|
|
326
|
-
assert.is(userMessages.length, 1, "the user's message appears exactly once");
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// ── 3. The classifier's own retry ladder ───────────────────────────────────
|
|
330
|
-
|
|
331
|
-
Suite('a wall during classification costs exactly one provider call', async () => {
|
|
332
|
-
// TWO specialists and no fallback, so `classify` really runs. Before the narrow
|
|
333
|
-
// rethrow this was FOUR calls: `classifierRetries + 1` classifier attempts, then
|
|
334
|
-
// a doomed agent turn.
|
|
335
|
-
const { calls, provider } = walledProvider();
|
|
336
|
-
const driver = new OrchestratingDriver(
|
|
337
|
-
makeRegistry(provider),
|
|
338
|
-
[agent({ name: 'A' }), agent({ name: 'B' })],
|
|
339
|
-
{ sessionKey: 'budget-5' },
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
const result = await driver.sendMessage('go');
|
|
343
|
-
|
|
344
|
-
assert.is(calls(), 1, 'no ladder clears a budget wall — do not climb it');
|
|
345
|
-
assert.is(failureReasonOf(result), 'budget-exhausted');
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
Suite('a classify-time wall lands on the transcript, without a routing apology', async () => {
|
|
349
|
-
// The old ordering emitted "I'm not sure how to help with that…" from the
|
|
350
|
-
// no-fallback exit and THEN routed anyway, so the user read a routing excuse
|
|
351
|
-
// the classifier never earned, immediately above the real error.
|
|
352
|
-
const { provider } = walledProvider();
|
|
353
|
-
const driver = new OrchestratingDriver(
|
|
354
|
-
makeRegistry(provider),
|
|
355
|
-
[agent({ name: 'A' }), agent({ name: 'B' })],
|
|
356
|
-
{ sessionKey: 'budget-6' },
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
await driver.sendMessage('go');
|
|
360
|
-
|
|
361
|
-
const history = driver.getRawHistory() as ChatMessage[];
|
|
362
|
-
const text = history.map((m) => m.content).join('\n');
|
|
363
|
-
assert.ok(text.includes('AI usage limit'), 'the real reason is what the user reads');
|
|
364
|
-
assert.not.ok(text.includes("I'm not sure how to help with that"));
|
|
365
|
-
// The user's own message must survive. `runOrchestratedTurn` dispatches it
|
|
366
|
-
// optimistically and leaves the real append to `chatDriver.sendMessage`, which
|
|
367
|
-
// a classify-time wall never reaches — so the driver's history held only the
|
|
368
|
-
// assistant bubble and the next render dropped what the user had typed.
|
|
369
|
-
assert.equal(
|
|
370
|
-
history.map((m) => m.role),
|
|
371
|
-
['user', 'assistant'],
|
|
372
|
-
'the question stays above the answer',
|
|
373
|
-
);
|
|
374
|
-
assert.is(history[0].content, 'go');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
Suite('an in-loop wall does not double-append the user message', async () => {
|
|
378
|
-
// Narrowness guard for the append above: `reportBudgetExhausted` also serves
|
|
379
|
-
// the in-loop path, where `sendMessage` has ALREADY appended the user message.
|
|
380
|
-
// Only the classify seam passes one, so this path must be unchanged. ONE
|
|
381
|
-
// specialist and no fallback, so `classify` short-circuits and the wall is hit
|
|
382
|
-
// inside a real tool loop.
|
|
383
|
-
const { provider } = walledProvider();
|
|
384
|
-
const driver = new OrchestratingDriver(makeRegistry(provider), [agent({ name: 'Solo' })], {
|
|
385
|
-
sessionKey: 'budget-6b',
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
await driver.sendMessage('go');
|
|
389
|
-
|
|
390
|
-
const history = driver.getRawHistory() as ChatMessage[];
|
|
391
|
-
assert.is(history.filter((m) => m.role === 'user').length, 1, 'one user message, not two');
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
Suite('a wall during classification never starts the fallback turn', async () => {
|
|
395
|
-
const { calls, provider } = walledProvider();
|
|
396
|
-
const driver = new OrchestratingDriver(
|
|
397
|
-
makeRegistry(provider),
|
|
398
|
-
[agent({ name: 'A' }), agent({ name: 'B' }), agent({ name: 'Catch-all', fallback: true })],
|
|
399
|
-
{ sessionKey: 'budget-7' },
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const result = await driver.sendMessage('go');
|
|
403
|
-
|
|
404
|
-
assert.is(calls(), 1, 'the fallback agent is not run into the same wall');
|
|
405
|
-
assert.is(failureReasonOf(result), 'budget-exhausted');
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
Suite('a plain classifier failure still retries and falls back (narrowness guard)', async () => {
|
|
409
|
-
// Proves the rethrow did not make every classify failure terminal: only the
|
|
410
|
-
// budget wall short-circuits, because only the budget wall is unclearable.
|
|
411
|
-
let classifierCalls = 0;
|
|
412
|
-
const provider: AIProvider = {
|
|
413
|
-
chat: async (
|
|
414
|
-
_h: ChatMessage[],
|
|
415
|
-
_u: string,
|
|
416
|
-
options?: ChatRequestOptions,
|
|
417
|
-
): Promise<ChatMessage> => {
|
|
418
|
-
if (options?.tools?.some((t) => t.name === 'select_agent')) {
|
|
419
|
-
classifierCalls += 1;
|
|
420
|
-
throw new Error('classifier is having a moment');
|
|
421
|
-
}
|
|
422
|
-
return { role: 'assistant', content: 'fallback handled it' };
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
const driver = new OrchestratingDriver(
|
|
426
|
-
makeRegistry(provider),
|
|
427
|
-
[agent({ name: 'A' }), agent({ name: 'B' }), agent({ name: 'Catch-all', fallback: true })],
|
|
428
|
-
{ sessionKey: 'budget-8' },
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
const result = await driver.sendMessage('go');
|
|
432
|
-
|
|
433
|
-
assert.is(classifierCalls, 3, 'classifierRetries (2) + the first attempt');
|
|
434
|
-
assert.is(result.reason, 'done');
|
|
435
|
-
assert.is(failureReasonOf(result), undefined, 'the fallback turn succeeded');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
Suite.run();
|