@jupyterlite/ai 0.17.0 → 0.19.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 (114) hide show
  1. package/lib/chat-commands/clear.d.ts +1 -0
  2. package/lib/chat-commands/index.d.ts +1 -0
  3. package/lib/chat-commands/skills.d.ts +2 -1
  4. package/lib/chat-model-handler.d.ts +4 -3
  5. package/lib/chat-model-handler.js +2 -1
  6. package/lib/chat-model.d.ts +148 -8
  7. package/lib/chat-model.js +368 -79
  8. package/lib/completion/completion-provider.d.ts +3 -1
  9. package/lib/completion/completion-provider.js +1 -2
  10. package/lib/completion/index.d.ts +1 -0
  11. package/lib/components/clear-button.d.ts +1 -0
  12. package/lib/components/clear-button.js +3 -4
  13. package/lib/components/completion-status.d.ts +1 -0
  14. package/lib/components/completion-status.js +5 -4
  15. package/lib/components/index.d.ts +1 -0
  16. package/lib/components/model-select.d.ts +1 -0
  17. package/lib/components/model-select.js +62 -67
  18. package/lib/components/save-button.d.ts +3 -2
  19. package/lib/components/save-button.js +4 -5
  20. package/lib/components/stop-button.d.ts +1 -0
  21. package/lib/components/stop-button.js +3 -4
  22. package/lib/components/tool-select.d.ts +3 -1
  23. package/lib/components/tool-select.js +47 -60
  24. package/lib/components/usage-display.d.ts +4 -2
  25. package/lib/components/usage-display.js +50 -61
  26. package/lib/diff-manager.d.ts +3 -1
  27. package/lib/index.d.ts +3 -2
  28. package/lib/index.js +50 -59
  29. package/lib/models/settings-model.d.ts +3 -1
  30. package/lib/models/settings-model.js +1 -0
  31. package/lib/rendered-message-outputarea.d.ts +1 -0
  32. package/lib/tokens.d.ts +48 -597
  33. package/lib/tokens.js +2 -31
  34. package/lib/widgets/ai-settings.d.ts +3 -1
  35. package/lib/widgets/ai-settings.js +185 -344
  36. package/lib/widgets/main-area-chat.d.ts +3 -3
  37. package/lib/widgets/main-area-chat.js +2 -4
  38. package/lib/widgets/provider-config-dialog.d.ts +2 -1
  39. package/lib/widgets/provider-config-dialog.js +102 -167
  40. package/package.json +111 -258
  41. package/schema/settings-model.json +6 -0
  42. package/src/chat-commands/skills.ts +2 -2
  43. package/src/chat-model-handler.ts +10 -6
  44. package/src/chat-model.ts +488 -96
  45. package/src/completion/completion-provider.ts +6 -6
  46. package/src/components/clear-button.tsx +0 -2
  47. package/src/components/completion-status.tsx +2 -2
  48. package/src/components/model-select.tsx +1 -1
  49. package/src/components/save-button.tsx +3 -3
  50. package/src/components/stop-button.tsx +0 -2
  51. package/src/components/tool-select.tsx +10 -9
  52. package/src/components/usage-display.tsx +4 -2
  53. package/src/diff-manager.ts +4 -3
  54. package/src/index.ts +103 -107
  55. package/src/models/settings-model.ts +7 -6
  56. package/src/tokens.ts +54 -744
  57. package/src/widgets/ai-settings.tsx +40 -11
  58. package/src/widgets/main-area-chat.ts +5 -8
  59. package/src/widgets/provider-config-dialog.tsx +8 -8
  60. package/LICENSE +0 -30
  61. package/README.md +0 -49
  62. package/lib/agent.d.ts +0 -277
  63. package/lib/agent.js +0 -1116
  64. package/lib/icons.d.ts +0 -3
  65. package/lib/icons.js +0 -8
  66. package/lib/providers/built-in-providers.d.ts +0 -21
  67. package/lib/providers/built-in-providers.js +0 -233
  68. package/lib/providers/generated-context-windows.d.ts +0 -8
  69. package/lib/providers/generated-context-windows.js +0 -96
  70. package/lib/providers/model-info.d.ts +0 -3
  71. package/lib/providers/model-info.js +0 -58
  72. package/lib/providers/models.d.ts +0 -37
  73. package/lib/providers/models.js +0 -28
  74. package/lib/providers/provider-registry.d.ts +0 -49
  75. package/lib/providers/provider-registry.js +0 -72
  76. package/lib/providers/provider-tools.d.ts +0 -36
  77. package/lib/providers/provider-tools.js +0 -93
  78. package/lib/skills/index.d.ts +0 -4
  79. package/lib/skills/index.js +0 -7
  80. package/lib/skills/parse-skill.d.ts +0 -25
  81. package/lib/skills/parse-skill.js +0 -69
  82. package/lib/skills/skill-loader.d.ts +0 -25
  83. package/lib/skills/skill-loader.js +0 -133
  84. package/lib/skills/skill-registry.d.ts +0 -31
  85. package/lib/skills/skill-registry.js +0 -100
  86. package/lib/skills/types.d.ts +0 -29
  87. package/lib/skills/types.js +0 -5
  88. package/lib/tools/commands.d.ts +0 -11
  89. package/lib/tools/commands.js +0 -154
  90. package/lib/tools/skills.d.ts +0 -9
  91. package/lib/tools/skills.js +0 -73
  92. package/lib/tools/tool-registry.d.ts +0 -35
  93. package/lib/tools/tool-registry.js +0 -55
  94. package/lib/tools/web.d.ts +0 -8
  95. package/lib/tools/web.js +0 -196
  96. package/src/agent.ts +0 -1441
  97. package/src/icons.ts +0 -11
  98. package/src/providers/built-in-providers.ts +0 -241
  99. package/src/providers/generated-context-windows.ts +0 -102
  100. package/src/providers/model-info.ts +0 -88
  101. package/src/providers/models.ts +0 -76
  102. package/src/providers/provider-registry.ts +0 -88
  103. package/src/providers/provider-tools.ts +0 -179
  104. package/src/skills/index.ts +0 -14
  105. package/src/skills/parse-skill.ts +0 -91
  106. package/src/skills/skill-loader.ts +0 -175
  107. package/src/skills/skill-registry.ts +0 -137
  108. package/src/skills/types.ts +0 -37
  109. package/src/tools/commands.ts +0 -210
  110. package/src/tools/skills.ts +0 -84
  111. package/src/tools/tool-registry.ts +0 -63
  112. package/src/tools/web.ts +0 -238
  113. package/src/types.d.ts +0 -4
  114. package/style/icons/jupyternaut-lite.svg +0 -7
package/src/agent.ts DELETED
@@ -1,1441 +0,0 @@
1
- import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
2
- import type { IMessageContent } from '@jupyter/chat';
3
- import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
- import { PromiseDelegate } from '@lumino/coreutils';
5
- import { ISignal, Signal } from '@lumino/signaling';
6
- import {
7
- generateText,
8
- ToolLoopAgent,
9
- type ModelMessage,
10
- type LanguageModel,
11
- stepCountIs,
12
- type StreamTextResult,
13
- type ToolApprovalRequestOutput,
14
- type TypedToolError,
15
- type TypedToolOutputDenied,
16
- type TypedToolResult,
17
- type UserContent,
18
- type AssistantModelMessage,
19
- APICallError
20
- } from 'ai';
21
- import { ISecretsManager } from 'jupyter-secrets-manager';
22
-
23
- import { createModel } from './providers/models';
24
- import { getEffectiveContextWindow } from './providers/model-info';
25
- import {
26
- createProviderTools,
27
- type IProviderCustomSettings
28
- } from './providers/provider-tools';
29
- import {
30
- type IAgentManager,
31
- type IAgentManagerFactory,
32
- type IAISettingsModel,
33
- type IProviderInfo,
34
- type IProviderRegistry,
35
- type ISkillRegistry,
36
- type ISkillSummary,
37
- type ITool,
38
- type IToolRegistry,
39
- type ITokenUsage,
40
- type ToolMap,
41
- SECRETS_NAMESPACE
42
- } from './tokens';
43
-
44
- /**
45
- * Interface for MCP client wrapper to track connection state
46
- */
47
- interface IMCPClientWrapper {
48
- name: string;
49
- client: MCPClient;
50
- }
51
-
52
- /**
53
- * Result from processing a stream, including approval info if applicable.
54
- */
55
- interface IStreamProcessResult {
56
- /**
57
- * Whether an approval request was encountered and processed.
58
- */
59
- approvalProcessed: boolean;
60
- /**
61
- * Whether the stream was aborted before completion.
62
- */
63
- aborted: boolean;
64
- /**
65
- * The approval response message to add to history (if approval was processed).
66
- */
67
- approvalResponse?: ModelMessage;
68
- }
69
-
70
- /**
71
- * The agent manager factory namespace.
72
- */
73
- export namespace AgentManagerFactory {
74
- export interface IOptions {
75
- /**
76
- * The settings model.
77
- */
78
- settingsModel: IAISettingsModel;
79
- /**
80
- * The skill registry for discovering skills.
81
- */
82
- skillRegistry?: ISkillRegistry;
83
- /**
84
- * The secrets manager.
85
- */
86
- secretsManager?: ISecretsManager;
87
- /**
88
- * The token used to request the secrets manager.
89
- */
90
- token: symbol | null;
91
- }
92
- }
93
-
94
- /**
95
- * The agent manager factory.
96
- */
97
- export class AgentManagerFactory implements IAgentManagerFactory {
98
- constructor(options: AgentManagerFactory.IOptions) {
99
- Private.setToken(options.token);
100
- this._settingsModel = options.settingsModel;
101
- this._skillRegistry = options.skillRegistry;
102
- this._secretsManager = options.secretsManager;
103
- this._mcpClients = [];
104
- this._mcpConnectionChanged = new Signal<this, boolean>(this);
105
-
106
- if (this._skillRegistry) {
107
- this._skillRegistry.skillsChanged.connect(() => {
108
- this.refreshSkillSnapshots();
109
- });
110
- }
111
-
112
- // Initialize agent on construction
113
- this._initializeAgents().catch(error =>
114
- console.warn('Failed to initialize agent in constructor:', error)
115
- );
116
-
117
- // Listen for settings changes
118
- this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
119
-
120
- // Disable the secrets manager if the token is empty.
121
- if (!options.token) {
122
- this._secretsManager = undefined;
123
- }
124
- }
125
-
126
- /**
127
- * Create a new agent.
128
- */
129
- createAgent(options: IAgentManager.IOptions): IAgentManager {
130
- const agentManager = new AgentManager({
131
- ...options,
132
- skillRegistry: this._skillRegistry,
133
- secretsManager: this._secretsManager
134
- });
135
- this._agentManagers.push(agentManager);
136
-
137
- // New chats can be created before MCP setup finishes.
138
- // Reinitialize them with connected MCP tools once it does.
139
- this._initQueue
140
- .then(() => this.getMCPTools())
141
- .then(mcpTools => {
142
- if (Object.keys(mcpTools).length > 0) {
143
- agentManager.initializeAgent(mcpTools);
144
- }
145
- })
146
- .catch(error =>
147
- console.warn('Failed to pass MCP tools to new agent:', error)
148
- );
149
-
150
- return agentManager;
151
- }
152
-
153
- /**
154
- * Signal emitted when MCP connection status changes
155
- */
156
- get mcpConnectionChanged(): ISignal<this, boolean> {
157
- return this._mcpConnectionChanged;
158
- }
159
-
160
- /**
161
- * Checks whether a specific MCP server is connected.
162
- * @param serverName The name of the MCP server to check
163
- * @returns True if the server is connected, false otherwise
164
- */
165
- isMCPServerConnected(serverName: string): boolean {
166
- return this._mcpClients.some(wrapper => wrapper.name === serverName);
167
- }
168
-
169
- /**
170
- * Gets the MCP tools from connected servers
171
- */
172
- async getMCPTools(): Promise<ToolMap> {
173
- const mcpTools: ToolMap = {};
174
-
175
- for (const wrapper of this._mcpClients) {
176
- try {
177
- const tools = await wrapper.client.tools();
178
- Object.assign(mcpTools, tools);
179
- } catch (error) {
180
- console.warn(
181
- `Failed to get tools from MCP server ${wrapper.name}:`,
182
- error
183
- );
184
- }
185
- }
186
-
187
- return mcpTools;
188
- }
189
-
190
- /**
191
- * Handles settings changes and reinitializes the agent.
192
- */
193
- private _onSettingsChanged(): void {
194
- this._initializeAgents().catch(error =>
195
- console.warn('Failed to initialize agent on settings change:', error)
196
- );
197
- }
198
-
199
- /**
200
- * Initializes MCP (Model Context Protocol) clients based on current settings.
201
- * Closes existing clients and connects to enabled servers from configuration.
202
- */
203
- private async _initializeMCPClients(): Promise<void> {
204
- const config = this._settingsModel.config;
205
- const enabledServers = config.mcpServers.filter(server => server.enabled);
206
- let connectionChanged = false;
207
-
208
- // Close existing clients
209
- for (const wrapper of this._mcpClients) {
210
- try {
211
- await wrapper.client.close();
212
- connectionChanged = true;
213
- } catch (error) {
214
- console.warn('Error closing MCP client:', error);
215
- }
216
- }
217
- this._mcpClients = [];
218
-
219
- for (const serverConfig of enabledServers) {
220
- try {
221
- const client = await createMCPClient({
222
- transport: {
223
- type: 'http',
224
- url: serverConfig.url
225
- }
226
- });
227
-
228
- this._mcpClients.push({
229
- name: serverConfig.name,
230
- client
231
- });
232
- connectionChanged = true;
233
- } catch (error) {
234
- console.warn(
235
- `Failed to connect to MCP server "${serverConfig.name}" at ${serverConfig.url}:`,
236
- error
237
- );
238
- }
239
- }
240
-
241
- // Emit connection change signal if there were any changes
242
- if (connectionChanged) {
243
- this._mcpConnectionChanged.emit(this._mcpClients.length > 0);
244
- }
245
- }
246
-
247
- /**
248
- * Initializes the AI agent with current settings and tools.
249
- * Sets up the agent with model configuration, tools, and MCP servers.
250
- */
251
- private async _initializeAgents(): Promise<void> {
252
- this._initQueue = this._initQueue
253
- .catch(() => undefined)
254
- .then(async () => {
255
- try {
256
- await this._initializeMCPClients();
257
- const mcpTools = await this.getMCPTools();
258
-
259
- this._agentManagers.forEach(manager => {
260
- manager.initializeAgent(mcpTools);
261
- });
262
- } catch (error) {
263
- console.warn('Failed to initialize agents:', error);
264
- }
265
- });
266
- return this._initQueue;
267
- }
268
-
269
- /**
270
- * Refresh skill snapshots across all agents.
271
- */
272
- refreshSkillSnapshots(): void {
273
- this._agentManagers.forEach(manager => {
274
- manager.refreshSkills();
275
- });
276
- }
277
-
278
- private _agentManagers: IAgentManager[] = [];
279
- private _settingsModel: IAISettingsModel;
280
- private _skillRegistry?: ISkillRegistry;
281
- private _secretsManager?: ISecretsManager;
282
- private _mcpClients: IMCPClientWrapper[];
283
- private _mcpConnectionChanged: Signal<this, boolean>;
284
- private _initQueue: Promise<void> = Promise.resolve();
285
- }
286
-
287
- /**
288
- * Default parameter values for agent configuration
289
- */
290
- const DEFAULT_TEMPERATURE = 0.7;
291
- const DEFAULT_MAX_TURNS = 25;
292
-
293
- /**
294
- * Cached configuration used to (re)build the agent.
295
- */
296
- interface IAgentConfig {
297
- model: LanguageModel;
298
- tools: ToolMap;
299
- temperature: number;
300
- maxOutputTokens?: number;
301
- maxTurns: number;
302
- baseSystemPrompt: string;
303
- shouldUseTools: boolean;
304
- }
305
-
306
- /**
307
- * Manages the AI agent lifecycle and execution loop.
308
- * Provides agent initialization, tool management, MCP server integration,
309
- * and handles the complete agent execution cycle.
310
- * Emits events for UI updates instead of directly manipulating the chat interface.
311
- */
312
- export class AgentManager implements IAgentManager {
313
- /**
314
- * Creates a new AgentManager instance.
315
- * @param options Configuration options for the agent manager
316
- */
317
- constructor(options: IAgentManager.IOptions) {
318
- this._settingsModel = options.settingsModel;
319
- this._toolRegistry = options.toolRegistry;
320
- this._providerRegistry = options.providerRegistry;
321
- this._skillRegistry = options.skillRegistry;
322
- this._secretsManager = options.secretsManager;
323
- this._selectedToolNames = [];
324
- this._agent = null;
325
- this._history = [];
326
- this._mcpTools = {};
327
- this._controller = null;
328
- this._agentEvent = new Signal<this, IAgentManager.IAgentEvent>(this);
329
- this._tokenUsage = options.tokenUsage ?? {
330
- inputTokens: 0,
331
- outputTokens: 0
332
- };
333
- this._tokenUsageChanged = new Signal<this, ITokenUsage>(this);
334
- this._skills = [];
335
- this._agentConfig = null;
336
- this._renderMimeRegistry = options.renderMimeRegistry;
337
- this._streaming.resolve();
338
-
339
- this.activeProvider =
340
- options.activeProvider ?? this._settingsModel.config.defaultProvider;
341
-
342
- // Initialize selected tools to all available tools by default
343
- if (this._toolRegistry) {
344
- this._selectedToolNames = Object.keys(this._toolRegistry.tools);
345
- }
346
- }
347
-
348
- /**
349
- * Signal emitted when agent events occur
350
- */
351
- get agentEvent(): ISignal<this, IAgentManager.IAgentEvent> {
352
- return this._agentEvent;
353
- }
354
-
355
- /**
356
- * Signal emitted when the active provider has changed.
357
- */
358
- get activeProviderChanged(): ISignal<this, string | undefined> {
359
- return this._activeProviderChanged;
360
- }
361
-
362
- /**
363
- * Gets the current token usage statistics.
364
- */
365
- get tokenUsage(): ITokenUsage {
366
- return this._tokenUsage;
367
- }
368
-
369
- /**
370
- * Signal emitted when token usage statistics change.
371
- */
372
- get tokenUsageChanged(): ISignal<this, ITokenUsage> {
373
- return this._tokenUsageChanged;
374
- }
375
-
376
- /**
377
- * Refresh the skills snapshot and rebuild the agent if resources are ready.
378
- */
379
- refreshSkills(): void {
380
- this._initQueue = this._initQueue
381
- .catch(() => undefined)
382
- .then(async () => {
383
- this._refreshSkills();
384
- if (!this._agentConfig) {
385
- return;
386
- }
387
- this._rebuildAgent();
388
- });
389
- }
390
-
391
- /**
392
- * The active provider for this agent.
393
- */
394
- get activeProvider(): string {
395
- return this._activeProvider;
396
- }
397
- set activeProvider(value: string) {
398
- const previousProvider = this._activeProvider;
399
- this._activeProvider = value;
400
-
401
- // Reset request-level context estimate only when switching between providers.
402
- if (previousProvider && previousProvider !== value) {
403
- this._tokenUsage.lastRequestInputTokens = undefined;
404
- }
405
-
406
- this._tokenUsage.contextWindow = this._getActiveContextWindow();
407
-
408
- this._tokenUsageChanged.emit(this._tokenUsage);
409
- this.initializeAgent();
410
- this._activeProviderChanged.emit(this._activeProvider);
411
- }
412
-
413
- /**
414
- * Sets the selected tools by name and reinitializes the agent.
415
- * @param toolNames Array of tool names to select
416
- */
417
- setSelectedTools(toolNames: string[]): void {
418
- this._selectedToolNames = [...toolNames];
419
- this.initializeAgent().catch(error =>
420
- console.warn('Failed to initialize agent on tools change:', error)
421
- );
422
- }
423
-
424
- /**
425
- * Gets the currently selected tools as a record.
426
- * @returns Record of selected tools
427
- */
428
- get selectedAgentTools(): ToolMap {
429
- if (!this._toolRegistry) {
430
- return {};
431
- }
432
-
433
- const result: ToolMap = {};
434
- for (const name of this._selectedToolNames) {
435
- const tool: ITool | null = this._toolRegistry.get(name);
436
- if (tool) {
437
- result[name] = tool;
438
- }
439
- }
440
-
441
- return result;
442
- }
443
-
444
- /**
445
- * Checks if the current configuration is valid for agent operations.
446
- * Uses the provider registry to determine if an API key is required.
447
- * @returns True if the configuration is valid, false otherwise
448
- */
449
- hasValidConfig(): boolean {
450
- const activeProviderConfig = this._settingsModel.getProvider(
451
- this._activeProvider
452
- );
453
- if (!activeProviderConfig) {
454
- return false;
455
- }
456
-
457
- if (!activeProviderConfig.model) {
458
- return false;
459
- }
460
-
461
- if (this._providerRegistry) {
462
- const providerInfo = this._providerRegistry.getProviderInfo(
463
- activeProviderConfig.provider
464
- );
465
- if (providerInfo?.apiKeyRequirement === 'required') {
466
- return !!activeProviderConfig.apiKey;
467
- }
468
- }
469
-
470
- return true;
471
- }
472
-
473
- /**
474
- * Clears conversation history and resets agent state.
475
- */
476
- async clearHistory(): Promise<void> {
477
- // Stop any ongoing streaming
478
- this.stopStreaming('Chat cleared');
479
-
480
- await this._streaming.promise;
481
-
482
- // Clear history and token usage
483
- this._history = [];
484
- this._tokenUsage = {
485
- inputTokens: 0,
486
- outputTokens: 0,
487
- contextWindow: this._getActiveContextWindow()
488
- };
489
- this._tokenUsageChanged.emit(this._tokenUsage);
490
- }
491
-
492
- /**
493
- * Sets the history with a list of messages from the chat.
494
- * @param messages The chat messages to set as history
495
- */
496
- setHistory(messages: IMessageContent[]): void {
497
- // Stop any ongoing streaming and reject awaiting approvals
498
- this.stopStreaming();
499
-
500
- for (const [approvalId, pending] of this._pendingApprovals) {
501
- pending.resolve(false, 'Chat history changed');
502
- this._agentEvent.emit({
503
- type: 'tool_approval_resolved',
504
- data: { approvalId, approved: false }
505
- });
506
- }
507
- this._pendingApprovals.clear();
508
-
509
- // Convert chat messages to model messages
510
- const modelMessages: ModelMessage[] = messages.map(msg => {
511
- const role =
512
- msg.sender.username === 'ai-assistant' ? 'assistant' : 'user';
513
- return {
514
- role,
515
- content: msg.body
516
- };
517
- });
518
- this._history = Private.sanitizeModelMessages(modelMessages);
519
- }
520
-
521
- /**
522
- * Stops the current streaming response by aborting the request.
523
- * Resolve any pending approval.
524
- */
525
- stopStreaming(reason?: string): void {
526
- this._controller?.abort();
527
-
528
- // Reject any pending approvals
529
- for (const [approvalId, pending] of this._pendingApprovals) {
530
- pending.resolve(false, reason ?? 'Stream ended by user');
531
- this._agentEvent.emit({
532
- type: 'tool_approval_resolved',
533
- data: { approvalId, approved: false }
534
- });
535
- }
536
- this._pendingApprovals.clear();
537
- }
538
-
539
- /**
540
- * Approves a pending tool call.
541
- * @param approvalId The approval ID to approve
542
- * @param reason Optional reason for approval
543
- */
544
- approveToolCall(approvalId: string, reason?: string): void {
545
- const pending = this._pendingApprovals.get(approvalId);
546
- if (pending) {
547
- pending.resolve(true, reason);
548
- this._pendingApprovals.delete(approvalId);
549
- this._agentEvent.emit({
550
- type: 'tool_approval_resolved',
551
- data: { approvalId, approved: true }
552
- });
553
- }
554
- }
555
-
556
- /**
557
- * Rejects a pending tool call.
558
- * @param approvalId The approval ID to reject
559
- * @param reason Optional reason for rejection
560
- */
561
- rejectToolCall(approvalId: string, reason?: string): void {
562
- const pending = this._pendingApprovals.get(approvalId);
563
- if (pending) {
564
- pending.resolve(false, reason);
565
- this._pendingApprovals.delete(approvalId);
566
- this._agentEvent.emit({
567
- type: 'tool_approval_resolved',
568
- data: { approvalId, approved: false }
569
- });
570
- }
571
- }
572
-
573
- /**
574
- * Generates AI response to user message using the agent.
575
- * Handles the complete execution cycle including tool calls.
576
- * @param message The user message to respond to (may include processed attachment content)
577
- */
578
- async generateResponse(message: UserContent): Promise<void> {
579
- this._streaming = new PromiseDelegate();
580
- this._controller = new AbortController();
581
- const responseHistory: ModelMessage[] = [];
582
-
583
- // Add user message to history
584
- responseHistory.push({
585
- role: 'user',
586
- content: message
587
- });
588
-
589
- try {
590
- // Ensure we have an agent
591
- if (!this._agent) {
592
- await this.initializeAgent();
593
- }
594
-
595
- if (!this._agent) {
596
- throw new Error('Failed to initialize agent');
597
- }
598
-
599
- let continueLoop = true;
600
- while (continueLoop) {
601
- const result = await this._agent.stream({
602
- messages: [...this._history, ...responseHistory],
603
- abortSignal: this._controller.signal
604
- });
605
-
606
- const streamResult = await this._processStreamResult(result);
607
-
608
- if (streamResult.aborted) {
609
- try {
610
- const responseMessages = await result.response;
611
- if (responseMessages.messages?.length) {
612
- this._history.push(
613
- ...Private.sanitizeModelMessages(responseMessages.messages)
614
- );
615
- }
616
- } catch {
617
- // Aborting before a step finishes leaves no completed response to persist.
618
- }
619
- break;
620
- }
621
-
622
- // Get response messages for completed steps.
623
- const responseMessages = await result.response;
624
-
625
- // Add response messages to history
626
- if (responseMessages.messages?.length) {
627
- responseHistory.push(...responseMessages.messages);
628
- }
629
-
630
- // Add approval response if processed
631
- if (streamResult.approvalResponse) {
632
- // Check if the last message is a tool message we can append to
633
- const lastMsg = responseHistory[responseHistory.length - 1];
634
- if (
635
- lastMsg &&
636
- lastMsg.role === 'tool' &&
637
- Array.isArray(lastMsg.content) &&
638
- Array.isArray(streamResult.approvalResponse.content)
639
- ) {
640
- const toolContent = lastMsg.content as unknown[];
641
- toolContent.push(...streamResult.approvalResponse.content);
642
- } else {
643
- // Add as separate message
644
- responseHistory.push(streamResult.approvalResponse);
645
- }
646
- }
647
-
648
- continueLoop = streamResult.approvalProcessed;
649
- }
650
-
651
- // Add the messages to the history only if the response ended without error.
652
- this._history.push(...Private.sanitizeModelMessages(responseHistory));
653
- } catch (error) {
654
- if ((error as Error).name !== 'AbortError') {
655
- let helpMessage = `${(error as Error).message}`;
656
-
657
- // Remove attachments from history on payload rejection errors
658
- if (
659
- APICallError.isInstance(error) &&
660
- (error.statusCode === 400 ||
661
- error.statusCode === 404 ||
662
- error.statusCode === 413 ||
663
- error.statusCode === 415 ||
664
- error.statusCode === 422)
665
- ) {
666
- for (const msg of [...this._history, ...responseHistory]) {
667
- if (msg.role === 'user' && Array.isArray(msg.content)) {
668
- const hasMedia = msg.content.some(p => p.type !== 'text');
669
- if (hasMedia) {
670
- const textContent = msg.content
671
- .filter(p => p.type === 'text')
672
- .map(p => (p as { text: string }).text)
673
- .join('\n');
674
- msg.content =
675
- textContent || '_Attachment removed due to error_';
676
- }
677
- }
678
- }
679
- helpMessage +=
680
- '\n\nAttachments have been removed from history. Please send your prompt again.';
681
- }
682
- this._agentEvent.emit({
683
- type: 'error',
684
- data: { error: new Error(helpMessage) }
685
- });
686
- this._history.push(...Private.sanitizeModelMessages(responseHistory));
687
- this._history.push({
688
- role: 'assistant',
689
- content: helpMessage
690
- });
691
- }
692
- } finally {
693
- this._controller = null;
694
- this._streaming.resolve();
695
- }
696
- }
697
-
698
- /**
699
- * Create a transient language model to request a text response which won't be added to history.
700
- * @param messages - the messages sequence to send to the model.
701
- */
702
- async textResponse(messages: ModelMessage[]): Promise<string> {
703
- try {
704
- const model = await this._createModel();
705
- const result = await generateText({
706
- model,
707
- messages
708
- });
709
- this._updateTokenUsage(result.totalUsage, result.totalUsage.inputTokens);
710
- return result.text;
711
- } catch (e) {
712
- throw `Error while getting the topic of the chat\n${e}`;
713
- }
714
- }
715
-
716
- /**
717
- * Updates cumulative token usage statistics from a completed model step.
718
- */
719
- private _updateTokenUsage(
720
- usage: { inputTokens?: number; outputTokens?: number } | undefined,
721
- lastRequestInputTokens?: number
722
- ): void {
723
- const contextWindow = this._getActiveContextWindow();
724
- const estimatedRequestInputTokens =
725
- lastRequestInputTokens ?? usage?.inputTokens;
726
-
727
- if (usage) {
728
- this._tokenUsage.inputTokens += usage.inputTokens ?? 0;
729
- this._tokenUsage.outputTokens += usage.outputTokens ?? 0;
730
- }
731
-
732
- this._tokenUsage.lastRequestInputTokens = estimatedRequestInputTokens;
733
- this._tokenUsage.contextWindow = contextWindow;
734
-
735
- this._tokenUsageChanged.emit(this._tokenUsage);
736
- }
737
-
738
- /**
739
- * Gets the configured context window for the active provider.
740
- */
741
- private _getActiveContextWindow(): number | undefined {
742
- const activeProviderConfig = this._settingsModel.getProvider(
743
- this._activeProvider
744
- );
745
- return getEffectiveContextWindow(
746
- activeProviderConfig,
747
- this._providerRegistry
748
- );
749
- }
750
-
751
- /**
752
- * Initializes the AI agent with current settings and tools.
753
- * Sets up the agent with model configuration, tools, and MCP tools.
754
- */
755
- initializeAgent = async (mcpTools?: ToolMap): Promise<void> => {
756
- this._initQueue = this._initQueue
757
- .catch(() => undefined)
758
- .then(async () => {
759
- try {
760
- this._refreshSkills();
761
- await this._prepareAgentConfig(mcpTools);
762
- this._rebuildAgent();
763
- } catch (error) {
764
- console.warn('Failed to initialize agent:', error);
765
- this._agent = null;
766
- }
767
- });
768
- return this._initQueue;
769
- };
770
-
771
- /**
772
- * Refresh the in-memory skills snapshot from the skill registry.
773
- */
774
- private _refreshSkills(): void {
775
- if (!this._skillRegistry) {
776
- this._skills = [];
777
- return;
778
- }
779
- this._skills = this._skillRegistry.listSkills();
780
- }
781
-
782
- /**
783
- * Prepare model, tools, and settings needed to (re)build the agent.
784
- */
785
- private async _prepareAgentConfig(mcpTools?: ToolMap): Promise<void> {
786
- const config = this._settingsModel.config;
787
- if (mcpTools !== undefined) {
788
- this._mcpTools = mcpTools;
789
- }
790
-
791
- const model = await this._createModel();
792
-
793
- const supportsToolCalling = this._supportsToolCalling();
794
- const canUseTools = config.toolsEnabled && supportsToolCalling;
795
- const hasFunctionToolRegistry = !!(
796
- this._toolRegistry && Object.keys(this._toolRegistry.tools).length > 0
797
- );
798
- const selectedFunctionTools =
799
- canUseTools && hasFunctionToolRegistry ? this.selectedAgentTools : {};
800
- const functionTools = canUseTools
801
- ? { ...selectedFunctionTools, ...this._mcpTools }
802
- : {};
803
-
804
- const activeProviderConfig = this._settingsModel.getProvider(
805
- this._activeProvider
806
- );
807
- const activeProviderInfo =
808
- activeProviderConfig && this._providerRegistry
809
- ? this._providerRegistry.getProviderInfo(activeProviderConfig.provider)
810
- : null;
811
- const contextWindow = getEffectiveContextWindow(
812
- activeProviderConfig,
813
- this._providerRegistry
814
- );
815
-
816
- this._tokenUsage.contextWindow = contextWindow;
817
- this._tokenUsageChanged.emit(this._tokenUsage);
818
-
819
- const temperature =
820
- activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
821
- const maxTokens = activeProviderConfig?.parameters?.maxOutputTokens;
822
- const maxTurns =
823
- activeProviderConfig?.parameters?.maxTurns ?? DEFAULT_MAX_TURNS;
824
-
825
- const tools = this._buildRuntimeTools({
826
- providerInfo: activeProviderInfo,
827
- customSettings: activeProviderConfig?.customSettings,
828
- functionTools,
829
- includeProviderTools: canUseTools
830
- });
831
-
832
- const shouldUseTools = canUseTools && Object.keys(tools).length > 0;
833
-
834
- this._agentConfig = {
835
- model,
836
- tools,
837
- temperature,
838
- maxOutputTokens: maxTokens,
839
- maxTurns,
840
- baseSystemPrompt: config.systemPrompt || '',
841
- shouldUseTools
842
- };
843
- }
844
-
845
- /**
846
- * Build the runtime tool map used by the agent.
847
- */
848
- private _buildRuntimeTools(options: {
849
- providerInfo?: IProviderInfo | null;
850
- customSettings?: IProviderCustomSettings;
851
- functionTools: ToolMap;
852
- includeProviderTools: boolean;
853
- }): ToolMap {
854
- const providerTools = options.includeProviderTools
855
- ? createProviderTools({
856
- providerInfo: options.providerInfo,
857
- customSettings: options.customSettings,
858
- hasFunctionTools: Object.keys(options.functionTools).length > 0
859
- })
860
- : {};
861
-
862
- return {
863
- ...providerTools,
864
- ...options.functionTools
865
- };
866
- }
867
-
868
- /**
869
- * Rebuild the agent using cached resources and the current skills snapshot.
870
- */
871
- private _rebuildAgent(): void {
872
- if (!this._agentConfig) {
873
- this._agent = null;
874
- return;
875
- }
876
-
877
- const {
878
- model,
879
- tools,
880
- temperature,
881
- maxOutputTokens,
882
- maxTurns,
883
- baseSystemPrompt,
884
- shouldUseTools
885
- } = this._agentConfig;
886
-
887
- const baseInstructions = shouldUseTools
888
- ? this._getEnhancedSystemPrompt(baseSystemPrompt, tools)
889
- : baseSystemPrompt || 'You are a helpful assistant.';
890
- const richOutputWorkflowInstruction = shouldUseTools
891
- ? '- When the user asks for visual or rich outputs, prefer running code/commands that produce those outputs and describe that they will be rendered in chat.'
892
- : '- When tools are unavailable, explain the limitation clearly and provide concrete steps the user can run to produce the desired rich outputs.';
893
- const supportedMimeTypesInstruction =
894
- this._getSupportedMimeTypesInstruction();
895
- const instructions = `${baseInstructions}
896
-
897
- RICH OUTPUT RENDERING:
898
- - The chat UI can render rich MIME outputs as separate assistant messages.
899
- - ${supportedMimeTypesInstruction}
900
- - Use only MIME types from the supported list when creating MIME bundles. Do not invent MIME keys.
901
- - Do not claim that you cannot display maps, images, or rich outputs in chat.
902
- ${richOutputWorkflowInstruction}`;
903
-
904
- this._agent = new ToolLoopAgent({
905
- model,
906
- instructions,
907
- tools,
908
- temperature,
909
- maxOutputTokens,
910
- stopWhen: stepCountIs(maxTurns)
911
- });
912
- }
913
-
914
- /**
915
- * Processes the stream result from agent execution.
916
- * Handles message streaming, tool calls, and emits appropriate events.
917
- * @param result The stream result from agent execution
918
- * @returns Processing result including approval info if applicable
919
- */
920
- private async _processStreamResult(
921
- result: StreamTextResult<ToolMap, never>
922
- ): Promise<IStreamProcessResult> {
923
- let fullResponse = '';
924
- let currentMessageId: string | null = null;
925
- const processResult: IStreamProcessResult = {
926
- approvalProcessed: false,
927
- aborted: false
928
- };
929
-
930
- for await (const part of result.fullStream) {
931
- switch (part.type) {
932
- case 'text-delta':
933
- if (!currentMessageId) {
934
- currentMessageId = `msg-${Date.now()}-${Math.random()}`;
935
- this._agentEvent.emit({
936
- type: 'message_start',
937
- data: { messageId: currentMessageId }
938
- });
939
- }
940
- fullResponse += part.text;
941
- this._agentEvent.emit({
942
- type: 'message_chunk',
943
- data: {
944
- messageId: currentMessageId,
945
- chunk: part.text,
946
- fullContent: fullResponse
947
- }
948
- });
949
- break;
950
-
951
- case 'tool-call':
952
- // Complete current message before tool call
953
- if (currentMessageId && fullResponse) {
954
- this._emitMessageComplete(currentMessageId, fullResponse);
955
- currentMessageId = null;
956
- fullResponse = '';
957
- }
958
- this._agentEvent.emit({
959
- type: 'tool_call_start',
960
- data: {
961
- callId: part.toolCallId,
962
- toolName: part.toolName,
963
- input: this._formatToolInput(JSON.stringify(part.input))
964
- }
965
- });
966
- break;
967
-
968
- case 'tool-result':
969
- this._handleToolResult(part);
970
- break;
971
-
972
- case 'tool-error':
973
- this._handleToolError(part);
974
- break;
975
-
976
- case 'tool-output-denied':
977
- this._handleToolOutputDenied(part);
978
- break;
979
-
980
- case 'tool-approval-request':
981
- // Complete current message before approval
982
- if (currentMessageId && fullResponse) {
983
- this._emitMessageComplete(currentMessageId, fullResponse);
984
- currentMessageId = null;
985
- fullResponse = '';
986
- }
987
- await this._handleApprovalRequest(part, processResult);
988
- break;
989
-
990
- case 'error':
991
- throw part.error;
992
-
993
- case 'finish-step':
994
- this._updateTokenUsage(part.usage, part.usage.inputTokens);
995
- break;
996
-
997
- case 'abort':
998
- processResult.aborted = true;
999
- break;
1000
-
1001
- // Ignore: text-start, text-end, finish, and others
1002
- default:
1003
- break;
1004
- }
1005
- }
1006
-
1007
- // Complete final message if content remains
1008
- if (currentMessageId && fullResponse) {
1009
- this._emitMessageComplete(currentMessageId, fullResponse);
1010
- }
1011
-
1012
- return processResult;
1013
- }
1014
-
1015
- /**
1016
- * Emits a message_complete event.
1017
- */
1018
- private _emitMessageComplete(messageId: string, content: string): void {
1019
- this._agentEvent.emit({
1020
- type: 'message_complete',
1021
- data: { messageId, content }
1022
- });
1023
- }
1024
-
1025
- /**
1026
- * Handles tool-result stream parts.
1027
- */
1028
- private _handleToolResult(part: TypedToolResult<ToolMap>): void {
1029
- const isError =
1030
- typeof part.output === 'object' &&
1031
- part.output !== null &&
1032
- 'success' in part.output &&
1033
- part.output.success === false;
1034
-
1035
- this._agentEvent.emit({
1036
- type: 'tool_call_complete',
1037
- data: {
1038
- callId: part.toolCallId,
1039
- toolName: part.toolName,
1040
- outputData: part.output,
1041
- isError
1042
- }
1043
- });
1044
- }
1045
-
1046
- /**
1047
- * Handles tool-error stream parts.
1048
- */
1049
- private _handleToolError(part: TypedToolError<ToolMap>): void {
1050
- const output =
1051
- typeof part.error === 'string'
1052
- ? part.error
1053
- : part.error instanceof Error
1054
- ? part.error.message
1055
- : JSON.stringify(part.error, null, 2);
1056
-
1057
- this._agentEvent.emit({
1058
- type: 'tool_call_complete',
1059
- data: {
1060
- callId: part.toolCallId,
1061
- toolName: part.toolName,
1062
- outputData: output,
1063
- isError: true
1064
- }
1065
- });
1066
- }
1067
-
1068
- /**
1069
- * Handles tool-output-denied stream parts.
1070
- */
1071
- private _handleToolOutputDenied(part: TypedToolOutputDenied<ToolMap>): void {
1072
- this._agentEvent.emit({
1073
- type: 'tool_call_complete',
1074
- data: {
1075
- callId: part.toolCallId,
1076
- toolName: part.toolName,
1077
- outputData: 'Tool output was denied.',
1078
- isError: true
1079
- }
1080
- });
1081
- }
1082
-
1083
- /**
1084
- * Handles tool-approval-request stream parts.
1085
- */
1086
- private async _handleApprovalRequest(
1087
- part: ToolApprovalRequestOutput<ToolMap>,
1088
- result: IStreamProcessResult
1089
- ): Promise<void> {
1090
- const { approvalId, toolCall } = part;
1091
-
1092
- this._agentEvent.emit({
1093
- type: 'tool_approval_request',
1094
- data: {
1095
- approvalId,
1096
- toolCallId: toolCall.toolCallId,
1097
- toolName: toolCall.toolName,
1098
- args: toolCall.input
1099
- }
1100
- });
1101
-
1102
- const approved = await this._waitForApproval(approvalId);
1103
-
1104
- result.approvalProcessed = true;
1105
- result.approvalResponse = {
1106
- role: 'tool',
1107
- content: [
1108
- {
1109
- type: 'tool-approval-response',
1110
- approvalId,
1111
- approved
1112
- }
1113
- ]
1114
- };
1115
- }
1116
-
1117
- /**
1118
- * Waits for user approval of a tool call.
1119
- * @param approvalId The approval ID to wait for
1120
- * @returns Promise that resolves to true if approved, false if rejected
1121
- */
1122
- private _waitForApproval(approvalId: string): Promise<boolean> {
1123
- return new Promise(resolve => {
1124
- this._pendingApprovals.set(approvalId, {
1125
- resolve: (approved: boolean) => {
1126
- resolve(approved);
1127
- }
1128
- });
1129
- });
1130
- }
1131
-
1132
- /**
1133
- * Formats tool input for display by pretty-printing JSON strings.
1134
- * @param input The tool input string to format
1135
- * @returns Pretty-printed JSON string
1136
- */
1137
- private _formatToolInput(input: string): string {
1138
- try {
1139
- const parsed = JSON.parse(input);
1140
- return JSON.stringify(parsed, null, 2);
1141
- } catch {
1142
- return input;
1143
- }
1144
- }
1145
-
1146
- /**
1147
- * Checks if the current provider supports tool calling.
1148
- * @returns True if the provider supports tool calling, false otherwise
1149
- */
1150
- private _supportsToolCalling(): boolean {
1151
- const activeProviderConfig = this._settingsModel.getProvider(
1152
- this._activeProvider
1153
- );
1154
- if (!activeProviderConfig || !this._providerRegistry) {
1155
- return false;
1156
- }
1157
-
1158
- const providerInfo = this._providerRegistry.getProviderInfo(
1159
- activeProviderConfig.provider
1160
- );
1161
-
1162
- // Default to true if supportsToolCalling is not specified
1163
- return providerInfo?.supportsToolCalling !== false;
1164
- }
1165
-
1166
- /**
1167
- * Creates a model instance based on current settings.
1168
- * @returns The configured model instance for the agent
1169
- */
1170
- private async _createModel() {
1171
- if (!this._activeProvider) {
1172
- throw new Error('No active provider configured');
1173
- }
1174
- const activeProviderConfig = this._settingsModel.getProvider(
1175
- this._activeProvider
1176
- );
1177
- if (!activeProviderConfig) {
1178
- throw new Error('No active provider configured');
1179
- }
1180
- const provider = activeProviderConfig.provider;
1181
- const model = activeProviderConfig.model;
1182
- const baseURL = activeProviderConfig.baseURL;
1183
-
1184
- let apiKey: string;
1185
- if (this._secretsManager && this._settingsModel.config.useSecretsManager) {
1186
- const token = Private.getToken();
1187
- if (!token) {
1188
- // This should never happen, the secrets manager should be disabled.
1189
- console.error(
1190
- '@jupyterlite/ai::AgentManager error: the settings manager token is not set.\nYou should disable the the secrets manager from the AI settings.'
1191
- );
1192
- apiKey = '';
1193
- } else {
1194
- apiKey =
1195
- (
1196
- await this._secretsManager.get(
1197
- token,
1198
- SECRETS_NAMESPACE,
1199
- `${provider}:apiKey`
1200
- )
1201
- )?.value ?? '';
1202
- }
1203
- } else {
1204
- apiKey = this._settingsModel.getApiKey(activeProviderConfig.id);
1205
- }
1206
-
1207
- return createModel(
1208
- {
1209
- provider,
1210
- model,
1211
- apiKey,
1212
- baseURL
1213
- },
1214
- this._providerRegistry
1215
- );
1216
- }
1217
-
1218
- /**
1219
- * Enhances the base system prompt with dynamic context like skills.
1220
- * @param baseSystemPrompt The base system prompt from settings
1221
- * @returns The enhanced system prompt with dynamic additions
1222
- */
1223
- private _getEnhancedSystemPrompt(
1224
- baseSystemPrompt: string,
1225
- tools: ToolMap
1226
- ): string {
1227
- let prompt = baseSystemPrompt;
1228
-
1229
- if (this._skills.length > 0) {
1230
- const lines = this._skills.map(
1231
- skill => `- ${skill.name}: ${skill.description}`
1232
- );
1233
- const skillsPrompt = `
1234
-
1235
- AGENT SKILLS:
1236
- Skills are provided via the skills registry and accessed through tools (not commands).
1237
- When a skill is relevant to the user's task, activate it by calling load_skill with the skill name to load its full instructions, then follow those instructions.
1238
- If the user explicitly asks for the latest list of skills, call discover_skills (optionally with a query).
1239
- Do NOT call discover_skills just to list skills; use the preloaded snapshot below instead unless you need to verify a skill not present in the snapshot.
1240
- If the load_skill result includes a non-empty "resources" array, those are bundled files (scripts, references, templates) you MUST load before proceeding. Only load the listed resource paths; never invent resource names. For each resource path, execute load_skill again with the resource argument, e.g.: load_skill({ name: "<skill>", resource: "<path>" }). Load all listed resources before starting the task.
1241
-
1242
- AVAILABLE SKILLS (preloaded snapshot):
1243
- ${lines.join('\n')}
1244
- `;
1245
- prompt += skillsPrompt;
1246
- }
1247
-
1248
- const toolNames = new Set(Object.keys(tools));
1249
- const hasBrowserFetch = toolNames.has('browser_fetch');
1250
- const hasWebFetch = toolNames.has('web_fetch');
1251
- const hasWebSearch = toolNames.has('web_search');
1252
-
1253
- if (hasBrowserFetch || hasWebFetch || hasWebSearch) {
1254
- const webRetrievalPrompt = `
1255
-
1256
- WEB RETRIEVAL POLICY:
1257
- - If the user asks about a specific URL and browser_fetch is available, call browser_fetch first for that URL.
1258
- - If browser_fetch fails due to CORS/network/access, try web_fetch (if available) for that same URL.
1259
- - If web_fetch fails with access/policy errors (for example: url_not_accessible or url_not_allowed) and browser_fetch is available, you MUST call browser_fetch for that same URL before searching.
1260
- - If either fetch method fails with temporary access/network issues (for example: network_or_cors), try the other fetch method if available before searching.
1261
- - Only fall back to web_search after both fetch methods fail or are unavailable.
1262
- - If the user explicitly asks to inspect one exact URL, do not skip directly to search unless both fetch methods fail or are unavailable.
1263
- - In your final response, state which retrieval method succeeded (browser_fetch, web_fetch, or web_search) and mention relevant limitations.
1264
- `;
1265
- prompt += webRetrievalPrompt;
1266
- }
1267
-
1268
- return prompt;
1269
- }
1270
-
1271
- /**
1272
- * Build an instruction line describing MIME types supported by this session.
1273
- */
1274
- private _getSupportedMimeTypesInstruction(): string {
1275
- const mimeTypes = this._renderMimeRegistry?.mimeTypes ?? [];
1276
- const safeMimeTypes = mimeTypes.filter(mimeType => {
1277
- const factory = this._renderMimeRegistry?.getFactory(mimeType);
1278
- return !!factory?.safe;
1279
- });
1280
-
1281
- if (safeMimeTypes.length === 0) {
1282
- return 'Supported MIME types are determined by the active JupyterLab renderers in this session.';
1283
- }
1284
-
1285
- return `Supported MIME types in this session: ${safeMimeTypes.join(', ')}`;
1286
- }
1287
-
1288
- // Private attributes
1289
- private _settingsModel: IAISettingsModel;
1290
- private _toolRegistry?: IToolRegistry;
1291
- private _providerRegistry?: IProviderRegistry;
1292
- private _skillRegistry?: ISkillRegistry;
1293
- private _secretsManager?: ISecretsManager;
1294
- private _selectedToolNames: string[];
1295
- private _agent: ToolLoopAgent<never, ToolMap> | null;
1296
- private _history: ModelMessage[];
1297
- private _mcpTools: ToolMap;
1298
- private _controller: AbortController | null;
1299
- private _agentEvent: Signal<this, IAgentManager.IAgentEvent>;
1300
- private _tokenUsage: ITokenUsage;
1301
- private _tokenUsageChanged: Signal<this, ITokenUsage>;
1302
- private _activeProvider: string = '';
1303
- private _activeProviderChanged = new Signal<this, string | undefined>(this);
1304
- private _skills: ISkillSummary[];
1305
- private _renderMimeRegistry?: IRenderMimeRegistry;
1306
- private _initQueue: Promise<void> = Promise.resolve();
1307
- private _agentConfig: IAgentConfig | null;
1308
- private _pendingApprovals: Map<
1309
- string,
1310
- { resolve: (approved: boolean, reason?: string) => void }
1311
- > = new Map();
1312
- private _streaming: PromiseDelegate<void> = new PromiseDelegate();
1313
- }
1314
-
1315
- namespace Private {
1316
- /**
1317
- * Sanitize the messages before adding them to the history.
1318
- *
1319
- * 1- Make sure the message sequence is not altered:
1320
- * - tool-call messages should have a corresponding tool-result (and vice-versa)
1321
- * - tool-approval-request should have a tool-approval-response (and vice-versa)
1322
- *
1323
- * 2- Keep only serializable messages by doing a JSON round-trip.
1324
- * Messages that cannot be serialized are dropped.
1325
- */
1326
- export const sanitizeModelMessages = (
1327
- messages: ModelMessage[]
1328
- ): ModelMessage[] => {
1329
- const sanitized: ModelMessage[] = [];
1330
- for (const message of messages) {
1331
- if (message.role === 'assistant') {
1332
- let newMessage: AssistantModelMessage | undefined;
1333
- if (!Array.isArray(message.content)) {
1334
- newMessage = message;
1335
- } else {
1336
- // Remove assistant message content without a required response.
1337
- const newContent: typeof message.content = [];
1338
- for (const assistantContent of message.content) {
1339
- let isContentValid = true;
1340
- if (assistantContent.type === 'tool-call') {
1341
- const toolCallId = assistantContent.toolCallId;
1342
- isContentValid = !!messages.find(
1343
- msg =>
1344
- msg.role === 'tool' &&
1345
- Array.isArray(msg.content) &&
1346
- msg.content.find(
1347
- content =>
1348
- content.type === 'tool-result' &&
1349
- content.toolCallId === toolCallId
1350
- )
1351
- );
1352
- } else if (assistantContent.type === 'tool-approval-request') {
1353
- const approvalId = assistantContent.approvalId;
1354
- isContentValid = !!messages.find(
1355
- msg =>
1356
- msg.role === 'tool' &&
1357
- Array.isArray(msg.content) &&
1358
- msg.content.find(
1359
- content =>
1360
- content.type === 'tool-approval-response' &&
1361
- content.approvalId === approvalId
1362
- )
1363
- );
1364
- }
1365
- if (isContentValid) {
1366
- newContent.push(assistantContent);
1367
- }
1368
- }
1369
- if (newContent.length) {
1370
- newMessage = { ...message, content: newContent };
1371
- }
1372
- }
1373
- if (newMessage) {
1374
- try {
1375
- sanitized.push(JSON.parse(JSON.stringify(newMessage)));
1376
- } catch {
1377
- // Drop messages that cannot be serialized
1378
- }
1379
- }
1380
- } else if (message.role === 'tool') {
1381
- // Remove tool message content without request.
1382
- const newContent: typeof message.content = [];
1383
- for (const toolContent of message.content) {
1384
- let isContentValid = true;
1385
- if (toolContent.type === 'tool-result') {
1386
- const toolCallId = toolContent.toolCallId;
1387
- isContentValid = !!sanitized.find(
1388
- msg =>
1389
- msg.role === 'assistant' &&
1390
- Array.isArray(msg.content) &&
1391
- msg.content.find(
1392
- content =>
1393
- content.type === 'tool-call' &&
1394
- content.toolCallId === toolCallId
1395
- )
1396
- );
1397
- } else if (toolContent.type === 'tool-approval-response') {
1398
- const approvalId = toolContent.approvalId;
1399
- isContentValid = !!sanitized.find(
1400
- msg =>
1401
- msg.role === 'assistant' &&
1402
- Array.isArray(msg.content) &&
1403
- msg.content.find(
1404
- content =>
1405
- content.type === 'tool-approval-request' &&
1406
- content.approvalId === approvalId
1407
- )
1408
- );
1409
- }
1410
- if (isContentValid) {
1411
- newContent.push(toolContent);
1412
- }
1413
- }
1414
- if (newContent.length) {
1415
- try {
1416
- sanitized.push(
1417
- JSON.parse(JSON.stringify({ ...message, content: newContent }))
1418
- );
1419
- } catch {
1420
- // Drop messages that cannot be serialized
1421
- }
1422
- }
1423
- } else {
1424
- // Message is a system or user message.
1425
- sanitized.push(message);
1426
- }
1427
- }
1428
- return sanitized.length === messages.length ? sanitized : [];
1429
- };
1430
-
1431
- /**
1432
- * The token to use with the secrets manager, setter and getter.
1433
- */
1434
- let secretsToken: symbol | null;
1435
- export function setToken(value: symbol | null): void {
1436
- secretsToken = value;
1437
- }
1438
- export function getToken(): symbol | null {
1439
- return secretsToken;
1440
- }
1441
- }