@genesislcap/ai-assistant 15.15.2 → 15.16.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 +271 -2
- package/dist/ai-assistant.d.ts +90 -1
- package/dist/chat-driver.cjs +72 -0
- package/dist/chat-driver.cjs.map +2 -2
- package/dist/chat-driver.mjs +72 -0
- package/dist/chat-driver.mjs.map +2 -2
- package/dist/custom-elements.json +221 -97
- package/dist/dts/components/ai-driver/ai-driver.d.ts +11 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +31 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +10 -0
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +2 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +1 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +3 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -1
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/file-attachments.test.d.ts +2 -0
- package/dist/dts/main/file-attachments.test.d.ts.map +1 -0
- package/dist/dts/main/interaction-cost.test.d.ts +2 -0
- package/dist/dts/main/interaction-cost.test.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +27 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/react.d.ts +2 -1
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/types/ai-chat-widget.d.ts +8 -0
- package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +75 -1
- package/dist/esm/components/chat-driver/chat-driver.test.js +90 -0
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +22 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +76 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +14 -0
- package/dist/esm/main/file-attachments.test.js +164 -0
- package/dist/esm/main/interaction-cost.test.js +107 -0
- package/dist/esm/main/main.js +193 -21
- package/dist/esm/main/main.template.js +1 -0
- package/dist/esm/state/debug-event-log.js +6 -0
- package/dist/react.cjs +6 -1
- package/dist/react.mjs +6 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/components/ai-driver/ai-driver.ts +11 -0
- package/src/components/chat-driver/chat-driver.test.ts +109 -0
- package/src/components/chat-driver/chat-driver.ts +78 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +29 -0
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +107 -0
- package/src/components/orchestrating-driver/orchestrating-driver.ts +17 -0
- package/src/index.ts +4 -1
- package/src/main/file-attachments.test.ts +215 -0
- package/src/main/interaction-cost.test.ts +140 -0
- package/src/main/main.template.ts +1 -0
- package/src/main/main.ts +204 -22
- package/src/state/debug-event-log.ts +8 -0
- package/src/types/ai-chat-widget.ts +8 -0
|
@@ -518,6 +518,7 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
518
518
|
:interactionId=${(m) => m.interaction.interactionId}
|
|
519
519
|
:resolved=${(m) => m.interaction.resolved}
|
|
520
520
|
@interaction-completed=${(m, c) => c.parent.handleInteractionCompleted(c.event)}
|
|
521
|
+
@interaction-cost=${(m, c) => c.parent.handleInteractionCost(c.event)}
|
|
521
522
|
></ai-chat-interaction-wrapper>
|
|
522
523
|
`)}
|
|
523
524
|
</div>
|
|
@@ -63,6 +63,12 @@ export const META_EVENT_IMPORTANCE = {
|
|
|
63
63
|
'provider.selected': 'normal',
|
|
64
64
|
'interaction.requested': 'normal',
|
|
65
65
|
'interaction.resolved': 'normal',
|
|
66
|
+
// Bookkeeping about an already-finished turn; interesting when reconciling a session total.
|
|
67
|
+
'interaction.external-cost': 'normal',
|
|
68
|
+
// Spend that was billed but could NOT be recorded (the message was trimmed/condensed away, or
|
|
69
|
+
// the value was not real spend). 'high': it is the one way the session total goes quietly
|
|
70
|
+
// wrong, so it must stand out when reconciling.
|
|
71
|
+
'interaction.external-cost-dropped': 'high',
|
|
66
72
|
'external-diagnostics.folded': 'normal',
|
|
67
73
|
'session.cleared': 'normal',
|
|
68
74
|
'session.switched': 'low',
|
package/dist/react.cjs
CHANGED
|
@@ -108,12 +108,14 @@ const AiChatBubble = React.forwardRef(function AiChatBubble(props, ref) {
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrapper(props, ref) {
|
|
111
|
-
const { onRequestScrollToBottom, onInteractionCompleted, children, ...rest } = props;
|
|
111
|
+
const { onRequestScrollToBottom, onInteractionCompleted, onInteractionCost, children, ...rest } = props;
|
|
112
112
|
const _innerRef = React.useRef(null);
|
|
113
113
|
const _onRequestScrollToBottomRef = React.useRef(onRequestScrollToBottom);
|
|
114
114
|
_onRequestScrollToBottomRef.current = onRequestScrollToBottom;
|
|
115
115
|
const _onInteractionCompletedRef = React.useRef(onInteractionCompleted);
|
|
116
116
|
_onInteractionCompletedRef.current = onInteractionCompleted;
|
|
117
|
+
const _onInteractionCostRef = React.useRef(onInteractionCost);
|
|
118
|
+
_onInteractionCostRef.current = onInteractionCost;
|
|
117
119
|
React.useLayoutEffect(() => {
|
|
118
120
|
const el = _innerRef.current;
|
|
119
121
|
if (!el) return;
|
|
@@ -121,9 +123,12 @@ const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrap
|
|
|
121
123
|
el.addEventListener('request-scroll-to-bottom', _onRequestScrollToBottomFn);
|
|
122
124
|
const _onInteractionCompletedFn = (e) => _onInteractionCompletedRef.current?.(e);
|
|
123
125
|
el.addEventListener('interaction-completed', _onInteractionCompletedFn);
|
|
126
|
+
const _onInteractionCostFn = (e) => _onInteractionCostRef.current?.(e);
|
|
127
|
+
el.addEventListener('interaction-cost', _onInteractionCostFn);
|
|
124
128
|
return () => {
|
|
125
129
|
el.removeEventListener('request-scroll-to-bottom', _onRequestScrollToBottomFn);
|
|
126
130
|
el.removeEventListener('interaction-completed', _onInteractionCompletedFn);
|
|
131
|
+
el.removeEventListener('interaction-cost', _onInteractionCostFn);
|
|
127
132
|
};
|
|
128
133
|
}, []);
|
|
129
134
|
return React.createElement(customElements.getName(AiChatInteractionWrapperWC) ?? 'ai-chat-interaction-wrapper', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
|
package/dist/react.mjs
CHANGED
|
@@ -106,12 +106,14 @@ export const AiChatBubble = React.forwardRef(function AiChatBubble(props, ref) {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
export const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrapper(props, ref) {
|
|
109
|
-
const { onRequestScrollToBottom, onInteractionCompleted, children, ...rest } = props;
|
|
109
|
+
const { onRequestScrollToBottom, onInteractionCompleted, onInteractionCost, children, ...rest } = props;
|
|
110
110
|
const _innerRef = React.useRef(null);
|
|
111
111
|
const _onRequestScrollToBottomRef = React.useRef(onRequestScrollToBottom);
|
|
112
112
|
_onRequestScrollToBottomRef.current = onRequestScrollToBottom;
|
|
113
113
|
const _onInteractionCompletedRef = React.useRef(onInteractionCompleted);
|
|
114
114
|
_onInteractionCompletedRef.current = onInteractionCompleted;
|
|
115
|
+
const _onInteractionCostRef = React.useRef(onInteractionCost);
|
|
116
|
+
_onInteractionCostRef.current = onInteractionCost;
|
|
115
117
|
React.useLayoutEffect(() => {
|
|
116
118
|
const el = _innerRef.current;
|
|
117
119
|
if (!el) return;
|
|
@@ -119,9 +121,12 @@ export const AiChatInteractionWrapper = React.forwardRef(function AiChatInteract
|
|
|
119
121
|
el.addEventListener('request-scroll-to-bottom', _onRequestScrollToBottomFn);
|
|
120
122
|
const _onInteractionCompletedFn = (e) => _onInteractionCompletedRef.current?.(e);
|
|
121
123
|
el.addEventListener('interaction-completed', _onInteractionCompletedFn);
|
|
124
|
+
const _onInteractionCostFn = (e) => _onInteractionCostRef.current?.(e);
|
|
125
|
+
el.addEventListener('interaction-cost', _onInteractionCostFn);
|
|
122
126
|
return () => {
|
|
123
127
|
el.removeEventListener('request-scroll-to-bottom', _onRequestScrollToBottomFn);
|
|
124
128
|
el.removeEventListener('interaction-completed', _onInteractionCompletedFn);
|
|
129
|
+
el.removeEventListener('interaction-cost', _onInteractionCostFn);
|
|
125
130
|
};
|
|
126
131
|
}, []);
|
|
127
132
|
return React.createElement(customElements.getName(AiChatInteractionWrapperWC) ?? 'ai-chat-interaction-wrapper', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.thinking-policy.test.ts","../src/components/chat-driver/chat-driver.trace-capture.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/chat-driver.turn-usage.test.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/persistence-broken-sources.test.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/usage-rows.test.ts","../src/utils/usage-rows.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
|
|
1
|
+
{"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.thinking-policy.test.ts","../src/components/chat-driver/chat-driver.trace-capture.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/chat-driver.turn-usage.test.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.cost.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/file-attachments.test.ts","../src/main/index.ts","../src/main/interaction-cost.test.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/persistence-broken-sources.test.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/usage-rows.test.ts","../src/utils/usage-rows.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/ai-assistant",
|
|
3
3
|
"description": "Genesis AI Assistant micro-frontend",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.16.0",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -73,26 +73,26 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@genesislcap/foundation-testing": "15.
|
|
77
|
-
"@genesislcap/genx": "15.
|
|
78
|
-
"@genesislcap/rollup-builder": "15.
|
|
79
|
-
"@genesislcap/ts-builder": "15.
|
|
80
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
81
|
-
"@genesislcap/vite-builder": "15.
|
|
82
|
-
"@genesislcap/webpack-builder": "15.
|
|
76
|
+
"@genesislcap/foundation-testing": "15.16.0",
|
|
77
|
+
"@genesislcap/genx": "15.16.0",
|
|
78
|
+
"@genesislcap/rollup-builder": "15.16.0",
|
|
79
|
+
"@genesislcap/ts-builder": "15.16.0",
|
|
80
|
+
"@genesislcap/uvu-playwright-builder": "15.16.0",
|
|
81
|
+
"@genesislcap/vite-builder": "15.16.0",
|
|
82
|
+
"@genesislcap/webpack-builder": "15.16.0",
|
|
83
83
|
"@types/dompurify": "^3.0.5",
|
|
84
84
|
"@types/marked": "^5.0.2",
|
|
85
85
|
"esbuild": "0.25.12"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@genesislcap/foundation-ai": "15.
|
|
89
|
-
"@genesislcap/foundation-logger": "15.
|
|
90
|
-
"@genesislcap/foundation-notifications": "15.
|
|
91
|
-
"@genesislcap/foundation-redux": "15.
|
|
92
|
-
"@genesislcap/foundation-ui": "15.
|
|
93
|
-
"@genesislcap/foundation-utils": "15.
|
|
94
|
-
"@genesislcap/rapid-design-system": "15.
|
|
95
|
-
"@genesislcap/web-core": "15.
|
|
88
|
+
"@genesislcap/foundation-ai": "15.16.0",
|
|
89
|
+
"@genesislcap/foundation-logger": "15.16.0",
|
|
90
|
+
"@genesislcap/foundation-notifications": "15.16.0",
|
|
91
|
+
"@genesislcap/foundation-redux": "15.16.0",
|
|
92
|
+
"@genesislcap/foundation-ui": "15.16.0",
|
|
93
|
+
"@genesislcap/foundation-utils": "15.16.0",
|
|
94
|
+
"@genesislcap/rapid-design-system": "15.16.0",
|
|
95
|
+
"@genesislcap/web-core": "15.16.0",
|
|
96
96
|
"dompurify": "^3.3.1",
|
|
97
97
|
"marked": "^17.0.3"
|
|
98
98
|
},
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"access": "public"
|
|
106
106
|
},
|
|
107
107
|
"customElements": "dist/custom-elements.json",
|
|
108
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "2cacfdef54a332ee571ae784b5b7629cf66f050f"
|
|
109
109
|
}
|
|
@@ -45,6 +45,17 @@ export interface AiDriver extends EventTarget {
|
|
|
45
45
|
* Resolve a pending blocking interaction with the given result.
|
|
46
46
|
*/
|
|
47
47
|
resolveInteraction(interactionId: string, result: unknown): void;
|
|
48
|
+
/**
|
|
49
|
+
* Record external spend against an interaction that has ALREADY resolved.
|
|
50
|
+
*
|
|
51
|
+
* For widgets that resolve early — so the assistant stops waiting — and then keep doing paid work
|
|
52
|
+
* on the user's behalf. Reports a positive DELTA; repeated calls sum. Returns false when the
|
|
53
|
+
* spend was NOT recorded — the value was not a positive finite amount, or the message is no
|
|
54
|
+
* longer in history.
|
|
55
|
+
*
|
|
56
|
+
* @beta
|
|
57
|
+
*/
|
|
58
|
+
recordExternalCost?(interactionId: string, costUsd: number): boolean;
|
|
48
59
|
|
|
49
60
|
/**
|
|
50
61
|
* The live {@link InteractionContext} for a pending interaction (GENC-1390), or
|
|
@@ -3568,3 +3568,112 @@ outcome(
|
|
|
3568
3568
|
);
|
|
3569
3569
|
|
|
3570
3570
|
outcome.run();
|
|
3571
|
+
|
|
3572
|
+
// ---------------------------------------------------------------------------
|
|
3573
|
+
// recordExternalCost — spend a widget incurs AFTER it has resolved.
|
|
3574
|
+
//
|
|
3575
|
+
// Some widgets resolve EARLY on purpose, so the assistant stops waiting while the
|
|
3576
|
+
// user deliberates, and then keep doing paid work for them. `resolveInteraction`
|
|
3577
|
+
// cannot serve that: its whole body is inside `if (interaction)` and it deletes the
|
|
3578
|
+
// entry, so a second resolution is a silent no-op and the spend never reached the
|
|
3579
|
+
// figure shown to the user.
|
|
3580
|
+
// ---------------------------------------------------------------------------
|
|
3581
|
+
|
|
3582
|
+
const postResolveCost = createLogicSuite('ChatDriver post-resolve external cost');
|
|
3583
|
+
|
|
3584
|
+
postResolveCost('adds spend to an interaction that has already resolved', async () => {
|
|
3585
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
3586
|
+
const pending = driver.requestInteraction('planning-question', { question: 'Pick one' });
|
|
3587
|
+
const id = driver.getHistory().at(-1)!.interaction!.interactionId;
|
|
3588
|
+
|
|
3589
|
+
driver.resolveInteraction(id, { status: 'approved', costUsd: 0.5 });
|
|
3590
|
+
await pending;
|
|
3591
|
+
|
|
3592
|
+
// The interaction is gone from `pendingInteractions` — this is exactly the state in
|
|
3593
|
+
// which the old path silently did nothing.
|
|
3594
|
+
assert.is(driver.recordExternalCost(id, 0.25), true);
|
|
3595
|
+
assert.is(driver.recordExternalCost(id, 0.25), true);
|
|
3596
|
+
|
|
3597
|
+
const msg = driver.getHistory().find((m) => m.interaction?.interactionId === id);
|
|
3598
|
+
// ADDITIVE: the resolution's 0.5 plus two 0.25 deltas.
|
|
3599
|
+
assert.is(msg?.externalCostUsd, 1);
|
|
3600
|
+
// …and the recorded resolution is untouched — this is not a second resolution.
|
|
3601
|
+
assert.equal(msg?.interaction?.resolved, { status: 'approved', costUsd: 0.5 });
|
|
3602
|
+
});
|
|
3603
|
+
|
|
3604
|
+
postResolveCost('works on an interaction that resolved with no cost at all', async () => {
|
|
3605
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
3606
|
+
const pending = driver.requestInteraction('planning-question', { question: 'q' });
|
|
3607
|
+
const id = driver.getHistory().at(-1)!.interaction!.interactionId;
|
|
3608
|
+
driver.resolveInteraction(id, { status: 'approved' });
|
|
3609
|
+
await pending;
|
|
3610
|
+
|
|
3611
|
+
assert.is(driver.recordExternalCost(id, 0.02), true);
|
|
3612
|
+
const msg = driver.getHistory().find((m) => m.interaction?.interactionId === id);
|
|
3613
|
+
assert.is(msg?.externalCostUsd, 0.02);
|
|
3614
|
+
});
|
|
3615
|
+
|
|
3616
|
+
postResolveCost('refuses values that are not real spend, and an unknown interaction', async () => {
|
|
3617
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
3618
|
+
const pending = driver.requestInteraction('planning-question', { question: 'q' });
|
|
3619
|
+
const id = driver.getHistory().at(-1)!.interaction!.interactionId;
|
|
3620
|
+
driver.resolveInteraction(id, { status: 'approved' });
|
|
3621
|
+
await pending;
|
|
3622
|
+
|
|
3623
|
+
// Includes the shapes only an untyped JS caller can produce — which is exactly who the
|
|
3624
|
+
// `typeof` guard is written for, and what the previous `as number` cast asserted nothing about.
|
|
3625
|
+
for (const bad of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, '0.5', null, undefined]) {
|
|
3626
|
+
assert.is(driver.recordExternalCost(id, bad as number), false);
|
|
3627
|
+
}
|
|
3628
|
+
// …and each refusal leaves a trace: the wrapper no longer pre-validates (it made this
|
|
3629
|
+
// unreachable through the event path, a widget's only way in), so this is the ONE validator
|
|
3630
|
+
// and it has to record what it dropped.
|
|
3631
|
+
const dropped = getMetaEvents('').filter((m) => m.type === 'interaction.external-cost-dropped');
|
|
3632
|
+
assert.ok(dropped.length >= 1, 'a refused amount must record why');
|
|
3633
|
+
// An empty id can never match a message, so it is refused before the search.
|
|
3634
|
+
assert.is(driver.recordExternalCost('', 0.1), false);
|
|
3635
|
+
// A message trimmed or condensed out of history is a real outcome, not a throw.
|
|
3636
|
+
assert.is(driver.recordExternalCost('no-such-interaction', 0.1), false);
|
|
3637
|
+
|
|
3638
|
+
const msg = driver.getHistory().find((m) => m.interaction?.interactionId === id);
|
|
3639
|
+
assert.is(msg?.externalCostUsd, undefined);
|
|
3640
|
+
});
|
|
3641
|
+
|
|
3642
|
+
postResolveCost(
|
|
3643
|
+
'announces the change NARROWLY — never on history-updated, which rebuilds the widget',
|
|
3644
|
+
async () => {
|
|
3645
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
3646
|
+
const pending = driver.requestInteraction('planning-question', { question: 'q' });
|
|
3647
|
+
const id = driver.getHistory().at(-1)!.interaction!.interactionId;
|
|
3648
|
+
driver.resolveInteraction(id, { status: 'approved' });
|
|
3649
|
+
await pending;
|
|
3650
|
+
|
|
3651
|
+
let narrow = 0;
|
|
3652
|
+
let costUsd: number | undefined;
|
|
3653
|
+
let interactionId: string | undefined;
|
|
3654
|
+
let historyUpdated = 0;
|
|
3655
|
+
driver.addEventListener('external-cost-recorded', (e) => {
|
|
3656
|
+
narrow += 1;
|
|
3657
|
+
({ costUsd, interactionId } = (
|
|
3658
|
+
e as CustomEvent<{ interactionId: string; costUsd: number }>
|
|
3659
|
+
).detail);
|
|
3660
|
+
});
|
|
3661
|
+
driver.addEventListener('history-updated', () => {
|
|
3662
|
+
historyUpdated += 1;
|
|
3663
|
+
});
|
|
3664
|
+
driver.recordExternalCost(id, 0.07);
|
|
3665
|
+
|
|
3666
|
+
// Announced, or the total is right in history and stale on screen.
|
|
3667
|
+
assert.is(narrow, 1);
|
|
3668
|
+
assert.is(costUsd, 0.07);
|
|
3669
|
+
assert.is(interactionId, id);
|
|
3670
|
+
// REGRESSION (reviewer catch on #2474): `history-updated` makes the host reassign `messages`,
|
|
3671
|
+
// which rebuilds the trailing interaction row — a `recycle: false` repeat that disposes its
|
|
3672
|
+
// views outright. For a RESOLVED interaction there is no `interactionContext` to remount
|
|
3673
|
+
// from, so announcing this way tore down and blanked the very widget that had just reported
|
|
3674
|
+
// its spend. Bookkeeping about a finished turn must not touch what is rendered.
|
|
3675
|
+
assert.is(historyUpdated, 0);
|
|
3676
|
+
},
|
|
3677
|
+
);
|
|
3678
|
+
|
|
3679
|
+
postResolveCost.run();
|
|
@@ -414,7 +414,7 @@ export interface ChatDriverConfig {
|
|
|
414
414
|
*
|
|
415
415
|
* Dispatches `'history-updated'` events on itself so the owning element can observe changes.
|
|
416
416
|
*
|
|
417
|
-
* @fires history-updated - Fired whenever the in-memory chat history changes (append, tool loop, interaction resolution) with the full history snapshot. detail: `ReadonlyArray<ChatMessage>`
|
|
417
|
+
* @fires history-updated - Fired whenever the in-memory chat history changes (append, tool loop, interaction resolution, post-resolve external cost) with the full history snapshot. detail: `ReadonlyArray<ChatMessage>`
|
|
418
418
|
*
|
|
419
419
|
* @beta
|
|
420
420
|
*/
|
|
@@ -1882,6 +1882,83 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1882
1882
|
});
|
|
1883
1883
|
}
|
|
1884
1884
|
|
|
1885
|
+
/**
|
|
1886
|
+
* Add external (non-LLM) spend to an interaction's message AFTER it has resolved.
|
|
1887
|
+
*
|
|
1888
|
+
* **Why this exists separately from `resolveInteraction`.** A widget's spend reaches the session
|
|
1889
|
+
* total only through `InteractionResult.costUsd`, which is read exactly once — `resolveInteraction`
|
|
1890
|
+
* does its whole job inside `if (interaction)` and then deletes the entry from
|
|
1891
|
+
* `pendingInteractions`. That is correct for the resolution itself, but it assumes a widget stops
|
|
1892
|
+
* costing money when it resolves, and several deliberately do not: a widget that resolves EARLY so
|
|
1893
|
+
* the assistant stops waiting (options shown, a preview rendered) may stay interactive for minutes
|
|
1894
|
+
* afterwards and do more paid work on the user's behalf. Re-emitting `interaction-completed` does
|
|
1895
|
+
* not help — with the interaction gone from the map the call is a silent no-op — so that spend had
|
|
1896
|
+
* nowhere to go and was simply missing from the figure shown to the user.
|
|
1897
|
+
*
|
|
1898
|
+
* Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum. It
|
|
1899
|
+
* touches only `externalCostUsd`; the recorded `interaction.resolved` payload is left exactly as
|
|
1900
|
+
* the resolution wrote it, because this is not a second resolution and must not read like one.
|
|
1901
|
+
*
|
|
1902
|
+
* Deliberately does NOT require the interaction to be pending, does not resurrect it, and has no
|
|
1903
|
+
* loading/turn side effects — this is bookkeeping about a turn that already happened.
|
|
1904
|
+
*
|
|
1905
|
+
* @param interactionId the interaction whose message owns the spend.
|
|
1906
|
+
* @param costUsd a positive delta in USD. Non-finite, zero and negative values are ignored.
|
|
1907
|
+
* @returns `true` when the spend was recorded; `false` when it was not — either the value was
|
|
1908
|
+
* not a positive finite amount, or the message is no longer in history (trimmed or condensed
|
|
1909
|
+
* away). A real outcome a caller may want to log rather than a failure to throw on; every
|
|
1910
|
+
* `false` also records an `interaction.external-cost-dropped` meta event here, so money can
|
|
1911
|
+
* never disappear without a trace.
|
|
1912
|
+
* @beta
|
|
1913
|
+
*/
|
|
1914
|
+
public recordExternalCost(interactionId: string, costUsd: number): boolean {
|
|
1915
|
+
const drop = (reason: string): false => {
|
|
1916
|
+
recordMetaEvent(this.sessionKey, 'interaction.external-cost-dropped', {
|
|
1917
|
+
interactionId,
|
|
1918
|
+
costUsd,
|
|
1919
|
+
reason,
|
|
1920
|
+
});
|
|
1921
|
+
return false;
|
|
1922
|
+
};
|
|
1923
|
+
if (!interactionId) return drop('no interactionId');
|
|
1924
|
+
if (typeof costUsd !== 'number' || !Number.isFinite(costUsd) || costUsd <= 0) {
|
|
1925
|
+
return drop('not a positive finite amount');
|
|
1926
|
+
}
|
|
1927
|
+
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
1928
|
+
// The message is gone (trimmed or condensed): the spend is real and already billed, so it is
|
|
1929
|
+
// recorded as dropped rather than silently discarded — an invisible gap in the session total
|
|
1930
|
+
// is exactly what this whole channel exists to close.
|
|
1931
|
+
if (idx === -1) return drop('interaction message no longer in history');
|
|
1932
|
+
this.history[idx] = {
|
|
1933
|
+
...this.history[idx],
|
|
1934
|
+
externalCostUsd: (this.history[idx].externalCostUsd ?? 0) + costUsd,
|
|
1935
|
+
};
|
|
1936
|
+
recordMetaEvent(this.sessionKey, 'interaction.external-cost', { interactionId, costUsd });
|
|
1937
|
+
// A NARROW event, deliberately not `history-updated`.
|
|
1938
|
+
//
|
|
1939
|
+
// The host answers `history-updated` by reassigning `messages`, which re-evaluates
|
|
1940
|
+
// `activeInteractionRow` — and the trailing interaction slot renders through a
|
|
1941
|
+
// `repeat(..., { recycle: false })`, so every view in it is disposed and rebuilt regardless of
|
|
1942
|
+
// item identity. That is correct for a PENDING interaction (GENC-1429: a recycled wrapper
|
|
1943
|
+
// carries the previous interaction's truthy `resolved` and swallows the next completion), and
|
|
1944
|
+
// survivable because `buildInteraction` hands a remounted pending widget the same driver-owned
|
|
1945
|
+
// `interactionContext`.
|
|
1946
|
+
//
|
|
1947
|
+
// A RESOLVED one has no such context — `resolveInteraction` disposed and deleted it — so
|
|
1948
|
+
// announcing this on `history-updated` would tear down and blank the very widget that just
|
|
1949
|
+
// reported, in exactly the flow this channel exists for (a user tweaking an option after the
|
|
1950
|
+
// interaction resolved). Bookkeeping about a finished turn must stay invisible, so the host
|
|
1951
|
+
// recomputes its totals from this driver's history and saves, without touching what is
|
|
1952
|
+
// rendered. Reviewer catch on #2474 — confirmed against the layout widget, whose tweak result
|
|
1953
|
+
// went off screen because the rebuild raced the session write that would have restored it.
|
|
1954
|
+
this.dispatchEvent(
|
|
1955
|
+
new CustomEvent<{ interactionId: string; costUsd: number }>('external-cost-recorded', {
|
|
1956
|
+
detail: { interactionId, costUsd },
|
|
1957
|
+
}),
|
|
1958
|
+
);
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1885
1962
|
/**
|
|
1886
1963
|
* Resolve a pending interaction. The wrapper component calls this on completion.
|
|
1887
1964
|
* Marks the interaction message as resolved so it renders read-only on re-render.
|
|
@@ -15,6 +15,7 @@ type InteractionHost = {
|
|
|
15
15
|
|
|
16
16
|
export type ChatInteractionEventsMap = {
|
|
17
17
|
'interaction-completed': InteractionResult<unknown> & { interactionId: string };
|
|
18
|
+
'interaction-cost': { interactionId: string; costUsd: number };
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -23,11 +24,18 @@ export type ChatInteractionEventsMap = {
|
|
|
23
24
|
*/
|
|
24
25
|
export type InteractionCompletedEventDetail = ChatInteractionEventsMap['interaction-completed'];
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Detail payload for the `interaction-cost` event — a POSITIVE DELTA in USD, not a running total.
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export type InteractionCostEventDetail = ChatInteractionEventsMap['interaction-cost'];
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* Hosts an interactive AI chat sub-component by tag name.
|
|
28
35
|
* @public
|
|
29
36
|
*
|
|
30
37
|
* @fires interaction-completed - Fired when the interaction finishes. detail: `InteractionCompletedEventDetail`
|
|
38
|
+
* @fires interaction-cost - Fired when a widget spends money AFTER resolving. detail: `InteractionCostEventDetail`
|
|
31
39
|
*/
|
|
32
40
|
@customElement({
|
|
33
41
|
name: 'ai-chat-interaction-wrapper',
|
|
@@ -167,6 +175,27 @@ export class AiChatInteractionWrapper extends GenesisElement {
|
|
|
167
175
|
});
|
|
168
176
|
});
|
|
169
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Spend the widget incurs AFTER it has resolved.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately NOT guarded by `this.resolved`, which is the entire point: a widget that
|
|
182
|
+
* resolves early so the assistant stops waiting can stay interactive for minutes and do more
|
|
183
|
+
* paid work for the user. The completion listener above is guarded because a second
|
|
184
|
+
* RESOLUTION would be wrong; a second charge is simply true, and before this event that spend
|
|
185
|
+
* had no way to reach the total the user is shown.
|
|
186
|
+
*/
|
|
187
|
+
element.addEventListener('interaction-cost', (e: CustomEvent) => {
|
|
188
|
+
e.stopPropagation();
|
|
189
|
+
// Forwarded UNVALIDATED. `recordExternalCost` already rejects anything that is not a
|
|
190
|
+
// positive finite amount AND records why it dropped it; validating here as well made that
|
|
191
|
+
// trace unreachable through the event path — which is a widget's only way in — so a widget
|
|
192
|
+
// emitting NaN or a negative got silently nothing, anywhere. One validator, one trace.
|
|
193
|
+
this.$emit('interaction-cost', {
|
|
194
|
+
interactionId: this.interactionId,
|
|
195
|
+
costUsd: e.detail?.costUsd,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
170
199
|
this.container.appendChild(element as unknown as Node);
|
|
171
200
|
this.observeWidgetResize(element as unknown as Element);
|
|
172
201
|
} catch (e) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIProvider,
|
|
3
|
+
AIProviderRegistry,
|
|
4
|
+
ChatMessage,
|
|
5
|
+
ChatRequestOptions,
|
|
6
|
+
} from '@genesislcap/foundation-ai';
|
|
7
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
8
|
+
import type { AgentConfig } from '../../config/config';
|
|
9
|
+
import type { ChatDriver } from '../chat-driver/chat-driver';
|
|
10
|
+
// Side-effect import — MUST precede `./orchestrating-driver` so the driver
|
|
11
|
+
// subclasses jsdom's EventTarget (CustomEvent dispatch then works in node).
|
|
12
|
+
import '../chat-driver/align-event-globals';
|
|
13
|
+
import { OrchestratingDriver } from './orchestrating-driver';
|
|
14
|
+
|
|
15
|
+
// GENC-1508 — `recordExternalCost` is OPTIONAL on `AiDriver`, so a missing delegation here
|
|
16
|
+
// is not a compile error: the host's `driver?.recordExternalCost?.(…)` simply evaluates to
|
|
17
|
+
// undefined and every post-resolve charge from every widget disappears — silently, and only
|
|
18
|
+
// in orchestrated setups. That is the same invisible-when-broken failure the method exists to
|
|
19
|
+
// fix, so the delegation gets a test rather than trust.
|
|
20
|
+
|
|
21
|
+
const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
|
|
22
|
+
get: () => provider,
|
|
23
|
+
default: () => provider,
|
|
24
|
+
defaultName: () => 'test',
|
|
25
|
+
names: () => ['test'],
|
|
26
|
+
getStatus: async () => null,
|
|
27
|
+
listStatuses: async () => [],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const noToolProvider = (): AIProvider =>
|
|
31
|
+
({
|
|
32
|
+
chat: async (
|
|
33
|
+
_history: ChatMessage[],
|
|
34
|
+
_userMessage: string,
|
|
35
|
+
_options?: ChatRequestOptions,
|
|
36
|
+
): Promise<ChatMessage> => ({ role: 'assistant', content: 'ok' }),
|
|
37
|
+
}) as AIProvider;
|
|
38
|
+
|
|
39
|
+
const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
|
|
40
|
+
({ description: 'test agent', ...overrides }) as AgentConfig;
|
|
41
|
+
|
|
42
|
+
const makeDriver = () =>
|
|
43
|
+
new OrchestratingDriver(makeRegistry(noToolProvider()), [agent({ name: 'Genesis Assistant' })], {
|
|
44
|
+
sessionKey: 'k',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** The orchestrator does not expose `requestInteraction`, so the interaction is ARRANGED on the
|
|
48
|
+
* wrapped driver. Every assertion below goes through the orchestrator's own public surface. */
|
|
49
|
+
const wrapped = (driver: OrchestratingDriver): ChatDriver =>
|
|
50
|
+
(driver as unknown as { chatDriver: ChatDriver }).chatDriver;
|
|
51
|
+
|
|
52
|
+
const Suite = createLogicSuite('orchestrating-driver external cost');
|
|
53
|
+
|
|
54
|
+
Suite('post-resolve spend delegates to the wrapped driver', async () => {
|
|
55
|
+
const driver = makeDriver();
|
|
56
|
+
const inner = wrapped(driver);
|
|
57
|
+
const pending = inner.requestInteraction('planning-question', { question: 'Pick one' });
|
|
58
|
+
const id = driver.getRawHistory().at(-1)!.interaction!.interactionId;
|
|
59
|
+
driver.resolveInteraction(id, { status: 'approved', costUsd: 0.5 });
|
|
60
|
+
await pending;
|
|
61
|
+
|
|
62
|
+
assert.is(driver.recordExternalCost(id, 0.25), true);
|
|
63
|
+
|
|
64
|
+
const msg = driver.getRawHistory().find((m) => m.interaction?.interactionId === id);
|
|
65
|
+
assert.is(msg?.externalCostUsd, 0.75);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
Suite('an unknown interaction is reported as unrecorded, not thrown', async () => {
|
|
69
|
+
const driver = makeDriver();
|
|
70
|
+
assert.is(driver.recordExternalCost('never-existed', 0.25), false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Suite('the announcement reaches the HOST, which only ever holds this wrapper', async () => {
|
|
74
|
+
// The delegation test above passes with the forward missing: the money still lands on the
|
|
75
|
+
// message. What breaks is quieter — `createDriver` hands the host an OrchestratingDriver for any
|
|
76
|
+
// non-empty `agents`, so an event the inner driver raises and the constructor's forward list
|
|
77
|
+
// omits is one the host never sees. The totals then never refresh and the save is never
|
|
78
|
+
// scheduled, and because this flow's interaction is the last thing in the session, "stale until
|
|
79
|
+
// the next history change" means permanently. Asserted on the WRAPPER for that reason.
|
|
80
|
+
const driver = makeDriver();
|
|
81
|
+
const inner = wrapped(driver);
|
|
82
|
+
const pending = inner.requestInteraction('planning-question', { question: 'Pick one' });
|
|
83
|
+
const id = driver.getRawHistory().at(-1)!.interaction!.interactionId;
|
|
84
|
+
driver.resolveInteraction(id, { status: 'approved' });
|
|
85
|
+
await pending;
|
|
86
|
+
|
|
87
|
+
let seen = 0;
|
|
88
|
+
let detail: { interactionId?: string; costUsd?: number } | undefined;
|
|
89
|
+
driver.addEventListener('external-cost-recorded', (e) => {
|
|
90
|
+
seen += 1;
|
|
91
|
+
detail = (e as CustomEvent).detail;
|
|
92
|
+
});
|
|
93
|
+
// …and never on `history-updated`, which would rebuild the reporting widget.
|
|
94
|
+
let historyUpdated = 0;
|
|
95
|
+
driver.addEventListener('history-updated', () => {
|
|
96
|
+
historyUpdated += 1;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
driver.recordExternalCost(id, 0.25);
|
|
100
|
+
|
|
101
|
+
assert.is(seen, 1);
|
|
102
|
+
assert.is(detail?.costUsd, 0.25);
|
|
103
|
+
assert.is(detail?.interactionId, id);
|
|
104
|
+
assert.is(historyUpdated, 0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
Suite.run();
|
|
@@ -223,12 +223,29 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
223
223
|
new CustomEvent('provider-changed', { detail: (e as CustomEvent).detail }),
|
|
224
224
|
);
|
|
225
225
|
});
|
|
226
|
+
// Post-resolve external cost. The host only ever holds THIS wrapper (`createDriver` returns
|
|
227
|
+
// one for any non-empty `agents`), so an event the inner driver raises and this list omits is
|
|
228
|
+
// an event the host never sees: `recordExternalCost` still delegates and the money still lands
|
|
229
|
+
// on the message, but the totals never refresh and the save is never scheduled — and in the
|
|
230
|
+
// flow this exists for the interaction is the last thing in the session, so "stale until the
|
|
231
|
+
// next history change" means permanently. Reviewer catch on #2474.
|
|
232
|
+
this.chatDriver.addEventListener('external-cost-recorded', (e: Event) => {
|
|
233
|
+
this.dispatchEvent(
|
|
234
|
+
new CustomEvent('external-cost-recorded', { detail: (e as CustomEvent).detail }),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
226
237
|
}
|
|
227
238
|
|
|
228
239
|
resolveInteraction(interactionId: string, result: unknown): void {
|
|
229
240
|
this.chatDriver.resolveInteraction(interactionId, result);
|
|
230
241
|
}
|
|
231
242
|
|
|
243
|
+
/** Delegated for the same reason as `resolveInteraction`: the history that owns the
|
|
244
|
+
* interaction — and therefore the cost — lives on the wrapped driver, not here. */
|
|
245
|
+
recordExternalCost(interactionId: string, costUsd: number): boolean {
|
|
246
|
+
return this.chatDriver.recordExternalCost(interactionId, costUsd);
|
|
247
|
+
}
|
|
248
|
+
|
|
232
249
|
getInteractionContext(interactionId: string): InteractionContext | undefined {
|
|
233
250
|
return this.chatDriver.getInteractionContext(interactionId);
|
|
234
251
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,10 @@ export * from './main/main.template';
|
|
|
4
4
|
export * from './components/agent-picker';
|
|
5
5
|
export * from './components/ai-driver';
|
|
6
6
|
export * from './components/chat-driver';
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
InteractionCompletedEventDetail,
|
|
9
|
+
InteractionCostEventDetail,
|
|
10
|
+
} from './components/chat-interaction-wrapper/chat-interaction-wrapper';
|
|
8
11
|
export * from './components/orchestrating-driver';
|
|
9
12
|
export * from './components/popout-manager';
|
|
10
13
|
export * from './channel/ai-activity-channel';
|