@jupyterlite/ai 0.16.0 → 0.18.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 (40) hide show
  1. package/lib/agent.d.ts +19 -10
  2. package/lib/agent.js +82 -46
  3. package/lib/chat-commands/clear.js +1 -1
  4. package/lib/chat-model-handler.d.ts +2 -3
  5. package/lib/chat-model-handler.js +6 -2
  6. package/lib/chat-model.d.ts +129 -26
  7. package/lib/chat-model.js +543 -160
  8. package/lib/components/clear-button.d.ts +1 -1
  9. package/lib/components/clear-button.js +1 -1
  10. package/lib/components/save-button.d.ts +2 -2
  11. package/lib/index.js +224 -59
  12. package/lib/models/settings-model.js +1 -0
  13. package/lib/providers/built-in-providers.js +1 -1
  14. package/lib/providers/{generated-context-windows.d.ts → generated-model-info.d.ts} +2 -2
  15. package/lib/providers/generated-model-info.js +502 -0
  16. package/lib/providers/model-info.d.ts +3 -0
  17. package/lib/providers/model-info.js +33 -0
  18. package/lib/tokens.d.ts +98 -15
  19. package/lib/tokens.js +1 -0
  20. package/lib/widgets/ai-settings.js +5 -0
  21. package/lib/widgets/main-area-chat.d.ts +3 -3
  22. package/lib/widgets/main-area-chat.js +9 -5
  23. package/package.json +3 -3
  24. package/schema/settings-model.json +6 -0
  25. package/src/agent.ts +100 -52
  26. package/src/chat-commands/clear.ts +1 -1
  27. package/src/chat-model-handler.ts +10 -3
  28. package/src/chat-model.ts +727 -210
  29. package/src/components/clear-button.tsx +3 -3
  30. package/src/components/save-button.tsx +3 -3
  31. package/src/index.ts +289 -83
  32. package/src/models/settings-model.ts +1 -0
  33. package/src/providers/built-in-providers.ts +1 -1
  34. package/src/providers/generated-model-info.ts +508 -0
  35. package/src/providers/model-info.ts +57 -0
  36. package/src/tokens.ts +100 -15
  37. package/src/widgets/ai-settings.tsx +26 -0
  38. package/src/widgets/main-area-chat.ts +14 -9
  39. package/lib/providers/generated-context-windows.js +0 -96
  40. package/src/providers/generated-context-windows.ts +0 -102
package/lib/tokens.d.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { ActiveCellManager, IMessage, IMessageContent } from '@jupyter/chat';
1
+ import { ActiveCellManager, IChatModel, IMessage } from '@jupyter/chat';
2
2
  import { VDomRenderer } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Token } from '@lumino/coreutils';
5
5
  import type { IDisposable } from '@lumino/disposable';
6
6
  import { ISignal } from '@lumino/signaling';
7
- import type { Tool, LanguageModel } from 'ai';
7
+ import type { Tool, LanguageModel, UserContent, ModelMessage } from 'ai';
8
8
  import { ISecretsManager } from 'jupyter-secrets-manager';
9
9
  import type { IModelOptions } from './providers/models';
10
- import { AIChatModel } from './chat-model';
11
10
  import type { ISkillDefinition, ISkillRegistration, ISkillResourceResult, ISkillSummary } from './skills/types';
12
11
  export type { ISkillDefinition, ISkillRegistration, ISkillResourceResult, ISkillSummary } from './skills/types';
13
12
  /**
@@ -17,6 +16,7 @@ export declare namespace CommandIds {
17
16
  const openSettings = "@jupyterlite/ai:open-settings";
18
17
  const reposition = "@jupyterlite/ai:reposition";
19
18
  const openChat = "@jupyterlite/ai:open-chat";
19
+ const openOrRevealChat = "@jupyterlite/ai:open-or-reveal-chat";
20
20
  const moveChat = "@jupyterlite/ai:move-chat";
21
21
  const refreshSkills = "@jupyterlite/ai:refresh-skills";
22
22
  const saveChat = "@jupyterlite/ai:save-chat";
@@ -158,6 +158,18 @@ export interface IProviderModelInfo {
158
158
  * Default context window for the model in tokens.
159
159
  */
160
160
  contextWindow?: number;
161
+ /**
162
+ * Whether the model supports image inputs.
163
+ */
164
+ supportsImages?: boolean;
165
+ /**
166
+ * Whether the model supports PDF inputs.
167
+ */
168
+ supportsPdf?: boolean;
169
+ /**
170
+ * Whether the model supports audio inputs.
171
+ */
172
+ supportsAudio?: boolean;
161
173
  }
162
174
  export interface IProviderInfo {
163
175
  /**
@@ -302,6 +314,7 @@ export interface IAIConfig {
302
314
  diffDisplayMode: 'split' | 'unified';
303
315
  skillsPaths: string[];
304
316
  chatBackupDirectory: string;
317
+ autoTitle: boolean;
305
318
  }
306
319
  export interface IAISettingsModel extends VDomRenderer.IModel {
307
320
  readonly config: IAIConfig;
@@ -400,13 +413,12 @@ export declare namespace IAgentManager {
400
413
  isError: boolean;
401
414
  };
402
415
  tool_approval_request: {
403
- approvalId: string;
404
416
  toolCallId: string;
405
417
  toolName: string;
406
418
  args: unknown;
407
419
  };
408
420
  tool_approval_resolved: {
409
- approvalId: string;
421
+ toolCallId: string;
410
422
  approved: boolean;
411
423
  };
412
424
  error: {
@@ -465,34 +477,39 @@ export interface IAgentManager {
465
477
  /**
466
478
  * Clears conversation history and resets agent state.
467
479
  */
468
- clearHistory(): void;
480
+ clearHistory(): Promise<void>;
469
481
  /**
470
- * Sets the conversation history with a list of messages from the chat.
471
- * @param messages The chat messages to set as history
482
+ * Sets the history from already-processed model messages.
483
+ * @param messages Pre-built model messages (may include binary content)
472
484
  */
473
- setHistory(messages: IMessageContent[]): void;
485
+ setHistory(messages: ModelMessage[]): void;
474
486
  /**
475
487
  * Stops the current streaming response by aborting the request.
476
488
  */
477
489
  stopStreaming(): void;
478
490
  /**
479
491
  * Approves a pending tool call.
480
- * @param approvalId The approval ID to approve
492
+ * @param toolCallId The tool call ID to approve
481
493
  * @param reason Optional reason for approval
482
494
  */
483
- approveToolCall(approvalId: string, reason?: string): void;
495
+ approveToolCall(toolCallId: string, reason?: string): void;
484
496
  /**
485
497
  * Rejects a pending tool call.
486
- * @param approvalId The approval ID to reject
498
+ * @param toolCallId The tool call ID to reject
487
499
  * @param reason Optional reason for rejection
488
500
  */
489
- rejectToolCall(approvalId: string, reason?: string): void;
501
+ rejectToolCall(toolCallId: string, reason?: string): void;
490
502
  /**
491
503
  * Generates AI response to user message using the agent.
492
504
  * Handles the complete execution cycle including tool calls.
493
505
  * @param message The user message to respond to (may include processed attachment content)
494
506
  */
495
- generateResponse(message: string): Promise<void>;
507
+ generateResponse(message: UserContent): Promise<void>;
508
+ /**
509
+ * Create a transient language model to request a text response, which won't be added to history.
510
+ * @param messages - the messages sequence to send to the model.
511
+ */
512
+ textResponse(messages: ModelMessage[]): Promise<string>;
496
513
  /**
497
514
  * Initializes the AI agent with current settings and tools.
498
515
  * Sets up the agent with model configuration, tools, and MCP tools.
@@ -527,6 +544,68 @@ export interface IAgentManagerFactory {
527
544
  getMCPTools(): Promise<ToolMap>;
528
545
  }
529
546
  export declare const IAgentManagerFactory: Token<IAgentManagerFactory>;
547
+ export interface IAIChatModel extends IChatModel {
548
+ /**
549
+ * A signal emitting when the chat name has changed.
550
+ */
551
+ readonly nameChanged: ISignal<IAIChatModel, string>;
552
+ /**
553
+ * The title of the chat.
554
+ */
555
+ title: string | null;
556
+ /**
557
+ * A signal emitting when the chat title has changed.
558
+ */
559
+ readonly titleChanged: ISignal<IAIChatModel, string | null>;
560
+ /**
561
+ * Whether to save the chat automatically.
562
+ */
563
+ autosave: boolean;
564
+ /**
565
+ * A signal emitting when the autosave flag changed.
566
+ */
567
+ readonly autosaveChanged: ISignal<IAIChatModel, boolean>;
568
+ /**
569
+ * Whether save/restore is available.
570
+ */
571
+ readonly saveAvailable: boolean;
572
+ /**
573
+ * A signal emitting when the token usage changed.
574
+ */
575
+ readonly tokenUsageChanged: ISignal<IAgentManager, ITokenUsage>;
576
+ /**
577
+ * The agent manager used in the model.
578
+ */
579
+ readonly agentManager: IAgentManager;
580
+ /**
581
+ * Save the chat as json file.
582
+ */
583
+ save(): Promise<void>;
584
+ /**
585
+ * Restore the chat from a json file.
586
+ *
587
+ * @param silent - Whether a log should be displayed in the console if the
588
+ * restoration is not possible.
589
+ */
590
+ restore(filepath: string, silent?: boolean): Promise<boolean>;
591
+ /**
592
+ * Request a title to this chat, regarding the message history.
593
+ */
594
+ requestTitle(): Promise<string>;
595
+ /**
596
+ * Removes a queued message by its ID.
597
+ * @param messageId The ID of the queued message to remove
598
+ */
599
+ removeQueuedMessage(messageId: string): void;
600
+ /**
601
+ * The current message queue
602
+ */
603
+ messageQueue: any[];
604
+ /**
605
+ * Whether the chat is currently busy processing a message
606
+ */
607
+ isBusy: boolean;
608
+ }
530
609
  /**
531
610
  * The interface for the chat model handler.
532
611
  */
@@ -534,7 +613,7 @@ export interface IChatModelHandler {
534
613
  /**
535
614
  * The function to create a new model.
536
615
  */
537
- createModel(options: ICreateChatOptions): AIChatModel;
616
+ createModel(options: ICreateChatOptions): IAIChatModel;
538
617
  /**
539
618
  * The active cell manager (to copy code from chat to cell).
540
619
  */
@@ -561,6 +640,10 @@ export interface ICreateChatOptions {
561
640
  * Whether the chat is autosaved or not.
562
641
  */
563
642
  autosave?: boolean;
643
+ /**
644
+ * An optional title to the chat.
645
+ */
646
+ title?: string | null;
564
647
  }
565
648
  /**
566
649
  * Token for the chat model handler.
package/lib/tokens.js CHANGED
@@ -7,6 +7,7 @@ export var CommandIds;
7
7
  CommandIds.openSettings = '@jupyterlite/ai:open-settings';
8
8
  CommandIds.reposition = '@jupyterlite/ai:reposition';
9
9
  CommandIds.openChat = '@jupyterlite/ai:open-chat';
10
+ CommandIds.openOrRevealChat = '@jupyterlite/ai:open-or-reveal-chat';
10
11
  CommandIds.moveChat = '@jupyterlite/ai:move-chat';
11
12
  CommandIds.refreshSkills = '@jupyterlite/ai:refresh-skills';
12
13
  CommandIds.saveChat = '@jupyterlite/ai:save-chat';
@@ -482,6 +482,11 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
482
482
  }), color: "primary" }), label: React.createElement(Box, null,
483
483
  React.createElement(Typography, { variant: "body1" }, trans.__('Send with Shift+Enter')),
484
484
  React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Use Shift+Enter to send messages (Enter creates new line)'))) }),
485
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.autoTitle, onChange: e => handleConfigUpdate({
486
+ autoTitle: e.target.checked
487
+ }), color: "primary" }), label: React.createElement(Box, null,
488
+ React.createElement(Typography, { variant: "body1" }, trans.__('Auto Title')),
489
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Automatically generate a chat title from the model for every message until there are 5 messages'))) }),
485
490
  React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showTokenUsage, onChange: e => handleConfigUpdate({
486
491
  showTokenUsage: e.target.checked
487
492
  }), color: "primary" }), label: React.createElement(Box, null,
@@ -2,8 +2,7 @@ import { ChatWidget } from '@jupyter/chat';
2
2
  import { MainAreaWidget } from '@jupyterlab/apputils';
3
3
  import type { TranslationBundle } from '@jupyterlab/translation';
4
4
  import { CommandRegistry } from '@lumino/commands';
5
- import { AIChatModel } from '../chat-model';
6
- import { type IAISettingsModel } from '../tokens';
5
+ import { IAIChatModel, type IAISettingsModel } from '../tokens';
7
6
  export declare namespace MainAreaChat {
8
7
  interface IOptions extends MainAreaWidget.IOptions<ChatWidget> {
9
8
  commands: CommandRegistry;
@@ -20,11 +19,12 @@ export declare class MainAreaChat extends MainAreaWidget<ChatWidget> {
20
19
  /**
21
20
  * Get the model of the chat.
22
21
  */
23
- get model(): AIChatModel;
22
+ get model(): IAIChatModel;
24
23
  /**
25
24
  * Get the area of the chat.
26
25
  */
27
26
  get area(): string | undefined;
28
27
  private _writersChanged;
28
+ private _titleChanged;
29
29
  private _outputAreaCompat;
30
30
  }
@@ -10,7 +10,8 @@ import { CommandIds } from '../tokens';
10
10
  export class MainAreaChat extends MainAreaWidget {
11
11
  constructor(options) {
12
12
  super(options);
13
- this.title.label = this.content.model.name;
13
+ this.title.label = this.model.name;
14
+ this.title.caption = this.model.title ?? this.model.name;
14
15
  const { trans } = options;
15
16
  // Move to side button.
16
17
  this.toolbar.addItem('moveToSide', new CommandToolbarButton({
@@ -42,13 +43,15 @@ export class MainAreaChat extends MainAreaWidget {
42
43
  this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
43
44
  chatPanel: this.content
44
45
  });
45
- this.model.writersChanged.connect(this._writersChanged);
46
+ this.model.writersChanged?.connect(this._writersChanged);
47
+ this.model.titleChanged.connect(this._titleChanged);
46
48
  }
47
49
  dispose() {
48
50
  super.dispose();
49
51
  // Dispose of the approval buttons widget when the chat is disposed.
50
52
  this._outputAreaCompat.dispose();
51
- this.model.writersChanged.disconnect(this._writersChanged);
53
+ this.model.writersChanged?.disconnect(this._writersChanged);
54
+ this.model.titleChanged.disconnect(this._titleChanged);
52
55
  }
53
56
  /**
54
57
  * Get the model of the chat.
@@ -66,13 +69,14 @@ export class MainAreaChat extends MainAreaWidget {
66
69
  // Check if AI is currently writing (streaming)
67
70
  const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
68
71
  if (aiWriting) {
69
- this.content.inputToolbarRegistry?.hide('send');
70
72
  this.content.inputToolbarRegistry?.show('stop');
71
73
  }
72
74
  else {
73
75
  this.content.inputToolbarRegistry?.hide('stop');
74
- this.content.inputToolbarRegistry?.show('send');
75
76
  }
76
77
  };
78
+ _titleChanged = () => {
79
+ this.title.caption = this.model.title ?? this.model.name;
80
+ };
77
81
  _outputAreaCompat;
78
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/ai",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -54,7 +54,7 @@
54
54
  "watch:labextension": "jupyter labextension watch .",
55
55
  "docs": "jupyter book start",
56
56
  "docs:build": "sed -e 's/\\[@/[/g' -e 's/@/\\&#64;/g' CHANGELOG.md > docs/_changelog_content.md && jupyter book build --html",
57
- "sync:model-context-windows": "node scripts/sync-model-context-windows.mjs && prettier --write src/providers/generated-context-windows.ts && eslint --fix src/providers/generated-context-windows.ts"
57
+ "sync:model-info": "node scripts/sync-model-info.mjs && prettier --write src/providers/generated-model-info.ts && eslint --fix src/providers/generated-model-info.ts"
58
58
  },
59
59
  "dependencies": {
60
60
  "@ai-sdk/anthropic": "^3.0.58",
@@ -89,7 +89,7 @@
89
89
  "@mui/icons-material": "^7",
90
90
  "@mui/material": "^7",
91
91
  "ai": "^6.0.116",
92
- "jupyter-chat-components": "^0.2.0",
92
+ "jupyter-chat-components": "^0.5.0",
93
93
  "jupyter-secrets-manager": "^0.5.0",
94
94
  "yaml": "^2.8.1",
95
95
  "zod": "^4.3.6"
@@ -210,6 +210,12 @@
210
210
  "type": "boolean",
211
211
  "default": false
212
212
  },
213
+ "autoTitle": {
214
+ "title": "Auto Title",
215
+ "description": "Automatically request a title from the model for every message until there are 5 messages",
216
+ "type": "boolean",
217
+ "default": false
218
+ },
213
219
  "showTokenUsage": {
214
220
  "title": "Show Token Usage",
215
221
  "description": "Display token usage information in the chat toolbar",
package/src/agent.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
2
- import type { IMessageContent } from '@jupyter/chat';
3
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
3
  import { PromiseDelegate } from '@lumino/coreutils';
5
4
  import { ISignal, Signal } from '@lumino/signaling';
6
5
  import {
6
+ generateText,
7
7
  ToolLoopAgent,
8
8
  type ModelMessage,
9
9
  type LanguageModel,
@@ -13,7 +13,9 @@ import {
13
13
  type TypedToolError,
14
14
  type TypedToolOutputDenied,
15
15
  type TypedToolResult,
16
- type AssistantModelMessage
16
+ type UserContent,
17
+ type AssistantModelMessage,
18
+ APICallError
17
19
  } from 'ai';
18
20
  import { ISecretsManager } from 'jupyter-secrets-manager';
19
21
 
@@ -487,31 +489,12 @@ export class AgentManager implements IAgentManager {
487
489
  }
488
490
 
489
491
  /**
490
- * Sets the history with a list of messages from the chat.
491
- * @param messages The chat messages to set as history
492
+ * Sets the history from already-processed model messages.
493
+ * @param messages Pre-built model messages (may include binary content)
492
494
  */
493
- setHistory(messages: IMessageContent[]): void {
494
- // Stop any ongoing streaming and reject awaiting approvals
495
- this.stopStreaming();
496
-
497
- for (const [approvalId, pending] of this._pendingApprovals) {
498
- pending.resolve(false, 'Chat history changed');
499
- this._agentEvent.emit({
500
- type: 'tool_approval_resolved',
501
- data: { approvalId, approved: false }
502
- });
503
- }
504
- this._pendingApprovals.clear();
505
-
506
- // Convert chat messages to model messages
507
- const modelMessages = messages.map(msg => {
508
- const isAIMessage = msg.sender.username === 'ai-assistant';
509
- return {
510
- role: isAIMessage ? 'assistant' : 'user',
511
- content: msg.body
512
- } as ModelMessage;
513
- });
514
- this._history = Private.sanitizeModelMessages(modelMessages);
495
+ setHistory(messages: ModelMessage[]): void {
496
+ this.stopStreaming('Chat history changed');
497
+ this._history = Private.sanitizeModelMessages(messages);
515
498
  }
516
499
 
517
500
  /**
@@ -522,11 +505,11 @@ export class AgentManager implements IAgentManager {
522
505
  this._controller?.abort();
523
506
 
524
507
  // Reject any pending approvals
525
- for (const [approvalId, pending] of this._pendingApprovals) {
508
+ for (const [toolCallId, pending] of this._pendingApprovals) {
526
509
  pending.resolve(false, reason ?? 'Stream ended by user');
527
510
  this._agentEvent.emit({
528
511
  type: 'tool_approval_resolved',
529
- data: { approvalId, approved: false }
512
+ data: { toolCallId, approved: false }
530
513
  });
531
514
  }
532
515
  this._pendingApprovals.clear();
@@ -534,34 +517,34 @@ export class AgentManager implements IAgentManager {
534
517
 
535
518
  /**
536
519
  * Approves a pending tool call.
537
- * @param approvalId The approval ID to approve
520
+ * @param toolCallId The tool call ID to approve
538
521
  * @param reason Optional reason for approval
539
522
  */
540
- approveToolCall(approvalId: string, reason?: string): void {
541
- const pending = this._pendingApprovals.get(approvalId);
523
+ approveToolCall(toolCallId: string, reason?: string): void {
524
+ const pending = this._pendingApprovals.get(toolCallId);
542
525
  if (pending) {
543
526
  pending.resolve(true, reason);
544
- this._pendingApprovals.delete(approvalId);
527
+ this._pendingApprovals.delete(toolCallId);
545
528
  this._agentEvent.emit({
546
529
  type: 'tool_approval_resolved',
547
- data: { approvalId, approved: true }
530
+ data: { toolCallId, approved: true }
548
531
  });
549
532
  }
550
533
  }
551
534
 
552
535
  /**
553
536
  * Rejects a pending tool call.
554
- * @param approvalId The approval ID to reject
537
+ * @param toolCallId The tool call ID to reject
555
538
  * @param reason Optional reason for rejection
556
539
  */
557
- rejectToolCall(approvalId: string, reason?: string): void {
558
- const pending = this._pendingApprovals.get(approvalId);
540
+ rejectToolCall(toolCallId: string, reason?: string): void {
541
+ const pending = this._pendingApprovals.get(toolCallId);
559
542
  if (pending) {
560
543
  pending.resolve(false, reason);
561
- this._pendingApprovals.delete(approvalId);
544
+ this._pendingApprovals.delete(toolCallId);
562
545
  this._agentEvent.emit({
563
546
  type: 'tool_approval_resolved',
564
- data: { approvalId, approved: false }
547
+ data: { toolCallId, approved: false }
565
548
  });
566
549
  }
567
550
  }
@@ -571,10 +554,17 @@ export class AgentManager implements IAgentManager {
571
554
  * Handles the complete execution cycle including tool calls.
572
555
  * @param message The user message to respond to (may include processed attachment content)
573
556
  */
574
- async generateResponse(message: string): Promise<void> {
557
+ async generateResponse(message: UserContent): Promise<void> {
575
558
  this._streaming = new PromiseDelegate();
576
559
  this._controller = new AbortController();
577
560
  const responseHistory: ModelMessage[] = [];
561
+
562
+ // Add user message to history
563
+ responseHistory.push({
564
+ role: 'user',
565
+ content: message
566
+ });
567
+
578
568
  try {
579
569
  // Ensure we have an agent
580
570
  if (!this._agent) {
@@ -585,12 +575,6 @@ export class AgentManager implements IAgentManager {
585
575
  throw new Error('Failed to initialize agent');
586
576
  }
587
577
 
588
- // Add user message to history
589
- responseHistory.push({
590
- role: 'user',
591
- content: message
592
- });
593
-
594
578
  let continueLoop = true;
595
579
  while (continueLoop) {
596
580
  const result = await this._agent.stream({
@@ -647,9 +631,32 @@ export class AgentManager implements IAgentManager {
647
631
  this._history.push(...Private.sanitizeModelMessages(responseHistory));
648
632
  } catch (error) {
649
633
  if ((error as Error).name !== 'AbortError') {
634
+ let helpMessage = `${(error as Error).message}`;
635
+
636
+ // Remove attachments from history on payload rejection errors
637
+ if (
638
+ APICallError.isInstance(error) &&
639
+ (error.statusCode === 400 ||
640
+ error.statusCode === 404 ||
641
+ error.statusCode === 413 ||
642
+ error.statusCode === 415 ||
643
+ error.statusCode === 422)
644
+ ) {
645
+ this._stripAttachments(
646
+ [...this._history, ...responseHistory],
647
+ '_Attachment removed due to error_'
648
+ );
649
+ helpMessage +=
650
+ '\n\nAttachments have been removed from history. Please send your prompt again.';
651
+ }
650
652
  this._agentEvent.emit({
651
653
  type: 'error',
652
- data: { error: error as Error }
654
+ data: { error: new Error(helpMessage) }
655
+ });
656
+ this._history.push(...Private.sanitizeModelMessages(responseHistory));
657
+ this._history.push({
658
+ role: 'assistant',
659
+ content: helpMessage
653
660
  });
654
661
  }
655
662
  } finally {
@@ -658,6 +665,24 @@ export class AgentManager implements IAgentManager {
658
665
  }
659
666
  }
660
667
 
668
+ /**
669
+ * Create a transient language model to request a text response which won't be added to history.
670
+ * @param messages - the messages sequence to send to the model.
671
+ */
672
+ async textResponse(messages: ModelMessage[]): Promise<string> {
673
+ try {
674
+ const model = await this._createModel();
675
+ const result = await generateText({
676
+ model,
677
+ messages
678
+ });
679
+ this._updateTokenUsage(result.totalUsage, result.totalUsage.inputTokens);
680
+ return result.text;
681
+ } catch (e) {
682
+ throw `Error while getting the topic of the chat\n${e}`;
683
+ }
684
+ }
685
+
661
686
  /**
662
687
  * Updates cumulative token usage statistics from a completed model step.
663
688
  */
@@ -680,6 +705,27 @@ export class AgentManager implements IAgentManager {
680
705
  this._tokenUsageChanged.emit(this._tokenUsage);
681
706
  }
682
707
 
708
+ /**
709
+ * Removes image and file parts from all user messages in the given list.
710
+ */
711
+ private _stripAttachments(
712
+ messages: ModelMessage[],
713
+ placeholder: string
714
+ ): void {
715
+ for (const msg of messages) {
716
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
717
+ const hasMedia = msg.content.some(p => p.type !== 'text');
718
+ if (hasMedia) {
719
+ const textContent = msg.content
720
+ .filter(p => p.type === 'text')
721
+ .map(p => (p as { text: string }).text)
722
+ .join('\n');
723
+ msg.content = textContent || placeholder;
724
+ }
725
+ }
726
+ }
727
+ }
728
+
683
729
  /**
684
730
  * Gets the configured context window for the active provider.
685
731
  */
@@ -932,6 +978,9 @@ ${richOutputWorkflowInstruction}`;
932
978
  await this._handleApprovalRequest(part, processResult);
933
979
  break;
934
980
 
981
+ case 'error':
982
+ throw part.error;
983
+
935
984
  case 'finish-step':
936
985
  this._updateTokenUsage(part.usage, part.usage.inputTokens);
937
986
  break;
@@ -940,7 +989,7 @@ ${richOutputWorkflowInstruction}`;
940
989
  processResult.aborted = true;
941
990
  break;
942
991
 
943
- // Ignore: text-start, text-end, finish, error, and others
992
+ // Ignore: text-start, text-end, finish, and others
944
993
  default:
945
994
  break;
946
995
  }
@@ -1034,14 +1083,13 @@ ${richOutputWorkflowInstruction}`;
1034
1083
  this._agentEvent.emit({
1035
1084
  type: 'tool_approval_request',
1036
1085
  data: {
1037
- approvalId,
1038
1086
  toolCallId: toolCall.toolCallId,
1039
1087
  toolName: toolCall.toolName,
1040
1088
  args: toolCall.input
1041
1089
  }
1042
1090
  });
1043
1091
 
1044
- const approved = await this._waitForApproval(approvalId);
1092
+ const approved = await this._waitForApproval(toolCall.toolCallId);
1045
1093
 
1046
1094
  result.approvalProcessed = true;
1047
1095
  result.approvalResponse = {
@@ -1058,12 +1106,12 @@ ${richOutputWorkflowInstruction}`;
1058
1106
 
1059
1107
  /**
1060
1108
  * Waits for user approval of a tool call.
1061
- * @param approvalId The approval ID to wait for
1109
+ * @param toolCallId The tool call ID to wait for approval
1062
1110
  * @returns Promise that resolves to true if approved, false if rejected
1063
1111
  */
1064
- private _waitForApproval(approvalId: string): Promise<boolean> {
1112
+ private _waitForApproval(toolCallId: string): Promise<boolean> {
1065
1113
  return new Promise(resolve => {
1066
- this._pendingApprovals.set(approvalId, {
1114
+ this._pendingApprovals.set(toolCallId, {
1067
1115
  resolve: (approved: boolean) => {
1068
1116
  resolve(approved);
1069
1117
  }
@@ -29,7 +29,7 @@ export class ClearCommandProvider implements IChatCommandProvider {
29
29
  const context = inputModel.chatContext as
30
30
  | AIChatModel.IAIChatContext
31
31
  | undefined;
32
- context?.clearMessages?.();
32
+ await context?.clearMessages?.();
33
33
 
34
34
  inputModel.value = '';
35
35
  inputModel.clearAttachments();
@@ -6,6 +6,7 @@ import { Contents } from '@jupyterlab/services';
6
6
  import { AIChatModel } from './chat-model';
7
7
  import type {
8
8
  IAgentManagerFactory,
9
+ IAIChatModel,
9
10
  IAISettingsModel,
10
11
  IChatModelHandler,
11
12
  ICreateChatOptions,
@@ -28,8 +29,9 @@ export class ChatModelHandler implements IChatModelHandler {
28
29
  this._contentsManager = options.contentsManager;
29
30
  }
30
31
 
31
- createModel(options: ICreateChatOptions): AIChatModel {
32
- const { name, activeProvider, tokenUsage, messages, autosave } = options;
32
+ createModel(options: ICreateChatOptions): IAIChatModel {
33
+ const { name, activeProvider, tokenUsage, messages, autosave, title } =
34
+ options;
33
35
 
34
36
  // Create Agent Manager first so it can be shared
35
37
  const agentManager = this._agentManagerFactory.createAgent({
@@ -48,7 +50,8 @@ export class ChatModelHandler implements IChatModelHandler {
48
50
  agentManager,
49
51
  activeCellManager: this._activeCellManager,
50
52
  documentManager: this._docManager,
51
- contentsManager: this._contentsManager
53
+ contentsManager: this._contentsManager,
54
+ providerRegistry: this._providerRegistry
52
55
  });
53
56
 
54
57
  messages?.forEach(message => {
@@ -58,6 +61,10 @@ export class ChatModelHandler implements IChatModelHandler {
58
61
 
59
62
  model.name = name;
60
63
 
64
+ if (title) {
65
+ model.title = title;
66
+ }
67
+
61
68
  return model;
62
69
  }
63
70