@genesislcap/ai-assistant 14.420.0 → 14.421.1

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 (105) hide show
  1. package/dist/ai-assistant.api.json +4061 -1416
  2. package/dist/ai-assistant.d.ts +594 -81
  3. package/dist/dts/channel/ai-activity-channel.d.ts +4 -22
  4. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  5. package/dist/dts/components/ai-driver/ai-driver.d.ts +52 -0
  6. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -0
  7. package/dist/dts/components/ai-driver/index.d.ts +2 -0
  8. package/dist/dts/components/ai-driver/index.d.ts.map +1 -0
  9. package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -8
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  11. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +3 -3
  12. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  13. package/dist/dts/components/chat-markdown/chat-markdown.d.ts +1 -1
  14. package/dist/dts/components/chat-markdown/chat-markdown.d.ts.map +1 -1
  15. package/dist/dts/components/halo-overlay.d.ts +13 -1
  16. package/dist/dts/components/halo-overlay.d.ts.map +1 -1
  17. package/dist/dts/components/orchestrating-driver/index.d.ts +2 -0
  18. package/dist/dts/components/orchestrating-driver/index.d.ts.map +1 -0
  19. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +39 -0
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -0
  21. package/dist/dts/components/popout-manager/index.d.ts +2 -0
  22. package/dist/dts/components/popout-manager/index.d.ts.map +1 -0
  23. package/dist/dts/components/popout-manager/popout-manager.d.ts +72 -0
  24. package/dist/dts/components/popout-manager/popout-manager.d.ts.map +1 -0
  25. package/dist/dts/config/config.d.ts +43 -15
  26. package/dist/dts/config/config.d.ts.map +1 -1
  27. package/dist/dts/config/fallback-agents.d.ts +20 -0
  28. package/dist/dts/config/fallback-agents.d.ts.map +1 -0
  29. package/dist/dts/config/index.d.ts +1 -0
  30. package/dist/dts/config/index.d.ts.map +1 -1
  31. package/dist/dts/index.d.ts +6 -0
  32. package/dist/dts/index.d.ts.map +1 -1
  33. package/dist/dts/main/main.d.ts +122 -21
  34. package/dist/dts/main/main.d.ts.map +1 -1
  35. package/dist/dts/main/main.styles.d.ts.map +1 -1
  36. package/dist/dts/main/main.template.d.ts.map +1 -1
  37. package/dist/dts/main/main.types.d.ts +16 -0
  38. package/dist/dts/main/main.types.d.ts.map +1 -1
  39. package/dist/dts/state/ai-assistant-slice.d.ts +38 -0
  40. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -0
  41. package/dist/dts/state/driver-registry.d.ts +22 -0
  42. package/dist/dts/state/driver-registry.d.ts.map +1 -0
  43. package/dist/dts/state/session-store.d.ts +37 -0
  44. package/dist/dts/state/session-store.d.ts.map +1 -0
  45. package/dist/dts/suggestions/chat-suggestions.d.ts +7 -0
  46. package/dist/dts/suggestions/chat-suggestions.d.ts.map +1 -0
  47. package/dist/dts/types/ai-chat-widget.d.ts +3 -2
  48. package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
  49. package/dist/dts/utils/index.d.ts +1 -0
  50. package/dist/dts/utils/index.d.ts.map +1 -1
  51. package/dist/dts/utils/tool-fold.d.ts +133 -0
  52. package/dist/dts/utils/tool-fold.d.ts.map +1 -0
  53. package/dist/esm/components/ai-driver/ai-driver.js +1 -0
  54. package/dist/esm/components/ai-driver/index.js +1 -0
  55. package/dist/esm/components/chat-driver/chat-driver.js +499 -67
  56. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +2 -2
  57. package/dist/esm/components/chat-markdown/chat-markdown.js +1 -1
  58. package/dist/esm/components/halo-overlay.js +53 -7
  59. package/dist/esm/components/orchestrating-driver/index.js +1 -0
  60. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +247 -0
  61. package/dist/esm/components/popout-manager/index.js +1 -0
  62. package/dist/esm/components/popout-manager/popout-manager.js +126 -0
  63. package/dist/esm/config/fallback-agents.js +26 -0
  64. package/dist/esm/config/index.js +1 -0
  65. package/dist/esm/index.js +6 -0
  66. package/dist/esm/main/main.js +546 -112
  67. package/dist/esm/main/main.styles.js +200 -4
  68. package/dist/esm/main/main.template.js +163 -63
  69. package/dist/esm/state/ai-assistant-slice.js +54 -0
  70. package/dist/esm/state/driver-registry.js +46 -0
  71. package/dist/esm/state/session-store.js +39 -0
  72. package/dist/esm/suggestions/chat-suggestions.js +147 -0
  73. package/dist/esm/utils/index.js +1 -0
  74. package/dist/esm/utils/tool-fold.js +92 -0
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/docs/migration-FUI-2495.md +339 -0
  77. package/docs/sub_agent.md +310 -0
  78. package/package.json +16 -15
  79. package/src/channel/ai-activity-channel.ts +4 -20
  80. package/src/components/ai-driver/ai-driver.ts +69 -0
  81. package/src/components/ai-driver/index.ts +1 -0
  82. package/src/components/chat-driver/chat-driver.ts +600 -73
  83. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +3 -3
  84. package/src/components/chat-markdown/chat-markdown.ts +1 -1
  85. package/src/components/halo-overlay.ts +45 -7
  86. package/src/components/orchestrating-driver/index.ts +1 -0
  87. package/src/components/orchestrating-driver/orchestrating-driver.ts +328 -0
  88. package/src/components/popout-manager/index.ts +1 -0
  89. package/src/components/popout-manager/popout-manager.ts +147 -0
  90. package/src/config/config.ts +45 -15
  91. package/src/config/fallback-agents.ts +29 -0
  92. package/src/config/index.ts +1 -0
  93. package/src/index.ts +6 -0
  94. package/src/main/main.styles.ts +200 -4
  95. package/src/main/main.template.ts +200 -80
  96. package/src/main/main.ts +567 -94
  97. package/src/main/main.types.ts +11 -0
  98. package/src/state/ai-assistant-slice.ts +80 -0
  99. package/src/state/driver-registry.ts +51 -0
  100. package/src/state/session-store.ts +56 -0
  101. package/src/suggestions/chat-suggestions.ts +158 -0
  102. package/src/types/ai-chat-widget.ts +4 -2
  103. package/src/utils/index.ts +1 -0
  104. package/src/utils/tool-fold.ts +181 -0
  105. package/docs/multi-agent-architecture.md +0 -198
@@ -1,8 +1,16 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
3
3
  import { logger } from '../../utils/logger';
4
+ import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
4
5
  const DEFAULT_MAX_TOOL_ITERATIONS = 50;
6
+ const DEFAULT_MAX_FOLD_OPERATIONS = 5;
7
+ const DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
5
8
  const MAX_MALFORMED_RETRIES = 2;
9
+ const SUGGESTIONS_HISTORY_WINDOW = 8;
10
+ /** Name reserved for the cross-agent handoff tool — injected by OrchestratingDriver. */
11
+ export const REQUEST_CONTINUATION_TOOL = 'request_continuation';
12
+ /** Paired in history for each `request_continuation` so tool_calls stay balanced for the provider. */
13
+ const HANDOFF_TOOL_RESULT_PLACEHOLDER = 'Handoff to another specialist — routing continues on the next turn.';
6
14
  /**
7
15
  * Plain TS class that drives a multi-turn chat conversation, including the tool-call loop.
8
16
  * Owned by `FoundationAiAssistant` — created in `connectedCallback`, torn down in `disconnectedCallback`.
@@ -14,24 +22,133 @@ const MAX_MALFORMED_RETRIES = 2;
14
22
  * @beta
15
23
  */
16
24
  export class ChatDriver extends EventTarget {
17
- constructor(aiProvider, toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS) {
25
+ constructor(aiProvider, toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS) {
18
26
  super();
19
27
  this.aiProvider = aiProvider;
20
- this.toolHandlers = toolHandlers;
21
- this.toolDefinitions = toolDefinitions;
22
- this.systemPrompt = systemPrompt;
23
- this.primerHistory = primerHistory;
24
28
  this.maxToolIterations = maxToolIterations;
25
29
  this.history = [];
26
30
  this.busy = false;
27
31
  this.pendingInteractions = new Map();
28
- if (!systemPrompt) {
29
- logger.error('ChatDriver: no systemPrompt provided. The assistant will have no instructions — set the systemPrompt property on foundation-ai-assistant.');
30
- }
32
+ /** Stack of fold frames — grows when a fold opens, shrinks when it closes. */
33
+ this.foldStack = [];
34
+ /** Consecutive fold open/close ops without a real tool call. Reset on real tool execution. */
35
+ this.consecutiveFoldOps = 0;
36
+ /** Consecutive unknown-tool calls without a real tool call. Reset on real tool execution. */
37
+ this.consecutiveUnknownToolCalls = 0;
38
+ this.toolHandlers = toolHandlers;
39
+ this.toolDefinitions = toolDefinitions;
40
+ this.systemPrompt = systemPrompt;
41
+ this.primerHistory = primerHistory;
42
+ this.maxFoldOperations = maxFoldOperations;
43
+ }
44
+ /**
45
+ * Swap in a new agent's configuration. Called by OrchestratingDriver before
46
+ * each specialist turn so the shared driver runs with the right tools and prompt.
47
+ */
48
+ applyAgent(config) {
49
+ var _a, _b;
50
+ this.systemPrompt = config.systemPrompt;
51
+ this.toolDefinitions = (_a = config.toolDefinitions) !== null && _a !== void 0 ? _a : [];
52
+ this.toolHandlers = (_b = config.toolHandlers) !== null && _b !== void 0 ? _b : {};
53
+ this.primerHistory = config.primerHistory;
54
+ this.activeAgentName = config.name;
55
+ // Reset fold state when agent changes — each specialist starts fresh
56
+ this.foldStack = [];
57
+ this.consecutiveFoldOps = 0;
58
+ }
59
+ /**
60
+ * Optional transform applied to conversation history immediately before each LLM request.
61
+ * Cleared when `undefined`. Does not alter stored history.
62
+ */
63
+ setProviderHistoryTransform(transform) {
64
+ this.providerHistoryTransform = transform;
31
65
  }
32
66
  getHistory() {
33
67
  return this.history;
34
68
  }
69
+ getRawHistory() {
70
+ return this.history;
71
+ }
72
+ /** Returns the current fold stack names for debugging. */
73
+ getActiveFoldNames() {
74
+ return this.foldStack.map((f) => f.foldName);
75
+ }
76
+ getSuggestions(history, prompt, count, allAgentInfo) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ if (!this.aiProvider.prompt) {
79
+ logger.warn('ChatDriver: AIProvider does not implement prompt()');
80
+ return [];
81
+ }
82
+ let agentContext = '';
83
+ let toolContext = '';
84
+ if (allAgentInfo === null || allAgentInfo === void 0 ? void 0 : allAgentInfo.length) {
85
+ const agentDescriptions = allAgentInfo
86
+ .map((agent) => {
87
+ const tools = agent.tools.map((t) => t.name).join(', ');
88
+ return `- ${agent.name} (${agent.description}): ${tools ? `(Tools: ${tools})` : 'No tools'}`;
89
+ })
90
+ .join('\n');
91
+ agentContext = `The assistant has the following capabilities:\n${agentDescriptions}`;
92
+ const allToolNames = allAgentInfo
93
+ .flatMap((agent) => agent.tools.map((t) => t.name))
94
+ .filter((value, index, self) => self.indexOf(value) === index)
95
+ .join(', ');
96
+ toolContext = allToolNames
97
+ ? `You have access to the following tools across all agents: ${allToolNames}.`
98
+ : '';
99
+ }
100
+ else if (this.activeAgentName) {
101
+ const toolNames = this.toolDefinitions.map((tool) => tool.name).join(', ');
102
+ agentContext = `You are currently acting as the "${this.activeAgentName}" agent.`;
103
+ toolContext = toolNames ? `You have access to the following tools: ${toolNames}.` : '';
104
+ }
105
+ let systemPrompt;
106
+ if (history.length === 0) {
107
+ systemPrompt = `You are generating short, simple starter prompts to show a user what an AI assistant can do. Generate exactly ${count} brief suggestions phrased as the user would write them. Keep them short and generic — do not invent specific names, IDs, or data (e.g. prefer "Search for a trade" over "Find all trades with Client A").`;
108
+ if (agentContext || toolContext) {
109
+ systemPrompt += " Base suggestions only on the agent's actual capabilities.";
110
+ if (agentContext)
111
+ systemPrompt += ` ${agentContext}`;
112
+ if (toolContext)
113
+ systemPrompt += ` ${toolContext}`;
114
+ systemPrompt += ' Do not suggest anything outside these capabilities.';
115
+ }
116
+ }
117
+ else {
118
+ systemPrompt = `You are generating suggested follow-up prompts that a user could send to an AI assistant. Generate exactly ${count} suggestions phrased as the user would write them — first-person requests or questions directed at the agent. The first ${Math.max(0, count - 1)} should be natural follow-ups to the last turn of conversation. Do not invent specific names, IDs, or data values that do not appear in the conversation history.`;
119
+ if (agentContext || toolContext) {
120
+ systemPrompt += ' Suggestions must only cover what the agent is capable of.';
121
+ if (agentContext)
122
+ systemPrompt += ` ${agentContext}`;
123
+ if (toolContext)
124
+ systemPrompt += ` ${toolContext}`;
125
+ systemPrompt += ' Do not suggest anything outside these capabilities.';
126
+ }
127
+ }
128
+ if (prompt) {
129
+ systemPrompt += ` Additional guidance: "${prompt}"`;
130
+ }
131
+ systemPrompt +=
132
+ ' Output only the suggestions — one per line, no numbering, no bullets, no commentary, no preamble. Do not describe or explain the suggestions.';
133
+ const conversationContext = history
134
+ .filter((m) => {
135
+ var _a, _b;
136
+ return (m.role === 'user' || m.role === 'assistant') &&
137
+ !((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) &&
138
+ !m.thinking &&
139
+ !!((_b = m.content) === null || _b === void 0 ? void 0 : _b.trim());
140
+ })
141
+ .slice(-SUGGESTIONS_HISTORY_WINDOW)
142
+ .map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
143
+ .join('\n');
144
+ const message = conversationContext || 'The conversation has not started yet.';
145
+ const text = yield this.aiProvider.prompt(message, { systemPrompt });
146
+ return text
147
+ .split('\n')
148
+ .map((s) => s.trim())
149
+ .filter(Boolean);
150
+ });
151
+ }
35
152
  isBusy() {
36
153
  return this.busy;
37
154
  }
@@ -62,11 +179,9 @@ export class ChatDriver extends EventTarget {
62
179
  resolveInteraction(interactionId, result) {
63
180
  const interaction = this.pendingInteractions.get(interactionId);
64
181
  if (interaction) {
65
- // Mark the message as resolved before resolving the promise so the next
66
- // history-updated event carries the updated flag.
67
- const msg = this.history.find((m) => { var _a; return ((_a = m.interaction) === null || _a === void 0 ? void 0 : _a.interactionId) === interactionId; });
68
- if (msg === null || msg === void 0 ? void 0 : msg.interaction) {
69
- msg.interaction.resolved = true;
182
+ const idx = this.history.findIndex((m) => { var _a; return ((_a = m.interaction) === null || _a === void 0 ? void 0 : _a.interactionId) === interactionId; });
183
+ if (idx !== -1) {
184
+ this.history[idx] = Object.assign(Object.assign({}, this.history[idx]), { interaction: Object.assign(Object.assign({}, this.history[idx].interaction), { resolved: result }) });
70
185
  this.dispatchEvent(new CustomEvent('history-updated', {
71
186
  detail: this.history,
72
187
  }));
@@ -84,57 +199,224 @@ export class ChatDriver extends EventTarget {
84
199
  */
85
200
  loadHistory(messages) {
86
201
  this.history = [...messages];
202
+ this.dispatchEvent(new CustomEvent('history-updated', {
203
+ detail: this.history,
204
+ }));
87
205
  }
88
206
  sendMessage(userInput, attachments) {
89
207
  return __awaiter(this, void 0, void 0, function* () {
90
208
  if (this.busy || (!userInput.trim() && !(attachments === null || attachments === void 0 ? void 0 : attachments.length)))
91
- return;
209
+ return { reason: 'done' };
92
210
  if (!this.aiProvider.chat) {
93
211
  logger.warn('ChatDriver: AIProvider does not implement chat()');
94
- return;
212
+ return { reason: 'done' };
95
213
  }
96
214
  this.busy = true;
97
215
  this.appendToHistory({ role: 'user', content: userInput, attachments });
98
216
  try {
99
- yield this.runToolLoop(userInput, attachments);
217
+ return yield this.runToolLoop(userInput, attachments);
218
+ }
219
+ catch (e) {
220
+ logger.error('ChatDriver error:', e);
221
+ this.appendToHistory({ role: 'assistant', content: 'Sorry, something went wrong.' });
222
+ return { reason: 'done' };
223
+ }
224
+ finally {
225
+ this.busy = false;
226
+ }
227
+ });
228
+ }
229
+ /**
230
+ * Continue the tool loop from current history without appending a new user message.
231
+ * Used by OrchestratingDriver after an agent handoff.
232
+ */
233
+ continueFromHistory(transientPrimer) {
234
+ return __awaiter(this, void 0, void 0, function* () {
235
+ if (this.busy)
236
+ return { reason: 'done' };
237
+ if (!this.aiProvider.chat) {
238
+ logger.warn('ChatDriver: AIProvider does not implement chat()');
239
+ return { reason: 'done' };
240
+ }
241
+ this.busy = true;
242
+ try {
243
+ return yield this.runToolLoop('', undefined, transientPrimer);
100
244
  }
101
245
  catch (e) {
102
246
  logger.error('ChatDriver error:', e);
103
247
  this.appendToHistory({ role: 'assistant', content: 'Sorry, something went wrong.' });
248
+ return { reason: 'done' };
104
249
  }
105
250
  finally {
106
251
  this.busy = false;
107
252
  }
108
253
  });
109
254
  }
110
- runToolLoop(userInput, attachments) {
255
+ // ---------------------------------------------------------------------------
256
+ // Fold mechanics
257
+ // ---------------------------------------------------------------------------
258
+ /** Extract ToolFold metadata from a handler, or undefined if it isn't a fold facade. */
259
+ getFold(toolName) {
260
+ const handler = this.toolHandlers[toolName];
261
+ return handler ? handler[TOOL_FOLD_SYMBOL] : undefined;
262
+ }
263
+ /**
264
+ * Search all currently registered fold facades (and their nested folds recursively)
265
+ * to find which fold contains a given tool name. Returns the immediate parent fold name.
266
+ */
267
+ findFoldContaining(toolName, handlers) {
268
+ const source = handlers !== null && handlers !== void 0 ? handlers : this.toolHandlers;
269
+ for (const [, handler] of Object.entries(source)) {
270
+ const fold = handler[TOOL_FOLD_SYMBOL];
271
+ if (!fold)
272
+ continue;
273
+ // Direct inner tool match
274
+ if (fold.handlers[toolName])
275
+ return fold.name;
276
+ // Recurse into nested folds
277
+ const nested = this.findFoldContaining(toolName, fold.handlers);
278
+ if (nested)
279
+ return fold.name;
280
+ }
281
+ return null;
282
+ }
283
+ /**
284
+ * Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
285
+ * the current tool set. Also injects the close tool. Does NOT touch the fold stack.
286
+ */
287
+ applyFoldToolSet(fold, foldName) {
288
+ const closeToolName = `close_${foldName}`;
289
+ const newDefs = [];
290
+ const newHandlers = {};
291
+ if (!fold.exclusive) {
292
+ // Non-exclusive: keep existing tools minus the facade we just opened
293
+ for (const def of this.toolDefinitions) {
294
+ if (def.name !== foldName)
295
+ newDefs.push(def);
296
+ }
297
+ for (const [name, handler] of Object.entries(this.toolHandlers)) {
298
+ if (name !== foldName)
299
+ newHandlers[name] = handler;
300
+ }
301
+ }
302
+ // Install inner tools from fold metadata
303
+ newDefs.push(...fold.tools);
304
+ Object.assign(newHandlers, fold.handlers);
305
+ // Inject the close tool
306
+ newDefs.push({
307
+ name: closeToolName,
308
+ description: `Close the ${foldName} fold and return to the previous set of tools.`,
309
+ parameters: { type: 'object', properties: {} },
310
+ });
311
+ newHandlers[closeToolName] = () => __awaiter(this, void 0, void 0, function* () { return this.closeFold(); });
312
+ this.toolDefinitions = newDefs;
313
+ this.toolHandlers = newHandlers;
314
+ }
315
+ /** Open a fold: push a stack frame, swap the tool set, return the response message. */
316
+ openFold(foldName, fold, args) {
317
+ // Shortcut dispatch: model passed inner tool args directly, e.g.
318
+ // trading_tools({ search_trades: { side: "BUY" } })
319
+ for (const key of Object.keys(args)) {
320
+ const innerHandler = fold.handlers[key];
321
+ if (innerHandler) {
322
+ logger.debug(`ChatDriver: fold shortcut dispatch "${foldName}" → "${key}"`);
323
+ // Open the fold first so the tool set is correct for subsequent calls
324
+ this.pushFoldFrame(foldName);
325
+ this.applyFoldToolSet(fold, foldName);
326
+ this.consecutiveFoldOps = 0; // shortcut dispatch counts as real work
327
+ const innerArgs = typeof args[key] === 'object' && args[key] !== null
328
+ ? args[key]
329
+ : {};
330
+ return innerHandler(innerArgs, {
331
+ requestInteraction: (c, d) => this.requestInteraction(c, d),
332
+ }).then((r) => (typeof r === 'string' ? r : JSON.stringify(r)));
333
+ }
334
+ }
335
+ // Normal two-step open
336
+ this.pushFoldFrame(foldName);
337
+ this.applyFoldToolSet(fold, foldName);
338
+ const innerToolNames = fold.tools.map((t) => t.name);
339
+ const closeToolName = `close_${foldName}`;
340
+ let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(', ')}.`;
341
+ if (fold.usageNotes)
342
+ message += ` Notes: ${fold.usageNotes}`;
343
+ message += ` Call ${closeToolName} when done to return to the previous tools.`;
344
+ return Promise.resolve(message);
345
+ }
346
+ pushFoldFrame(foldName) {
347
+ this.foldStack.push({
348
+ foldName,
349
+ previousDefinitions: [...this.toolDefinitions],
350
+ previousHandlers: Object.assign({}, this.toolHandlers),
351
+ });
352
+ }
353
+ /** Close the top fold: pop the stack frame, restore the previous tool set. */
354
+ closeFold() {
355
+ const frame = this.foldStack.pop();
356
+ if (!frame)
357
+ return 'No fold is currently open.';
358
+ this.toolDefinitions = frame.previousDefinitions;
359
+ this.toolHandlers = frame.previousHandlers;
360
+ const toolNames = this.toolDefinitions.map((t) => t.name);
361
+ return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(', ')}.`;
362
+ }
363
+ /** Build the fold-awareness suffix appended to the system prompt each LLM call. */
364
+ buildFoldSystemPromptSuffix() {
365
+ // Collect fold facades from the current handler map
366
+ const activeFolds = [];
367
+ for (const handler of Object.values(this.toolHandlers)) {
368
+ const fold = handler[TOOL_FOLD_SYMBOL];
369
+ if (fold)
370
+ activeFolds.push(fold);
371
+ }
372
+ if (activeFolds.length === 0 && this.foldStack.length === 0)
373
+ return '';
374
+ const parts = ['\n\n--- Tool Folds ---'];
375
+ parts.push('Some tools are grouped into folds. You may see tool calls in the conversation history for tools that are not currently available — they are inside a fold. To access them, invoke the fold tool first.');
376
+ if (this.foldStack.length > 0) {
377
+ const current = this.foldStack[this.foldStack.length - 1];
378
+ parts.push(`You are currently inside the "${current.foldName}" fold. Call close_${current.foldName} when you are done with these tools.`);
379
+ }
380
+ for (const fold of activeFolds) {
381
+ parts.push(`• ${fold.name}: ${fold.description}`);
382
+ }
383
+ return parts.join('\n');
384
+ }
385
+ // ---------------------------------------------------------------------------
386
+ // Tool loop
387
+ // ---------------------------------------------------------------------------
388
+ runToolLoop(userInput, attachments, transientPrimer) {
111
389
  return __awaiter(this, void 0, void 0, function* () {
112
390
  var _a, _b, _c, _d, _e, _f;
113
- const baseOptions = {
114
- systemPrompt: this.systemPrompt,
115
- tools: this.toolDefinitions.length ? this.toolDefinitions : undefined,
116
- };
117
- // History has the user message at the end — pass everything before it as history,
118
- // and the user input as the userMessage argument.
391
+ if (!this.systemPrompt) {
392
+ logger.warn('ChatDriver: no systemPrompt set. The assistant will have no instructions — provide a systemPrompt via agents config or the foundation-ai-assistant property.');
393
+ }
119
394
  let currentInput = userInput;
120
395
  let currentAttachments = attachments;
121
396
  let iterations = 0;
122
397
  let malformedAttempts = 0;
398
+ const startIteration = currentInput ? 1 : 0;
123
399
  while (iterations < this.maxToolIterations) {
124
400
  iterations += 1;
125
- // On the first iteration, the last item in history is the user message which is
126
- // passed separately as currentInput — exclude it. On subsequent iterations, the
127
- // full history (including tool results) should be sent and currentInput is empty.
128
- // primerHistory (if provided) is prepended to every call but never stored in
129
- // this.history and never shown in the UI.
130
- const primer = (_a = this.primerHistory) !== null && _a !== void 0 ? _a : [];
131
- const historyForCall = iterations === 1 ? [...primer, ...this.history.slice(0, -1)] : [...primer, ...this.history];
132
- // On malformed-call retries, augment the system prompt to steer the model
133
- // away from generating Python-style batched function call syntax.
401
+ const foldSuffix = this.buildFoldSystemPromptSuffix();
402
+ const baseSystemPrompt = this.systemPrompt
403
+ ? `${this.systemPrompt}${foldSuffix}`
404
+ : foldSuffix || undefined;
405
+ const primer = [...((_a = this.primerHistory) !== null && _a !== void 0 ? _a : []), ...(transientPrimer !== null && transientPrimer !== void 0 ? transientPrimer : [])];
406
+ const baseHistory = iterations === startIteration ? this.history.slice(0, -1) : this.history;
407
+ const historyForProvider = this.providerHistoryTransform
408
+ ? this.providerHistoryTransform([...baseHistory])
409
+ : baseHistory;
410
+ const historyForCall = [...primer, ...historyForProvider];
134
411
  const systemPrompt = malformedAttempts > 0
135
- ? `${(_b = baseOptions.systemPrompt) !== null && _b !== void 0 ? _b : ''}\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.`
136
- : baseOptions.systemPrompt;
137
- const options = Object.assign(Object.assign({}, baseOptions), { systemPrompt, attachments: currentAttachments });
412
+ ? `${baseSystemPrompt !== null && baseSystemPrompt !== void 0 ? 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.`
413
+ : baseSystemPrompt;
414
+ const options = {
415
+ systemPrompt,
416
+ // Strip fold-only properties (foldEvent, foldPath) before sending to provider
417
+ tools: this.toolDefinitions.length ? this.toolDefinitions : undefined,
418
+ attachments: currentAttachments,
419
+ };
138
420
  let response;
139
421
  try {
140
422
  // eslint-disable-next-line no-await-in-loop
@@ -145,7 +427,7 @@ export class ChatDriver extends EventTarget {
145
427
  malformedAttempts += 1;
146
428
  if (malformedAttempts < MAX_MALFORMED_RETRIES) {
147
429
  logger.warn(`ChatDriver: MALFORMED_FUNCTION_CALL, retrying (${malformedAttempts}/${MAX_MALFORMED_RETRIES})`);
148
- iterations -= 1; // don't consume an iteration budget slot for a failed attempt
430
+ iterations -= 1;
149
431
  continue;
150
432
  }
151
433
  logger.error('ChatDriver: MALFORMED_FUNCTION_CALL, max retries reached');
@@ -153,65 +435,215 @@ export class ChatDriver extends EventTarget {
153
435
  role: 'assistant',
154
436
  content: "I'm sorry, I wasn't able to complete that request. Please try rephrasing or breaking it into smaller steps.",
155
437
  });
156
- return;
438
+ return { reason: 'done' };
157
439
  }
158
440
  throw e;
159
441
  }
160
- currentAttachments = undefined; // attachments only sent on first call
161
- const isThinkingStep = response.content && ((_c = response.toolCalls) === null || _c === void 0 ? void 0 : _c.length);
162
- const isEmptyResponse = !((_d = response.content) === null || _d === void 0 ? void 0 : _d.trim()) && !((_e = response.toolCalls) === null || _e === void 0 ? void 0 : _e.length);
442
+ currentAttachments = undefined;
443
+ const isThinkingStep = response.content && ((_b = response.toolCalls) === null || _b === void 0 ? void 0 : _b.length);
444
+ const isEmptyResponse = !((_c = response.content) === null || _c === void 0 ? void 0 : _c.trim()) && !((_d = response.toolCalls) === null || _d === void 0 ? void 0 : _d.length);
163
445
  if (isEmptyResponse) {
164
- // Do nothing, discard empty/whitespace-only responses
446
+ malformedAttempts += 1;
447
+ if (malformedAttempts < MAX_MALFORMED_RETRIES) {
448
+ logger.warn(`ChatDriver: empty model response, retrying (${malformedAttempts}/${MAX_MALFORMED_RETRIES})`);
449
+ iterations -= 1;
450
+ continue;
451
+ }
452
+ logger.error('ChatDriver: empty model response after all retries');
453
+ this.appendToHistory({
454
+ role: 'assistant',
455
+ content: 'Remote agent returned no response.',
456
+ });
457
+ return { reason: 'done' };
165
458
  }
166
459
  else if (isThinkingStep) {
167
- // Separate thinking message and tool call message
168
460
  this.appendToHistory(Object.assign(Object.assign({}, response), { toolCalls: undefined, thinking: true }));
169
461
  this.appendToHistory(Object.assign(Object.assign({}, response), { content: '' }));
170
462
  }
171
463
  else {
172
464
  this.appendToHistory(response);
173
465
  }
174
- if (!((_f = response.toolCalls) === null || _f === void 0 ? void 0 : _f.length)) {
175
- // Terminal text response — done
466
+ if (!((_e = response.toolCalls) === null || _e === void 0 ? void 0 : _e.length)) {
176
467
  break;
177
468
  }
178
- // Execute all tool calls for this iteration concurrently, then append results in order
179
- // eslint-disable-next-line no-await-in-loop
180
- const toolResults = yield Promise.all(response.toolCalls.map((tc) => __awaiter(this, void 0, void 0, function* () {
181
- const handler = this.toolHandlers[tc.name];
182
- if (!handler) {
183
- logger.warn(`ChatDriver: no handler registered for tool "${tc.name}"`);
184
- return { toolCallId: tc.id, content: `Unknown tool: ${tc.name}` };
185
- }
186
- try {
187
- const result = yield handler(tc.args, {
188
- requestInteraction: (componentName, data) => this.requestInteraction(componentName, data),
469
+ const [toolCalls, systemCalls] = response.toolCalls.reduce((acc, tc) => {
470
+ if (tc.name === REQUEST_CONTINUATION_TOOL)
471
+ acc[1].push(tc);
472
+ else
473
+ acc[0].push(tc);
474
+ return acc;
475
+ }, [[], []]);
476
+ const executedById = new Map();
477
+ const unknownToolIds = new Set();
478
+ let anyRealToolExecuted = false;
479
+ let hitUnknownToolLimit = false;
480
+ if (toolCalls.length > 0) {
481
+ // eslint-disable-next-line no-await-in-loop
482
+ yield Promise.all(toolCalls.map((tc) => __awaiter(this, void 0, void 0, function* () {
483
+ // Check for fold facade
484
+ const fold = this.getFold(tc.name);
485
+ if (fold) {
486
+ this.consecutiveFoldOps += 1;
487
+ if (this.consecutiveFoldOps > this.maxFoldOperations) {
488
+ logger.warn(`ChatDriver: fold operation limit (${this.maxFoldOperations}) reached — injecting guidance`);
489
+ executedById.set(tc.id, {
490
+ toolCallId: tc.id,
491
+ content: `You have opened and closed folds ${this.consecutiveFoldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`,
492
+ });
493
+ return;
494
+ }
495
+ const content = yield this.openFold(tc.name, fold, tc.args);
496
+ executedById.set(tc.id, { toolCallId: tc.id, content });
497
+ // Fold open/close does NOT count as a real iteration — decrement to compensate
498
+ iterations -= 1;
499
+ return;
500
+ }
501
+ // Check for close-fold tool
502
+ if (tc.name.startsWith('close_') && this.foldStack.length > 0) {
503
+ const topFoldName = this.foldStack[this.foldStack.length - 1].foldName;
504
+ if (tc.name === `close_${topFoldName}`) {
505
+ this.consecutiveFoldOps += 1;
506
+ if (this.consecutiveFoldOps > this.maxFoldOperations) {
507
+ executedById.set(tc.id, {
508
+ toolCallId: tc.id,
509
+ content: `You have opened and closed folds ${this.consecutiveFoldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`,
510
+ });
511
+ return;
512
+ }
513
+ const content = this.closeFold();
514
+ executedById.set(tc.id, { toolCallId: tc.id, content });
515
+ iterations -= 1;
516
+ return;
517
+ }
518
+ }
519
+ // Regular tool — check if it's inside a fold and guide the model
520
+ const handler = this.toolHandlers[tc.name];
521
+ if (!handler) {
522
+ const containingFold = this.findFoldContaining(tc.name);
523
+ if (containingFold) {
524
+ logger.debug(`ChatDriver: model called folded tool "${tc.name}" — guiding to open "${containingFold}"`);
525
+ executedById.set(tc.id, {
526
+ toolCallId: tc.id,
527
+ content: `"${tc.name}" is not directly available. It is inside the "${containingFold}" fold. Call ${containingFold} first to access it.`,
528
+ });
529
+ // Guidance does not count as a real iteration or fold op
530
+ iterations -= 1;
531
+ }
532
+ else {
533
+ this.consecutiveUnknownToolCalls += 1;
534
+ logger.warn(`ChatDriver: no handler registered for tool "${tc.name}" (${this.consecutiveUnknownToolCalls}/${DEFAULT_MAX_UNKNOWN_TOOL_CALLS}). Available tools: ${Object.keys(this.toolHandlers).join(', ') || '(none)'}`);
535
+ executedById.set(tc.id, { toolCallId: tc.id, content: `Unknown tool: ${tc.name}` });
536
+ unknownToolIds.add(tc.id);
537
+ if (this.consecutiveUnknownToolCalls >= DEFAULT_MAX_UNKNOWN_TOOL_CALLS) {
538
+ hitUnknownToolLimit = true;
539
+ }
540
+ }
541
+ return;
542
+ }
543
+ // Real tool execution
544
+ try {
545
+ const result = yield handler(tc.args, {
546
+ requestInteraction: (componentName, data) => this.requestInteraction(componentName, data),
547
+ });
548
+ const content = typeof result === 'string' ? result : JSON.stringify(result);
549
+ executedById.set(tc.id, { toolCallId: tc.id, content });
550
+ anyRealToolExecuted = true;
551
+ }
552
+ catch (e) {
553
+ logger.error(`ChatDriver tool "${tc.name}" failed:`, e);
554
+ executedById.set(tc.id, {
555
+ toolCallId: tc.id,
556
+ content: `Tool error: ${e.message}`,
557
+ });
558
+ anyRealToolExecuted = true; // treat errors as real work for fold op counting
559
+ }
560
+ })));
561
+ }
562
+ // Reset counters whenever a real tool executes
563
+ if (anyRealToolExecuted) {
564
+ this.consecutiveFoldOps = 0;
565
+ this.consecutiveUnknownToolCalls = 0;
566
+ }
567
+ // Tag tool calls with fold UI metadata before appending results
568
+ const foldPath = this.foldStack.map((f) => f.foldName);
569
+ for (const tc of response.toolCalls) {
570
+ if (tc.name === REQUEST_CONTINUATION_TOOL) {
571
+ this.appendToHistory({
572
+ role: 'tool',
573
+ content: '',
574
+ toolResult: { toolCallId: tc.id, content: HANDOFF_TOOL_RESULT_PLACEHOLDER },
189
575
  });
190
- const content = typeof result === 'string' ? result : JSON.stringify(result);
191
- return { toolCallId: tc.id, content };
192
576
  }
193
- catch (e) {
194
- logger.error(`ChatDriver tool "${tc.name}" failed:`, e);
195
- return { toolCallId: tc.id, content: `Tool error: ${e.message}` };
577
+ else {
578
+ const r = executedById.get(tc.id);
579
+ if (r) {
580
+ this.appendToHistory({ role: 'tool', content: '', toolResult: r });
581
+ }
582
+ }
583
+ }
584
+ // Back-patch foldEvent and foldPath onto the tool call message we just appended.
585
+ // The response was appended before execution — find it and annotate.
586
+ let tcMsgIdx = -1;
587
+ for (let i = this.history.length - 1; i >= 0; i -= 1) {
588
+ if (this.history[i].role === 'assistant' && ((_f = this.history[i].toolCalls) === null || _f === void 0 ? void 0 : _f.length)) {
589
+ tcMsgIdx = i;
590
+ break;
196
591
  }
197
- })));
198
- for (const result of toolResults) {
592
+ }
593
+ if (tcMsgIdx !== -1) {
594
+ const tcMsg = this.history[tcMsgIdx];
595
+ const availableToolNames = Object.keys(this.toolHandlers);
596
+ const annotatedCalls = tcMsg.toolCalls.map((tc) => {
597
+ var _a, _b, _c, _d;
598
+ const isFoldOpen = !!this.getFold(tc.name) ||
599
+ (
600
+ // Was a fold facade at time of the call (now the tool set has changed)
601
+ // — detect by checking if the result message indicated a fold open
602
+ (_b = (_a = executedById.get(tc.id)) === null || _a === void 0 ? void 0 : _a.content) === null || _b === void 0 ? void 0 : _b.startsWith('Fold opened:'));
603
+ const isFoldClose = (_d = (_c = executedById.get(tc.id)) === null || _c === void 0 ? void 0 : _c.content) === null || _d === void 0 ? void 0 : _d.startsWith('Fold closed:');
604
+ const isUnknown = unknownToolIds.has(tc.id);
605
+ return Object.assign(Object.assign({}, tc), { foldEvent: isFoldOpen
606
+ ? 'open'
607
+ : isFoldClose
608
+ ? 'close'
609
+ : undefined,
610
+ // Use the fold path that was active at the START of this iteration (before any opens/closes)
611
+ foldPath: !isFoldOpen && !isFoldClose && foldPath.length > 0 ? foldPath : undefined, unknown: isUnknown || undefined, availableTools: isUnknown ? availableToolNames : undefined });
612
+ });
613
+ this.history[tcMsgIdx] = Object.assign(Object.assign({}, tcMsg), { toolCalls: annotatedCalls });
614
+ this.dispatchEvent(new CustomEvent('history-updated', {
615
+ detail: this.history,
616
+ }));
617
+ }
618
+ if (hitUnknownToolLimit) {
619
+ logger.error(`ChatDriver: unknown-tool limit (${DEFAULT_MAX_UNKNOWN_TOOL_CALLS}) reached — stopping`);
199
620
  this.appendToHistory({
200
- role: 'tool',
201
- content: '',
202
- toolResult: result,
621
+ role: 'assistant',
622
+ content: "I'm sorry, I repeatedly tried to use tools that don't exist. Please check your agent configuration or try rephrasing your request.",
203
623
  });
624
+ return { reason: 'done' };
625
+ }
626
+ const firstContinuation = systemCalls[0];
627
+ if (firstContinuation) {
628
+ const { summary, remaining_task: remainingTask } = firstContinuation.args;
629
+ return { reason: 'agent-handoff', summary, remainingTask };
204
630
  }
205
- // Next iteration sends an empty string — the tool results are in history
206
631
  currentInput = '';
207
632
  }
208
633
  if (iterations >= this.maxToolIterations) {
209
634
  logger.warn('ChatDriver: reached max tool iterations, stopping');
635
+ this.appendToHistory({
636
+ role: 'assistant',
637
+ content: "I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off.",
638
+ });
210
639
  }
640
+ return { reason: 'done' };
211
641
  });
212
642
  }
213
643
  appendToHistory(message) {
214
- this.history = [...this.history, message];
644
+ const tagged = this.activeAgentName
645
+ ? Object.assign(Object.assign({}, message), { agentName: this.activeAgentName }) : message;
646
+ this.history = [...this.history, tagged];
215
647
  this.dispatchEvent(new CustomEvent('history-updated', {
216
648
  detail: this.history,
217
649
  }));