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