@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.
Files changed (58) hide show
  1. package/dist/ai-assistant.api.json +271 -2
  2. package/dist/ai-assistant.d.ts +90 -1
  3. package/dist/chat-driver.cjs +72 -0
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +72 -0
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +221 -97
  8. package/dist/dts/components/ai-driver/ai-driver.d.ts +11 -0
  9. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +31 -1
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +10 -0
  13. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +2 -0
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +1 -0
  16. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +3 -0
  17. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  18. package/dist/dts/index.d.ts +1 -1
  19. package/dist/dts/index.d.ts.map +1 -1
  20. package/dist/dts/main/file-attachments.test.d.ts +2 -0
  21. package/dist/dts/main/file-attachments.test.d.ts.map +1 -0
  22. package/dist/dts/main/interaction-cost.test.d.ts +2 -0
  23. package/dist/dts/main/interaction-cost.test.d.ts.map +1 -0
  24. package/dist/dts/main/main.d.ts +27 -0
  25. package/dist/dts/main/main.d.ts.map +1 -1
  26. package/dist/dts/main/main.template.d.ts.map +1 -1
  27. package/dist/dts/react.d.ts +2 -1
  28. package/dist/dts/state/debug-event-log.d.ts +1 -1
  29. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  30. package/dist/dts/types/ai-chat-widget.d.ts +8 -0
  31. package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
  32. package/dist/esm/components/chat-driver/chat-driver.js +75 -1
  33. package/dist/esm/components/chat-driver/chat-driver.test.js +90 -0
  34. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +22 -0
  35. package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +76 -0
  36. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +14 -0
  37. package/dist/esm/main/file-attachments.test.js +164 -0
  38. package/dist/esm/main/interaction-cost.test.js +107 -0
  39. package/dist/esm/main/main.js +193 -21
  40. package/dist/esm/main/main.template.js +1 -0
  41. package/dist/esm/state/debug-event-log.js +6 -0
  42. package/dist/react.cjs +6 -1
  43. package/dist/react.mjs +6 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +17 -17
  46. package/src/components/ai-driver/ai-driver.ts +11 -0
  47. package/src/components/chat-driver/chat-driver.test.ts +109 -0
  48. package/src/components/chat-driver/chat-driver.ts +78 -1
  49. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +29 -0
  50. package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +107 -0
  51. package/src/components/orchestrating-driver/orchestrating-driver.ts +17 -0
  52. package/src/index.ts +4 -1
  53. package/src/main/file-attachments.test.ts +215 -0
  54. package/src/main/interaction-cost.test.ts +140 -0
  55. package/src/main/main.template.ts +1 -0
  56. package/src/main/main.ts +204 -22
  57. package/src/state/debug-event-log.ts +8 -0
  58. 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.15.2",
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.15.2",
77
- "@genesislcap/genx": "15.15.2",
78
- "@genesislcap/rollup-builder": "15.15.2",
79
- "@genesislcap/ts-builder": "15.15.2",
80
- "@genesislcap/uvu-playwright-builder": "15.15.2",
81
- "@genesislcap/vite-builder": "15.15.2",
82
- "@genesislcap/webpack-builder": "15.15.2",
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.15.2",
89
- "@genesislcap/foundation-logger": "15.15.2",
90
- "@genesislcap/foundation-notifications": "15.15.2",
91
- "@genesislcap/foundation-redux": "15.15.2",
92
- "@genesislcap/foundation-ui": "15.15.2",
93
- "@genesislcap/foundation-utils": "15.15.2",
94
- "@genesislcap/rapid-design-system": "15.15.2",
95
- "@genesislcap/web-core": "15.15.2",
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": "93190f6fb9fbb494a02b6a3fae68bdd1538797ff"
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 { InteractionCompletedEventDetail } from './components/chat-interaction-wrapper/chat-interaction-wrapper';
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';