@genesislcap/ai-assistant 14.434.0 → 14.436.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 (33) hide show
  1. package/dist/ai-assistant.api.json +1513 -70
  2. package/dist/ai-assistant.d.ts +367 -7
  3. package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
  4. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  5. package/dist/dts/components/chat-driver/chat-driver.d.ts +79 -3
  6. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  7. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +23 -0
  8. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  9. package/dist/dts/config/config.d.ts +106 -2
  10. package/dist/dts/config/config.d.ts.map +1 -1
  11. package/dist/dts/config/define-stateful-agent.d.ts +115 -0
  12. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -0
  13. package/dist/dts/index.d.ts +1 -0
  14. package/dist/dts/index.d.ts.map +1 -1
  15. package/dist/dts/main/main.d.ts +36 -4
  16. package/dist/dts/main/main.d.ts.map +1 -1
  17. package/dist/dts/main/main.template.d.ts.map +1 -1
  18. package/dist/esm/components/chat-driver/chat-driver.js +126 -11
  19. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +192 -33
  20. package/dist/esm/config/define-stateful-agent.js +174 -0
  21. package/dist/esm/index.js +1 -0
  22. package/dist/esm/main/main.js +164 -21
  23. package/dist/esm/main/main.template.js +2 -11
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +16 -16
  26. package/src/components/ai-driver/ai-driver.ts +9 -0
  27. package/src/components/chat-driver/chat-driver.ts +178 -8
  28. package/src/components/orchestrating-driver/orchestrating-driver.ts +191 -17
  29. package/src/config/config.ts +112 -2
  30. package/src/config/define-stateful-agent.ts +293 -0
  31. package/src/index.ts +1 -0
  32. package/src/main/main.template.ts +2 -9
  33. package/src/main/main.ts +167 -14
package/src/main/main.ts CHANGED
@@ -109,12 +109,33 @@ avoidTreeShaking(
109
109
  AgentPicker,
110
110
  );
111
111
 
112
- /** Recursively strips `toolHandlers` from an agent and all its sub-agents. */
112
+ /**
113
+ * Recursively strips non-serializable fields from an agent before storing in Redux:
114
+ * - `toolHandlers` (functions),
115
+ * - `onActivate` / `onDeactivate` (lifecycle hooks, functions),
116
+ * - function-form `systemPrompt` / `toolDefinitions` (downgraded to `undefined`
117
+ * in the snapshot — the live config on the driver is still the source of
118
+ * truth; the slice only stores a serializable projection).
119
+ */
113
120
  function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
114
- const { toolHandlers: _, subAgents, ...rest } = agent;
121
+ const {
122
+ toolHandlers: _h,
123
+ onActivate: _on,
124
+ onDeactivate: _off,
125
+ getDebugSnapshot: _g,
126
+ subAgents,
127
+ systemPrompt,
128
+ toolDefinitions,
129
+ ...rest
130
+ } = agent;
131
+ const stripped = {
132
+ ...rest,
133
+ systemPrompt: typeof systemPrompt === 'function' ? undefined : systemPrompt,
134
+ toolDefinitions: typeof toolDefinitions === 'function' ? undefined : toolDefinitions,
135
+ };
115
136
  return subAgents?.length
116
- ? { ...rest, subAgents: subAgents.map(stripHandlers) as AgentConfig[] }
117
- : rest;
137
+ ? { ...stripped, subAgents: subAgents.map(stripHandlers) as AgentConfig[] }
138
+ : stripped;
118
139
  }
119
140
 
120
141
  /**
@@ -504,8 +525,15 @@ export class FoundationAiAssistant extends GenesisElement {
504
525
  // down a mid-flight driver and creating a fresh idle one.
505
526
  if (this.driver.isBusy()) return;
506
527
  const history = this.driver.getRawHistory?.() ?? [];
528
+ const oldDriver = this.driver;
507
529
  this.unwireDriver();
508
530
  deleteDriver(key);
531
+ // Fire the active agent's `onDeactivate` so machines / per-agent state get
532
+ // torn down before the replacement driver instantiates them again. Fire
533
+ // and forget — the old driver is about to be GC'd, errors are non-fatal.
534
+ if (oldDriver && 'dispose' in oldDriver && typeof oldDriver.dispose === 'function') {
535
+ void oldDriver.dispose();
536
+ }
509
537
  this.driver = getOrCreateDriver(key, () => this.createDriver());
510
538
  this.wireDriver();
511
539
  if (history.length) this.driver.loadHistory([...history]);
@@ -529,30 +557,72 @@ export class FoundationAiAssistant extends GenesisElement {
529
557
  * current agent configuration. Does not wire event listeners or register in
530
558
  * the driver registry.
531
559
  */
560
+ /**
561
+ * Warn at config time if any stateful agent (one with lifecycle hooks) is
562
+ * configured in a way that makes it unreachable: `excludeFromClassifier`
563
+ * removes the classifier path, so the only entry left is manual pinning —
564
+ * which requires both `manualSelection.enabled` on the agent and the picker
565
+ * itself being enabled at the assistant level.
566
+ */
567
+ private warnUnreachableStatefulAgents(): void {
568
+ const pickerDisabled = this.agentPicker === 'disabled';
569
+ for (const a of this.agents ?? []) {
570
+ const isStateful = !!(a.onActivate || a.onDeactivate);
571
+ if (!isStateful) continue;
572
+ // Specialists only — defineStatefulAgent rejects fallback.
573
+ const excluded = 'excludeFromClassifier' in a && a.excludeFromClassifier === true;
574
+ if (!excluded) continue;
575
+ const manual = a.manualSelection?.enabled === true;
576
+ if (manual && !pickerDisabled) continue;
577
+ const reason = pickerDisabled
578
+ ? 'the picker is disabled at chatConfig.picker.mode'
579
+ : 'manualSelection.enabled is not set on the agent';
580
+ logger.warn(
581
+ `Stateful agent "${a.name}" is unreachable: excludeFromClassifier is true and ${reason}. ` +
582
+ 'Either remove excludeFromClassifier, enable manualSelection on the agent, or enable the picker.',
583
+ );
584
+ }
585
+ }
586
+
532
587
  private createDriver(): AiDriver {
588
+ this.warnUnreachableStatefulAgents();
533
589
  const agent = this.chatConfig.agent ?? {};
534
590
  const { agents } = this;
535
591
 
536
- if (agents && agents.length > 1) {
592
+ // Always route through OrchestratingDriver when agents are configured
593
+ // even with a single agent — so lifecycle hooks (`onActivate`/
594
+ // `onDeactivate`), auto-pin, and release semantics fire uniformly. The
595
+ // classifier short-circuits when there's only one viable candidate, so
596
+ // single-agent setups pay no LLM cost for routing. With no agents
597
+ // configured at all, fall back to a bare ChatDriver — there's nothing to
598
+ // orchestrate and no hooks to fire.
599
+ if (agents && agents.length > 0) {
600
+ if (agents.length === 1 && !agents[0].fallback) {
601
+ // Preset for getters that read activeAgent before the first turn
602
+ // (e.g. chatInputDuringExecution). The orchestrator will overwrite
603
+ // this via `agent-changed` once applyAgent fires on first sendMessage.
604
+ this.activeAgent = agents[0];
605
+ }
537
606
  return new OrchestratingDriver(this.aiProvider, agents, {
607
+ sessionKey: this.getStateKey() ?? '',
538
608
  maxHandoffs: agent.maxHandoffs,
539
609
  classifierHistoryLength: agent.classifierHistoryLength,
540
610
  classifierRetries: agent.classifierRetries,
541
611
  maxToolIterations: agent.maxToolIterations,
542
612
  maxFoldOperations: agent.maxFoldOperations,
613
+ maxTurnSnapshots: agent.maxTurnSnapshots,
543
614
  });
544
615
  }
545
616
 
546
- const singleAgent = agents?.[0];
547
- this.activeAgent = singleAgent;
548
617
  return new ChatDriver(
549
618
  this.aiProvider,
550
- singleAgent?.toolHandlers ?? {},
551
- singleAgent?.toolDefinitions ?? [],
552
- singleAgent?.systemPrompt,
553
- singleAgent?.primerHistory,
619
+ {},
620
+ [],
621
+ undefined,
622
+ undefined,
554
623
  agent.maxToolIterations,
555
624
  agent.maxFoldOperations,
625
+ agent.maxTurnSnapshots,
556
626
  );
557
627
  }
558
628
 
@@ -623,13 +693,25 @@ export class FoundationAiAssistant extends GenesisElement {
623
693
  const onAgentChanged = (e: Event) => {
624
694
  this.activeAgent = (e as CustomEvent<AgentConfig>).detail;
625
695
  };
696
+ // Orchestrator-initiated pin changes (auto-pin on stateful activation,
697
+ // auto-clear on release). User-initiated pins flow the other direction
698
+ // via the `pinnedAgentName` setter, which already calls setPinnedAgent
699
+ // back into the driver — guard against the redundant round-trip.
700
+ const onPinnedChanged = (e: Event) => {
701
+ const name = (e as CustomEvent<string | null>).detail;
702
+ if (this._sessionRef?.store.aiAssistant.pinnedAgentName !== name) {
703
+ this._sessionRef?.actions.aiAssistant.setPinnedAgentName(name);
704
+ }
705
+ };
626
706
  driver.addEventListener('orchestrating-start', onOrchStart);
627
707
  driver.addEventListener('orchestrating-stop', onOrchStop);
628
708
  driver.addEventListener('agent-changed', onAgentChanged);
709
+ driver.addEventListener('pinned-changed', onPinnedChanged);
629
710
  cleanups.push(
630
711
  () => driver.removeEventListener('orchestrating-start', onOrchStart),
631
712
  () => driver.removeEventListener('orchestrating-stop', onOrchStop),
632
713
  () => driver.removeEventListener('agent-changed', onAgentChanged),
714
+ () => driver.removeEventListener('pinned-changed', onPinnedChanged),
633
715
  );
634
716
  }
635
717
 
@@ -964,6 +1046,44 @@ export class FoundationAiAssistant extends GenesisElement {
964
1046
  return this.agents?.find((a) => a.name === this.pinnedAgentName)?.manualSelection?.hint;
965
1047
  }
966
1048
 
1049
+ /**
1050
+ * The pin is locked when a stateful agent (one with lifecycle hooks) is
1051
+ * *actively running* — i.e. its `onActivate` has fired and it owns live
1052
+ * state. Until the user sends their first message, a freshly pinned stateful
1053
+ * agent is not yet active and the picker should remain free; the user might
1054
+ * change their mind and unpin without anything to clean up.
1055
+ *
1056
+ * We derive from `activeAgent` (set by the orchestrator after `onActivate`
1057
+ * completes) rather than `pinnedAgentName` (set immediately on picker
1058
+ * click). The serialized `activeAgent` strips lifecycle hooks, so we look
1059
+ * up the live config from `this.agents` to check for them.
1060
+ */
1061
+ @volatile
1062
+ get pinLocked(): boolean {
1063
+ if (!this.activeAgent) return false;
1064
+ const live = this.agents?.find((a) => a.name === this.activeAgent!.name);
1065
+ if (!live) return false;
1066
+ return !!(live.onActivate || live.onDeactivate);
1067
+ }
1068
+
1069
+ /** Tooltip shown on the picker toggle button. */
1070
+ @volatile
1071
+ get agentToggleTitle(): string {
1072
+ if (this.pinLocked) {
1073
+ const name = this.pinnedAgentName ?? 'This agent';
1074
+ return `${name} is in the middle of a guided flow. Agent switching is disabled until the flow completes.`;
1075
+ }
1076
+ if (this.agentPickerOpen) {
1077
+ return 'Close agent picker';
1078
+ }
1079
+ if (this.pinnedAgentName !== null) {
1080
+ return this.pinnedAgentHint
1081
+ ? `${this.pinnedAgentName} — ${this.pinnedAgentHint}`
1082
+ : this.pinnedAgentName;
1083
+ }
1084
+ return 'Attempts to route messages to the correct available agent. Click to manually pin an agent.';
1085
+ }
1086
+
967
1087
  /**
968
1088
  * Tint applied to the pin icon when an agent is pinned. Picked from the
969
1089
  * brand palette by agent position (modulo the palette length), so each agent
@@ -1009,6 +1129,18 @@ export class FoundationAiAssistant extends GenesisElement {
1009
1129
 
1010
1130
  getDebugLog() {
1011
1131
  const timestamp = new Date().toISOString().replace(/:/g, '-');
1132
+ // Live agent reference — `this.activeAgent` is the redux-stripped projection,
1133
+ // so getDebugSnapshot is not on it. Look up the live config from the agents
1134
+ // array to get the function back.
1135
+ const liveActiveAgent = this.activeAgent
1136
+ ? this.agents?.find((a) => a.name === this.activeAgent!.name)
1137
+ : undefined;
1138
+ let activeDebugSnapshot: unknown;
1139
+ try {
1140
+ activeDebugSnapshot = liveActiveAgent?.getDebugSnapshot?.();
1141
+ } catch (e) {
1142
+ activeDebugSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
1143
+ }
1012
1144
  return {
1013
1145
  messages: this.driver?.getRawHistory?.() ?? this.messages,
1014
1146
  meta: {
@@ -1016,13 +1148,29 @@ export class FoundationAiAssistant extends GenesisElement {
1016
1148
  host: window.location.host,
1017
1149
  agentSummary: this.agents?.map((a) => ({
1018
1150
  ...a,
1019
- toolDefinitions: expandToolTree(a.toolDefinitions ?? [], a.toolHandlers ?? {}),
1151
+ toolDefinitions: Array.isArray(a.toolDefinitions)
1152
+ ? expandToolTree(a.toolDefinitions, a.toolHandlers ?? {})
1153
+ : typeof a.toolDefinitions === 'function'
1154
+ ? '<dynamic — resolved per turn>'
1155
+ : [],
1020
1156
  toolHandlers: undefined,
1157
+ onActivate: undefined,
1158
+ onDeactivate: undefined,
1159
+ getDebugSnapshot: undefined,
1021
1160
  })),
1022
- activeSystemPrompt: this.activeAgent?.systemPrompt,
1161
+ activeSystemPrompt:
1162
+ typeof this.activeAgent?.systemPrompt === 'function'
1163
+ ? '<dynamic — resolved per turn>'
1164
+ : this.activeAgent?.systemPrompt,
1023
1165
  activePrimerHistory: this.activeAgent?.primerHistory,
1024
1166
  activeFoldStack:
1025
1167
  this.driver instanceof ChatDriver ? this.driver.getActiveFoldNames() : undefined,
1168
+ // Snapshot captured fresh at log-export time — reflects state NOW, which
1169
+ // may have transitioned since the last LLM call.
1170
+ activeDebugSnapshot,
1171
+ // Per-LLM-call timeline — pairs each turn with the prompt + tool surface
1172
+ // + agent state that drove it. Capped to the most recent N entries.
1173
+ turnSnapshots: this.driver?.getTurnSnapshots?.() ?? [],
1026
1174
  debug: this.debugStateFactory?.(),
1027
1175
  },
1028
1176
  };
@@ -1167,7 +1315,12 @@ export class FoundationAiAssistant extends GenesisElement {
1167
1315
  {
1168
1316
  name: this.activeAgent.name,
1169
1317
  description: 'description' in this.activeAgent ? this.activeAgent.description : '',
1170
- tools: this.activeAgent.toolDefinitions ?? [],
1318
+ // Dynamic-tools agents have no resolvable tool list outside a
1319
+ // per-turn context — suggestions just get an empty hint, matching
1320
+ // what `OrchestratingDriver.getSuggestions` does for them.
1321
+ tools: Array.isArray(this.activeAgent.toolDefinitions)
1322
+ ? this.activeAgent.toolDefinitions
1323
+ : [],
1171
1324
  },
1172
1325
  ]
1173
1326
  : undefined;