@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
@@ -2270,7 +2270,7 @@
2270
2270
  {
2271
2271
  "kind": "Interface",
2272
2272
  "canonicalReference": "@genesislcap/ai-assistant!AiChatWidget:interface",
2273
- "docComment": "/**\n * Interface that AI inline interaction components implement. The `AiChatInteractionWrapper` sets `interactionId` and `interaction` on the rendered element on every (re)mount, and replaces `interaction` (pending → resolved) when the interaction completes.\n *\n * @remarks\n *\n * GENC-1390 replaced the previous flat `data` / `resolved` / `shouldAutoFocus` surface with the discriminated {@link AiChatInteraction}. This is a breaking change on this `@beta` interface widgets read `this.interaction` (narrowing on `status`) instead of the removed flat fields.\n *\n * @beta\n */\n",
2273
+ "docComment": "/**\n * Interface that AI inline interaction components implement. The `AiChatInteractionWrapper` sets `interactionId` and `interaction` on the rendered element on every (re)mount, and replaces `interaction` (pending → resolved) when the interaction completes.\n *\n * @remarks\n *\n * A widget talks back by dispatching two events, both caught by the wrapper: `interaction-completed` resolves the interaction (once a second is ignored), and `interaction-cost` reports a positive USD delta for paid work done AFTER resolving, which repeated calls sum. The second exists for widgets that resolve early so the assistant stops waiting, then keep working for the user; without it that spend is billed but missing from the total the user sees.\n *\n * @beta\n */\n",
2274
2274
  "excerptTokens": [
2275
2275
  {
2276
2276
  "kind": "Content",
@@ -3205,6 +3205,67 @@
3205
3205
  ],
3206
3206
  "name": "primeRestoredAgentState"
3207
3207
  },
3208
+ {
3209
+ "kind": "MethodSignature",
3210
+ "canonicalReference": "@genesislcap/ai-assistant!AiDriver#recordExternalCost:member(1)",
3211
+ "docComment": "/**\n * Record external spend against an interaction that has ALREADY resolved.\n *\n * For widgets that resolve early — so the assistant stops waiting — and then keep doing paid work on the user's behalf. Reports a positive DELTA; repeated calls sum. Returns false when the spend was NOT recorded — the value was not a positive finite amount, or the message is no longer in history.\n *\n * @beta\n */\n",
3212
+ "excerptTokens": [
3213
+ {
3214
+ "kind": "Content",
3215
+ "text": "recordExternalCost?(interactionId: "
3216
+ },
3217
+ {
3218
+ "kind": "Content",
3219
+ "text": "string"
3220
+ },
3221
+ {
3222
+ "kind": "Content",
3223
+ "text": ", costUsd: "
3224
+ },
3225
+ {
3226
+ "kind": "Content",
3227
+ "text": "number"
3228
+ },
3229
+ {
3230
+ "kind": "Content",
3231
+ "text": "): "
3232
+ },
3233
+ {
3234
+ "kind": "Content",
3235
+ "text": "boolean"
3236
+ },
3237
+ {
3238
+ "kind": "Content",
3239
+ "text": ";"
3240
+ }
3241
+ ],
3242
+ "isOptional": true,
3243
+ "returnTypeTokenRange": {
3244
+ "startIndex": 5,
3245
+ "endIndex": 6
3246
+ },
3247
+ "releaseTag": "Beta",
3248
+ "overloadIndex": 1,
3249
+ "parameters": [
3250
+ {
3251
+ "parameterName": "interactionId",
3252
+ "parameterTypeTokenRange": {
3253
+ "startIndex": 1,
3254
+ "endIndex": 2
3255
+ },
3256
+ "isOptional": false
3257
+ },
3258
+ {
3259
+ "parameterName": "costUsd",
3260
+ "parameterTypeTokenRange": {
3261
+ "startIndex": 3,
3262
+ "endIndex": 4
3263
+ },
3264
+ "isOptional": false
3265
+ }
3266
+ ],
3267
+ "name": "recordExternalCost"
3268
+ },
3208
3269
  {
3209
3270
  "kind": "MethodSignature",
3210
3271
  "canonicalReference": "@genesislcap/ai-assistant!AiDriver#resolveInteraction:member(1)",
@@ -4294,7 +4355,7 @@
4294
4355
  {
4295
4356
  "kind": "Class",
4296
4357
  "canonicalReference": "@genesislcap/ai-assistant!ChatDriver:class",
4297
- "docComment": "/**\n * Plain TS class that drives a multi-turn chat conversation, including the tool-call loop. Owned by `FoundationAiAssistant` — created in `connectedCallback`, torn down in `disconnectedCallback`.\n *\n * Dispatches `'history-updated'` events on itself so the owning element can observe changes.\n *\n * @fires\n *\n * history-updated - Fired whenever the in-memory chat history changes (append, tool loop, interaction resolution) with the full history snapshot. detail: `ReadonlyArray<ChatMessage>`\n *\n * @beta\n */\n",
4358
+ "docComment": "/**\n * Plain TS class that drives a multi-turn chat conversation, including the tool-call loop. Owned by `FoundationAiAssistant` — created in `connectedCallback`, torn down in `disconnectedCallback`.\n *\n * Dispatches `'history-updated'` events on itself so the owning element can observe changes.\n *\n * @fires\n *\n * 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>`\n *\n * @beta\n */\n",
4298
4359
  "excerptTokens": [
4299
4360
  {
4300
4361
  "kind": "Content",
@@ -5265,6 +5326,70 @@
5265
5326
  "isAbstract": false,
5266
5327
  "name": "markAsSubAgent"
5267
5328
  },
5329
+ {
5330
+ "kind": "Method",
5331
+ "canonicalReference": "@genesislcap/ai-assistant!ChatDriver#recordExternalCost:member(1)",
5332
+ "docComment": "/**\n * Add external (non-LLM) spend to an interaction's message AFTER it has resolved.\n *\n * **Why this exists separately from `resolveInteraction`.** A widget's spend reaches the session total only through `InteractionResult.costUsd`, which is read exactly once — `resolveInteraction` does its whole job inside `if (interaction)` and then deletes the entry from `pendingInteractions`. That is correct for the resolution itself, but it assumes a widget stops costing money when it resolves, and several deliberately do not: a widget that resolves EARLY so the assistant stops waiting (options shown, a preview rendered) may stay interactive for minutes afterwards and do more paid work on the user's behalf. Re-emitting `interaction-completed` does not help — with the interaction gone from the map the call is a silent no-op — so that spend had nowhere to go and was simply missing from the figure shown to the user.\n *\n * Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum. It touches only `externalCostUsd`; the recorded `interaction.resolved` payload is left exactly as the resolution wrote it, because this is not a second resolution and must not read like one.\n *\n * Deliberately does NOT require the interaction to be pending, does not resurrect it, and has no loading/turn side effects — this is bookkeeping about a turn that already happened.\n *\n * @param interactionId - the interaction whose message owns the spend.\n *\n * @param costUsd - a positive delta in USD. Non-finite, zero and negative values are ignored.\n *\n * @returns `true` when the spend was recorded; `false` when it was not — either the value was not a positive finite amount, or the message is no longer in history (trimmed or condensed away). A real outcome a caller may want to log rather than a failure to throw on; every `false` also records an `interaction.external-cost-dropped` meta event here, so money can never disappear without a trace.\n *\n * @beta\n */\n",
5333
+ "excerptTokens": [
5334
+ {
5335
+ "kind": "Content",
5336
+ "text": "recordExternalCost(interactionId: "
5337
+ },
5338
+ {
5339
+ "kind": "Content",
5340
+ "text": "string"
5341
+ },
5342
+ {
5343
+ "kind": "Content",
5344
+ "text": ", costUsd: "
5345
+ },
5346
+ {
5347
+ "kind": "Content",
5348
+ "text": "number"
5349
+ },
5350
+ {
5351
+ "kind": "Content",
5352
+ "text": "): "
5353
+ },
5354
+ {
5355
+ "kind": "Content",
5356
+ "text": "boolean"
5357
+ },
5358
+ {
5359
+ "kind": "Content",
5360
+ "text": ";"
5361
+ }
5362
+ ],
5363
+ "isStatic": false,
5364
+ "returnTypeTokenRange": {
5365
+ "startIndex": 5,
5366
+ "endIndex": 6
5367
+ },
5368
+ "releaseTag": "Beta",
5369
+ "isProtected": false,
5370
+ "overloadIndex": 1,
5371
+ "parameters": [
5372
+ {
5373
+ "parameterName": "interactionId",
5374
+ "parameterTypeTokenRange": {
5375
+ "startIndex": 1,
5376
+ "endIndex": 2
5377
+ },
5378
+ "isOptional": false
5379
+ },
5380
+ {
5381
+ "parameterName": "costUsd",
5382
+ "parameterTypeTokenRange": {
5383
+ "startIndex": 3,
5384
+ "endIndex": 4
5385
+ },
5386
+ "isOptional": false
5387
+ }
5388
+ ],
5389
+ "isOptional": false,
5390
+ "isAbstract": false,
5391
+ "name": "recordExternalCost"
5392
+ },
5268
5393
  {
5269
5394
  "kind": "Method",
5270
5395
  "canonicalReference": "@genesislcap/ai-assistant!ChatDriver#reportBudgetExhausted:member(1)",
@@ -9077,6 +9202,55 @@
9077
9202
  "isAbstract": false,
9078
9203
  "name": "handleInteractionCompleted"
9079
9204
  },
9205
+ {
9206
+ "kind": "Method",
9207
+ "canonicalReference": "@genesislcap/ai-assistant!FoundationAiAssistant#handleInteractionCost:member(1)",
9208
+ "docComment": "/**\n * A widget reporting spend it incurred AFTER resolving.\n *\n * Note what this deliberately does NOT do: no `startLoadingTimer()`. This is bookkeeping about a turn that already finished, so it must be invisible — showing a spinner would tell the user the assistant is working when nothing is pending.\n */\n",
9209
+ "excerptTokens": [
9210
+ {
9211
+ "kind": "Content",
9212
+ "text": "handleInteractionCost(e: "
9213
+ },
9214
+ {
9215
+ "kind": "Reference",
9216
+ "text": "Event",
9217
+ "canonicalReference": "!Event:interface"
9218
+ },
9219
+ {
9220
+ "kind": "Content",
9221
+ "text": "): "
9222
+ },
9223
+ {
9224
+ "kind": "Content",
9225
+ "text": "void"
9226
+ },
9227
+ {
9228
+ "kind": "Content",
9229
+ "text": ";"
9230
+ }
9231
+ ],
9232
+ "isStatic": false,
9233
+ "returnTypeTokenRange": {
9234
+ "startIndex": 3,
9235
+ "endIndex": 4
9236
+ },
9237
+ "releaseTag": "Beta",
9238
+ "isProtected": false,
9239
+ "overloadIndex": 1,
9240
+ "parameters": [
9241
+ {
9242
+ "parameterName": "e",
9243
+ "parameterTypeTokenRange": {
9244
+ "startIndex": 1,
9245
+ "endIndex": 2
9246
+ },
9247
+ "isOptional": false
9248
+ }
9249
+ ],
9250
+ "isOptional": false,
9251
+ "isAbstract": false,
9252
+ "name": "handleInteractionCost"
9253
+ },
9080
9254
  {
9081
9255
  "kind": "Method",
9082
9256
  "canonicalReference": "@genesislcap/ai-assistant!FoundationAiAssistant#handlePopout:member(1)",
@@ -14280,6 +14454,37 @@
14280
14454
  ],
14281
14455
  "extendsTokenRanges": []
14282
14456
  },
14457
+ {
14458
+ "kind": "TypeAlias",
14459
+ "canonicalReference": "@genesislcap/ai-assistant!InteractionCostEventDetail:type",
14460
+ "docComment": "/**\n * Detail payload for the `interaction-cost` event — a POSITIVE DELTA in USD, not a running total.\n *\n * @public\n */\n",
14461
+ "excerptTokens": [
14462
+ {
14463
+ "kind": "Content",
14464
+ "text": "export type InteractionCostEventDetail = "
14465
+ },
14466
+ {
14467
+ "kind": "Reference",
14468
+ "text": "ChatInteractionEventsMap",
14469
+ "canonicalReference": "@genesislcap/ai-assistant!~ChatInteractionEventsMap:type"
14470
+ },
14471
+ {
14472
+ "kind": "Content",
14473
+ "text": "['interaction-cost']"
14474
+ },
14475
+ {
14476
+ "kind": "Content",
14477
+ "text": ";"
14478
+ }
14479
+ ],
14480
+ "fileUrlPath": "src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts",
14481
+ "releaseTag": "Public",
14482
+ "name": "InteractionCostEventDetail",
14483
+ "typeTokenRange": {
14484
+ "startIndex": 1,
14485
+ "endIndex": 3
14486
+ }
14487
+ },
14283
14488
  {
14284
14489
  "kind": "Function",
14285
14490
  "canonicalReference": "@genesislcap/ai-assistant!isAiAssistantDocked:function(1)",
@@ -15256,6 +15461,70 @@
15256
15461
  "isAbstract": false,
15257
15462
  "name": "primeRestoredAgentState"
15258
15463
  },
15464
+ {
15465
+ "kind": "Method",
15466
+ "canonicalReference": "@genesislcap/ai-assistant!OrchestratingDriver#recordExternalCost:member(1)",
15467
+ "docComment": "/**\n * Delegated for the same reason as `resolveInteraction`: the history that owns the interaction — and therefore the cost — lives on the wrapped driver, not here.\n */\n",
15468
+ "excerptTokens": [
15469
+ {
15470
+ "kind": "Content",
15471
+ "text": "recordExternalCost(interactionId: "
15472
+ },
15473
+ {
15474
+ "kind": "Content",
15475
+ "text": "string"
15476
+ },
15477
+ {
15478
+ "kind": "Content",
15479
+ "text": ", costUsd: "
15480
+ },
15481
+ {
15482
+ "kind": "Content",
15483
+ "text": "number"
15484
+ },
15485
+ {
15486
+ "kind": "Content",
15487
+ "text": "): "
15488
+ },
15489
+ {
15490
+ "kind": "Content",
15491
+ "text": "boolean"
15492
+ },
15493
+ {
15494
+ "kind": "Content",
15495
+ "text": ";"
15496
+ }
15497
+ ],
15498
+ "isStatic": false,
15499
+ "returnTypeTokenRange": {
15500
+ "startIndex": 5,
15501
+ "endIndex": 6
15502
+ },
15503
+ "releaseTag": "Beta",
15504
+ "isProtected": false,
15505
+ "overloadIndex": 1,
15506
+ "parameters": [
15507
+ {
15508
+ "parameterName": "interactionId",
15509
+ "parameterTypeTokenRange": {
15510
+ "startIndex": 1,
15511
+ "endIndex": 2
15512
+ },
15513
+ "isOptional": false
15514
+ },
15515
+ {
15516
+ "parameterName": "costUsd",
15517
+ "parameterTypeTokenRange": {
15518
+ "startIndex": 3,
15519
+ "endIndex": 4
15520
+ },
15521
+ "isOptional": false
15522
+ }
15523
+ ],
15524
+ "isOptional": false,
15525
+ "isAbstract": false,
15526
+ "name": "recordExternalCost"
15527
+ },
15259
15528
  {
15260
15529
  "kind": "Method",
15261
15530
  "canonicalReference": "@genesislcap/ai-assistant!OrchestratingDriver#resolveInteraction:member(1)",
@@ -532,6 +532,14 @@ export declare class AiChatMarkdown extends GenesisElement {
532
532
  * change on this `@beta` interface — widgets read `this.interaction` (narrowing on
533
533
  * `status`) instead of the removed flat fields.
534
534
  *
535
+ * @remarks
536
+ * A widget talks back by dispatching two events, both caught by the wrapper:
537
+ * `interaction-completed` resolves the interaction (once — a second is ignored), and
538
+ * `interaction-cost` reports a positive USD delta for paid work done AFTER resolving,
539
+ * which repeated calls sum. The second exists for widgets that resolve early so the
540
+ * assistant stops waiting, then keep working for the user; without it that spend is
541
+ * billed but missing from the total the user sees.
542
+ *
535
543
  * @beta
536
544
  */
537
545
  export declare interface AiChatWidget<TData = unknown, TResult = unknown> {
@@ -597,6 +605,17 @@ export declare interface AiDriver extends EventTarget {
597
605
  * Resolve a pending blocking interaction with the given result.
598
606
  */
599
607
  resolveInteraction(interactionId: string, result: unknown): void;
608
+ /**
609
+ * Record external spend against an interaction that has ALREADY resolved.
610
+ *
611
+ * For widgets that resolve early — so the assistant stops waiting — and then keep doing paid work
612
+ * on the user's behalf. Reports a positive DELTA; repeated calls sum. Returns false when the
613
+ * spend was NOT recorded — the value was not a positive finite amount, or the message is no
614
+ * longer in history.
615
+ *
616
+ * @beta
617
+ */
618
+ recordExternalCost?(interactionId: string, costUsd: number): boolean;
600
619
  /**
601
620
  * The live {@link InteractionContext} for a pending interaction (GENC-1390), or
602
621
  * `undefined` if the interaction is resolved/evicted or the driver owns no
@@ -1196,7 +1215,7 @@ export declare type CachePolicyInput = CachePolicy | ((ctx: SystemPromptContext)
1196
1215
  *
1197
1216
  * Dispatches `'history-updated'` events on itself so the owning element can observe changes.
1198
1217
  *
1199
- * @fires history-updated - Fired whenever the in-memory chat history changes (append, tool loop, interaction resolution) with the full history snapshot. detail: `ReadonlyArray<ChatMessage>`
1218
+ * @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>`
1200
1219
  *
1201
1220
  * @beta
1202
1221
  */
@@ -1858,6 +1877,36 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1858
1877
  * bubble and/or shows the "Assistant" label.
1859
1878
  */
1860
1879
  requestInteraction<T>(componentName: string, data: any, options?: InteractionRequestOptions): Promise<T>;
1880
+ /**
1881
+ * Add external (non-LLM) spend to an interaction's message AFTER it has resolved.
1882
+ *
1883
+ * **Why this exists separately from `resolveInteraction`.** A widget's spend reaches the session
1884
+ * total only through `InteractionResult.costUsd`, which is read exactly once — `resolveInteraction`
1885
+ * does its whole job inside `if (interaction)` and then deletes the entry from
1886
+ * `pendingInteractions`. That is correct for the resolution itself, but it assumes a widget stops
1887
+ * costing money when it resolves, and several deliberately do not: a widget that resolves EARLY so
1888
+ * the assistant stops waiting (options shown, a preview rendered) may stay interactive for minutes
1889
+ * afterwards and do more paid work on the user's behalf. Re-emitting `interaction-completed` does
1890
+ * not help — with the interaction gone from the map the call is a silent no-op — so that spend had
1891
+ * nowhere to go and was simply missing from the figure shown to the user.
1892
+ *
1893
+ * Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum. It
1894
+ * touches only `externalCostUsd`; the recorded `interaction.resolved` payload is left exactly as
1895
+ * the resolution wrote it, because this is not a second resolution and must not read like one.
1896
+ *
1897
+ * Deliberately does NOT require the interaction to be pending, does not resurrect it, and has no
1898
+ * loading/turn side effects — this is bookkeeping about a turn that already happened.
1899
+ *
1900
+ * @param interactionId the interaction whose message owns the spend.
1901
+ * @param costUsd a positive delta in USD. Non-finite, zero and negative values are ignored.
1902
+ * @returns `true` when the spend was recorded; `false` when it was not — either the value was
1903
+ * not a positive finite amount, or the message is no longer in history (trimmed or condensed
1904
+ * away). A real outcome a caller may want to log rather than a failure to throw on; every
1905
+ * `false` also records an `interaction.external-cost-dropped` meta event here, so money can
1906
+ * never disappear without a trace.
1907
+ * @beta
1908
+ */
1909
+ recordExternalCost(interactionId: string, costUsd: number): boolean;
1861
1910
  /**
1862
1911
  * Resolve a pending interaction. The wrapper component calls this on completion.
1863
1912
  * Marks the interaction message as resolved so it renders read-only on re-render.
@@ -2057,6 +2106,10 @@ declare type ChatInteractionEventsMap = {
2057
2106
  'interaction-completed': InteractionResult<unknown> & {
2058
2107
  interactionId: string;
2059
2108
  };
2109
+ 'interaction-cost': {
2110
+ interactionId: string;
2111
+ costUsd: number;
2112
+ };
2060
2113
  };
2061
2114
 
2062
2115
  export { ChatThinkingPolicy }
@@ -3637,6 +3690,22 @@ export declare class FoundationAiAssistant extends GenesisElement {
3637
3690
  persistenceChanged(oldValue: SessionPersistenceConfig | undefined, newValue: SessionPersistenceConfig | undefined): void;
3638
3691
  showingSplashChanged(): void;
3639
3692
  private syncShowingSplash;
3693
+ /**
3694
+ * Recompute the session's cost and token buckets from a transcript.
3695
+ *
3696
+ * Takes the list rather than reading `this.messages`, because it has two callers with
3697
+ * different sources: the `messages` setter passes the rendered list, and post-resolve external
3698
+ * cost passes the DRIVER's history — spend recorded there must reach the total WITHOUT
3699
+ * reassigning `messages`, which would re-render the live interaction row. See
3700
+ * `onExternalCostRecorded`.
3701
+ *
3702
+ * Recomputing (rather than incrementing on append) keeps the totals correct under any mutation
3703
+ * of the message list — clear-chat, re-render, restore. `sumUsage` recurses into
3704
+ * `toolCall.subAgentTrace`, so work a sub-agent did (possibly on another provider at other
3705
+ * rates) counts, and it reads the usage a compaction banked onto its summary, so shrinking
3706
+ * history no longer shrinks the totals.
3707
+ */
3708
+ private syncUsageTotals;
3640
3709
  /**
3641
3710
  * Runs side effects that were previously in `messagesChanged()`.
3642
3711
  * Called from the `messages` setter after dispatching to the store.
@@ -4043,6 +4112,9 @@ export declare class FoundationAiAssistant extends GenesisElement {
4043
4112
  removeAttachmentError(message: string): void;
4044
4113
  private isAcceptedFile;
4045
4114
  private readFileAsText;
4115
+ /** The base64 payload of a file, without any `data:` prefix — the raw form
4116
+ * `ChatImageAttachment.data` specifies (transports add their own framing). */
4117
+ private readFileAsBase64;
4046
4118
  private processFiles;
4047
4119
  private loadSelectedFiles;
4048
4120
  handleSendClick(): void;
@@ -4089,6 +4161,14 @@ export declare class FoundationAiAssistant extends GenesisElement {
4089
4161
  private send;
4090
4162
  onChatHeaderMouseDown(e: MouseEvent): void;
4091
4163
  handleInteractionCompleted(e: Event): void;
4164
+ /**
4165
+ * A widget reporting spend it incurred AFTER resolving.
4166
+ *
4167
+ * Note what this deliberately does NOT do: no `startLoadingTimer()`. This is bookkeeping about a
4168
+ * turn that already finished, so it must be invisible — showing a spinner would tell the user the
4169
+ * assistant is working when nothing is pending.
4170
+ */
4171
+ handleInteractionCost(e: Event): void;
4092
4172
  /**
4093
4173
  * The live {@link InteractionContext} for a pending interaction (GENC-1390).
4094
4174
  * `AiChatInteractionWrapper` pierces this across the shadow boundary to build the
@@ -4305,6 +4385,12 @@ export declare interface InteractionContext<TState extends object = Record<strin
4305
4385
  onDispose(fn: () => void): void;
4306
4386
  }
4307
4387
 
4388
+ /**
4389
+ * Detail payload for the `interaction-cost` event — a POSITIVE DELTA in USD, not a running total.
4390
+ * @public
4391
+ */
4392
+ export declare type InteractionCostEventDetail = ChatInteractionEventsMap['interaction-cost'];
4393
+
4308
4394
  /** True when the assistant is active in the layout panel (docked) rather than the bubble. */
4309
4395
  export declare function isAiAssistantDocked(): boolean;
4310
4396
 
@@ -4422,6 +4508,9 @@ export declare class OrchestratingDriver extends EventTarget implements AiDriver
4422
4508
  budgetExhaustedMessage?: string;
4423
4509
  });
4424
4510
  resolveInteraction(interactionId: string, result: unknown): void;
4511
+ /** Delegated for the same reason as `resolveInteraction`: the history that owns the
4512
+ * interaction — and therefore the cost — lives on the wrapped driver, not here. */
4513
+ recordExternalCost(interactionId: string, costUsd: number): boolean;
4425
4514
  getInteractionContext(interactionId: string): InteractionContext | undefined;
4426
4515
  isBusy(): boolean;
4427
4516
  /** Currently active provider name from the underlying ChatDriver. */
@@ -3585,6 +3585,12 @@ var META_EVENT_IMPORTANCE = {
3585
3585
  "provider.selected": "normal",
3586
3586
  "interaction.requested": "normal",
3587
3587
  "interaction.resolved": "normal",
3588
+ // Bookkeeping about an already-finished turn; interesting when reconciling a session total.
3589
+ "interaction.external-cost": "normal",
3590
+ // Spend that was billed but could NOT be recorded (the message was trimmed/condensed away, or
3591
+ // the value was not real spend). 'high': it is the one way the session total goes quietly
3592
+ // wrong, so it must stand out when reconciling.
3593
+ "interaction.external-cost-dropped": "high",
3588
3594
  "external-diagnostics.folded": "normal",
3589
3595
  "session.cleared": "normal",
3590
3596
  "session.switched": "low",
@@ -5070,6 +5076,62 @@ Output format (strict):
5070
5076
  });
5071
5077
  });
5072
5078
  }
5079
+ /**
5080
+ * Add external (non-LLM) spend to an interaction's message AFTER it has resolved.
5081
+ *
5082
+ * **Why this exists separately from `resolveInteraction`.** A widget's spend reaches the session
5083
+ * total only through `InteractionResult.costUsd`, which is read exactly once — `resolveInteraction`
5084
+ * does its whole job inside `if (interaction)` and then deletes the entry from
5085
+ * `pendingInteractions`. That is correct for the resolution itself, but it assumes a widget stops
5086
+ * costing money when it resolves, and several deliberately do not: a widget that resolves EARLY so
5087
+ * the assistant stops waiting (options shown, a preview rendered) may stay interactive for minutes
5088
+ * afterwards and do more paid work on the user's behalf. Re-emitting `interaction-completed` does
5089
+ * not help — with the interaction gone from the map the call is a silent no-op — so that spend had
5090
+ * nowhere to go and was simply missing from the figure shown to the user.
5091
+ *
5092
+ * Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum. It
5093
+ * touches only `externalCostUsd`; the recorded `interaction.resolved` payload is left exactly as
5094
+ * the resolution wrote it, because this is not a second resolution and must not read like one.
5095
+ *
5096
+ * Deliberately does NOT require the interaction to be pending, does not resurrect it, and has no
5097
+ * loading/turn side effects — this is bookkeeping about a turn that already happened.
5098
+ *
5099
+ * @param interactionId the interaction whose message owns the spend.
5100
+ * @param costUsd a positive delta in USD. Non-finite, zero and negative values are ignored.
5101
+ * @returns `true` when the spend was recorded; `false` when it was not — either the value was
5102
+ * not a positive finite amount, or the message is no longer in history (trimmed or condensed
5103
+ * away). A real outcome a caller may want to log rather than a failure to throw on; every
5104
+ * `false` also records an `interaction.external-cost-dropped` meta event here, so money can
5105
+ * never disappear without a trace.
5106
+ * @beta
5107
+ */
5108
+ recordExternalCost(interactionId, costUsd) {
5109
+ const drop = (reason) => {
5110
+ recordMetaEvent(this.sessionKey, "interaction.external-cost-dropped", {
5111
+ interactionId,
5112
+ costUsd,
5113
+ reason
5114
+ });
5115
+ return false;
5116
+ };
5117
+ if (!interactionId) return drop("no interactionId");
5118
+ if (typeof costUsd !== "number" || !Number.isFinite(costUsd) || costUsd <= 0) {
5119
+ return drop("not a positive finite amount");
5120
+ }
5121
+ const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
5122
+ if (idx === -1) return drop("interaction message no longer in history");
5123
+ this.history[idx] = {
5124
+ ...this.history[idx],
5125
+ externalCostUsd: (this.history[idx].externalCostUsd ?? 0) + costUsd
5126
+ };
5127
+ recordMetaEvent(this.sessionKey, "interaction.external-cost", { interactionId, costUsd });
5128
+ this.dispatchEvent(
5129
+ new CustomEvent("external-cost-recorded", {
5130
+ detail: { interactionId, costUsd }
5131
+ })
5132
+ );
5133
+ return true;
5134
+ }
5073
5135
  /**
5074
5136
  * Resolve a pending interaction. The wrapper component calls this on completion.
5075
5137
  * Marks the interaction message as resolved so it renders read-only on re-render.
@@ -6509,10 +6571,20 @@ var OrchestratingDriver = class extends EventTarget {
6509
6571
  new CustomEvent("provider-changed", { detail: e.detail })
6510
6572
  );
6511
6573
  });
6574
+ this.chatDriver.addEventListener("external-cost-recorded", (e) => {
6575
+ this.dispatchEvent(
6576
+ new CustomEvent("external-cost-recorded", { detail: e.detail })
6577
+ );
6578
+ });
6512
6579
  }
6513
6580
  resolveInteraction(interactionId, result) {
6514
6581
  this.chatDriver.resolveInteraction(interactionId, result);
6515
6582
  }
6583
+ /** Delegated for the same reason as `resolveInteraction`: the history that owns the
6584
+ * interaction — and therefore the cost — lives on the wrapped driver, not here. */
6585
+ recordExternalCost(interactionId, costUsd) {
6586
+ return this.chatDriver.recordExternalCost(interactionId, costUsd);
6587
+ }
6516
6588
  getInteractionContext(interactionId) {
6517
6589
  return this.chatDriver.getInteractionContext(interactionId);
6518
6590
  }