@jupyterlite/ai 0.9.0 → 0.10.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/README.md +5 -214
- package/lib/agent.d.ts +58 -66
- package/lib/agent.js +274 -300
- package/lib/approval-buttons.d.ts +19 -82
- package/lib/approval-buttons.js +36 -289
- package/lib/chat-model-registry.d.ts +6 -0
- package/lib/chat-model-registry.js +4 -1
- package/lib/chat-model.d.ts +19 -54
- package/lib/chat-model.js +243 -303
- package/lib/components/clear-button.d.ts +6 -1
- package/lib/components/clear-button.js +8 -3
- package/lib/components/completion-status.d.ts +5 -0
- package/lib/components/completion-status.js +5 -4
- package/lib/components/model-select.d.ts +6 -1
- package/lib/components/model-select.js +9 -8
- package/lib/components/stop-button.d.ts +6 -1
- package/lib/components/stop-button.js +8 -3
- package/lib/components/token-usage-display.d.ts +5 -0
- package/lib/components/token-usage-display.js +2 -2
- package/lib/components/tool-select.d.ts +6 -1
- package/lib/components/tool-select.js +6 -5
- package/lib/index.js +62 -38
- package/lib/models/settings-model.d.ts +1 -1
- package/lib/providers/built-in-providers.js +38 -19
- package/lib/providers/models.d.ts +3 -3
- package/lib/providers/provider-registry.d.ts +3 -4
- package/lib/providers/provider-registry.js +1 -4
- package/lib/tokens.d.ts +5 -6
- package/lib/tools/commands.d.ts +2 -1
- package/lib/tools/commands.js +37 -46
- package/lib/tools/file.js +49 -73
- package/lib/tools/notebook.js +370 -445
- package/lib/widgets/ai-settings.d.ts +6 -0
- package/lib/widgets/ai-settings.js +72 -71
- package/lib/widgets/main-area-chat.d.ts +2 -0
- package/lib/widgets/main-area-chat.js +5 -2
- package/lib/widgets/provider-config-dialog.d.ts +2 -0
- package/lib/widgets/provider-config-dialog.js +34 -34
- package/package.json +12 -12
- package/src/agent.ts +342 -361
- package/src/approval-buttons.ts +43 -389
- package/src/chat-model-registry.ts +9 -1
- package/src/chat-model.ts +355 -370
- package/src/completion/completion-provider.ts +2 -3
- package/src/components/clear-button.tsx +16 -3
- package/src/components/completion-status.tsx +18 -4
- package/src/components/model-select.tsx +21 -8
- package/src/components/stop-button.tsx +16 -3
- package/src/components/token-usage-display.tsx +14 -2
- package/src/components/tool-select.tsx +23 -5
- package/src/index.ts +80 -36
- package/src/models/settings-model.ts +1 -1
- package/src/providers/built-in-providers.ts +38 -19
- package/src/providers/models.ts +3 -3
- package/src/providers/provider-registry.ts +4 -8
- package/src/tokens.ts +5 -6
- package/src/tools/commands.ts +39 -50
- package/src/tools/file.ts +49 -75
- package/src/tools/notebook.ts +451 -510
- package/src/widgets/ai-settings.tsx +153 -84
- package/src/widgets/main-area-chat.ts +8 -2
- package/src/widgets/provider-config-dialog.tsx +54 -41
- package/style/base.css +13 -73
- package/lib/mcp/browser.d.ts +0 -68
- package/lib/mcp/browser.js +0 -138
- package/src/mcp/browser.ts +0 -220
package/src/agent.ts
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from '
|
|
3
|
+
ToolLoopAgent,
|
|
4
|
+
type ModelMessage,
|
|
5
|
+
stepCountIs,
|
|
6
|
+
type StreamTextResult,
|
|
7
|
+
type Tool,
|
|
8
|
+
type ToolApprovalRequestOutput,
|
|
9
|
+
type TypedToolResult
|
|
10
|
+
} from 'ai';
|
|
11
|
+
import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
|
|
11
12
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
12
13
|
|
|
13
|
-
import { BrowserMCPServerStreamableHttp } from './mcp/browser';
|
|
14
14
|
import { AISettingsModel } from './models/settings-model';
|
|
15
15
|
import { createModel } from './providers/models';
|
|
16
16
|
import type { IProviderRegistry } from './tokens';
|
|
17
17
|
import { ITool, IToolRegistry, ITokenUsage, SECRETS_NAMESPACE } from './tokens';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Interface for MCP client wrapper to track connection state
|
|
21
|
+
*/
|
|
22
|
+
interface IMCPClientWrapper {
|
|
23
|
+
name: string;
|
|
24
|
+
client: MCPClient;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ToolMap = Record<string, Tool>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result from processing a stream, including approval info if applicable.
|
|
31
|
+
*/
|
|
32
|
+
interface IStreamProcessResult {
|
|
33
|
+
/**
|
|
34
|
+
* Whether an approval request was encountered and processed.
|
|
35
|
+
*/
|
|
36
|
+
approvalProcessed: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The approval response message to add to history (if approval was processed).
|
|
39
|
+
*/
|
|
40
|
+
approvalResponse?: ModelMessage;
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
export namespace AgentManagerFactory {
|
|
20
44
|
export interface IOptions {
|
|
21
45
|
/**
|
|
@@ -37,7 +61,7 @@ export class AgentManagerFactory {
|
|
|
37
61
|
Private.setToken(options.token);
|
|
38
62
|
this._settingsModel = options.settingsModel;
|
|
39
63
|
this._secretsManager = options.secretsManager;
|
|
40
|
-
this.
|
|
64
|
+
this._mcpClients = [];
|
|
41
65
|
this._mcpConnectionChanged = new Signal<this, boolean>(this);
|
|
42
66
|
|
|
43
67
|
// Initialize agent on construction
|
|
@@ -71,7 +95,28 @@ export class AgentManagerFactory {
|
|
|
71
95
|
* @returns True if the server is connected, false otherwise
|
|
72
96
|
*/
|
|
73
97
|
isMCPServerConnected(serverName: string): boolean {
|
|
74
|
-
return this.
|
|
98
|
+
return this._mcpClients.some(wrapper => wrapper.name === serverName);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Gets the MCP tools from connected servers
|
|
103
|
+
*/
|
|
104
|
+
async getMCPTools(): Promise<ToolMap> {
|
|
105
|
+
const mcpTools: ToolMap = {};
|
|
106
|
+
|
|
107
|
+
for (const wrapper of this._mcpClients) {
|
|
108
|
+
try {
|
|
109
|
+
const tools = await wrapper.client.tools();
|
|
110
|
+
Object.assign(mcpTools, tools);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(
|
|
113
|
+
`Failed to get tools from MCP server ${wrapper.name}:`,
|
|
114
|
+
error
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return mcpTools;
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
/**
|
|
@@ -84,34 +129,38 @@ export class AgentManagerFactory {
|
|
|
84
129
|
}
|
|
85
130
|
|
|
86
131
|
/**
|
|
87
|
-
* Initializes MCP (Model Context Protocol)
|
|
88
|
-
* Closes existing
|
|
132
|
+
* Initializes MCP (Model Context Protocol) clients based on current settings.
|
|
133
|
+
* Closes existing clients and connects to enabled servers from configuration.
|
|
89
134
|
*/
|
|
90
|
-
private async
|
|
135
|
+
private async _initializeMCPClients(): Promise<void> {
|
|
91
136
|
const config = this._settingsModel.config;
|
|
92
137
|
const enabledServers = config.mcpServers.filter(server => server.enabled);
|
|
93
138
|
let connectionChanged = false;
|
|
94
139
|
|
|
95
|
-
// Close existing
|
|
96
|
-
for (const
|
|
140
|
+
// Close existing clients
|
|
141
|
+
for (const wrapper of this._mcpClients) {
|
|
97
142
|
try {
|
|
98
|
-
await
|
|
143
|
+
await wrapper.client.close();
|
|
99
144
|
connectionChanged = true;
|
|
100
145
|
} catch (error) {
|
|
101
|
-
console.warn('Error closing MCP
|
|
146
|
+
console.warn('Error closing MCP client:', error);
|
|
102
147
|
}
|
|
103
148
|
}
|
|
104
|
-
this.
|
|
149
|
+
this._mcpClients = [];
|
|
105
150
|
|
|
106
|
-
// Initialize new servers
|
|
107
151
|
for (const serverConfig of enabledServers) {
|
|
108
152
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
153
|
+
const client = await createMCPClient({
|
|
154
|
+
transport: {
|
|
155
|
+
type: 'http',
|
|
156
|
+
url: serverConfig.url
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this._mcpClients.push({
|
|
161
|
+
name: serverConfig.name,
|
|
162
|
+
client
|
|
112
163
|
});
|
|
113
|
-
await mcpServer.connect();
|
|
114
|
-
this._mcpServers.push(mcpServer);
|
|
115
164
|
connectionChanged = true;
|
|
116
165
|
} catch (error) {
|
|
117
166
|
console.warn(
|
|
@@ -123,7 +172,7 @@ export class AgentManagerFactory {
|
|
|
123
172
|
|
|
124
173
|
// Emit connection change signal if there were any changes
|
|
125
174
|
if (connectionChanged) {
|
|
126
|
-
this._mcpConnectionChanged.emit(this.
|
|
175
|
+
this._mcpConnectionChanged.emit(this._mcpClients.length > 0);
|
|
127
176
|
}
|
|
128
177
|
}
|
|
129
178
|
|
|
@@ -138,11 +187,11 @@ export class AgentManagerFactory {
|
|
|
138
187
|
this._isInitializing = true;
|
|
139
188
|
|
|
140
189
|
try {
|
|
141
|
-
await this.
|
|
142
|
-
const
|
|
190
|
+
await this._initializeMCPClients();
|
|
191
|
+
const mcpTools = await this.getMCPTools();
|
|
143
192
|
|
|
144
193
|
this._agentManagers.forEach(manager => {
|
|
145
|
-
manager.initializeAgent(
|
|
194
|
+
manager.initializeAgent(mcpTools);
|
|
146
195
|
});
|
|
147
196
|
} catch (error) {
|
|
148
197
|
console.warn('Failed to initialize agents:', error);
|
|
@@ -154,7 +203,7 @@ export class AgentManagerFactory {
|
|
|
154
203
|
private _agentManagers: AgentManager[] = [];
|
|
155
204
|
private _settingsModel: AISettingsModel;
|
|
156
205
|
private _secretsManager?: ISecretsManager;
|
|
157
|
-
private
|
|
206
|
+
private _mcpClients: IMCPClientWrapper[];
|
|
158
207
|
private _mcpConnectionChanged: Signal<this, boolean>;
|
|
159
208
|
private _isInitializing: boolean = false;
|
|
160
209
|
}
|
|
@@ -192,19 +241,15 @@ export interface IAgentEventTypeMap {
|
|
|
192
241
|
output: string;
|
|
193
242
|
isError: boolean;
|
|
194
243
|
};
|
|
195
|
-
|
|
196
|
-
|
|
244
|
+
tool_approval_request: {
|
|
245
|
+
approvalId: string;
|
|
246
|
+
toolCallId: string;
|
|
197
247
|
toolName: string;
|
|
198
|
-
|
|
199
|
-
callId?: string;
|
|
248
|
+
args: unknown;
|
|
200
249
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
interruptionId: string;
|
|
205
|
-
toolName: string;
|
|
206
|
-
toolInput: string;
|
|
207
|
-
}>;
|
|
250
|
+
tool_approval_resolved: {
|
|
251
|
+
approvalId: string;
|
|
252
|
+
approved: boolean;
|
|
208
253
|
};
|
|
209
254
|
error: {
|
|
210
255
|
error: Error;
|
|
@@ -261,7 +306,7 @@ export interface IAgentManagerOptions {
|
|
|
261
306
|
/**
|
|
262
307
|
* Manages the AI agent lifecycle and execution loop.
|
|
263
308
|
* Provides agent initialization, tool management, MCP server integration,
|
|
264
|
-
* and handles the complete agent execution cycle
|
|
309
|
+
* and handles the complete agent execution cycle.
|
|
265
310
|
* Emits events for UI updates instead of directly manipulating the chat interface.
|
|
266
311
|
*/
|
|
267
312
|
export class AgentManager {
|
|
@@ -276,13 +321,10 @@ export class AgentManager {
|
|
|
276
321
|
this._secretsManager = options.secretsManager;
|
|
277
322
|
this._selectedToolNames = [];
|
|
278
323
|
this._agent = null;
|
|
279
|
-
this._runner = new Runner({ tracingDisabled: true });
|
|
280
324
|
this._history = [];
|
|
281
|
-
this.
|
|
325
|
+
this._mcpTools = {};
|
|
282
326
|
this._isInitializing = false;
|
|
283
327
|
this._controller = null;
|
|
284
|
-
this._pendingApprovals = new Map();
|
|
285
|
-
this._interruptedState = null;
|
|
286
328
|
this._agentEvent = new Signal<this, IAgentEvent>(this);
|
|
287
329
|
this._tokenUsage = options.tokenUsage ?? {
|
|
288
330
|
inputTokens: 0,
|
|
@@ -351,19 +393,19 @@ export class AgentManager {
|
|
|
351
393
|
}
|
|
352
394
|
|
|
353
395
|
/**
|
|
354
|
-
* Gets the currently selected tools as
|
|
355
|
-
* @returns
|
|
396
|
+
* Gets the currently selected tools as a record.
|
|
397
|
+
* @returns Record of selected tools
|
|
356
398
|
*/
|
|
357
|
-
get selectedAgentTools(): ITool
|
|
399
|
+
get selectedAgentTools(): Record<string, ITool> {
|
|
358
400
|
if (!this._toolRegistry) {
|
|
359
|
-
return
|
|
401
|
+
return {};
|
|
360
402
|
}
|
|
361
403
|
|
|
362
|
-
const result: ITool
|
|
404
|
+
const result: Record<string, ITool> = {};
|
|
363
405
|
for (const name of this._selectedToolNames) {
|
|
364
406
|
const tool: ITool | null = this._toolRegistry.get(name);
|
|
365
407
|
if (tool) {
|
|
366
|
-
result
|
|
408
|
+
result[name] = tool;
|
|
367
409
|
}
|
|
368
410
|
}
|
|
369
411
|
|
|
@@ -401,14 +443,23 @@ export class AgentManager {
|
|
|
401
443
|
|
|
402
444
|
/**
|
|
403
445
|
* Clears conversation history and resets agent state.
|
|
404
|
-
* Removes all conversation history, pending approvals, and interrupted state.
|
|
405
446
|
*/
|
|
406
447
|
clearHistory(): void {
|
|
407
|
-
|
|
408
|
-
this.
|
|
448
|
+
// Stop any ongoing streaming
|
|
449
|
+
this.stopStreaming();
|
|
450
|
+
|
|
451
|
+
// Reject any pending approvals
|
|
452
|
+
for (const [approvalId, pending] of this._pendingApprovals) {
|
|
453
|
+
pending.resolve(false, 'Chat cleared');
|
|
454
|
+
this._agentEvent.emit({
|
|
455
|
+
type: 'tool_approval_resolved',
|
|
456
|
+
data: { approvalId, approved: false }
|
|
457
|
+
});
|
|
458
|
+
}
|
|
409
459
|
this._pendingApprovals.clear();
|
|
410
|
-
|
|
411
|
-
//
|
|
460
|
+
|
|
461
|
+
// Clear history and token usage
|
|
462
|
+
this._history = [];
|
|
412
463
|
this._tokenUsage = { inputTokens: 0, outputTokens: 0 };
|
|
413
464
|
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
414
465
|
}
|
|
@@ -420,13 +471,46 @@ export class AgentManager {
|
|
|
420
471
|
this._controller?.abort();
|
|
421
472
|
}
|
|
422
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Approves a pending tool call.
|
|
476
|
+
* @param approvalId The approval ID to approve
|
|
477
|
+
* @param reason Optional reason for approval
|
|
478
|
+
*/
|
|
479
|
+
approveToolCall(approvalId: string, reason?: string): void {
|
|
480
|
+
const pending = this._pendingApprovals.get(approvalId);
|
|
481
|
+
if (pending) {
|
|
482
|
+
pending.resolve(true, reason);
|
|
483
|
+
this._pendingApprovals.delete(approvalId);
|
|
484
|
+
this._agentEvent.emit({
|
|
485
|
+
type: 'tool_approval_resolved',
|
|
486
|
+
data: { approvalId, approved: true }
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Rejects a pending tool call.
|
|
493
|
+
* @param approvalId The approval ID to reject
|
|
494
|
+
* @param reason Optional reason for rejection
|
|
495
|
+
*/
|
|
496
|
+
rejectToolCall(approvalId: string, reason?: string): void {
|
|
497
|
+
const pending = this._pendingApprovals.get(approvalId);
|
|
498
|
+
if (pending) {
|
|
499
|
+
pending.resolve(false, reason);
|
|
500
|
+
this._pendingApprovals.delete(approvalId);
|
|
501
|
+
this._agentEvent.emit({
|
|
502
|
+
type: 'tool_approval_resolved',
|
|
503
|
+
data: { approvalId, approved: false }
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
423
508
|
/**
|
|
424
509
|
* Generates AI response to user message using the agent.
|
|
425
|
-
* Handles the complete execution cycle including tool calls
|
|
510
|
+
* Handles the complete execution cycle including tool calls.
|
|
426
511
|
* @param message The user message to respond to (may include processed attachment content)
|
|
427
512
|
*/
|
|
428
513
|
async generateResponse(message: string): Promise<void> {
|
|
429
|
-
const config = this._settingsModel.config;
|
|
430
514
|
this._controller = new AbortController();
|
|
431
515
|
|
|
432
516
|
try {
|
|
@@ -439,166 +523,80 @@ export class AgentManager {
|
|
|
439
523
|
throw new Error('Failed to initialize agent');
|
|
440
524
|
}
|
|
441
525
|
|
|
442
|
-
const shouldUseTools =
|
|
443
|
-
config.toolsEnabled &&
|
|
444
|
-
this._selectedToolNames.length > 0 &&
|
|
445
|
-
this._toolRegistry &&
|
|
446
|
-
Object.keys(this._toolRegistry.tools).length > 0 &&
|
|
447
|
-
this._supportsToolCalling();
|
|
448
|
-
|
|
449
526
|
// Add user message to history
|
|
450
|
-
this._history.push(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const activeProviderConfig = this._settingsModel.getProvider(
|
|
454
|
-
this._activeProvider
|
|
455
|
-
);
|
|
456
|
-
const maxTurns =
|
|
457
|
-
activeProviderConfig?.parameters?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
458
|
-
|
|
459
|
-
// Main agentic loop
|
|
460
|
-
let result = await this._runner.run(this._agent, this._history, {
|
|
461
|
-
stream: true,
|
|
462
|
-
signal: this._controller.signal,
|
|
463
|
-
...(shouldUseTools && { maxTurns })
|
|
527
|
+
this._history.push({
|
|
528
|
+
role: 'user',
|
|
529
|
+
content: message
|
|
464
530
|
});
|
|
465
531
|
|
|
466
|
-
|
|
532
|
+
let continueLoop = true;
|
|
533
|
+
while (continueLoop) {
|
|
534
|
+
const result = await this._agent.stream({
|
|
535
|
+
messages: this._history,
|
|
536
|
+
abortSignal: this._controller.signal
|
|
537
|
+
});
|
|
467
538
|
|
|
468
|
-
|
|
469
|
-
result.interruptions && result.interruptions.length > 0;
|
|
539
|
+
const streamResult = await this._processStreamResult(result);
|
|
470
540
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
541
|
+
// Get response messages and update token usage
|
|
542
|
+
const responseMessages = await result.response;
|
|
543
|
+
this._updateTokenUsage(await result.usage);
|
|
474
544
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await this._handleSingleToolApproval(interruptions[0]);
|
|
545
|
+
// Add response messages to history
|
|
546
|
+
if (responseMessages.messages?.length) {
|
|
547
|
+
this._history.push(...responseMessages.messages);
|
|
479
548
|
}
|
|
480
549
|
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
550
|
+
// Add approval response if processed
|
|
551
|
+
if (streamResult.approvalResponse) {
|
|
552
|
+
// Check if the last message is a tool message we can append to
|
|
553
|
+
const lastMsg = this._history[this._history.length - 1];
|
|
554
|
+
if (
|
|
555
|
+
lastMsg &&
|
|
556
|
+
lastMsg.role === 'tool' &&
|
|
557
|
+
Array.isArray(lastMsg.content) &&
|
|
558
|
+
Array.isArray(streamResult.approvalResponse.content)
|
|
559
|
+
) {
|
|
560
|
+
const toolContent = lastMsg.content as unknown[];
|
|
561
|
+
toolContent.push(...streamResult.approvalResponse.content);
|
|
562
|
+
} else {
|
|
563
|
+
// Add as separate message
|
|
564
|
+
this._history.push(streamResult.approvalResponse);
|
|
565
|
+
}
|
|
484
566
|
}
|
|
485
567
|
|
|
486
|
-
|
|
487
|
-
result = await this._runner.run(this._agent!, result.state, {
|
|
488
|
-
stream: true,
|
|
489
|
-
signal: this._controller.signal,
|
|
490
|
-
maxTurns
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
await this._processRunResult(result);
|
|
494
|
-
hasInterruptions =
|
|
495
|
-
result.interruptions && result.interruptions.length > 0;
|
|
568
|
+
continueLoop = streamResult.approvalProcessed;
|
|
496
569
|
}
|
|
497
|
-
|
|
498
|
-
// Clear interrupted state
|
|
499
|
-
this._interruptedState = null;
|
|
500
|
-
this._history = result.history;
|
|
501
570
|
} catch (error) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
571
|
+
if ((error as Error).name !== 'AbortError') {
|
|
572
|
+
this._agentEvent.emit({
|
|
573
|
+
type: 'error',
|
|
574
|
+
data: { error: error as Error }
|
|
575
|
+
});
|
|
576
|
+
}
|
|
506
577
|
} finally {
|
|
507
578
|
this._controller = null;
|
|
508
579
|
}
|
|
509
580
|
}
|
|
510
581
|
|
|
511
582
|
/**
|
|
512
|
-
*
|
|
513
|
-
* @param interruptionId The interruption ID to approve
|
|
583
|
+
* Updates token usage statistics.
|
|
514
584
|
*/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (this._interruptedState) {
|
|
525
|
-
this._interruptedState.state.approve(pending.interruption);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
pending.approved = true;
|
|
529
|
-
this._pendingApprovals.delete(interruptionId);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Rejects a tool call by interruption ID.
|
|
534
|
-
* @param interruptionId The interruption ID to reject
|
|
535
|
-
*/
|
|
536
|
-
async rejectToolCall(interruptionId: string): Promise<void> {
|
|
537
|
-
const pending = this._pendingApprovals.get(interruptionId);
|
|
538
|
-
if (!pending) {
|
|
539
|
-
console.warn(
|
|
540
|
-
`No pending approval found for interruption ${interruptionId}`
|
|
541
|
-
);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (this._interruptedState) {
|
|
546
|
-
this._interruptedState.state.reject(pending.interruption);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
pending.approved = false;
|
|
550
|
-
this._pendingApprovals.delete(interruptionId);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Approves all tools in a group by group ID.
|
|
555
|
-
* @param groupId The group ID containing the tool calls
|
|
556
|
-
* @param interruptionIds Array of interruption IDs to approve
|
|
557
|
-
*/
|
|
558
|
-
async approveGroupedToolCalls(
|
|
559
|
-
groupId: string,
|
|
560
|
-
interruptionIds: string[]
|
|
561
|
-
): Promise<void> {
|
|
562
|
-
for (const interruptionId of interruptionIds) {
|
|
563
|
-
const pending = this._pendingApprovals.get(interruptionId);
|
|
564
|
-
if (pending && pending.groupId === groupId) {
|
|
565
|
-
if (this._interruptedState) {
|
|
566
|
-
this._interruptedState.state.approve(pending.interruption);
|
|
567
|
-
}
|
|
568
|
-
pending.approved = true;
|
|
569
|
-
this._pendingApprovals.delete(interruptionId);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Rejects all tools in a group by group ID.
|
|
576
|
-
* @param groupId The group ID containing the tool calls
|
|
577
|
-
* @param interruptionIds Array of interruption IDs to reject
|
|
578
|
-
*/
|
|
579
|
-
async rejectGroupedToolCalls(
|
|
580
|
-
groupId: string,
|
|
581
|
-
interruptionIds: string[]
|
|
582
|
-
): Promise<void> {
|
|
583
|
-
for (const interruptionId of interruptionIds) {
|
|
584
|
-
const pending = this._pendingApprovals.get(interruptionId);
|
|
585
|
-
if (pending && pending.groupId === groupId) {
|
|
586
|
-
if (this._interruptedState) {
|
|
587
|
-
this._interruptedState.state.reject(pending.interruption);
|
|
588
|
-
}
|
|
589
|
-
pending.approved = false;
|
|
590
|
-
this._pendingApprovals.delete(interruptionId);
|
|
591
|
-
}
|
|
585
|
+
private _updateTokenUsage(
|
|
586
|
+
usage: { inputTokens?: number; outputTokens?: number } | undefined
|
|
587
|
+
): void {
|
|
588
|
+
if (usage) {
|
|
589
|
+
this._tokenUsage.inputTokens += usage.inputTokens ?? 0;
|
|
590
|
+
this._tokenUsage.outputTokens += usage.outputTokens ?? 0;
|
|
591
|
+
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
592
592
|
}
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
/**
|
|
596
596
|
* Initializes the AI agent with current settings and tools.
|
|
597
|
-
* Sets up the agent with model configuration, tools, and MCP
|
|
597
|
+
* Sets up the agent with model configuration, tools, and MCP tools.
|
|
598
598
|
*/
|
|
599
|
-
initializeAgent = async (
|
|
600
|
-
mcpServers?: BrowserMCPServerStreamableHttp[]
|
|
601
|
-
): Promise<void> => {
|
|
599
|
+
initializeAgent = async (mcpTools?: ToolMap): Promise<void> => {
|
|
602
600
|
if (this._isInitializing) {
|
|
603
601
|
return;
|
|
604
602
|
}
|
|
@@ -606,8 +604,8 @@ export class AgentManager {
|
|
|
606
604
|
|
|
607
605
|
try {
|
|
608
606
|
const config = this._settingsModel.config;
|
|
609
|
-
if (
|
|
610
|
-
this.
|
|
607
|
+
if (mcpTools !== undefined) {
|
|
608
|
+
this._mcpTools = mcpTools;
|
|
611
609
|
}
|
|
612
610
|
|
|
613
611
|
const model = await this._createModel();
|
|
@@ -619,7 +617,9 @@ export class AgentManager {
|
|
|
619
617
|
Object.keys(this._toolRegistry.tools).length > 0 &&
|
|
620
618
|
this._supportsToolCalling();
|
|
621
619
|
|
|
622
|
-
const tools = shouldUseTools
|
|
620
|
+
const tools = shouldUseTools
|
|
621
|
+
? { ...this.selectedAgentTools, ...this._mcpTools }
|
|
622
|
+
: this._mcpTools;
|
|
623
623
|
|
|
624
624
|
const activeProviderConfig = this._settingsModel.getProvider(
|
|
625
625
|
this._activeProvider
|
|
@@ -627,22 +627,21 @@ export class AgentManager {
|
|
|
627
627
|
|
|
628
628
|
const temperature =
|
|
629
629
|
activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
|
|
630
|
-
const maxTokens = activeProviderConfig?.parameters?.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
630
|
+
const maxTokens = activeProviderConfig?.parameters?.maxOutputTokens;
|
|
631
|
+
const maxTurns =
|
|
632
|
+
activeProviderConfig?.parameters?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
633
|
+
|
|
634
|
+
const instructions = shouldUseTools
|
|
635
|
+
? this._getEnhancedSystemPrompt(config.systemPrompt || '')
|
|
636
|
+
: config.systemPrompt || 'You are a helpful assistant.';
|
|
637
|
+
|
|
638
|
+
this._agent = new ToolLoopAgent({
|
|
639
|
+
model,
|
|
640
|
+
instructions,
|
|
639
641
|
tools,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
maxTokens
|
|
644
|
-
}
|
|
645
|
-
})
|
|
642
|
+
temperature,
|
|
643
|
+
maxOutputTokens: maxTokens,
|
|
644
|
+
stopWhen: stepCountIs(maxTurns)
|
|
646
645
|
});
|
|
647
646
|
} catch (error) {
|
|
648
647
|
console.warn('Failed to initialize agent:', error);
|
|
@@ -653,196 +652,180 @@ export class AgentManager {
|
|
|
653
652
|
};
|
|
654
653
|
|
|
655
654
|
/**
|
|
656
|
-
* Processes the result
|
|
655
|
+
* Processes the stream result from agent execution.
|
|
657
656
|
* Handles message streaming, tool calls, and emits appropriate events.
|
|
658
|
-
* @param result The
|
|
657
|
+
* @param result The stream result from agent execution
|
|
658
|
+
* @returns Processing result including approval info if applicable
|
|
659
659
|
*/
|
|
660
|
-
private async
|
|
661
|
-
result:
|
|
662
|
-
): Promise<
|
|
660
|
+
private async _processStreamResult(
|
|
661
|
+
result: StreamTextResult<ToolMap, never>
|
|
662
|
+
): Promise<IStreamProcessResult> {
|
|
663
663
|
let fullResponse = '';
|
|
664
664
|
let currentMessageId: string | null = null;
|
|
665
|
+
const processResult: IStreamProcessResult = { approvalProcessed: false };
|
|
665
666
|
|
|
666
|
-
for await (const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
currentMessageId = `msg-${Date.now()}-${Math.random()}`;
|
|
672
|
-
fullResponse = '';
|
|
673
|
-
this._agentEvent.emit({
|
|
674
|
-
type: 'message_start',
|
|
675
|
-
data: { messageId: currentMessageId }
|
|
676
|
-
});
|
|
677
|
-
} else if (data.type === 'output_text_delta') {
|
|
678
|
-
if (currentMessageId) {
|
|
679
|
-
const chunk = data.delta || '';
|
|
680
|
-
fullResponse += chunk;
|
|
667
|
+
for await (const part of result.fullStream) {
|
|
668
|
+
switch (part.type) {
|
|
669
|
+
case 'text-delta':
|
|
670
|
+
if (!currentMessageId) {
|
|
671
|
+
currentMessageId = `msg-${Date.now()}-${Math.random()}`;
|
|
681
672
|
this._agentEvent.emit({
|
|
682
|
-
type: '
|
|
683
|
-
data: {
|
|
684
|
-
messageId: currentMessageId,
|
|
685
|
-
chunk,
|
|
686
|
-
fullContent: fullResponse
|
|
687
|
-
}
|
|
673
|
+
type: 'message_start',
|
|
674
|
+
data: { messageId: currentMessageId }
|
|
688
675
|
});
|
|
689
676
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
677
|
+
fullResponse += part.text;
|
|
678
|
+
this._agentEvent.emit({
|
|
679
|
+
type: 'message_chunk',
|
|
680
|
+
data: {
|
|
681
|
+
messageId: currentMessageId,
|
|
682
|
+
chunk: part.text,
|
|
683
|
+
fullContent: fullResponse
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
break;
|
|
687
|
+
|
|
688
|
+
case 'tool-call':
|
|
689
|
+
// Complete current message before tool call
|
|
690
|
+
if (currentMessageId && fullResponse) {
|
|
691
|
+
this._emitMessageComplete(currentMessageId, fullResponse);
|
|
699
692
|
currentMessageId = null;
|
|
693
|
+
fullResponse = '';
|
|
700
694
|
}
|
|
695
|
+
this._agentEvent.emit({
|
|
696
|
+
type: 'tool_call_start',
|
|
697
|
+
data: {
|
|
698
|
+
callId: part.toolCallId,
|
|
699
|
+
toolName: part.toolName,
|
|
700
|
+
input: this._formatToolInput(JSON.stringify(part.input))
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
break;
|
|
704
|
+
|
|
705
|
+
case 'tool-result':
|
|
706
|
+
this._handleToolResult(part);
|
|
707
|
+
break;
|
|
701
708
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const modelEvent = data.event as any;
|
|
709
|
-
if (modelEvent.type === 'tool-call') {
|
|
710
|
-
this._handleToolCallStart(modelEvent);
|
|
709
|
+
case 'tool-approval-request':
|
|
710
|
+
// Complete current message before approval
|
|
711
|
+
if (currentMessageId && fullResponse) {
|
|
712
|
+
this._emitMessageComplete(currentMessageId, fullResponse);
|
|
713
|
+
currentMessageId = null;
|
|
714
|
+
fullResponse = '';
|
|
711
715
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
716
|
+
await this._handleApprovalRequest(part, processResult);
|
|
717
|
+
break;
|
|
718
|
+
|
|
719
|
+
// Ignore: text-start, text-end, finish, error, and others
|
|
720
|
+
default:
|
|
721
|
+
break;
|
|
717
722
|
}
|
|
718
723
|
}
|
|
719
|
-
}
|
|
720
724
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
* @returns Pretty-printed JSON string
|
|
725
|
-
*/
|
|
726
|
-
private _formatToolInput(input: string): string {
|
|
727
|
-
try {
|
|
728
|
-
// Parse and re-stringify with formatting
|
|
729
|
-
const parsed = JSON.parse(input);
|
|
730
|
-
return JSON.stringify(parsed, null, 2);
|
|
731
|
-
} catch {
|
|
732
|
-
// If parsing fails, return the string as-is
|
|
733
|
-
return input;
|
|
725
|
+
// Complete final message if content remains
|
|
726
|
+
if (currentMessageId && fullResponse) {
|
|
727
|
+
this._emitMessageComplete(currentMessageId, fullResponse);
|
|
734
728
|
}
|
|
729
|
+
|
|
730
|
+
return processResult;
|
|
735
731
|
}
|
|
736
732
|
|
|
737
733
|
/**
|
|
738
|
-
*
|
|
739
|
-
* @param modelEvent The model event containing tool call information
|
|
734
|
+
* Emits a message_complete event.
|
|
740
735
|
*/
|
|
741
|
-
private
|
|
742
|
-
const toolCallId = modelEvent.toolCallId;
|
|
743
|
-
const toolName = modelEvent.toolName;
|
|
744
|
-
const toolInput = modelEvent.input;
|
|
745
|
-
|
|
736
|
+
private _emitMessageComplete(messageId: string, content: string): void {
|
|
746
737
|
this._agentEvent.emit({
|
|
747
|
-
type: '
|
|
748
|
-
data: {
|
|
749
|
-
callId: toolCallId,
|
|
750
|
-
toolName,
|
|
751
|
-
input: this._formatToolInput(toolInput)
|
|
752
|
-
}
|
|
738
|
+
type: 'message_complete',
|
|
739
|
+
data: { messageId, content }
|
|
753
740
|
});
|
|
754
741
|
}
|
|
755
742
|
|
|
756
743
|
/**
|
|
757
|
-
* Handles tool
|
|
758
|
-
* @param event The tool output event containing result information
|
|
744
|
+
* Handles tool-result stream parts.
|
|
759
745
|
*/
|
|
760
|
-
private
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
typeof toolCallOutput.output === 'string'
|
|
766
|
-
? toolCallOutput.output
|
|
767
|
-
: JSON.stringify(toolCallOutput.output, null, 2);
|
|
768
|
-
|
|
746
|
+
private _handleToolResult(part: TypedToolResult<ToolMap>): void {
|
|
747
|
+
const output =
|
|
748
|
+
typeof part.output === 'string'
|
|
749
|
+
? part.output
|
|
750
|
+
: JSON.stringify(part.output, null, 2);
|
|
769
751
|
const isError =
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
toolCallOutput.rawItem.type === 'function_call_result'
|
|
775
|
-
? toolCallOutput.rawItem.name
|
|
776
|
-
: 'Unknown Tool';
|
|
752
|
+
typeof part.output === 'object' &&
|
|
753
|
+
part.output !== null &&
|
|
754
|
+
'success' in part.output &&
|
|
755
|
+
part.output.success === false;
|
|
777
756
|
|
|
778
757
|
this._agentEvent.emit({
|
|
779
758
|
type: 'tool_call_complete',
|
|
780
759
|
data: {
|
|
781
|
-
callId,
|
|
782
|
-
toolName,
|
|
783
|
-
output
|
|
760
|
+
callId: part.toolCallId,
|
|
761
|
+
toolName: part.toolName,
|
|
762
|
+
output,
|
|
784
763
|
isError
|
|
785
764
|
}
|
|
786
765
|
});
|
|
787
766
|
}
|
|
788
767
|
|
|
789
768
|
/**
|
|
790
|
-
* Handles approval
|
|
791
|
-
* @param interruption The tool approval interruption item
|
|
769
|
+
* Handles tool-approval-request stream parts.
|
|
792
770
|
*/
|
|
793
|
-
private async
|
|
794
|
-
|
|
771
|
+
private async _handleApprovalRequest(
|
|
772
|
+
part: ToolApprovalRequestOutput<ToolMap>,
|
|
773
|
+
result: IStreamProcessResult
|
|
795
774
|
): Promise<void> {
|
|
796
|
-
const
|
|
797
|
-
const toolInput = interruption.rawItem.arguments || '{}';
|
|
798
|
-
const interruptionId = `int-${Date.now()}-${Math.random()}`;
|
|
799
|
-
const callId =
|
|
800
|
-
interruption.rawItem.type === 'function_call'
|
|
801
|
-
? interruption.rawItem.callId
|
|
802
|
-
: undefined;
|
|
803
|
-
|
|
804
|
-
this._pendingApprovals.set(interruptionId, { interruption });
|
|
775
|
+
const { approvalId, toolCall } = part;
|
|
805
776
|
|
|
806
777
|
this._agentEvent.emit({
|
|
807
|
-
type: '
|
|
778
|
+
type: 'tool_approval_request',
|
|
808
779
|
data: {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
780
|
+
approvalId,
|
|
781
|
+
toolCallId: toolCall.toolCallId,
|
|
782
|
+
toolName: toolCall.toolName,
|
|
783
|
+
args: toolCall.input
|
|
813
784
|
}
|
|
814
785
|
});
|
|
786
|
+
|
|
787
|
+
const approved = await this._waitForApproval(approvalId);
|
|
788
|
+
|
|
789
|
+
result.approvalProcessed = true;
|
|
790
|
+
result.approvalResponse = {
|
|
791
|
+
role: 'tool',
|
|
792
|
+
content: [
|
|
793
|
+
{
|
|
794
|
+
type: 'tool-approval-response',
|
|
795
|
+
approvalId,
|
|
796
|
+
approved
|
|
797
|
+
}
|
|
798
|
+
]
|
|
799
|
+
};
|
|
815
800
|
}
|
|
816
801
|
|
|
817
802
|
/**
|
|
818
|
-
*
|
|
819
|
-
* @param
|
|
803
|
+
* Waits for user approval of a tool call.
|
|
804
|
+
* @param approvalId The approval ID to wait for
|
|
805
|
+
* @returns Promise that resolves to true if approved, false if rejected
|
|
820
806
|
*/
|
|
821
|
-
private
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const interruptionId = `int-${Date.now()}-${Math.random()}`;
|
|
829
|
-
|
|
830
|
-
this._pendingApprovals.set(interruptionId, { interruption, groupId });
|
|
831
|
-
|
|
832
|
-
return {
|
|
833
|
-
interruptionId,
|
|
834
|
-
toolName,
|
|
835
|
-
toolInput: this._formatToolInput(toolInput)
|
|
836
|
-
};
|
|
807
|
+
private _waitForApproval(approvalId: string): Promise<boolean> {
|
|
808
|
+
return new Promise(resolve => {
|
|
809
|
+
this._pendingApprovals.set(approvalId, {
|
|
810
|
+
resolve: (approved: boolean) => {
|
|
811
|
+
resolve(approved);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
837
814
|
});
|
|
815
|
+
}
|
|
838
816
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Formats tool input for display by pretty-printing JSON strings.
|
|
819
|
+
* @param input The tool input string to format
|
|
820
|
+
* @returns Pretty-printed JSON string
|
|
821
|
+
*/
|
|
822
|
+
private _formatToolInput(input: string): string {
|
|
823
|
+
try {
|
|
824
|
+
const parsed = JSON.parse(input);
|
|
825
|
+
return JSON.stringify(parsed, null, 2);
|
|
826
|
+
} catch {
|
|
827
|
+
return input;
|
|
828
|
+
}
|
|
846
829
|
}
|
|
847
830
|
|
|
848
831
|
/**
|
|
@@ -955,22 +938,20 @@ TOOL SELECTION GUIDELINES:
|
|
|
955
938
|
private _providerRegistry?: IProviderRegistry;
|
|
956
939
|
private _secretsManager?: ISecretsManager;
|
|
957
940
|
private _selectedToolNames: string[];
|
|
958
|
-
private _agent:
|
|
959
|
-
private
|
|
960
|
-
private
|
|
961
|
-
private _mcpServers: BrowserMCPServerStreamableHttp[];
|
|
941
|
+
private _agent: ToolLoopAgent<never, ToolMap> | null;
|
|
942
|
+
private _history: ModelMessage[];
|
|
943
|
+
private _mcpTools: ToolMap;
|
|
962
944
|
private _isInitializing: boolean;
|
|
963
945
|
private _controller: AbortController | null;
|
|
964
|
-
private _pendingApprovals: Map<
|
|
965
|
-
string,
|
|
966
|
-
{ interruption: RunToolApprovalItem; approved?: boolean; groupId?: string }
|
|
967
|
-
>;
|
|
968
|
-
private _interruptedState: any;
|
|
969
946
|
private _agentEvent: Signal<this, IAgentEvent>;
|
|
970
947
|
private _tokenUsage: ITokenUsage;
|
|
971
948
|
private _tokenUsageChanged: Signal<this, ITokenUsage>;
|
|
972
949
|
private _activeProvider: string = '';
|
|
973
950
|
private _activeProviderChanged = new Signal<this, string | undefined>(this);
|
|
951
|
+
private _pendingApprovals: Map<
|
|
952
|
+
string,
|
|
953
|
+
{ resolve: (approved: boolean, reason?: string) => void }
|
|
954
|
+
> = new Map();
|
|
974
955
|
}
|
|
975
956
|
|
|
976
957
|
namespace Private {
|