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