@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
@@ -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';
@@ -18,6 +20,7 @@ const DEFAULT_MAX_TOOL_ITERATIONS = 50;
18
20
  const DEFAULT_MAX_FOLD_OPERATIONS = 5;
19
21
  const DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
20
22
  const MAX_MALFORMED_RETRIES = 2;
23
+ const MAX_EMPTY_RESPONSE_RETRIES = 3;
21
24
  const SUGGESTIONS_HISTORY_WINDOW = 8;
22
25
 
23
26
  /** Name reserved for the cross-agent handoff tool — injected by OrchestratingDriver. */
@@ -77,6 +80,16 @@ export class ChatDriver extends EventTarget implements AiDriver {
77
80
  private consecutiveUnknownToolCalls = 0;
78
81
  private readonly maxFoldOperations: number;
79
82
 
83
+ /** Sub-agents declared on the active agent config, keyed by name. */
84
+ private subAgentsMap: Map<string, AgentConfig> = new Map();
85
+ /**
86
+ * Set by `completeSubAgent` inside a sub-agent tool handler. Checked at the
87
+ * same point in the loop as `REQUEST_CONTINUATION_TOOL` — after tool results
88
+ * are appended — so the exit path mirrors the system-call pattern.
89
+ * `undefined` means the loop has not been stopped early.
90
+ */
91
+ private subAgentCompletion: { result: unknown } | undefined;
92
+
80
93
  constructor(
81
94
  private readonly aiProvider: AIProvider,
82
95
  toolHandlers: ChatToolHandlers = {},
@@ -104,11 +117,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
104
117
  this.toolHandlers = config.toolHandlers ?? {};
105
118
  this.primerHistory = config.primerHistory;
106
119
  this.activeAgentName = config.name;
120
+ this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
107
121
  // Reset fold state when agent changes — each specialist starts fresh
108
122
  this.foldStack = [];
109
123
  this.consecutiveFoldOps = 0;
110
124
  }
111
125
 
126
+ /**
127
+ * Returns the early-stop result set by `completeSubAgent`, if any.
128
+ * Called by a parent `ChatDriver` after running this instance as a sub-agent.
129
+ */
130
+ getSubAgentCompletion(): { result: unknown } | undefined {
131
+ return this.subAgentCompletion;
132
+ }
133
+
112
134
  /**
113
135
  * Optional transform applied to conversation history immediately before each LLM request.
114
136
  * Cleared when `undefined`. Does not alter stored history.
@@ -281,6 +303,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
281
303
  }
282
304
 
283
305
  this.busy = true;
306
+ this.subAgentCompletion = undefined;
284
307
  this.appendToHistory({ role: 'user', content: userInput, attachments });
285
308
 
286
309
  try {
@@ -294,6 +317,116 @@ export class ChatDriver extends EventTarget implements AiDriver {
294
317
  }
295
318
  }
296
319
 
320
+ /**
321
+ * Builds the context object passed to every tool handler call.
322
+ * Centralised here so fold shortcut dispatch and the main tool loop use the
323
+ * same context without duplication.
324
+ *
325
+ * @param traceCapture - Optional per-invocation slot. When provided, the trace
326
+ * from any sub-agent call is written here rather than to shared instance state,
327
+ * so parallel tool calls each capture their own trace independently.
328
+ */
329
+ private buildHandlerContext(traceCapture?: { trace?: ChatMessage[] }) {
330
+ return {
331
+ requestInteraction: <T>(componentName: string, data: any): Promise<T> =>
332
+ this.requestInteraction(componentName, data),
333
+ ...(this.subAgentsMap.size > 0 && {
334
+ requestSubAgent: <T = never>(
335
+ name: string,
336
+ options?: SubAgentRequestOptions,
337
+ ): Promise<T | string> =>
338
+ this.invokeSubAgent<T>(name, options).then(({ result, trace }) => {
339
+ if (traceCapture) traceCapture.trace = trace;
340
+ return result;
341
+ }),
342
+ }),
343
+ completeSubAgent: (result: unknown): void => {
344
+ if (this.subAgentCompletion) {
345
+ logger.warn(
346
+ `ChatDriver(${this.activeAgentName ?? 'unknown'}): completeSubAgent called more than once — ignoring`,
347
+ );
348
+ return;
349
+ }
350
+ this.subAgentCompletion = { result };
351
+ },
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Creates a child `ChatDriver` for the named sub-agent, runs it to completion,
357
+ * and returns its structured result (or final text fallback) together with the
358
+ * full child conversation trace. Callers receive both values so each parallel
359
+ * invocation can capture its own trace without touching shared instance state.
360
+ */
361
+ private async invokeSubAgent<T = never>(
362
+ name: string,
363
+ options?: SubAgentRequestOptions,
364
+ ): Promise<{ result: T | string; trace: ChatMessage[] }> {
365
+ const subConfig = this.subAgentsMap.get(name);
366
+ if (!subConfig) {
367
+ const available = [...this.subAgentsMap.keys()].join(', ') || '(none)';
368
+ throw new Error(
369
+ `Sub-agent "${name}" not found on agent "${this.activeAgentName}". Available: ${available}`,
370
+ );
371
+ }
372
+
373
+ const { task, historyCap, context } = options ?? {};
374
+
375
+ // Exclude the current in-flight assistant message (the one with tool calls that
376
+ // triggered this invocation) — it has unresolved tool calls that would confuse
377
+ // the sub-agent into thinking it needs to handle tools it doesn't own.
378
+ const lastMsg = this.history[this.history.length - 1];
379
+ const baseHistory =
380
+ lastMsg?.role === 'assistant' && lastMsg.toolCalls?.length
381
+ ? this.history.slice(0, -1)
382
+ : this.history;
383
+
384
+ const snapshotHistory =
385
+ historyCap != null ? applyHistoryCap(baseHistory, historyCap) : [...baseHistory];
386
+
387
+ const contextMessages: ChatMessage[] = context
388
+ ? [{ role: 'user', content: `[Sub-agent context]: ${JSON.stringify(context)}` }]
389
+ : [];
390
+
391
+ const effectivePrimer: ChatMessage[] = [
392
+ ...snapshotHistory,
393
+ ...contextMessages,
394
+ ...(subConfig.primerHistory ?? []),
395
+ ];
396
+
397
+ const child = new ChatDriver(this.aiProvider);
398
+ child.applyAgent({ ...subConfig, primerHistory: effectivePrimer });
399
+
400
+ const forwardTrace = (e: Event) => {
401
+ this.dispatchEvent(
402
+ new CustomEvent('sub-agent-history-updated', {
403
+ detail: { agentName: subConfig.name, history: (e as CustomEvent<ChatMessage[]>).detail },
404
+ }),
405
+ );
406
+ };
407
+ child.addEventListener('history-updated', forwardTrace);
408
+
409
+ this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: { name } }));
410
+ try {
411
+ await child.sendMessage(task ?? '');
412
+ } finally {
413
+ child.removeEventListener('history-updated', forwardTrace);
414
+ this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: { name } }));
415
+ }
416
+
417
+ const trace = child.getHistory() as ChatMessage[];
418
+ const completion = child.getSubAgentCompletion();
419
+
420
+ if (completion) {
421
+ return { result: completion.result as T, trace };
422
+ }
423
+
424
+ const finalMsg = [...trace]
425
+ .reverse()
426
+ .find((m) => m.role === 'assistant' && !m.toolCalls?.length && m.content?.trim());
427
+ return { result: (finalMsg?.content ?? '') as string, trace };
428
+ }
429
+
297
430
  /**
298
431
  * Continue the tool loop from current history without appending a new user message.
299
432
  * Used by OrchestratingDriver after an agent handoff.
@@ -306,6 +439,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
306
439
  }
307
440
 
308
441
  this.busy = true;
442
+ this.subAgentCompletion = undefined;
309
443
  try {
310
444
  return await this.runToolLoop('', undefined, transientPrimer);
311
445
  } catch (e) {
@@ -400,9 +534,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
400
534
  typeof args[key] === 'object' && args[key] !== null
401
535
  ? (args[key] as Record<string, unknown>)
402
536
  : {};
403
- return innerHandler(innerArgs, {
404
- requestInteraction: (c, d) => this.requestInteraction(c, d),
405
- }).then((r) => (typeof r === 'string' ? r : JSON.stringify(r)));
537
+ return innerHandler(innerArgs, this.buildHandlerContext()).then((r) =>
538
+ typeof r === 'string' ? r : JSON.stringify(r),
539
+ );
406
540
  }
407
541
  }
408
542
 
@@ -487,7 +621,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
487
621
  let currentAttachments: ChatAttachment[] | undefined = attachments;
488
622
  let iterations = 0;
489
623
  let malformedAttempts = 0;
490
- const startIteration = currentInput ? 1 : 0;
624
+ let emptyResponseAttempts = 0;
625
+ // True only for the very first LLM call. Used to exclude the pending user message
626
+ // from history (it is passed separately as currentInput). Must not be derived from
627
+ // `iterations` because fold operations decrement iterations, which would incorrectly
628
+ // re-trigger the slice on subsequent calls after a fold open/close.
629
+ let firstLlmCall = !!currentInput;
491
630
 
492
631
  while (iterations < this.maxToolIterations) {
493
632
  iterations += 1;
@@ -498,7 +637,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
498
637
  : foldSuffix || undefined;
499
638
 
500
639
  const primer = [...(this.primerHistory ?? []), ...(transientPrimer ?? [])];
501
- const baseHistory = iterations === startIteration ? this.history.slice(0, -1) : this.history;
640
+ const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
641
+ firstLlmCall = false;
502
642
  const historyForProvider = this.providerHistoryTransform
503
643
  ? this.providerHistoryTransform([...baseHistory])
504
644
  : baseHistory;
@@ -507,7 +647,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
507
647
  const systemPrompt =
508
648
  malformedAttempts > 0
509
649
  ? `${baseSystemPrompt ?? ''}\n\nIMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools.`
510
- : baseSystemPrompt;
650
+ : emptyResponseAttempts > 0
651
+ ? `${baseSystemPrompt ?? ''}\n\nIMPORTANT: You must respond to the user's message. Call the appropriate tool or provide a text response — do not return an empty response.`
652
+ : baseSystemPrompt;
511
653
 
512
654
  const options: ChatRequestOptions = {
513
655
  systemPrompt,
@@ -547,10 +689,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
547
689
  const isEmptyResponse = !response.content?.trim() && !response.toolCalls?.length;
548
690
 
549
691
  if (isEmptyResponse) {
550
- malformedAttempts += 1;
551
- if (malformedAttempts < MAX_MALFORMED_RETRIES) {
692
+ emptyResponseAttempts += 1;
693
+ if (emptyResponseAttempts < MAX_EMPTY_RESPONSE_RETRIES) {
552
694
  logger.warn(
553
- `ChatDriver: empty model response, retrying (${malformedAttempts}/${MAX_MALFORMED_RETRIES})`,
695
+ `ChatDriver: empty model response, retrying (${emptyResponseAttempts}/${MAX_EMPTY_RESPONSE_RETRIES})`,
554
696
  );
555
697
  iterations -= 1;
556
698
  continue;
@@ -581,7 +723,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
581
723
  [[], []],
582
724
  );
583
725
 
584
- const executedById = new Map<string, { toolCallId: string; content: string }>();
726
+ const executedById = new Map<
727
+ string,
728
+ { toolCallId: string; content: string; subAgentTrace?: ChatMessage[] }
729
+ >();
585
730
  const unknownToolIds = new Set<string>();
586
731
  let anyRealToolExecuted = false;
587
732
  let hitUnknownToolLimit = false;
@@ -660,12 +805,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
660
805
 
661
806
  // Real tool execution
662
807
  try {
663
- const result = await handler(tc.args, {
664
- requestInteraction: (componentName, data) =>
665
- this.requestInteraction(componentName, data),
666
- });
808
+ const traceCapture: { trace?: ChatMessage[] } = {};
809
+ const result = await handler(tc.args, this.buildHandlerContext(traceCapture));
667
810
  const content = typeof result === 'string' ? result : JSON.stringify(result);
668
- executedById.set(tc.id, { toolCallId: tc.id, content });
811
+ executedById.set(tc.id, {
812
+ toolCallId: tc.id,
813
+ content,
814
+ subAgentTrace: traceCapture.trace,
815
+ });
669
816
  anyRealToolExecuted = true;
670
817
  } catch (e) {
671
818
  logger.error(`ChatDriver tool "${tc.name}" failed:`, e);
@@ -734,6 +881,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
734
881
  foldPath: !isFoldOpen && !isFoldClose && foldPath.length > 0 ? foldPath : undefined,
735
882
  unknown: isUnknown || undefined,
736
883
  availableTools: isUnknown ? availableToolNames : undefined,
884
+ subAgentTrace: executedById.get(tc.id)?.subAgentTrace,
737
885
  };
738
886
  });
739
887
  this.history[tcMsgIdx] = { ...tcMsg, toolCalls: annotatedCalls };
@@ -765,6 +913,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
765
913
  return { reason: 'agent-handoff', summary, remainingTask };
766
914
  }
767
915
 
916
+ // Sub-agent early exit — checked here so the exit point mirrors the
917
+ // system-call pattern above. Set by completeSubAgent() in a tool handler.
918
+ if (this.subAgentCompletion) {
919
+ return { reason: 'done' };
920
+ }
921
+
768
922
  currentInput = '';
769
923
  }
770
924
 
@@ -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);