@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.
Files changed (66) hide show
  1. package/README.md +5 -214
  2. package/lib/agent.d.ts +58 -66
  3. package/lib/agent.js +274 -300
  4. package/lib/approval-buttons.d.ts +19 -82
  5. package/lib/approval-buttons.js +36 -289
  6. package/lib/chat-model-registry.d.ts +6 -0
  7. package/lib/chat-model-registry.js +4 -1
  8. package/lib/chat-model.d.ts +19 -54
  9. package/lib/chat-model.js +243 -303
  10. package/lib/components/clear-button.d.ts +6 -1
  11. package/lib/components/clear-button.js +8 -3
  12. package/lib/components/completion-status.d.ts +5 -0
  13. package/lib/components/completion-status.js +5 -4
  14. package/lib/components/model-select.d.ts +6 -1
  15. package/lib/components/model-select.js +9 -8
  16. package/lib/components/stop-button.d.ts +6 -1
  17. package/lib/components/stop-button.js +8 -3
  18. package/lib/components/token-usage-display.d.ts +5 -0
  19. package/lib/components/token-usage-display.js +2 -2
  20. package/lib/components/tool-select.d.ts +6 -1
  21. package/lib/components/tool-select.js +6 -5
  22. package/lib/index.js +62 -38
  23. package/lib/models/settings-model.d.ts +1 -1
  24. package/lib/providers/built-in-providers.js +38 -19
  25. package/lib/providers/models.d.ts +3 -3
  26. package/lib/providers/provider-registry.d.ts +3 -4
  27. package/lib/providers/provider-registry.js +1 -4
  28. package/lib/tokens.d.ts +5 -6
  29. package/lib/tools/commands.d.ts +2 -1
  30. package/lib/tools/commands.js +37 -46
  31. package/lib/tools/file.js +49 -73
  32. package/lib/tools/notebook.js +370 -445
  33. package/lib/widgets/ai-settings.d.ts +6 -0
  34. package/lib/widgets/ai-settings.js +72 -71
  35. package/lib/widgets/main-area-chat.d.ts +2 -0
  36. package/lib/widgets/main-area-chat.js +5 -2
  37. package/lib/widgets/provider-config-dialog.d.ts +2 -0
  38. package/lib/widgets/provider-config-dialog.js +34 -34
  39. package/package.json +12 -12
  40. package/src/agent.ts +342 -361
  41. package/src/approval-buttons.ts +43 -389
  42. package/src/chat-model-registry.ts +9 -1
  43. package/src/chat-model.ts +355 -370
  44. package/src/completion/completion-provider.ts +2 -3
  45. package/src/components/clear-button.tsx +16 -3
  46. package/src/components/completion-status.tsx +18 -4
  47. package/src/components/model-select.tsx +21 -8
  48. package/src/components/stop-button.tsx +16 -3
  49. package/src/components/token-usage-display.tsx +14 -2
  50. package/src/components/tool-select.tsx +23 -5
  51. package/src/index.ts +80 -36
  52. package/src/models/settings-model.ts +1 -1
  53. package/src/providers/built-in-providers.ts +38 -19
  54. package/src/providers/models.ts +3 -3
  55. package/src/providers/provider-registry.ts +4 -8
  56. package/src/tokens.ts +5 -6
  57. package/src/tools/commands.ts +39 -50
  58. package/src/tools/file.ts +49 -75
  59. package/src/tools/notebook.ts +451 -510
  60. package/src/widgets/ai-settings.tsx +153 -84
  61. package/src/widgets/main-area-chat.ts +8 -2
  62. package/src/widgets/provider-config-dialog.tsx +54 -41
  63. package/style/base.css +13 -73
  64. package/lib/mcp/browser.d.ts +0 -68
  65. package/lib/mcp/browser.js +0 -138
  66. 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
- Agent,
4
- AgentInputItem,
5
- Runner,
6
- RunToolApprovalItem,
7
- RunToolCallOutputItem,
8
- StreamedRunResult,
9
- user
10
- } from '@openai/agents';
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._mcpServers = [];
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._mcpServers.some(server => server.name === serverName);
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) servers based on current settings.
88
- * Closes existing servers and connects to enabled servers from configuration.
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 _initializeMCPServers(): Promise<void> {
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 servers
96
- for (const server of this._mcpServers) {
140
+ // Close existing clients
141
+ for (const wrapper of this._mcpClients) {
97
142
  try {
98
- await server.close();
143
+ await wrapper.client.close();
99
144
  connectionChanged = true;
100
145
  } catch (error) {
101
- console.warn('Error closing MCP server:', error);
146
+ console.warn('Error closing MCP client:', error);
102
147
  }
103
148
  }
104
- this._mcpServers = [];
149
+ this._mcpClients = [];
105
150
 
106
- // Initialize new servers
107
151
  for (const serverConfig of enabledServers) {
108
152
  try {
109
- const mcpServer = new BrowserMCPServerStreamableHttp({
110
- url: serverConfig.url,
111
- name: serverConfig.name
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._mcpServers.length > 0);
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._initializeMCPServers();
142
- const mcpServers = this._mcpServers.filter(server => server !== null);
190
+ await this._initializeMCPClients();
191
+ const mcpTools = await this.getMCPTools();
143
192
 
144
193
  this._agentManagers.forEach(manager => {
145
- manager.initializeAgent(mcpServers);
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 _mcpServers: BrowserMCPServerStreamableHttp[];
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
- tool_approval_required: {
196
- interruptionId: string;
244
+ tool_approval_request: {
245
+ approvalId: string;
246
+ toolCallId: string;
197
247
  toolName: string;
198
- toolInput: string;
199
- callId?: string;
248
+ args: unknown;
200
249
  };
201
- grouped_approval_required: {
202
- groupId: string;
203
- approvals: Array<{
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 including tool approvals.
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._mcpServers = [];
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 OpenAI agents tools.
355
- * @returns Array of selected tools formatted for OpenAI agents
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.push(tool);
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
- this._history = [];
408
- this._runner = new Runner({ tracingDisabled: true });
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
- this._interruptedState = null;
411
- // Reset token usage
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 and approvals.
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(user(message));
451
-
452
- // Get provider-specific maxTurns or use default
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
- await this._processRunResult(result);
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
- let hasInterruptions =
469
- result.interruptions && result.interruptions.length > 0;
539
+ const streamResult = await this._processStreamResult(result);
470
540
 
471
- while (hasInterruptions) {
472
- this._interruptedState = result;
473
- const interruptions = result.interruptions!;
541
+ // Get response messages and update token usage
542
+ const responseMessages = await result.response;
543
+ this._updateTokenUsage(await result.usage);
474
544
 
475
- if (interruptions.length > 1) {
476
- await this._handleGroupedToolApprovals(interruptions);
477
- } else {
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
- // Wait for all approvals to be resolved
482
- while (this._pendingApprovals.size > 0) {
483
- await new Promise(resolve => setTimeout(resolve, 100));
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
- // Continue execution
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
- this._agentEvent.emit({
503
- type: 'error',
504
- data: { error: error as Error }
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
- * Approves a tool call by interruption ID.
513
- * @param interruptionId The interruption ID to approve
583
+ * Updates token usage statistics.
514
584
  */
515
- async approveToolCall(interruptionId: string): Promise<void> {
516
- const pending = this._pendingApprovals.get(interruptionId);
517
- if (!pending) {
518
- console.warn(
519
- `No pending approval found for interruption ${interruptionId}`
520
- );
521
- return;
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 servers.
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 (mcpServers !== undefined) {
610
- this._mcpServers = mcpServers;
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 ? this.selectedAgentTools : [];
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?.maxTokens;
631
-
632
- this._agent = new Agent({
633
- name: 'Assistant',
634
- instructions: shouldUseTools
635
- ? this._getEnhancedSystemPrompt(config.systemPrompt || '')
636
- : config.systemPrompt || 'You are a helpful assistant.',
637
- model: model,
638
- mcpServers: this._mcpServers,
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
- ...(temperature && {
641
- modelSettings: {
642
- temperature,
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 stream from agent execution.
655
+ * Processes the stream result from agent execution.
657
656
  * Handles message streaming, tool calls, and emits appropriate events.
658
- * @param result The async iterable result from agent execution
657
+ * @param result The stream result from agent execution
658
+ * @returns Processing result including approval info if applicable
659
659
  */
660
- private async _processRunResult(
661
- result: StreamedRunResult<any, any>
662
- ): Promise<void> {
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 event of result) {
667
- if (event.type === 'raw_model_stream_event') {
668
- const data = event.data;
669
-
670
- if (data.type === 'response_started') {
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: 'message_chunk',
683
- data: {
684
- messageId: currentMessageId,
685
- chunk,
686
- fullContent: fullResponse
687
- }
673
+ type: 'message_start',
674
+ data: { messageId: currentMessageId }
688
675
  });
689
676
  }
690
- } else if (data.type === 'response_done') {
691
- if (currentMessageId) {
692
- this._agentEvent.emit({
693
- type: 'message_complete',
694
- data: {
695
- messageId: currentMessageId,
696
- content: fullResponse
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
- const usage = data.response.usage;
703
- const { inputTokens, outputTokens } = usage;
704
- this._tokenUsage.inputTokens += inputTokens;
705
- this._tokenUsage.outputTokens += outputTokens;
706
- this._tokenUsageChanged.emit(this._tokenUsage);
707
- } else if (data.type === 'model') {
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
- } else if (event.type === 'run_item_stream_event') {
714
- if (event.name === 'tool_output') {
715
- this._handleToolOutput(event);
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
- * Formats tool input for display by pretty-printing JSON strings.
723
- * @param input The tool input string to format
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
- * Handles the start of a tool call from the model event.
739
- * @param modelEvent The model event containing tool call information
734
+ * Emits a message_complete event.
740
735
  */
741
- private _handleToolCallStart(modelEvent: any): void {
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: 'tool_call_start',
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 execution output and completion.
758
- * @param event The tool output event containing result information
744
+ * Handles tool-result stream parts.
759
745
  */
760
- private _handleToolOutput(event: any): void {
761
- const toolEvent = event;
762
- const toolCallOutput = toolEvent.item as RunToolCallOutputItem;
763
- const callId = toolCallOutput.rawItem.callId;
764
- const resultText =
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
- toolCallOutput.rawItem.type === 'function_call_result' &&
771
- toolCallOutput.rawItem.status === 'incomplete';
772
-
773
- const toolName =
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: resultText,
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 request for a single tool call.
791
- * @param interruption The tool approval interruption item
769
+ * Handles tool-approval-request stream parts.
792
770
  */
793
- private async _handleSingleToolApproval(
794
- interruption: RunToolApprovalItem
771
+ private async _handleApprovalRequest(
772
+ part: ToolApprovalRequestOutput<ToolMap>,
773
+ result: IStreamProcessResult
795
774
  ): Promise<void> {
796
- const toolName = interruption.rawItem.name || 'Unknown Tool';
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: 'tool_approval_required',
778
+ type: 'tool_approval_request',
808
779
  data: {
809
- interruptionId,
810
- toolName,
811
- toolInput: this._formatToolInput(toolInput),
812
- callId
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
- * Handles approval requests for multiple grouped tool calls.
819
- * @param interruptions Array of tool approval interruption items
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 async _handleGroupedToolApprovals(
822
- interruptions: RunToolApprovalItem[]
823
- ): Promise<void> {
824
- const groupId = `group-${Date.now()}-${Math.random()}`;
825
- const approvals = interruptions.map(interruption => {
826
- const toolName = interruption.rawItem.name || 'Unknown Tool';
827
- const toolInput = interruption.rawItem.arguments || '{}';
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
- this._agentEvent.emit({
840
- type: 'grouped_approval_required',
841
- data: {
842
- groupId,
843
- approvals
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: Agent | null;
959
- private _runner: Runner;
960
- private _history: AgentInputItem[];
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 {