@genesislcap/ai-assistant 14.421.0 → 14.421.1-FUI-2511.2

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 +119 -12
  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 +161 -11
  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
@@ -7,9 +7,11 @@ import type {
7
7
  ChatToolCall,
8
8
  ChatToolDefinition,
9
9
  ChatToolHandlers,
10
+ SubAgentRequestOptions,
10
11
  } from '@genesislcap/foundation-ai';
11
12
  import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
12
13
  import type { AgentConfig } from '../../config/config';
14
+ import { applyHistoryCap } from '../../utils/history-transform';
13
15
  import { logger } from '../../utils/logger';
14
16
  import { TOOL_FOLD_SYMBOL, type ToolFold } from '../../utils/tool-fold';
15
17
  import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
@@ -77,6 +79,16 @@ export class ChatDriver extends EventTarget implements AiDriver {
77
79
  private consecutiveUnknownToolCalls = 0;
78
80
  private readonly maxFoldOperations: number;
79
81
 
82
+ /** Sub-agents declared on the active agent config, keyed by name. */
83
+ private subAgentsMap: Map<string, AgentConfig> = new Map();
84
+ /**
85
+ * Set by `completeSubAgent` inside a sub-agent tool handler. Checked at the
86
+ * same point in the loop as `REQUEST_CONTINUATION_TOOL` — after tool results
87
+ * are appended — so the exit path mirrors the system-call pattern.
88
+ * `undefined` means the loop has not been stopped early.
89
+ */
90
+ private subAgentCompletion: { result: unknown } | undefined;
91
+
80
92
  constructor(
81
93
  private readonly aiProvider: AIProvider,
82
94
  toolHandlers: ChatToolHandlers = {},
@@ -104,11 +116,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
104
116
  this.toolHandlers = config.toolHandlers ?? {};
105
117
  this.primerHistory = config.primerHistory;
106
118
  this.activeAgentName = config.name;
119
+ this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
107
120
  // Reset fold state when agent changes — each specialist starts fresh
108
121
  this.foldStack = [];
109
122
  this.consecutiveFoldOps = 0;
110
123
  }
111
124
 
125
+ /**
126
+ * Returns the early-stop result set by `completeSubAgent`, if any.
127
+ * Called by a parent `ChatDriver` after running this instance as a sub-agent.
128
+ */
129
+ getSubAgentCompletion(): { result: unknown } | undefined {
130
+ return this.subAgentCompletion;
131
+ }
132
+
112
133
  /**
113
134
  * Optional transform applied to conversation history immediately before each LLM request.
114
135
  * Cleared when `undefined`. Does not alter stored history.
@@ -281,6 +302,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
281
302
  }
282
303
 
283
304
  this.busy = true;
305
+ this.subAgentCompletion = undefined;
284
306
  this.appendToHistory({ role: 'user', content: userInput, attachments });
285
307
 
286
308
  try {
@@ -294,6 +316,116 @@ export class ChatDriver extends EventTarget implements AiDriver {
294
316
  }
295
317
  }
296
318
 
319
+ /**
320
+ * Builds the context object passed to every tool handler call.
321
+ * Centralised here so fold shortcut dispatch and the main tool loop use the
322
+ * same context without duplication.
323
+ *
324
+ * @param traceCapture - Optional per-invocation slot. When provided, the trace
325
+ * from any sub-agent call is written here rather than to shared instance state,
326
+ * so parallel tool calls each capture their own trace independently.
327
+ */
328
+ private buildHandlerContext(traceCapture?: { trace?: ChatMessage[] }) {
329
+ return {
330
+ requestInteraction: <T>(componentName: string, data: any): Promise<T> =>
331
+ this.requestInteraction(componentName, data),
332
+ ...(this.subAgentsMap.size > 0 && {
333
+ requestSubAgent: <T = never>(
334
+ name: string,
335
+ options?: SubAgentRequestOptions,
336
+ ): Promise<T | string> =>
337
+ this.invokeSubAgent<T>(name, options).then(({ result, trace }) => {
338
+ if (traceCapture) traceCapture.trace = trace;
339
+ return result;
340
+ }),
341
+ }),
342
+ completeSubAgent: (result: unknown): void => {
343
+ if (this.subAgentCompletion) {
344
+ logger.warn(
345
+ `ChatDriver(${this.activeAgentName ?? 'unknown'}): completeSubAgent called more than once — ignoring`,
346
+ );
347
+ return;
348
+ }
349
+ this.subAgentCompletion = { result };
350
+ },
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Creates a child `ChatDriver` for the named sub-agent, runs it to completion,
356
+ * and returns its structured result (or final text fallback) together with the
357
+ * full child conversation trace. Callers receive both values so each parallel
358
+ * invocation can capture its own trace without touching shared instance state.
359
+ */
360
+ private async invokeSubAgent<T = never>(
361
+ name: string,
362
+ options?: SubAgentRequestOptions,
363
+ ): Promise<{ result: T | string; trace: ChatMessage[] }> {
364
+ const subConfig = this.subAgentsMap.get(name);
365
+ if (!subConfig) {
366
+ const available = [...this.subAgentsMap.keys()].join(', ') || '(none)';
367
+ throw new Error(
368
+ `Sub-agent "${name}" not found on agent "${this.activeAgentName}". Available: ${available}`,
369
+ );
370
+ }
371
+
372
+ const { task, historyCap, context } = options ?? {};
373
+
374
+ // Exclude the current in-flight assistant message (the one with tool calls that
375
+ // triggered this invocation) — it has unresolved tool calls that would confuse
376
+ // the sub-agent into thinking it needs to handle tools it doesn't own.
377
+ const lastMsg = this.history[this.history.length - 1];
378
+ const baseHistory =
379
+ lastMsg?.role === 'assistant' && lastMsg.toolCalls?.length
380
+ ? this.history.slice(0, -1)
381
+ : this.history;
382
+
383
+ const snapshotHistory =
384
+ historyCap != null ? applyHistoryCap(baseHistory, historyCap) : [...baseHistory];
385
+
386
+ const contextMessages: ChatMessage[] = context
387
+ ? [{ role: 'user', content: `[Sub-agent context]: ${JSON.stringify(context)}` }]
388
+ : [];
389
+
390
+ const effectivePrimer: ChatMessage[] = [
391
+ ...snapshotHistory,
392
+ ...contextMessages,
393
+ ...(subConfig.primerHistory ?? []),
394
+ ];
395
+
396
+ const child = new ChatDriver(this.aiProvider);
397
+ child.applyAgent({ ...subConfig, primerHistory: effectivePrimer });
398
+
399
+ const forwardTrace = (e: Event) => {
400
+ this.dispatchEvent(
401
+ new CustomEvent('sub-agent-history-updated', {
402
+ detail: { agentName: subConfig.name, history: (e as CustomEvent<ChatMessage[]>).detail },
403
+ }),
404
+ );
405
+ };
406
+ child.addEventListener('history-updated', forwardTrace);
407
+
408
+ this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: { name } }));
409
+ try {
410
+ await child.sendMessage(task ?? '');
411
+ } finally {
412
+ child.removeEventListener('history-updated', forwardTrace);
413
+ this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: { name } }));
414
+ }
415
+
416
+ const trace = child.getHistory() as ChatMessage[];
417
+ const completion = child.getSubAgentCompletion();
418
+
419
+ if (completion) {
420
+ return { result: completion.result as T, trace };
421
+ }
422
+
423
+ const finalMsg = [...trace]
424
+ .reverse()
425
+ .find((m) => m.role === 'assistant' && !m.toolCalls?.length && m.content?.trim());
426
+ return { result: (finalMsg?.content ?? '') as string, trace };
427
+ }
428
+
297
429
  /**
298
430
  * Continue the tool loop from current history without appending a new user message.
299
431
  * Used by OrchestratingDriver after an agent handoff.
@@ -306,6 +438,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
306
438
  }
307
439
 
308
440
  this.busy = true;
441
+ this.subAgentCompletion = undefined;
309
442
  try {
310
443
  return await this.runToolLoop('', undefined, transientPrimer);
311
444
  } catch (e) {
@@ -400,9 +533,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
400
533
  typeof args[key] === 'object' && args[key] !== null
401
534
  ? (args[key] as Record<string, unknown>)
402
535
  : {};
403
- return innerHandler(innerArgs, {
404
- requestInteraction: (c, d) => this.requestInteraction(c, d),
405
- }).then((r) => (typeof r === 'string' ? r : JSON.stringify(r)));
536
+ return innerHandler(innerArgs, this.buildHandlerContext()).then((r) =>
537
+ typeof r === 'string' ? r : JSON.stringify(r),
538
+ );
406
539
  }
407
540
  }
408
541
 
@@ -487,7 +620,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
487
620
  let currentAttachments: ChatAttachment[] | undefined = attachments;
488
621
  let iterations = 0;
489
622
  let malformedAttempts = 0;
490
- const startIteration = currentInput ? 1 : 0;
623
+ // True only for the very first LLM call. Used to exclude the pending user message
624
+ // from history (it is passed separately as currentInput). Must not be derived from
625
+ // `iterations` because fold operations decrement iterations, which would incorrectly
626
+ // re-trigger the slice on subsequent calls after a fold open/close.
627
+ let firstLlmCall = !!currentInput;
491
628
 
492
629
  while (iterations < this.maxToolIterations) {
493
630
  iterations += 1;
@@ -498,7 +635,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
498
635
  : foldSuffix || undefined;
499
636
 
500
637
  const primer = [...(this.primerHistory ?? []), ...(transientPrimer ?? [])];
501
- const baseHistory = iterations === startIteration ? this.history.slice(0, -1) : this.history;
638
+ const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
639
+ firstLlmCall = false;
502
640
  const historyForProvider = this.providerHistoryTransform
503
641
  ? this.providerHistoryTransform([...baseHistory])
504
642
  : baseHistory;
@@ -581,7 +719,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
581
719
  [[], []],
582
720
  );
583
721
 
584
- const executedById = new Map<string, { toolCallId: string; content: string }>();
722
+ const executedById = new Map<
723
+ string,
724
+ { toolCallId: string; content: string; subAgentTrace?: ChatMessage[] }
725
+ >();
585
726
  const unknownToolIds = new Set<string>();
586
727
  let anyRealToolExecuted = false;
587
728
  let hitUnknownToolLimit = false;
@@ -660,12 +801,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
660
801
 
661
802
  // Real tool execution
662
803
  try {
663
- const result = await handler(tc.args, {
664
- requestInteraction: (componentName, data) =>
665
- this.requestInteraction(componentName, data),
666
- });
804
+ const traceCapture: { trace?: ChatMessage[] } = {};
805
+ const result = await handler(tc.args, this.buildHandlerContext(traceCapture));
667
806
  const content = typeof result === 'string' ? result : JSON.stringify(result);
668
- executedById.set(tc.id, { toolCallId: tc.id, content });
807
+ executedById.set(tc.id, {
808
+ toolCallId: tc.id,
809
+ content,
810
+ subAgentTrace: traceCapture.trace,
811
+ });
669
812
  anyRealToolExecuted = true;
670
813
  } catch (e) {
671
814
  logger.error(`ChatDriver tool "${tc.name}" failed:`, e);
@@ -734,6 +877,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
734
877
  foldPath: !isFoldOpen && !isFoldClose && foldPath.length > 0 ? foldPath : undefined,
735
878
  unknown: isUnknown || undefined,
736
879
  availableTools: isUnknown ? availableToolNames : undefined,
880
+ subAgentTrace: executedById.get(tc.id)?.subAgentTrace,
737
881
  };
738
882
  });
739
883
  this.history[tcMsgIdx] = { ...tcMsg, toolCalls: annotatedCalls };
@@ -765,6 +909,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
765
909
  return { reason: 'agent-handoff', summary, remainingTask };
766
910
  }
767
911
 
912
+ // Sub-agent early exit — checked here so the exit point mirrors the
913
+ // system-call pattern above. Set by completeSubAgent() in a tool handler.
914
+ if (this.subAgentCompletion) {
915
+ return { reason: 'done' };
916
+ }
917
+
768
918
  currentInput = '';
769
919
  }
770
920
 
@@ -6,6 +6,7 @@ import type {
6
6
  ChatRequestOptions,
7
7
  } from '@genesislcap/foundation-ai';
8
8
  import type { AgentConfig, FallbackAgentConfig, SpecialistAgentConfig } from '../../config/config';
9
+ import { transformHistoryForAgent } from '../../utils/history-transform';
9
10
  import { logger } from '../../utils/logger';
10
11
  import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
11
12
  import { ChatDriver, REQUEST_CONTINUATION_TOOL } from '../chat-driver/chat-driver';
@@ -54,27 +55,6 @@ function buildFallbackSystemPrompt(
54
55
  return `You are a helpful assistant. You cannot directly help with the user's request, but the following specialists are available:\n\n${agentList}\n\nPolitely let the user know what you can help with and invite them to rephrase their request.`;
55
56
  }
56
57
 
57
- /**
58
- * Prepares history for the LLM only: masks tool call args and results from other
59
- * agents so the active specialist is not confused by tools it does not have.
60
- * Canonical history in `ChatDriver` stays unmasked for UI and logging.
61
- */
62
- function transformHistoryForAgent(history: ChatMessage[], agentName: string): ChatMessage[] {
63
- return history.map((msg) => {
64
- if (!msg.agentName || msg.agentName === agentName) return msg;
65
- if (msg.toolCalls?.length) {
66
- return { ...msg, toolCalls: msg.toolCalls.map((tc) => ({ ...tc, args: {} })) };
67
- }
68
- if (msg.toolResult) {
69
- return {
70
- ...msg,
71
- toolResult: { ...msg.toolResult, content: "[other agent's tool result omitted]" },
72
- };
73
- }
74
- return msg;
75
- });
76
- }
77
-
78
58
  /**
79
59
  * Orchestrates multiple specialist agents. Sits between `FoundationAiAssistant`
80
60
  * and `ChatDriver`, classifying each user message and routing it to the right
@@ -132,10 +112,18 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
132
112
  options.maxFoldOperations,
133
113
  );
134
114
 
135
- // Proxy history-updated events from the shared driver
115
+ // Proxy events from the shared driver
136
116
  this.chatDriver.addEventListener('history-updated', (e: Event) => {
137
117
  this.dispatchEvent(new CustomEvent('history-updated', { detail: (e as CustomEvent).detail }));
138
118
  });
119
+ this.chatDriver.addEventListener('sub-agent-history-updated', (e: Event) => {
120
+ this.dispatchEvent(
121
+ new CustomEvent('sub-agent-history-updated', { detail: (e as CustomEvent).detail }),
122
+ );
123
+ });
124
+ this.chatDriver.addEventListener('sub-agent-stop', (e: Event) => {
125
+ this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: (e as CustomEvent).detail }));
126
+ });
139
127
  }
140
128
 
141
129
  resolveInteraction(interactionId: string, result: unknown): void {
@@ -22,6 +22,10 @@ interface BaseAgentConfig {
22
22
  * Used to establish agent identity and behavioural rules.
23
23
  */
24
24
  primerHistory?: ChatMessage[];
25
+ /**
26
+ * Sub-agents available to this agent's tool handlers via `requestSubAgent`.
27
+ */
28
+ subAgents?: AgentConfig[];
25
29
  }
26
30
 
27
31
  /**
@@ -67,3 +71,23 @@ export interface FallbackAgentConfig extends BaseAgentConfig {
67
71
  * @beta
68
72
  */
69
73
  export type AgentConfig = SpecialistAgentConfig | FallbackAgentConfig;
74
+
75
+ /**
76
+ * Identity helper that infers the narrowest possible type for an agent config,
77
+ * preserving string literal types (including `name`) without requiring `as const`.
78
+ *
79
+ * Use this when you need `typeof myAgent` to carry the literal `name` type —
80
+ * for example, when wiring `ChatToolHandlers<typeof myAgent>`.
81
+ *
82
+ * ```ts
83
+ * const myAgent = defineAgent({ name: 'my_agent', ... });
84
+ * type Handlers = ChatToolHandlers<typeof myAgent>;
85
+ * // requestSubAgent name param is now typed as 'my_agent'
86
+ * ```
87
+ *
88
+ * @beta
89
+ */
90
+
91
+ export function defineAgent<const T extends AgentConfig>(config: T): T {
92
+ return config;
93
+ }
@@ -451,6 +451,68 @@ export const styles = css`
451
451
  padding-left: 8px;
452
452
  }
453
453
 
454
+ .live-sub-agent-trace {
455
+ animation: slide-in-left 0.25s ease-out;
456
+ border-left: 2px solid var(--neutral-stroke-rest);
457
+ padding: 4px 8px;
458
+ margin: 4px 0;
459
+ opacity: 80%;
460
+ }
461
+
462
+ .live-sub-agent-name {
463
+ font-family: monospace;
464
+ font-size: 0.8em;
465
+ opacity: 70%;
466
+ font-style: italic;
467
+ display: block;
468
+ margin-bottom: 2px;
469
+ }
470
+
471
+ .sub-agent-trace {
472
+ margin-top: 6px;
473
+ border-left: 2px solid var(--neutral-stroke-rest);
474
+ padding-left: 8px;
475
+ }
476
+
477
+ .sub-agent-trace-summary {
478
+ font-family: monospace;
479
+ font-size: 0.85em;
480
+ opacity: 70%;
481
+ cursor: pointer;
482
+ user-select: none;
483
+ padding: 2px 0;
484
+ }
485
+
486
+ .sub-agent-trace-summary:hover {
487
+ opacity: 100%;
488
+ }
489
+
490
+ .sub-agent-message {
491
+ font-family: monospace;
492
+ font-size: 0.85em;
493
+ margin-top: 4px;
494
+ }
495
+
496
+ .sub-agent-assistant {
497
+ opacity: 85%;
498
+ white-space: pre-wrap;
499
+ }
500
+
501
+ .sub-agent-tool-call {
502
+ opacity: 60%;
503
+ }
504
+
505
+ .sub-agent-tool-name::before {
506
+ content: '⚙ ';
507
+ }
508
+
509
+ .sub-agent-tool-result {
510
+ opacity: 50%;
511
+ border-left: 1px solid var(--neutral-stroke-rest);
512
+ padding-left: 6px;
513
+ white-space: pre-wrap;
514
+ }
515
+
454
516
  .input-row {
455
517
  display: flex;
456
518
  gap: calc(var(--design-unit) * 2px);