@genesislcap/ai-assistant 15.10.6 → 15.12.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 (70) hide show
  1. package/dist/ai-assistant.api.json +226 -0
  2. package/dist/ai-assistant.d.ts +100 -3
  3. package/dist/chat-driver.cjs +540 -117
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +524 -116
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +439 -66
  8. package/dist/dts/chat-driver-node.d.ts +5 -2
  9. package/dist/dts/chat-driver-node.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +20 -3
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +2 -0
  13. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +1 -0
  14. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +2 -0
  15. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +1 -0
  16. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  17. package/dist/dts/components/settings-modal/settings-modal.template.d.ts +1 -1
  18. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  19. package/dist/dts/config/config.d.ts +39 -2
  20. package/dist/dts/config/config.d.ts.map +1 -1
  21. package/dist/dts/config/define-stateful-agent.d.ts +15 -1
  22. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  23. package/dist/dts/main/main.d.ts +25 -0
  24. package/dist/dts/main/main.d.ts.map +1 -1
  25. package/dist/dts/main/main.template.d.ts.map +1 -1
  26. package/dist/dts/react.d.ts +9 -8
  27. package/dist/dts/utils/strip-agent-handlers.d.ts +1 -1
  28. package/dist/dts/utils/sum-usage.d.ts +37 -4
  29. package/dist/dts/utils/sum-usage.d.ts.map +1 -1
  30. package/dist/dts/utils/usage-rows.d.ts +102 -0
  31. package/dist/dts/utils/usage-rows.d.ts.map +1 -0
  32. package/dist/dts/utils/usage-rows.test.d.ts +2 -0
  33. package/dist/dts/utils/usage-rows.test.d.ts.map +1 -0
  34. package/dist/esm/chat-driver-node.js +36 -1
  35. package/dist/esm/components/chat-driver/chat-driver.js +73 -10
  36. package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +137 -0
  37. package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +200 -0
  38. package/dist/esm/components/settings-modal/settings-modal.styles.js +22 -0
  39. package/dist/esm/components/settings-modal/settings-modal.template.js +25 -4
  40. package/dist/esm/components/settings-modal/settings-modal.template.test.js +1 -1
  41. package/dist/esm/config/define-stateful-agent.js +11 -0
  42. package/dist/esm/main/blocked-state.test.js +1 -0
  43. package/dist/esm/main/budget-meter.test.js +42 -0
  44. package/dist/esm/main/main.js +49 -0
  45. package/dist/esm/main/main.template.js +20 -1
  46. package/dist/esm/utils/strip-agent-handlers.js +1 -1
  47. package/dist/esm/utils/sum-usage.js +37 -4
  48. package/dist/esm/utils/usage-rows.js +90 -0
  49. package/dist/esm/utils/usage-rows.test.js +189 -0
  50. package/dist/react.cjs +11 -6
  51. package/dist/react.mjs +10 -5
  52. package/dist/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +17 -17
  54. package/src/chat-driver-node.ts +58 -0
  55. package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +185 -0
  56. package/src/components/chat-driver/chat-driver.trace-capture.test.ts +251 -0
  57. package/src/components/chat-driver/chat-driver.ts +90 -10
  58. package/src/components/settings-modal/settings-modal.styles.ts +22 -0
  59. package/src/components/settings-modal/settings-modal.template.test.ts +1 -1
  60. package/src/components/settings-modal/settings-modal.template.ts +27 -2
  61. package/src/config/config.ts +50 -1
  62. package/src/config/define-stateful-agent.ts +37 -0
  63. package/src/main/blocked-state.test.ts +1 -0
  64. package/src/main/budget-meter.test.ts +50 -0
  65. package/src/main/main.template.ts +19 -1
  66. package/src/main/main.ts +47 -0
  67. package/src/utils/strip-agent-handlers.ts +1 -1
  68. package/src/utils/sum-usage.ts +37 -4
  69. package/src/utils/usage-rows.test.ts +237 -0
  70. package/src/utils/usage-rows.ts +187 -0
@@ -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.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/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/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/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/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.10.6",
4
+ "version": "15.12.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.10.6",
77
- "@genesislcap/genx": "15.10.6",
78
- "@genesislcap/rollup-builder": "15.10.6",
79
- "@genesislcap/ts-builder": "15.10.6",
80
- "@genesislcap/uvu-playwright-builder": "15.10.6",
81
- "@genesislcap/vite-builder": "15.10.6",
82
- "@genesislcap/webpack-builder": "15.10.6",
76
+ "@genesislcap/foundation-testing": "15.12.0",
77
+ "@genesislcap/genx": "15.12.0",
78
+ "@genesislcap/rollup-builder": "15.12.0",
79
+ "@genesislcap/ts-builder": "15.12.0",
80
+ "@genesislcap/uvu-playwright-builder": "15.12.0",
81
+ "@genesislcap/vite-builder": "15.12.0",
82
+ "@genesislcap/webpack-builder": "15.12.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.10.6",
89
- "@genesislcap/foundation-logger": "15.10.6",
90
- "@genesislcap/foundation-notifications": "15.10.6",
91
- "@genesislcap/foundation-redux": "15.10.6",
92
- "@genesislcap/foundation-ui": "15.10.6",
93
- "@genesislcap/foundation-utils": "15.10.6",
94
- "@genesislcap/rapid-design-system": "15.10.6",
95
- "@genesislcap/web-core": "15.10.6",
88
+ "@genesislcap/foundation-ai": "15.12.0",
89
+ "@genesislcap/foundation-logger": "15.12.0",
90
+ "@genesislcap/foundation-notifications": "15.12.0",
91
+ "@genesislcap/foundation-redux": "15.12.0",
92
+ "@genesislcap/foundation-ui": "15.12.0",
93
+ "@genesislcap/foundation-utils": "15.12.0",
94
+ "@genesislcap/rapid-design-system": "15.12.0",
95
+ "@genesislcap/web-core": "15.12.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": "2fd12f28a04c8dc20974ab54d3ea143b80229510"
108
+ "gitHead": "06f5ab51086c6616c6b96a7a1aebccddb3af7881"
109
109
  }
@@ -44,8 +44,33 @@ export {
44
44
  SUPPORTED_ANTHROPIC_MODEL_IDS,
45
45
  SUPPORTED_GEMINI_MODEL_IDS,
46
46
  isObservableAIProviderRegistry,
47
+ // Request pricing. Re-exported HERE, not left to a direct `@genesislcap/foundation-ai`
48
+ // import, for the same reason as everything above it: a headless host that adds its own
49
+ // foundation-ai dependency gets a second copy, and the driver's malformed/truncated
50
+ // handling does `instanceof` on the transports' error classes. Reaching the pricing
51
+ // should not cost a consumer that guarantee.
52
+ //
53
+ // These take RAW provider usage rather than a `ChatMessage` — see the foundation-ai
54
+ // module docs. They are for the path where nothing stamped a cost (a proxied or
55
+ // server-side call holding a usage block); when a message already carries `cost`, use
56
+ // it, because it was computed with the per-TTL cache split the message no longer has.
57
+ ANTHROPIC_CACHE_READ_MULTIPLIER,
58
+ ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER,
59
+ ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER,
60
+ anthropicRatesFor,
61
+ anthropicTokenCost,
62
+ GEMINI_CACHED_INPUT_MULTIPLIER,
63
+ GEMINI_LONG_CONTEXT_THRESHOLD,
64
+ geminiRatesFor,
65
+ geminiTokenCost,
66
+ // Resolves a model id to its vendor, so a caller holding only `message.model` can pick
67
+ // between the two cost functions above without maintaining its own model→vendor map.
68
+ vendorOfModel,
47
69
  } from '@genesislcap/foundation-ai';
48
70
  export type {
71
+ // Re-exported alongside `sumUsage` below — a consumer that can call the function but cannot NAME
72
+ // its return type has to restate the shape by hand, which is how the buckets get mis-summed.
73
+ AggregateUsage,
49
74
  AIProvider,
50
75
  AIProviderRegistry,
51
76
  AnthropicModelId,
@@ -53,12 +78,45 @@ export type {
53
78
  ChatFallback,
54
79
  ChatMessage,
55
80
  ChatRequestOptions,
81
+ ChatThinkingPolicy,
56
82
  ChatToolChoice,
57
83
  ChatToolDefinition,
58
84
  ChatToolHandlers,
59
85
  GeminiModelId,
86
+ // Pricing input/output shapes, alongside the functions re-exported above — a caller that
87
+ // can invoke them but cannot name their argument or result has to restate the shape by
88
+ // hand, which is how the token buckets get mixed up between the two providers.
89
+ AnthropicUsageRecord,
90
+ GeminiUsageRecord,
91
+ TokenCost,
92
+ TokenCostBreakdown,
93
+ TokenRates,
60
94
  } from '@genesislcap/foundation-ai';
61
95
 
96
+ // Usage/cost summation. A headless consumer attributing spend to a run has no other route to it:
97
+ // the transports' `getLifetimeCost()` is per-instance and lifetime-scoped, so a shared registry
98
+ // makes it unattributable, and re-deriving cost from token counts is wrong in the one direction
99
+ // that matters (`inputTokens` is total prompt size INCLUDING the cached part, so re-pricing charges
100
+ // every cached token at full rate). `sumUsage` reads the per-request cost the transports already
101
+ // stamped, and recurses into `toolCalls[].subAgentTrace` — where the entire spend of a delegating
102
+ // agent lives — and into `compaction.rolledUpUsage`, which is spend whose messages no longer exist.
103
+ // Both are invisible when omitted: the total is merely too low, which reads as a cheap run.
104
+ //
105
+ // `addUsage`/`emptyUsage` are the composition primitives for a conversation that CONTINUES across
106
+ // rounds — see the guidance on `sumUsage` itself. `totalTokens` exists because the four buckets of
107
+ // an `AggregateUsage` are disjoint and safe to add, which the per-message fields are NOT.
108
+ export { addUsage, emptyUsage, sumUsage, totalTokens } from './utils/sum-usage';
109
+
110
+ // Per-call projection, for the cases an aggregate cannot serve: usage rows for
111
+ // per-project attribution, a cost dashboard, or auditing which turns came back
112
+ // unpriced. `usageRows` is guaranteed to reconcile with `sumUsage` — including spend a
113
+ // compaction banked, which has no message of its own and which a naive per-message walk
114
+ // therefore drops while the total keeps it. Both take RAW history; `UsageRow` is
115
+ // deliberately not a `ChatMessage`, so passing rows back into `sumUsage` (which recurses
116
+ // itself, and would double-count) does not compile.
117
+ export { usageRows } from './utils/usage-rows';
118
+ export type { UsageRow } from './utils/usage-rows';
119
+
62
120
  // Debug-log harvesting (GENC-1461 unified diagnostics). A headless consumer collates its driver's
63
121
  // log — `buildTimelineEntries({ turnSnapshots: driver.getTurnSnapshots(), messages:
64
122
  // driver.getHistory(), metaEvents: getMetaEvents(sessionKey) })` → `DiagnosticEntry[]` — ships it to
@@ -0,0 +1,185 @@
1
+ import type {
2
+ AIProvider,
3
+ AIProviderRegistry,
4
+ ChatMessage,
5
+ ChatRequestOptions,
6
+ ChatThinkingPolicy,
7
+ } from '@genesislcap/foundation-ai';
8
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
9
+ import type { AgentConfig } from '../../config/config';
10
+ // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
11
+ // jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
12
+ import './align-event-globals';
13
+ import { ChatDriver } from './chat-driver';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Per-turn `thinkingPolicy` resolution.
17
+ //
18
+ // The cost case for this option is the SHAPE of a tool loop, not a single call: the
19
+ // opening turn is a real decision and worth reasoning over, while the iterations that
20
+ // follow mostly pick the next tool from a narrow set — and on a thinking model each of
21
+ // those bills reasoning at the full output rate. That saving only exists if the
22
+ // resolver runs per iteration, so these tests assert the sequence across a loop, not
23
+ // just that one value arrives.
24
+ //
25
+ // The other half is the undefined case. Every agent written before this option leaves
26
+ // it unset, and a resolver may answer `undefined` on any given turn; both must reach
27
+ // the transport as `undefined` so the model keeps its own default. `undefined` reaching
28
+ // the wire as `'off'` would be a silent capability regression, and as `'auto'` a silent
29
+ // bill increase — so it is asserted explicitly rather than assumed.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Captures the policy seen on each model call; calls one tool, then finishes. */
33
+ const capturingProvider = (): AIProvider & { seen: Array<ChatThinkingPolicy | undefined> } => {
34
+ const seen: Array<ChatThinkingPolicy | undefined> = [];
35
+ let turns = 0;
36
+ return {
37
+ seen,
38
+ chat: async (
39
+ _history: ChatMessage[],
40
+ _userMessage: string,
41
+ options?: ChatRequestOptions,
42
+ ): Promise<ChatMessage> => {
43
+ seen.push(options?.thinkingPolicy);
44
+ turns += 1;
45
+ // Two tool calls, so the loop runs three model calls in total — enough for a
46
+ // per-turn resolver to say something different on the later ones.
47
+ if (turns <= 2) {
48
+ return {
49
+ role: 'assistant',
50
+ content: '',
51
+ toolCalls: [{ id: `t${turns}`, name: 'step', args: {} }],
52
+ };
53
+ }
54
+ return { role: 'assistant', content: 'done' };
55
+ },
56
+ };
57
+ };
58
+
59
+ const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
60
+ get: () => provider,
61
+ default: () => provider,
62
+ defaultName: () => 'test',
63
+ names: () => ['test'],
64
+ getStatus: async () => null,
65
+ listStatuses: async () => [],
66
+ });
67
+
68
+ const agent = (overrides: Partial<AgentConfig>): AgentConfig =>
69
+ ({
70
+ name: 'worker',
71
+ description: 'test agent',
72
+ toolDefinitions: [
73
+ { name: 'step', description: 'step', parameters: { type: 'object', properties: {} } },
74
+ ],
75
+ toolHandlers: { step: async () => 'stepped' },
76
+ ...overrides,
77
+ }) as AgentConfig;
78
+
79
+ /** Run one user turn through a driver carrying `config`, and return the policies seen. */
80
+ const policiesFor = async (
81
+ config: Partial<AgentConfig>,
82
+ ): Promise<Array<ChatThinkingPolicy | undefined>> => {
83
+ const provider = capturingProvider();
84
+ const driver = new ChatDriver(makeRegistry(provider), {
85
+ maxToolIterations: 10,
86
+ maxFoldOperations: 5,
87
+ sessionKey: '',
88
+ });
89
+ driver.applyAgent(agent(config));
90
+ await driver.sendMessage('go');
91
+ return provider.seen;
92
+ };
93
+
94
+ const suite = createLogicSuite('ChatDriver thinkingPolicy');
95
+
96
+ suite('leaves the policy undefined when the agent does not set one', async () => {
97
+ // The compatibility case: every existing agent. Undefined must reach the transport
98
+ // as undefined so each model keeps its own default posture — not silently coerced
99
+ // to 'off' (a capability regression) or 'auto' (a bill increase).
100
+ const seen = await policiesFor({});
101
+ assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
102
+ assert.equal(
103
+ seen.filter((p) => p !== undefined),
104
+ [],
105
+ 'no turn invents a policy',
106
+ );
107
+ });
108
+
109
+ suite('applies a static policy to every turn of the loop', async () => {
110
+ const seen = await policiesFor({ thinkingPolicy: 'off' });
111
+ assert.ok(seen.length >= 3);
112
+ assert.equal([...new Set(seen)], ['off'], 'a static value is not just a first-turn setting');
113
+ });
114
+
115
+ suite('pins the resolved policy for the whole tool loop', async () => {
116
+ // A tool-use loop is ONE assistant turn and Anthropic requires a single thinking mode
117
+ // for its duration. Toggling part-way does not error — the API silently disables
118
+ // thinking for that request and strips blocks that would leave the turn structure
119
+ // invalid, so an 'auto' -> 'off' switch loses the continuity the opening call
120
+ // established and 'off' -> 'auto' never delivers the reasoning asked for. It also
121
+ // invalidates the prompt cache, costing more than the reasoning it meant to save.
122
+ //
123
+ // So a resolver that changes its mind mid-loop must NOT be honoured mid-loop. This
124
+ // asserts the opposite of what it looks like it should: the later values are ignored.
125
+ let call = 0;
126
+ const seen = await policiesFor({
127
+ thinkingPolicy: () => {
128
+ call += 1;
129
+ return call === 1 ? 'auto' : 'off';
130
+ },
131
+ });
132
+ assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
133
+ assert.equal(
134
+ [...new Set(seen)],
135
+ ['auto'],
136
+ 'the first call decides; later resolutions do not take effect until the next user turn',
137
+ );
138
+ });
139
+
140
+ suite('re-resolves on the next user turn', async () => {
141
+ // The flip side: pinning is per turn, not for the driver's lifetime, so a state change
142
+ // between turns still lands.
143
+ const provider = capturingProvider();
144
+ const driver = new ChatDriver(makeRegistry(provider), {
145
+ maxToolIterations: 10,
146
+ maxFoldOperations: 5,
147
+ sessionKey: '',
148
+ });
149
+ let turn = 0;
150
+ driver.applyAgent(
151
+ agent({
152
+ thinkingPolicy: () => (turn === 0 ? 'auto' : 'off'),
153
+ }),
154
+ );
155
+ await driver.sendMessage('go');
156
+ const firstTurn = [...provider.seen];
157
+ turn = 1;
158
+ await driver.sendMessage('again');
159
+ const secondTurn = provider.seen.slice(firstTurn.length);
160
+
161
+ assert.equal([...new Set(firstTurn)], ['auto'], 'turn one holds its posture');
162
+ assert.equal([...new Set(secondTurn)], ['off'], 'turn two picks up the new one');
163
+ });
164
+
165
+ suite('passes undefined through when the resolver declines to choose', async () => {
166
+ // A resolver may answer on some turns and not others. `undefined` is its third answer —
167
+ // "leave this model alone" — and must not be normalised into a value. Asserted across
168
+ // the whole loop because the first call's answer is the one that gets pinned.
169
+ const seen = await policiesFor({ thinkingPolicy: () => undefined });
170
+ assert.ok(seen.length >= 3);
171
+ assert.equal(
172
+ seen.filter((p) => p !== undefined),
173
+ [],
174
+ 'declining is not the same as choosing',
175
+ );
176
+ });
177
+
178
+ suite('awaits an async resolver', async () => {
179
+ const seen = await policiesFor({
180
+ thinkingPolicy: async () => 'off' as ChatThinkingPolicy,
181
+ });
182
+ assert.equal([...new Set(seen)], ['off'], 'a promise is resolved, not passed through');
183
+ });
184
+
185
+ suite.run();
@@ -0,0 +1,251 @@
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 { sumUsage } from '../../utils/sum-usage';
10
+ import { usageRows } from '../../utils/usage-rows';
11
+ // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
12
+ // jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
13
+ import './align-event-globals';
14
+ import { ChatDriver } from './chat-driver';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Sub-agent trace capture when ONE tool call invokes SEVERAL sub-agents.
18
+ //
19
+ // The capture slot used to be a single `{ trace?: ChatMessage[] }` per tool call,
20
+ // assigned by each `requestSubAgent`. That covers N tool calls × 1 sub-agent — the
21
+ // case its docblock named — but not 1 tool call × N sub-agents, which is what a
22
+ // code-driven scheduler or a retry produces. All but the last trace was dropped, so
23
+ // those children ran, were billed, and then had no record in history.
24
+ //
25
+ // The failure was invisible to every existing check: `sumUsage` and `usageRows` both
26
+ // recurse into `subAgentTrace`, so they summed a truncated input and still reconciled
27
+ // with each other perfectly. Measured at 1 of 7 traces kept on a seven-way fan-out,
28
+ // ~2.3x under the true cost.
29
+ //
30
+ // These tests assert the traces SURVIVE, which is upstream of any cost assertion.
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /** Answers by rule: concurrent children under one turn make a FIFO queue nondeterministic. */
34
+ const ruleProvider = (): AIProvider => {
35
+ let parentTurns = 0;
36
+ return {
37
+ chat: async (
38
+ _history: ChatMessage[],
39
+ _userMessage: string,
40
+ options?: ChatRequestOptions,
41
+ ): Promise<ChatMessage> => {
42
+ const tools = (options?.tools ?? []).map((t) => t.name);
43
+ // Child turn: finishes via its completion tool, and reports usage so the
44
+ // reconciliation assertions have something to add up.
45
+ if (tools.includes('work')) {
46
+ return {
47
+ role: 'assistant',
48
+ content: '',
49
+ model: 'claude-sonnet-5',
50
+ cost: 0.01,
51
+ inputTokens: 100,
52
+ outputTokens: 20,
53
+ toolCalls: [{ id: 'w1', name: 'work', args: {} }],
54
+ };
55
+ }
56
+ if (tools.includes('delegate') && parentTurns === 0) {
57
+ parentTurns += 1;
58
+ return {
59
+ role: 'assistant',
60
+ content: '',
61
+ model: 'claude-sonnet-5',
62
+ cost: 0.02,
63
+ inputTokens: 200,
64
+ outputTokens: 30,
65
+ toolCalls: [{ id: 'd0', name: 'delegate', args: {} }],
66
+ };
67
+ }
68
+ return { role: 'assistant', content: 'done', model: 'claude-sonnet-5' };
69
+ },
70
+ };
71
+ };
72
+
73
+ const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
74
+ get: () => provider,
75
+ default: () => provider,
76
+ defaultName: () => 'test',
77
+ names: () => ['test'],
78
+ getStatus: async () => null,
79
+ listStatuses: async () => [],
80
+ });
81
+
82
+ const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
83
+ ({ description: 'test agent', ...overrides }) as AgentConfig;
84
+
85
+ /** A child that reports one unit of usage and completes. */
86
+ const worker = (name: string): AgentConfig =>
87
+ agent({
88
+ name,
89
+ toolDefinitions: [
90
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
91
+ ],
92
+ toolHandlers: {
93
+ work: async (_args, ctx) => {
94
+ ctx.completeSubAgent?.({ ok: true });
95
+ return 'worked';
96
+ },
97
+ },
98
+ });
99
+
100
+ /**
101
+ * A parent whose single `delegate` tool call invokes `names` in turn — the shape a
102
+ * code-driven scheduler produces, and the one the old single slot truncated.
103
+ */
104
+ const boss = (children: AgentConfig[], invoke: string[]): AgentConfig =>
105
+ agent({
106
+ name: 'boss',
107
+ subAgents: children,
108
+ toolDefinitions: [
109
+ { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
110
+ ],
111
+ toolHandlers: {
112
+ delegate: async (_args, ctx) => {
113
+ await Promise.all(invoke.map((n) => ctx.requestSubAgent!(n, { task: 'go' })));
114
+ return 'delegated';
115
+ },
116
+ },
117
+ });
118
+
119
+ const run = async (config: AgentConfig): Promise<readonly ChatMessage[]> => {
120
+ const driver = new ChatDriver(makeRegistry(ruleProvider()), {
121
+ maxToolIterations: 20,
122
+ maxFoldOperations: 5,
123
+ sessionKey: '',
124
+ });
125
+ driver.applyAgent(config);
126
+ await driver.sendMessage('go');
127
+ return driver.getHistory();
128
+ };
129
+
130
+ const tracesIn = (history: readonly ChatMessage[]): readonly ChatMessage[][] =>
131
+ history.flatMap((m) =>
132
+ (m.toolCalls ?? []).flatMap((tc) => (tc.subAgentTrace ? [tc.subAgentTrace] : [])),
133
+ );
134
+
135
+ const suite = createLogicSuite('ChatDriver sub-agent trace capture');
136
+
137
+ // ---------------------------------------------------------------------------
138
+
139
+ suite('keeps every trace when one tool call invokes three sub-agents', async () => {
140
+ const names = ['gen_a', 'gen_b', 'gen_c'];
141
+ const history = await run(boss(names.map(worker), names));
142
+
143
+ const traces = tracesIn(history);
144
+ assert.is(traces.length, 1, 'one tool call, so one concatenated trace');
145
+
146
+ // Each child contributes at least its own assistant turn. Before the fix this was
147
+ // one child's worth regardless of how many ran.
148
+ const seen = new Set(traces[0].map((m) => m.agentName).filter(Boolean));
149
+ assert.equal(
150
+ [...seen].sort(),
151
+ names,
152
+ `every invoked child must appear — got ${JSON.stringify([...seen])}`,
153
+ );
154
+ });
155
+
156
+ suite('prices every child — usageRows emits a row per child and reconciles', async () => {
157
+ // The reconciliation test extended to a fan-out. The single-child version passed
158
+ // before the fix AND after it, which is exactly why it did not catch this.
159
+ const names = ['gen_a', 'gen_b', 'gen_c'];
160
+ const history = await run(boss(names.map(worker), names));
161
+
162
+ const rows = usageRows(history);
163
+ const childRows = rows.filter((r) => r.subAgentDepth === 1);
164
+ assert.is(childRows.length, names.length, 'one row per child that ran');
165
+
166
+ const rowTotal = rows.reduce((n, r) => n + (r.costUsd ?? 0) + (r.externalCostUsd ?? 0), 0);
167
+ assert.ok(
168
+ Math.abs(rowTotal - sumUsage(history).costUsd) < 1e-12,
169
+ `rows ${rowTotal} vs sumUsage ${sumUsage(history).costUsd}`,
170
+ );
171
+ // Parent turn ($0.02) + three children ($0.01 each). Asserted as a number so a
172
+ // regression that silently drops a child fails here rather than only in the
173
+ // reconciliation above, which would still agree with a truncated input.
174
+ assert.ok(Math.abs(rowTotal - 0.05) < 1e-12, `expected 0.05, got ${rowTotal}`);
175
+ });
176
+
177
+ suite('keeps both traces when the same sub-agent is invoked twice (a retry)', async () => {
178
+ // The retry shape: one handler, one child name, two invocations. Under the old slot
179
+ // the first attempt's trace was overwritten by the second.
180
+ const history = await run(boss([worker('gen_a')], ['gen_a', 'gen_a']));
181
+
182
+ const traces = tracesIn(history);
183
+ assert.is(traces.length, 1);
184
+ const childTurns = traces[0].filter((m) => m.agentName === 'gen_a' && m.cost != null);
185
+ assert.is(childTurns.length, 2, 'both attempts survive, not just the last');
186
+
187
+ const rows = usageRows(history).filter((r) => r.subAgentDepth === 1);
188
+ assert.is(rows.length, 2, 'and both are priced');
189
+ });
190
+
191
+ suite('keeps child traces when the parent handler throws after they ran', async () => {
192
+ // Children that completed were billed by the provider. If the error path drops
193
+ // their traces, the run under-reports exactly as the single-slot bug did — and
194
+ // just as silently, since `sumUsage` and `usageRows` would still agree with each
195
+ // other over the truncated input. PR review.
196
+ const names = ['gen_a', 'gen_b'];
197
+ const parent = agent({
198
+ name: 'boss',
199
+ subAgents: names.map(worker),
200
+ toolDefinitions: [
201
+ { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
202
+ ],
203
+ toolHandlers: {
204
+ delegate: async (_args, ctx) => {
205
+ await Promise.all(names.map((n) => ctx.requestSubAgent!(n, { task: 'go' })));
206
+ throw new Error('post-processing the children failed');
207
+ },
208
+ },
209
+ });
210
+
211
+ const history = await run(parent);
212
+ const traces = tracesIn(history);
213
+ assert.is(traces.length, 1, 'the failed tool call still carries its trace');
214
+ const seen = [...new Set(traces[0].map((m) => m.agentName).filter(Boolean))].sort();
215
+ assert.equal(seen, names, 'both children survive the throw');
216
+
217
+ const childRows = usageRows(history).filter((r) => r.subAgentDepth === 1);
218
+ assert.is(childRows.length, 2, 'and both are still priced');
219
+ });
220
+
221
+ suite('leaves subAgentTrace undefined when no sub-agent ran', async () => {
222
+ // Presence is the signal readers key off (`usage-rows.ts`, the UI's `when(...)`),
223
+ // so an empty array would be a different and wrong claim.
224
+ const history = await run(
225
+ agent({
226
+ name: 'boss',
227
+ subAgents: [worker('gen_a')],
228
+ toolDefinitions: [
229
+ {
230
+ name: 'delegate',
231
+ description: 'delegate',
232
+ parameters: { type: 'object', properties: {} },
233
+ },
234
+ ],
235
+ toolHandlers: { delegate: async () => 'did it myself' },
236
+ }),
237
+ );
238
+
239
+ for (const m of history) {
240
+ for (const tc of m.toolCalls ?? []) {
241
+ assert.is(tc.subAgentTrace, undefined, 'not an empty array');
242
+ }
243
+ }
244
+ assert.equal(
245
+ usageRows(history).filter((r) => r.subAgentDepth === 1),
246
+ [],
247
+ 'no child rows',
248
+ );
249
+ });
250
+
251
+ suite.run();