@genesislcap/ai-assistant 14.495.0 → 14.496.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 (36) hide show
  1. package/dist/ai-assistant.api.json +576 -211
  2. package/dist/ai-assistant.d.ts +128 -5
  3. package/dist/chat-driver.cjs +5608 -0
  4. package/dist/chat-driver.cjs.map +7 -0
  5. package/dist/chat-driver.mjs +5566 -0
  6. package/dist/chat-driver.mjs.map +7 -0
  7. package/dist/custom-elements.json +306 -2
  8. package/dist/dts/channel/ai-activity-bus.d.ts +36 -0
  9. package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
  10. package/dist/dts/chat-driver-node.d.ts +28 -0
  11. package/dist/dts/chat-driver-node.d.ts.map +1 -0
  12. package/dist/dts/components/chat-driver/chat-driver.d.ts +49 -5
  13. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +3 -0
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  16. package/dist/dts/config/config.d.ts +37 -2
  17. package/dist/dts/config/config.d.ts.map +1 -1
  18. package/dist/dts/main/main.d.ts.map +1 -1
  19. package/dist/esm/channel/ai-activity-bus.js +48 -12
  20. package/dist/esm/chat-driver-node.js +33 -0
  21. package/dist/esm/components/chat-driver/chat-driver.js +28 -14
  22. package/dist/esm/components/chat-driver/chat-driver.test.js +32 -40
  23. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +7 -1
  24. package/dist/esm/main/main.js +8 -1
  25. package/dist/esm/main/popout-interaction-gate.test.js +6 -10
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +29 -19
  28. package/scripts/build-chat-driver-node.mjs +42 -0
  29. package/src/channel/ai-activity-bus.ts +64 -10
  30. package/src/chat-driver-node.ts +54 -0
  31. package/src/components/chat-driver/chat-driver.test.ts +36 -62
  32. package/src/components/chat-driver/chat-driver.ts +96 -28
  33. package/src/components/orchestrating-driver/orchestrating-driver.ts +10 -11
  34. package/src/config/config.ts +40 -1
  35. package/src/main/main.ts +8 -11
  36. package/src/main/popout-interaction-gate.test.ts +6 -11
@@ -1,5 +1,4 @@
1
1
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { agenticActivityBus } from '../channel/ai-activity-bus';
3
2
  import { FoundationAiAssistant } from './main';
4
3
  // Hold a reference so the custom-element registration isn't tree-shaken.
5
4
  FoundationAiAssistant;
@@ -11,16 +10,13 @@ FoundationAiAssistant;
11
10
  // (`main.template.ts`). This suite pins the GATE LOGIC (`hasActivePendingInteraction`)
12
11
  // that binding depends on, so a change to when the gate opens/closes is caught.
13
12
  //
14
- // Note: this deliberately does NOT mount the assistant. Mounting runs
15
- // `connectedCallback`, which activates the `agenticActivityBus` `BroadcastChannel` and
16
- // leaves the runner's event loop open (the suite hangs on exit). A getter-level test
17
- // needs no mount and stays fast + reliable. `document.createElement` only upgrades the
18
- // element (constructor) it does not connect so no bus subscription is created;
19
- // importing the module still opens the module-load channel, so close it after the suite.
13
+ // Note: this deliberately does NOT mount the assistant. Mounting runs `connectedCallback`,
14
+ // which subscribes to `agenticActivityBus` — opening a cross-tab `BroadcastChannel` in a browser
15
+ // env and leaving the runner's event loop open. A getter-level test needs no mount and stays fast
16
+ // + reliable. `document.createElement` only upgrades the element (constructor) — it does not
17
+ // connectso nothing subscribes, and the bus opens its channel lazily on first use, so none is
18
+ // ever created here (no teardown needed).
20
19
  const Suite = createLogicSuite('FoundationAiAssistant popout interaction gate');
21
- Suite.after(() => {
22
- agenticActivityBus.close();
23
- });
24
20
  /** A fresh (unconnected) element with a fake session store wired in. */
25
21
  function elementWith(state, messages) {
26
22
  const el = document.createElement('foundation-ai-assistant');
@@ -1 +1 @@
1
- {"root":["../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.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.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.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/index.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/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/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/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/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/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.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.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.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/index.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/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/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/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/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/tool-fold.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": "14.495.0",
4
+ "version": "14.496.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -27,6 +27,11 @@
27
27
  "types": "./dist/dts/config/index.d.ts",
28
28
  "default": "./dist/esm/config/index.js"
29
29
  },
30
+ "./chat-driver": {
31
+ "types": "./dist/dts/chat-driver-node.d.ts",
32
+ "import": "./dist/chat-driver.mjs",
33
+ "require": "./dist/chat-driver.cjs"
34
+ },
30
35
  "./package.json": "./package.json",
31
36
  "./react": {
32
37
  "types": "./dist/dts/react.d.ts",
@@ -38,11 +43,15 @@
38
43
  "*": {
39
44
  "config": [
40
45
  "./dist/dts/config/index.d.ts"
46
+ ],
47
+ "chat-driver": [
48
+ "./dist/dts/chat-driver-node.d.ts"
41
49
  ]
42
50
  }
43
51
  },
44
52
  "scripts": {
45
- "build": "genx build -b ts",
53
+ "build": "genx build -b ts && node ./scripts/build-chat-driver-node.mjs",
54
+ "build:chat-driver": "node ./scripts/build-chat-driver-node.mjs",
46
55
  "build:webpack": "genx build",
47
56
  "build:webpack:stats": "genx analyze",
48
57
  "circular": "npx -y madge --extensions ts --circular ./src",
@@ -64,25 +73,26 @@
64
73
  }
65
74
  },
66
75
  "devDependencies": {
67
- "@genesislcap/foundation-testing": "14.495.0",
68
- "@genesislcap/genx": "14.495.0",
69
- "@genesislcap/rollup-builder": "14.495.0",
70
- "@genesislcap/ts-builder": "14.495.0",
71
- "@genesislcap/uvu-playwright-builder": "14.495.0",
72
- "@genesislcap/vite-builder": "14.495.0",
73
- "@genesislcap/webpack-builder": "14.495.0",
76
+ "@genesislcap/foundation-testing": "14.496.0",
77
+ "@genesislcap/genx": "14.496.0",
78
+ "@genesislcap/rollup-builder": "14.496.0",
79
+ "@genesislcap/ts-builder": "14.496.0",
80
+ "@genesislcap/uvu-playwright-builder": "14.496.0",
81
+ "@genesislcap/vite-builder": "14.496.0",
82
+ "@genesislcap/webpack-builder": "14.496.0",
74
83
  "@types/dompurify": "^3.0.5",
75
- "@types/marked": "^5.0.2"
84
+ "@types/marked": "^5.0.2",
85
+ "esbuild": "0.25.12"
76
86
  },
77
87
  "dependencies": {
78
- "@genesislcap/foundation-ai": "14.495.0",
79
- "@genesislcap/foundation-logger": "14.495.0",
80
- "@genesislcap/foundation-notifications": "14.495.0",
81
- "@genesislcap/foundation-redux": "14.495.0",
82
- "@genesislcap/foundation-ui": "14.495.0",
83
- "@genesislcap/foundation-utils": "14.495.0",
84
- "@genesislcap/rapid-design-system": "14.495.0",
85
- "@genesislcap/web-core": "14.495.0",
88
+ "@genesislcap/foundation-ai": "14.496.0",
89
+ "@genesislcap/foundation-logger": "14.496.0",
90
+ "@genesislcap/foundation-notifications": "14.496.0",
91
+ "@genesislcap/foundation-redux": "14.496.0",
92
+ "@genesislcap/foundation-ui": "14.496.0",
93
+ "@genesislcap/foundation-utils": "14.496.0",
94
+ "@genesislcap/rapid-design-system": "14.496.0",
95
+ "@genesislcap/web-core": "14.496.0",
86
96
  "dompurify": "^3.3.1",
87
97
  "marked": "^17.0.3"
88
98
  },
@@ -95,5 +105,5 @@
95
105
  "access": "public"
96
106
  },
97
107
  "customElements": "dist/custom-elements.json",
98
- "gitHead": "24d26a75271717d40c0b6168b983bd8536a3db7f"
108
+ "gitHead": "2ae3ec60cab8440c6beb1da84e2d47239b11e9b5"
99
109
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Bundles the Node-safe `./chat-driver` subpath into self-contained, Node-loadable dual output
3
+ * (`dist/chat-driver.mjs` + `dist/chat-driver.cjs`).
4
+ *
5
+ * Why a bundler and not just tsc: `genx build -b ts` emits bundler-target ESM (extensionless
6
+ * relative imports + a bare `tslib` import) that Node's own loader can't resolve. This step bundles
7
+ * `src/chat-driver-node.ts` and its closure (including `@genesislcap/foundation-ai`, `tslib`, and
8
+ * the logger) inline, so a plain Node/CommonJS host can `require`/`import` the driver stack with no
9
+ * bundler and no jsdom. The FAST/DOM layer is never in this entry's closure, and `foundation-ai`'s
10
+ * DI module (its only `@microsoft/fast-foundation` importer) tree-shakes out.
11
+ *
12
+ * Runs after `genx build -b ts` (which produces the matching `dist/dts/chat-driver-node.d.ts`).
13
+ */
14
+ import { dirname, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import * as esbuild from 'esbuild';
17
+
18
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
19
+ const entry = resolve(packageRoot, 'src/chat-driver-node.ts');
20
+
21
+ /** @type {import('esbuild').BuildOptions} */
22
+ const shared = {
23
+ entryPoints: [entry],
24
+ bundle: true,
25
+ platform: 'node',
26
+ target: 'node22',
27
+ // Self-contained: bundle third-party deps (foundation-ai, tslib, logger) inline so the artifact
28
+ // is Node-loadable standalone. Node built-ins are external automatically under platform: 'node'.
29
+ sourcemap: 'linked',
30
+ logLevel: 'info',
31
+ };
32
+
33
+ await esbuild.build({
34
+ ...shared,
35
+ format: 'esm',
36
+ outfile: resolve(packageRoot, 'dist/chat-driver.mjs'),
37
+ });
38
+ await esbuild.build({
39
+ ...shared,
40
+ format: 'cjs',
41
+ outfile: resolve(packageRoot, 'dist/chat-driver.cjs'),
42
+ });
@@ -10,10 +10,37 @@ export interface AgenticActivityBusConfig {
10
10
  crossTabEvents?: (keyof AgenticActivityEvents)[];
11
11
  /** BroadcastChannel name. Only used when crossTabEvents is non-empty. */
12
12
  channelName?: string;
13
+ /**
14
+ * Lazy alternative to {@link AgenticActivityBusConfig.channelName}: called the first
15
+ * time a cross-tab channel is actually needed, in a browser only. Deferring the name
16
+ * computation keeps side effects (e.g. `sessionStorage` reads) out of module-eval and
17
+ * off the non-browser path. Ignored if `channelName` is also set.
18
+ */
19
+ channelNameFactory?: () => string;
13
20
  }
14
21
 
15
22
  type AnyHandler = (detail: any) => void;
16
23
 
24
+ /**
25
+ * Minimal activity-bus surface the drivers depend on. `ChatDriver`/`OrchestratingDriver`
26
+ * only ever *publish* — accepting this narrow interface (rather than importing the module
27
+ * singleton) lets a headless/server or unit-test host omit the bus entirely, so no live
28
+ * `BroadcastChannel` is ever opened on that path. See {@link NOOP_ACTIVITY_BUS}.
29
+ *
30
+ * @beta
31
+ */
32
+ export interface ActivityBus {
33
+ publish<K extends keyof AgenticActivityEvents>(type: K, detail: AgenticActivityEvents[K]): void;
34
+ }
35
+
36
+ /**
37
+ * No-op {@link ActivityBus}. The default when no bus is injected (Node, tests, headless
38
+ * server use) — publishing goes nowhere and nothing is left open to tear down.
39
+ *
40
+ * @beta
41
+ */
42
+ export const NOOP_ACTIVITY_BUS: ActivityBus = { publish: () => {} };
43
+
17
44
  /**
18
45
  * Typed in-process event bus for AI assistant activity events.
19
46
  *
@@ -31,7 +58,9 @@ type AnyHandler = (detail: any) => void;
31
58
  export class AgenticActivityBus {
32
59
  private readonly subscribers = new Map<string, Set<AnyHandler>>();
33
60
  private readonly crossTabEvents: Set<string>;
61
+ private readonly channelNameFactory?: () => string;
34
62
  private channel?: BroadcastChannel;
63
+ private channelInitialised = false;
35
64
 
36
65
  /**
37
66
  * @param config - Optional configuration. Omit to create an in-memory-only bus.
@@ -39,13 +68,28 @@ export class AgenticActivityBus {
39
68
  */
40
69
  constructor(config?: AgenticActivityBusConfig) {
41
70
  this.crossTabEvents = new Set(config?.crossTabEvents ?? []);
42
- if (this.crossTabEvents.size > 0 && config?.channelName) {
43
- this.channel = new BroadcastChannel(config.channelName);
44
- this.channel.onmessage = (ev) => {
45
- const { type, detail } = (ev.data ?? {}) as { type?: string; detail?: unknown };
46
- if (type) this.notifyLocal(type, detail);
47
- };
48
- }
71
+ this.channelNameFactory =
72
+ config?.channelName != null ? () => config.channelName! : config?.channelNameFactory;
73
+ }
74
+
75
+ /**
76
+ * Lazily open the cross-tab `BroadcastChannel` on first use. Deferred (rather than
77
+ * created in the constructor) so that importing this module, or constructing the bus
78
+ * off-browser (Node, tests, headless server), never opens a live channel or reads
79
+ * `sessionStorage` — both would keep a Node event loop alive and hang the process.
80
+ * Cross-tab forwarding is inherently browser-only, so channel creation is gated on a
81
+ * browser environment; elsewhere the bus stays purely in-process.
82
+ */
83
+ private ensureChannel(): void {
84
+ if (this.channelInitialised) return;
85
+ this.channelInitialised = true;
86
+ const inBrowser = typeof window !== 'undefined' && typeof BroadcastChannel !== 'undefined';
87
+ if (this.crossTabEvents.size === 0 || !this.channelNameFactory || !inBrowser) return;
88
+ this.channel = new BroadcastChannel(this.channelNameFactory());
89
+ this.channel.onmessage = (ev) => {
90
+ const { type, detail } = (ev.data ?? {}) as { type?: string; detail?: unknown };
91
+ if (type) this.notifyLocal(type, detail);
92
+ };
49
93
  }
50
94
 
51
95
  /**
@@ -58,6 +102,9 @@ export class AgenticActivityBus {
58
102
  type: K,
59
103
  handler: (detail: AgenticActivityEvents[K]) => void,
60
104
  ): () => void {
105
+ // Open the cross-tab channel (browser only) so this subscriber receives events
106
+ // forwarded from other tabs; no-op off-browser.
107
+ this.ensureChannel();
61
108
  let handlers = this.subscribers.get(type as string);
62
109
  if (!handlers) {
63
110
  handlers = new Set();
@@ -76,8 +123,9 @@ export class AgenticActivityBus {
76
123
  */
77
124
  publish<K extends keyof AgenticActivityEvents>(type: K, detail: AgenticActivityEvents[K]): void {
78
125
  this.notifyLocal(type as string, detail);
79
- if (this.crossTabEvents.has(type as string) && this.channel) {
80
- this.channel.postMessage({ type, detail });
126
+ if (this.crossTabEvents.has(type as string)) {
127
+ this.ensureChannel();
128
+ this.channel?.postMessage({ type, detail });
81
129
  }
82
130
  }
83
131
 
@@ -124,6 +172,9 @@ export class AgenticActivityBus {
124
172
  * instances on the same origin.
125
173
  */
126
174
  function getTabScopedChannelName(base: string): string {
175
+ // Belt-and-braces: this only runs in a browser (see `ensureChannel`), but guard anyway
176
+ // so a direct call off-browser never throws a `sessionStorage is not defined` ReferenceError.
177
+ if (typeof sessionStorage === 'undefined') return `${base}-nostorage`;
127
178
  const key = `${base}-tab-id`;
128
179
  let id = sessionStorage.getItem(key);
129
180
  if (!id) {
@@ -151,5 +202,8 @@ function getTabScopedChannelName(base: string): string {
151
202
  */
152
203
  export const agenticActivityBus = new AgenticActivityBus({
153
204
  crossTabEvents: ['halo-start', 'halo-stop', 'tool-loop-start', 'tool-loop-end'],
154
- channelName: getTabScopedChannelName('foundation-ai-activity'),
205
+ // Lazy: the tab-scoped name (and its `sessionStorage` read) is computed on first
206
+ // cross-tab use in a browser, not at module-eval — so importing this module is
207
+ // side-effect-free and Node/test-safe.
208
+ channelNameFactory: () => getTabScopedChannelName('foundation-ai-activity'),
155
209
  });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Node-safe headless entry point for the AI assistant driver stack (`@genesislcap/ai-assistant/chat-driver`).
3
+ *
4
+ * Re-exports ONLY the framework-agnostic pieces — {@link ChatDriver}, `OrchestratingDriver`, the
5
+ * agent-config surface (`defineAgent`, `defineStatefulAgent`, and the config/input types), and the
6
+ * activity-bus types/`NOOP_ACTIVITY_BUS` — WITHOUT the DOM/FAST component layer that the package's
7
+ * main entry (`.`) pulls in via `main/main`. This lets a Node / CommonJS host (server, unit tests,
8
+ * benches) construct and drive `ChatDriver` over `foundation-ai` providers with no jsdom and no
9
+ * custom-element registration.
10
+ *
11
+ * Every module re-exported here is DOM-clean at import time (no `window`/`document`/`sessionStorage`
12
+ * eval-time access, no `@genesislcap/web-core`). The activity bus is imported for its types and the
13
+ * no-op default; its cross-tab `BroadcastChannel` is created lazily and only in a browser, so
14
+ * pulling this entry into Node opens nothing.
15
+ *
16
+ * @packageDocumentation
17
+ * @beta
18
+ */
19
+ export * from './components/chat-driver';
20
+ export * from './components/orchestrating-driver';
21
+ export * from './channel/ai-activity-channel';
22
+ export * from './channel/ai-activity-bus';
23
+ export * from './config/config';
24
+ export * from './config/define-stateful-agent';
25
+ export * from './config/fallback-agents';
26
+
27
+ // Provider-construction surface, re-exported from `@genesislcap/foundation-ai` so a headless
28
+ // consumer builds its registry from the SAME bundled foundation-ai instance the driver uses. That
29
+ // shared identity matters: `ChatDriver`'s malformed/truncated handling does `instanceof` on the
30
+ // transports' error classes, so a second foundation-ai copy (a separate dep) would silently break
31
+ // those checks. NAMED re-exports only — `export *` would drag in the FAST-DI module
32
+ // (`@microsoft/fast-foundation` → `fast-element` → `document` at eval); by naming just these,
33
+ // esbuild tree-shakes the DI module out and the bundle stays Node-loadable.
34
+ export {
35
+ AnthropicProvider,
36
+ AnthropicTransport,
37
+ GeminiProvider,
38
+ GeminiTransport,
39
+ MutableAIProviderRegistry,
40
+ isObservableAIProviderRegistry,
41
+ } from '@genesislcap/foundation-ai';
42
+ export type {
43
+ AIProvider,
44
+ AIProviderRegistry,
45
+ AnthropicModelId,
46
+ CachePolicy,
47
+ ChatFallback,
48
+ ChatMessage,
49
+ ChatRequestOptions,
50
+ ChatToolChoice,
51
+ ChatToolDefinition,
52
+ ChatToolHandlers,
53
+ GeminiModelId,
54
+ } from '@genesislcap/foundation-ai';
@@ -17,7 +17,7 @@ import {
17
17
  ResponseTruncatedError,
18
18
  } from '@genesislcap/foundation-ai';
19
19
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
20
- import { agenticActivityBus } from '../../channel/ai-activity-bus';
20
+ import { type ActivityBus, AgenticActivityBus } from '../../channel/ai-activity-bus';
21
21
  import type { AgentConfig } from '../../config/config';
22
22
  import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
23
23
  import { sumCosts } from '../../utils/sum-costs';
@@ -120,18 +120,18 @@ const callsTool = (name: string, id: string): ChatMessage => ({
120
120
  const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
121
121
  ({ description: 'test agent', ...overrides }) as AgentConfig;
122
122
 
123
- const makeDriver = (config: AgentConfig, provider: AIProvider, sessionKey = ''): ChatDriver => {
124
- const driver = new ChatDriver(
125
- makeRegistry(provider),
126
- {},
127
- [],
128
- undefined,
129
- undefined,
130
- 50,
131
- 5,
132
- undefined,
123
+ const makeDriver = (
124
+ config: AgentConfig,
125
+ provider: AIProvider,
126
+ sessionKey = '',
127
+ activityBus?: ActivityBus,
128
+ ): ChatDriver => {
129
+ const driver = new ChatDriver(makeRegistry(provider), {
130
+ maxToolIterations: 50,
131
+ maxFoldOperations: 5,
133
132
  sessionKey,
134
- );
133
+ activityBus,
134
+ });
135
135
  driver.applyAgent(config);
136
136
  return driver;
137
137
  };
@@ -159,13 +159,6 @@ const unresolvedEvents = (sessionKey: string): Array<Record<string, unknown>> =>
159
159
 
160
160
  const stale = createLogicSuite('ChatDriver stale-tool detection');
161
161
 
162
- // The driver imports the `agenticActivityBus` singleton, which opens a
163
- // BroadcastChannel at module load. An open channel keeps the test page alive
164
- // and hangs the runner, so close it once the suite finishes.
165
- stale.after(() => {
166
- agenticActivityBus.close();
167
- });
168
-
169
162
  stale('guides the model when it calls a tool that an earlier state exposed', async () => {
170
163
  // State A exposes tool_a; calling it advances to state B, which exposes only
171
164
  // tool_b. A factory-form agent narrows the tool set per turn, mirroring how
@@ -363,10 +356,6 @@ stale.run();
363
356
 
364
357
  const hook = createLogicSuite('ChatDriver onUnresolvedTool hook');
365
358
 
366
- hook.after(() => {
367
- agenticActivityBus.close();
368
- });
369
-
370
359
  hook('replaces the default for a hallucinated tool when the hook returns a string', async () => {
371
360
  const config = agent({
372
361
  name: 'Hooked',
@@ -722,12 +711,6 @@ cancel.run();
722
711
 
723
712
  const subagent = createLogicSuite('ChatDriver sub-agents');
724
713
 
725
- subagent.after(() => {
726
- // Safe to call again even if `stale` already closed it — close() is
727
- // idempotent and cross-tab publishes are guarded by `&& this.channel`.
728
- agenticActivityBus.close();
729
- });
730
-
731
714
  /** A sub-agent named `worker` that finishes by calling `completeSubAgent`. */
732
715
  const completingWorker = (result: unknown): AgentConfig =>
733
716
  agent({
@@ -1536,17 +1519,18 @@ interactionContextLifecycle.run();
1536
1519
 
1537
1520
  const interactionBus = createLogicSuite('ChatDriver interaction activity-bus signals');
1538
1521
 
1539
- interactionBus.after(() => {
1540
- agenticActivityBus.close();
1541
- });
1522
+ // These two tests need to observe what the driver publishes, so they inject a local
1523
+ // in-memory bus (no `crossTabEvents` → no BroadcastChannel is ever opened, nothing to close)
1524
+ // rather than the shared singleton. Every other suite lets the driver default to the no-op bus.
1542
1525
 
1543
1526
  interactionBus('brackets a park with interaction-requested then -resolved', async () => {
1544
1527
  const events: string[] = [];
1528
+ const bus = new AgenticActivityBus();
1545
1529
  const unsubs = [
1546
- agenticActivityBus.subscribe('interaction-requested', () => events.push('requested')),
1547
- agenticActivityBus.subscribe('interaction-resolved', () => events.push('resolved')),
1530
+ bus.subscribe('interaction-requested', () => events.push('requested')),
1531
+ bus.subscribe('interaction-resolved', () => events.push('resolved')),
1548
1532
  ];
1549
- const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
1533
+ const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]), '', bus);
1550
1534
 
1551
1535
  const pending = driver.requestInteraction('w', {});
1552
1536
  assert.equal(events, ['requested'], 'parking fires interaction-requested');
@@ -1562,8 +1546,9 @@ interactionBus('brackets a park with interaction-requested then -resolved', asyn
1562
1546
 
1563
1547
  interactionBus('a timed-out interaction still fires interaction-resolved', async () => {
1564
1548
  const events: string[] = [];
1565
- const unsub = agenticActivityBus.subscribe('interaction-resolved', () => events.push('resolved'));
1566
- const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
1549
+ const bus = new AgenticActivityBus();
1550
+ const unsub = bus.subscribe('interaction-resolved', () => events.push('resolved'));
1551
+ const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]), '', bus);
1567
1552
 
1568
1553
  // Never resolved by a user — the timeout path runs the same teardown, so it
1569
1554
  // must signal the bus too (else the button would stay enabled after a timeout).
@@ -1719,17 +1704,13 @@ const makeObservableRegistry = (initial: AIProvider): ObservableTestRegistry =>
1719
1704
  };
1720
1705
 
1721
1706
  const makeDriverWithRegistry = (config: AgentConfig, registry: AIProviderRegistry): ChatDriver => {
1722
- const driver = new ChatDriver(registry, {}, [], undefined, undefined, 50, 5, undefined, '');
1707
+ const driver = new ChatDriver(registry, { maxToolIterations: 50, maxFoldOperations: 5 });
1723
1708
  driver.applyAgent(config);
1724
1709
  return driver;
1725
1710
  };
1726
1711
 
1727
1712
  const observable = createLogicSuite('ChatDriver observable provider registry');
1728
1713
 
1729
- observable.after(() => {
1730
- agenticActivityBus.close();
1731
- });
1732
-
1733
1714
  observable(
1734
1715
  'a registry change clears the resolved-provider cache so the next turn uses the new provider',
1735
1716
  async () => {
@@ -1828,10 +1809,6 @@ const modelProvider = (model: string | undefined, responses: ChatMessage[]): AIP
1828
1809
 
1829
1810
  const modelAttr = createLogicSuite('ChatDriver per-message model attribution');
1830
1811
 
1831
- modelAttr.after(() => {
1832
- agenticActivityBus.close();
1833
- });
1834
-
1835
1812
  modelAttr('stamps the resolved model and registry name onto an assistant reply', async () => {
1836
1813
  const provider = modelProvider('gemini-2.5-flash-lite', [
1837
1814
  { role: 'assistant', content: 'hi there' },
@@ -1918,9 +1895,6 @@ modelAttr.run();
1918
1895
  // ---------------------------------------------------------------------------
1919
1896
 
1920
1897
  const condense = createLogicSuite('ChatDriver condenseWhen');
1921
- condense.after(() => {
1922
- agenticActivityBus.close();
1923
- });
1924
1898
 
1925
1899
  const bigBody = 'A'.repeat(2000);
1926
1900
 
@@ -2193,8 +2167,12 @@ condense.run();
2193
2167
 
2194
2168
  const outcome = createLogicSuite('ChatDriver turn-outcome surfacing');
2195
2169
 
2170
+ // This branch routes activity events to an INJECTED bus (default NOOP), not the module
2171
+ // singleton, so these tests inject a local bus and observe the same seam the driver publishes to.
2172
+ const outcomeBus = new AgenticActivityBus();
2173
+
2196
2174
  outcome.after(() => {
2197
- agenticActivityBus.close();
2175
+ outcomeBus.close();
2198
2176
  });
2199
2177
 
2200
2178
  /**
@@ -2209,7 +2187,7 @@ const captureLoopEnd = (): {
2209
2187
  } => {
2210
2188
  let last: { failureReason?: TurnFailureReason } | undefined;
2211
2189
  let seen = false;
2212
- const stop = agenticActivityBus.subscribe('tool-loop-end', (d) => {
2190
+ const stop = outcomeBus.subscribe('tool-loop-end', (d) => {
2213
2191
  last = d;
2214
2192
  seen = true;
2215
2193
  });
@@ -2223,17 +2201,12 @@ const makeCappedDriver = (
2223
2201
  maxIterations: number,
2224
2202
  sessionKey = '',
2225
2203
  ): ChatDriver => {
2226
- const driver = new ChatDriver(
2227
- makeRegistry(provider),
2228
- {},
2229
- [],
2230
- undefined,
2231
- undefined,
2232
- maxIterations,
2233
- 5,
2234
- undefined,
2204
+ const driver = new ChatDriver(makeRegistry(provider), {
2205
+ maxToolIterations: maxIterations,
2206
+ maxFoldOperations: 5,
2235
2207
  sessionKey,
2236
- );
2208
+ activityBus: outcomeBus,
2209
+ });
2237
2210
  driver.applyAgent(config);
2238
2211
  return driver;
2239
2212
  };
@@ -2250,7 +2223,7 @@ const assertSurfacesReason = async (
2250
2223
  c,
2251
2224
  p,
2252
2225
  k,
2253
- ) => makeDriver(c, p, k),
2226
+ ) => makeDriver(c, p, k, outcomeBus),
2254
2227
  ): Promise<void> => {
2255
2228
  clearMetaEventRegistry();
2256
2229
  const sessionKey = `outcome-${label}`;
@@ -2364,6 +2337,7 @@ outcome('a clean turn leaves the legacy shape byte-unchanged (no failureReason)'
2364
2337
  config,
2365
2338
  scriptedProvider([{ role: 'assistant', content: 'hi' }]),
2366
2339
  'outcome-ok',
2340
+ outcomeBus,
2367
2341
  );
2368
2342
  const cap = captureLoopEnd();
2369
2343