@genesislcap/ai-assistant 14.421.1 → 14.422.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 (37) hide show
  1. package/dist/ai-assistant.api.json +191 -1
  2. package/dist/ai-assistant.d.ts +60 -0
  3. package/dist/dts/components/chat-driver/chat-driver.d.ts +33 -0
  4. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  5. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  6. package/dist/dts/config/config.d.ts +20 -0
  7. package/dist/dts/config/config.d.ts.map +1 -1
  8. package/dist/dts/main/main.d.ts +6 -0
  9. package/dist/dts/main/main.d.ts.map +1 -1
  10. package/dist/dts/main/main.styles.d.ts.map +1 -1
  11. package/dist/dts/main/main.template.d.ts +16 -0
  12. package/dist/dts/main/main.template.d.ts.map +1 -1
  13. package/dist/dts/state/ai-assistant-slice.d.ts +6 -0
  14. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  15. package/dist/dts/state/session-store.d.ts +2 -0
  16. package/dist/dts/state/session-store.d.ts.map +1 -1
  17. package/dist/dts/utils/history-transform.d.ts +13 -0
  18. package/dist/dts/utils/history-transform.d.ts.map +1 -0
  19. package/dist/esm/components/chat-driver/chat-driver.js +127 -16
  20. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +8 -20
  21. package/dist/esm/config/config.js +18 -1
  22. package/dist/esm/main/main.js +43 -11
  23. package/dist/esm/main/main.styles.js +62 -0
  24. package/dist/esm/main/main.template.js +122 -71
  25. package/dist/esm/state/ai-assistant-slice.js +8 -0
  26. package/dist/esm/utils/history-transform.js +35 -0
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/docs/sub_agent.md +149 -211
  29. package/package.json +16 -16
  30. package/src/components/chat-driver/chat-driver.ts +169 -15
  31. package/src/components/orchestrating-driver/orchestrating-driver.ts +10 -22
  32. package/src/config/config.ts +24 -0
  33. package/src/main/main.styles.ts +62 -0
  34. package/src/main/main.template.ts +189 -117
  35. package/src/main/main.ts +43 -9
  36. package/src/state/ai-assistant-slice.ts +12 -0
  37. package/src/utils/history-transform.ts +40 -0
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Whitespace in FAST templates
3
+ * ────────────────────────────
4
+ * The formatter adds newlines/indentation to static template string parts,
5
+ * which FAST renders as real text nodes. With `white-space: pre-wrap` these
6
+ * are visible as blank lines. To avoid this:
7
+ *
8
+ * - Apply `white-space: pre-wrap` only on leaf elements whose *content* needs
9
+ * it, never on containers.
10
+ * - Inside pre-wrap elements keep content on the same line as the opening tag:
11
+ * GOOD `<div>${expr}</div>` / BAD `<div>\n ${expr}\n</div>`
12
+ * - Extract shared or deeply-nested templates as named module-level consts to
13
+ * keep indentation shallow.
14
+ * - Chain adjacent `when()` calls as `${when(a,tA)}${when(b,tB)}` — separate
15
+ * lines let the formatter inject whitespace between them.
16
+ */
17
+
1
18
  import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
2
19
  import type { ChatAttachment, ChatMessage, ChatToolCall } from '@genesislcap/foundation-ai';
3
20
  import { html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
@@ -72,6 +89,64 @@ const senderLabel: Record<string, string> = {
72
89
  ai: 'Assistant',
73
90
  };
74
91
 
92
+ // ─── Sub-agent trace fragments ────────────────────────────────────────────────
93
+
94
+ const subAgentAssistantTemplate = html<ChatMessage>`
95
+ <div class="sub-agent-message sub-agent-assistant">${(m) => m.content}</div>
96
+ `;
97
+
98
+ const subAgentToolCallTemplate = html<ChatMessage>`
99
+ <div class="sub-agent-message sub-agent-tool-call">
100
+ ${repeat(
101
+ (m) => m.toolCalls!,
102
+ html<ChatToolCall>`
103
+ <span class="sub-agent-tool-name">${(tc) => tc.name}</span>
104
+ `,
105
+ )}
106
+ </div>
107
+ `;
108
+
109
+ const subAgentToolResultTemplate = html<ChatMessage>`
110
+ <div class="sub-agent-message sub-agent-tool-result">${(m) => m.toolResult!.content}</div>
111
+ `;
112
+
113
+ /** Renders one row of a sub-agent trace (assistant text, tool call, or tool result). */
114
+ const subAgentMessageRowTemplate = html<ChatMessage>`
115
+ ${when(
116
+ (m) => m.role === 'assistant' && !m.toolCalls?.length && !!m.content,
117
+ subAgentAssistantTemplate,
118
+ )}${when((m) => !!m.toolCalls?.length, subAgentToolCallTemplate)}${when(
119
+ (m) => m.role === 'tool' && !!m.toolResult?.content,
120
+ subAgentToolResultTemplate,
121
+ )}
122
+ `;
123
+
124
+ /** Collapsed <details> trace shown inside a tool-call card once the sub-agent finishes. */
125
+ const subAgentTraceTemplate = html<ChatToolCall>`
126
+ <details class="sub-agent-trace">
127
+ <summary class="sub-agent-trace-summary">
128
+ ${(tc) => tc.subAgentTrace![0]?.agentName ?? 'Sub-agent'} trace
129
+ </summary>
130
+ ${repeat(
131
+ (tc) => tc.subAgentTrace!.filter((m) => m.role !== 'user'),
132
+ subAgentMessageRowTemplate,
133
+ )}
134
+ </details>
135
+ `;
136
+
137
+ /** Live streaming trace panel shown while a sub-agent is actively running. */
138
+ const liveSubAgentTraceTemplate = html<FoundationAiAssistant>`
139
+ <div class="live-sub-agent-trace" part="live-sub-agent-trace">
140
+ <span class="live-sub-agent-name">${(x) => x.liveSubAgentName ?? 'Sub-agent'}</span>
141
+ ${repeat(
142
+ (x) => x.liveSubAgentTrace.filter((m) => m.role !== 'user'),
143
+ subAgentMessageRowTemplate,
144
+ )}
145
+ </div>
146
+ `;
147
+
148
+ // ─── Public factory ───────────────────────────────────────────────────────────
149
+
75
150
  /** @internal */
76
151
  export const FoundationAiAssistantTemplate = (
77
152
  designSystemPrefix: string,
@@ -83,6 +158,118 @@ export const FoundationAiAssistantTemplate = (
83
158
  const iconTag = `${designSystemPrefix}-icon`;
84
159
  const progressTag = `${designSystemPrefix}-progress`;
85
160
 
161
+ // ── Tool call item ──────────────────────────────────────────────────────────
162
+
163
+ const toolCallItemTemplate = html<ChatToolCall>`
164
+ ${when(
165
+ (tc) => tc.foldEvent === 'open',
166
+ html<ChatToolCall>`
167
+ <pre class="payload fold-event fold-open">&#9658; ${(tc) => tc.name}</pre>
168
+ `,
169
+ )}
170
+ ${when(
171
+ (tc) => tc.foldEvent === 'close',
172
+ html<ChatToolCall>`
173
+ <pre class="payload fold-event fold-close">&#9668; ${(tc) => tc.name}</pre>
174
+ `,
175
+ )}
176
+ ${when(
177
+ (tc) => !tc.foldEvent && !isChatToolCallUnknown(tc),
178
+ html<ChatToolCall>`
179
+ <pre class="payload">
180
+ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<strong>${(tc) =>
181
+ tc.name}</strong>(${(tc) => JSON.stringify(tc.args, null, 2)})</pre
182
+ >
183
+ `,
184
+ )}
185
+ ${when(
186
+ isChatToolCallUnknown,
187
+ html<ChatToolCall>`
188
+ <pre class="payload unknown-tool">${(tc) => unknownToolPayload(tc)}</pre>
189
+ `,
190
+ )}
191
+ ${when((tc) => !!tc.subAgentTrace?.length, subAgentTraceTemplate)}
192
+ `;
193
+
194
+ // ── Message row ─────────────────────────────────────────────────────────────
195
+
196
+ const messageRowTemplate = html<ChatMessage, FoundationAiAssistant>`
197
+ ${when(
198
+ (m) => m.role === 'system-event',
199
+ html<ChatMessage>`
200
+ <div class="agent-switch-indicator" part="agent-switch-indicator">
201
+ <span class="agent-switch-label">${(m) => m.content}</span>
202
+ </div>
203
+ `,
204
+ )}
205
+ ${when(
206
+ (m) => m.role !== 'system-event',
207
+ html<ChatMessage, FoundationAiAssistant>`
208
+ <div class="message-row ${(m) => messageType(m)}">
209
+ ${when(
210
+ (m) => m.role !== 'user',
211
+ html<ChatMessage, FoundationAiAssistant>`
212
+ <div class="avatar ${(m) => messageType(m)}">
213
+ ${when(
214
+ (m, c) => !!(c.parent as FoundationAiAssistant).imageSrc,
215
+ html<ChatMessage, FoundationAiAssistant>`
216
+ <img
217
+ src="${(m, c) => (c.parent as FoundationAiAssistant).imageSrc}"
218
+ alt="Assistant"
219
+ class="avatar-img"
220
+ />
221
+ `,
222
+ )}
223
+ ${when(
224
+ (m, c) => !(c.parent as FoundationAiAssistant).imageSrc,
225
+ genesisIconTemplate,
226
+ )}
227
+ </div>
228
+ `,
229
+ )}
230
+ <div class="message ${(m) => messageType(m)}">
231
+ <div class="sender">
232
+ ${(m, c) =>
233
+ messageType(m) === 'ai-function' &&
234
+ m.agentName &&
235
+ (c.parent as FoundationAiAssistant).showAgentSwitchIndicator
236
+ ? `Tool Call · ${m.agentName}`
237
+ : senderLabel[messageType(m)]}
238
+ </div>
239
+ <div class="content">
240
+ ${when(
241
+ (m) => m.content,
242
+ html<ChatMessage>`
243
+ <ai-chat-markdown :content="${(m) => m.content}"></ai-chat-markdown>
244
+ `,
245
+ )}
246
+ ${when(
247
+ (m) => m.toolCalls,
248
+ html<ChatMessage>`
249
+ ${repeat((m) => m.toolCalls ?? [], toolCallItemTemplate)}
250
+ `,
251
+ )}
252
+ ${when(
253
+ (m) => m.interaction,
254
+ html<ChatMessage, FoundationAiAssistant>`
255
+ <ai-chat-interaction-wrapper
256
+ :componentName=${(m) => m.interaction!.componentName}
257
+ :data=${(m) => m.interaction!.data}
258
+ :interactionId=${(m) => m.interaction!.interactionId}
259
+ :resolved=${(m) => m.interaction!.resolved}
260
+ @interaction-completed=${(m, c) => c.parent.handleInteractionCompleted(c.event)}
261
+ ></ai-chat-interaction-wrapper>
262
+ `,
263
+ )}
264
+ </div>
265
+ </div>
266
+ </div>
267
+ `,
268
+ )}
269
+ `;
270
+
271
+ // ── Root template ───────────────────────────────────────────────────────────
272
+
86
273
  return html<FoundationAiAssistant>`
87
274
  <div class="chat-wrapper" part="chat-wrapper">
88
275
  ${when(
@@ -247,123 +434,8 @@ export const FoundationAiAssistantTemplate = (
247
434
  </div>
248
435
 
249
436
  <div class="messages" part="messages" ${ref('messagesEl')}>
250
-
251
- ${repeat(
252
- (x) => x.visibleMessages,
253
- html<ChatMessage, FoundationAiAssistant>`
254
- ${when(
255
- (m) => m.role === 'system-event',
256
- html<ChatMessage, FoundationAiAssistant>`
257
- <div class="agent-switch-indicator" part="agent-switch-indicator">
258
- <span class="agent-switch-label">${(m) => m.content}</span>
259
- </div>
260
- `,
261
- )}
262
- ${when(
263
- (m) => m.role !== 'system-event',
264
- html<ChatMessage, FoundationAiAssistant>`
265
- <div class="message-row ${(m) => messageType(m)}">
266
- ${when(
267
- (m) => m.role !== 'user',
268
- html<ChatMessage, FoundationAiAssistant>`
269
- <div class="avatar ${(m) => messageType(m)}">
270
- ${when(
271
- (m, c) => !!(c.parent as FoundationAiAssistant).imageSrc,
272
- html<ChatMessage, FoundationAiAssistant>`
273
- <img
274
- src="${(m, c) => (c.parent as FoundationAiAssistant).imageSrc}"
275
- alt="Assistant"
276
- class="avatar-img"
277
- />
278
- `,
279
- )}
280
- ${when(
281
- (m, c) => !(c.parent as FoundationAiAssistant).imageSrc,
282
- genesisIconTemplate,
283
- )}
284
- </div>
285
- `,
286
- )}
287
- <div class="message ${(m) => messageType(m)}">
288
- <div class="sender">
289
- ${(m, c) =>
290
- messageType(m) === 'ai-function' &&
291
- m.agentName &&
292
- (c.parent as FoundationAiAssistant).showAgentSwitchIndicator
293
- ? `Tool Call · ${m.agentName}`
294
- : senderLabel[messageType(m)]}
295
- </div>
296
- <div class="content">
297
- ${when(
298
- (m) => m.content,
299
- html<ChatMessage, FoundationAiAssistant>`
300
- <ai-chat-markdown :content="${(m) => m.content}"></ai-chat-markdown>
301
- `,
302
- )}
303
- ${when(
304
- (m) => m.toolCalls,
305
- html<ChatMessage, FoundationAiAssistant>`
306
- ${repeat(
307
- (m) => m.toolCalls ?? [],
308
- html<ChatToolCall>`
309
- ${when(
310
- (tc) => tc.foldEvent === 'open',
311
- html<ChatToolCall>`
312
- <pre class="payload fold-event fold-open">
313
- &#9658; ${(tc) => tc.name}</pre
314
- >
315
- `,
316
- )}
317
- ${when(
318
- (tc) => tc.foldEvent === 'close',
319
- html<ChatToolCall>`
320
- <pre class="payload fold-event fold-close">
321
- &#9668; ${(tc) => tc.name}</pre
322
- >
323
- `,
324
- )}
325
- ${when(
326
- (tc) => !tc.foldEvent && !isChatToolCallUnknown(tc),
327
- html<ChatToolCall>`
328
- <pre class="payload">
329
- ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<strong>${(tc) =>
330
- tc.name}</strong>(${(tc) =>
331
- JSON.stringify(tc.args, null, 2)})</pre
332
- >
333
- `,
334
- )}
335
- ${when(
336
- isChatToolCallUnknown,
337
- html<ChatToolCall>`
338
- <pre class="payload unknown-tool">
339
- ${(tc) => unknownToolPayload(tc)}</pre
340
- >
341
- `,
342
- )}
343
- `,
344
- )}
345
- `,
346
- )}
347
- ${when(
348
- (m) => m.interaction,
349
- html<ChatMessage, FoundationAiAssistant>`
350
- <ai-chat-interaction-wrapper
351
- :componentName=${(m) => m.interaction!.componentName}
352
- :data=${(m) => m.interaction!.data}
353
- :interactionId=${(m) => m.interaction!.interactionId}
354
- :resolved=${(m) => m.interaction!.resolved}
355
- @interaction-completed=${(m, c) =>
356
- c.parent.handleInteractionCompleted(c.event)}
357
- ></ai-chat-interaction-wrapper>
358
- `,
359
- )}
360
- </div>
361
- </div>
362
- </div>
363
- `,
364
- )}
365
- `,
366
- )}
437
+ ${repeat((x) => x.visibleMessages, messageRowTemplate)}
438
+ ${when((x) => x.liveSubAgentTrace.length > 0 && x.showToolCalls, liveSubAgentTraceTemplate)}
367
439
  ${when(
368
440
  (x) =>
369
441
  x.showLoadingIndicator &&
package/src/main/main.ts CHANGED
@@ -76,6 +76,14 @@ avoidTreeShaking(
76
76
  ChatSuggestions,
77
77
  );
78
78
 
79
+ /** Recursively strips `toolHandlers` from an agent and all its sub-agents. */
80
+ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
81
+ const { toolHandlers: _, subAgents, ...rest } = agent;
82
+ return subAgents?.length
83
+ ? { ...rest, subAgents: subAgents.map(stripHandlers) as AgentConfig[] }
84
+ : rest;
85
+ }
86
+
79
87
  /**
80
88
  * Foundation AI Assistant component.
81
89
  *
@@ -145,15 +153,11 @@ export class FoundationAiAssistant extends GenesisElement {
145
153
  return this._sessionRef?.store.aiAssistant.activeAgent;
146
154
  }
147
155
  set activeAgent(value: AgentConfig | undefined) {
148
- // Strip toolHandlers before storing — functions are non-serializable and Redux
149
- // serializable-state middleware will warn. toolHandlers are never read back from
150
- // the store; they are always sourced from this.agents when the driver is built.
151
- if (value) {
152
- const { toolHandlers: _, ...serializable } = value;
153
- this._sessionRef?.actions.aiAssistant.setActiveAgent(serializable);
154
- } else {
155
- this._sessionRef?.actions.aiAssistant.setActiveAgent(undefined);
156
- }
156
+ // Strip toolHandlers recursively before storing — functions are non-serializable
157
+ // and Redux serializable-state middleware will warn. toolHandlers are never read
158
+ // back from the store; they are always sourced from this.agents when the driver
159
+ // is built.
160
+ this._sessionRef?.actions.aiAssistant.setActiveAgent(value ? stripHandlers(value) : undefined);
157
161
  }
158
162
 
159
163
  get suggestionsState(): SuggestionsState {
@@ -195,6 +199,20 @@ export class FoundationAiAssistant extends GenesisElement {
195
199
  this._sessionRef?.actions.aiAssistant.setEnabledAnimations(value);
196
200
  }
197
201
 
202
+ get liveSubAgentTrace(): ChatMessage[] {
203
+ return this._sessionRef?.store.aiAssistant.liveSubAgentTrace ?? [];
204
+ }
205
+ set liveSubAgentTrace(value: ChatMessage[]) {
206
+ this._sessionRef?.actions.aiAssistant.setLiveSubAgentTrace(value);
207
+ }
208
+
209
+ get liveSubAgentName(): string | null {
210
+ return this._sessionRef?.store.aiAssistant.liveSubAgentName ?? null;
211
+ }
212
+ set liveSubAgentName(value: string | null) {
213
+ this._sessionRef?.actions.aiAssistant.setLiveSubAgentName(value);
214
+ }
215
+
198
216
  /** Most recent prompt token count from the AI provider, if available. */
199
217
  get contextTokens(): number | undefined {
200
218
  return this._sessionRef?.store.aiAssistant.contextTokens;
@@ -421,8 +439,24 @@ export class FoundationAiAssistant extends GenesisElement {
421
439
  };
422
440
  driver.addEventListener('history-updated', onHistoryUpdated);
423
441
 
442
+ const onSubAgentHistoryUpdated = (e: Event) => {
443
+ const { agentName, history } = (e as CustomEvent).detail;
444
+ this.liveSubAgentName = agentName;
445
+ // structuredClone so Immer freezes an independent copy, not the child
446
+ // driver's own history array (which is still being mutated by the tool loop).
447
+ this.liveSubAgentTrace = structuredClone(history);
448
+ };
449
+ const onSubAgentStop = () => {
450
+ this.liveSubAgentTrace = [];
451
+ this.liveSubAgentName = null;
452
+ };
453
+ driver.addEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated);
454
+ driver.addEventListener('sub-agent-stop', onSubAgentStop);
455
+
424
456
  const cleanups: (() => void)[] = [
425
457
  () => driver.removeEventListener('history-updated', onHistoryUpdated),
458
+ () => driver.removeEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated),
459
+ () => driver.removeEventListener('sub-agent-stop', onSubAgentStop),
426
460
  ];
427
461
 
428
462
  if (driver instanceof OrchestratingDriver) {
@@ -22,6 +22,10 @@ export interface AiAssistantSessionState {
22
22
  activeAgent: Omit<AgentConfig, 'toolHandlers'> | undefined;
23
23
  /** Draft text in the input box — preserved across pop-in/pop-out cycles. */
24
24
  inputValue: string;
25
+ /** Live trace from a currently-executing sub-agent. Cleared on sub-agent-stop. */
26
+ liveSubAgentTrace: ChatMessage[];
27
+ /** Name of the currently-executing sub-agent, or null when idle. */
28
+ liveSubAgentName: string | null;
25
29
  }
26
30
 
27
31
  export const defaultSessionState: AiAssistantSessionState = {
@@ -36,6 +40,8 @@ export const defaultSessionState: AiAssistantSessionState = {
36
40
  contextLimit: undefined,
37
41
  activeAgent: undefined,
38
42
  inputValue: '',
43
+ liveSubAgentTrace: [],
44
+ liveSubAgentName: null,
39
45
  };
40
46
 
41
47
  export const aiAssistantSlice = createSlice({
@@ -75,6 +81,12 @@ export const aiAssistantSlice = createSlice({
75
81
  setInputValue(state, action: PayloadAction<string>) {
76
82
  state.inputValue = action.payload;
77
83
  },
84
+ setLiveSubAgentTrace(state, action: PayloadAction<ChatMessage[]>) {
85
+ state.liveSubAgentTrace = action.payload;
86
+ },
87
+ setLiveSubAgentName(state, action: PayloadAction<string | null>) {
88
+ state.liveSubAgentName = action.payload;
89
+ },
78
90
  },
79
91
  selectors: {},
80
92
  });
@@ -0,0 +1,40 @@
1
+ import type { ChatMessage } from '@genesislcap/foundation-ai';
2
+
3
+ /**
4
+ * Masks the tool-specific payload of a single message — clears tool call args
5
+ * and replaces tool result content with a placeholder. Used when passing history
6
+ * to an agent that should not see another agent's implementation detail.
7
+ */
8
+ function maskToolPayload(msg: ChatMessage): ChatMessage {
9
+ if (msg.toolCalls?.length) {
10
+ return { ...msg, toolCalls: msg.toolCalls.map((tc) => ({ ...tc, args: {} })) };
11
+ }
12
+ if (msg.toolResult) {
13
+ return {
14
+ ...msg,
15
+ toolResult: { ...msg.toolResult, content: "[other agent's tool result omitted]" },
16
+ };
17
+ }
18
+ return msg;
19
+ }
20
+
21
+ /**
22
+ * Prepares history for the LLM only: masks tool call args and results from other
23
+ * agents so the active specialist is not confused by tools it does not have.
24
+ * Canonical history in `ChatDriver` stays unmasked for UI and logging.
25
+ */
26
+ export function transformHistoryForAgent(history: ChatMessage[], agentName: string): ChatMessage[] {
27
+ return history.map((msg) => {
28
+ if (!msg.agentName || msg.agentName === agentName) return msg;
29
+ return maskToolPayload(msg);
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Applies a history cap for sub-agent context: the last `cap` messages are passed
35
+ * verbatim; older messages have their tool payloads masked.
36
+ */
37
+ export function applyHistoryCap(history: readonly ChatMessage[], cap: number): ChatMessage[] {
38
+ const cutoff = history.length - cap;
39
+ return history.map((msg, i) => (i < cutoff ? maskToolPayload(msg) : msg));
40
+ }