@jupyterlite/ai 0.14.0 → 0.16.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 (64) hide show
  1. package/lib/agent.d.ts +33 -115
  2. package/lib/agent.js +192 -106
  3. package/lib/chat-model-handler.d.ts +9 -11
  4. package/lib/chat-model-handler.js +9 -4
  5. package/lib/chat-model.d.ts +84 -13
  6. package/lib/chat-model.js +214 -136
  7. package/lib/completion/completion-provider.d.ts +2 -3
  8. package/lib/components/completion-status.d.ts +2 -2
  9. package/lib/components/index.d.ts +1 -1
  10. package/lib/components/index.js +1 -1
  11. package/lib/components/model-select.d.ts +3 -3
  12. package/lib/components/save-button.d.ts +31 -0
  13. package/lib/components/save-button.js +41 -0
  14. package/lib/components/tool-select.d.ts +3 -4
  15. package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +13 -14
  16. package/lib/components/usage-display.js +109 -0
  17. package/lib/diff-manager.d.ts +2 -3
  18. package/lib/index.d.ts +2 -4
  19. package/lib/index.js +186 -28
  20. package/lib/models/settings-model.d.ts +11 -53
  21. package/lib/models/settings-model.js +38 -22
  22. package/lib/providers/built-in-providers.js +22 -36
  23. package/lib/providers/generated-context-windows.d.ts +8 -0
  24. package/lib/providers/generated-context-windows.js +96 -0
  25. package/lib/providers/model-info.d.ts +3 -0
  26. package/lib/providers/model-info.js +58 -0
  27. package/lib/tokens.d.ts +361 -36
  28. package/lib/tokens.js +18 -13
  29. package/lib/tools/commands.d.ts +2 -3
  30. package/lib/widgets/ai-settings.d.ts +3 -5
  31. package/lib/widgets/ai-settings.js +12 -0
  32. package/lib/widgets/main-area-chat.d.ts +2 -3
  33. package/lib/widgets/main-area-chat.js +12 -12
  34. package/lib/widgets/provider-config-dialog.d.ts +1 -2
  35. package/lib/widgets/provider-config-dialog.js +34 -34
  36. package/package.json +17 -10
  37. package/schema/settings-model.json +18 -1
  38. package/src/agent.ts +275 -248
  39. package/src/chat-model-handler.ts +25 -21
  40. package/src/chat-model.ts +307 -196
  41. package/src/completion/completion-provider.ts +7 -4
  42. package/src/components/completion-status.tsx +3 -3
  43. package/src/components/index.ts +1 -1
  44. package/src/components/model-select.tsx +4 -3
  45. package/src/components/save-button.tsx +84 -0
  46. package/src/components/tool-select.tsx +10 -4
  47. package/src/components/usage-display.tsx +208 -0
  48. package/src/diff-manager.ts +4 -4
  49. package/src/index.ts +250 -58
  50. package/src/models/settings-model.ts +46 -88
  51. package/src/providers/built-in-providers.ts +22 -36
  52. package/src/providers/generated-context-windows.ts +102 -0
  53. package/src/providers/model-info.ts +88 -0
  54. package/src/tokens.ts +438 -58
  55. package/src/tools/commands.ts +2 -3
  56. package/src/widgets/ai-settings.tsx +69 -15
  57. package/src/widgets/main-area-chat.ts +18 -15
  58. package/src/widgets/provider-config-dialog.tsx +96 -61
  59. package/style/base.css +17 -195
  60. package/lib/approval-buttons.d.ts +0 -49
  61. package/lib/approval-buttons.js +0 -79
  62. package/lib/components/token-usage-display.js +0 -72
  63. package/src/approval-buttons.ts +0 -115
  64. package/src/components/token-usage-display.tsx +0 -138
package/src/agent.ts CHANGED
@@ -1,3 +1,7 @@
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';
1
5
  import { ISignal, Signal } from '@lumino/signaling';
2
6
  import {
3
7
  ToolLoopAgent,
@@ -5,29 +9,32 @@ import {
5
9
  type LanguageModel,
6
10
  stepCountIs,
7
11
  type StreamTextResult,
8
- type Tool,
9
12
  type ToolApprovalRequestOutput,
10
13
  type TypedToolError,
11
14
  type TypedToolOutputDenied,
12
- type TypedToolResult
15
+ type TypedToolResult,
16
+ type AssistantModelMessage
13
17
  } from 'ai';
14
- import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
15
18
  import { ISecretsManager } from 'jupyter-secrets-manager';
16
- import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
17
19
 
18
- import { AISettingsModel } from './models/settings-model';
19
20
  import { createModel } from './providers/models';
21
+ import { getEffectiveContextWindow } from './providers/model-info';
20
22
  import {
21
23
  createProviderTools,
22
24
  type IProviderCustomSettings
23
25
  } from './providers/provider-tools';
24
- import type { IProviderInfo, IProviderRegistry } from './tokens';
25
26
  import {
26
- ISkillRegistry,
27
- ISkillSummary,
28
- ITool,
29
- IToolRegistry,
30
- ITokenUsage,
27
+ type IAgentManager,
28
+ type IAgentManagerFactory,
29
+ type IAISettingsModel,
30
+ type IProviderInfo,
31
+ type IProviderRegistry,
32
+ type ISkillRegistry,
33
+ type ISkillSummary,
34
+ type ITool,
35
+ type IToolRegistry,
36
+ type ITokenUsage,
37
+ type ToolMap,
31
38
  SECRETS_NAMESPACE
32
39
  } from './tokens';
33
40
 
@@ -39,8 +46,6 @@ interface IMCPClientWrapper {
39
46
  client: MCPClient;
40
47
  }
41
48
 
42
- type ToolMap = Record<string, Tool>;
43
-
44
49
  /**
45
50
  * Result from processing a stream, including approval info if applicable.
46
51
  */
@@ -49,18 +54,25 @@ interface IStreamProcessResult {
49
54
  * Whether an approval request was encountered and processed.
50
55
  */
51
56
  approvalProcessed: boolean;
57
+ /**
58
+ * Whether the stream was aborted before completion.
59
+ */
60
+ aborted: boolean;
52
61
  /**
53
62
  * The approval response message to add to history (if approval was processed).
54
63
  */
55
64
  approvalResponse?: ModelMessage;
56
65
  }
57
66
 
67
+ /**
68
+ * The agent manager factory namespace.
69
+ */
58
70
  export namespace AgentManagerFactory {
59
71
  export interface IOptions {
60
72
  /**
61
73
  * The settings model.
62
74
  */
63
- settingsModel: AISettingsModel;
75
+ settingsModel: IAISettingsModel;
64
76
  /**
65
77
  * The skill registry for discovering skills.
66
78
  */
@@ -75,7 +87,11 @@ export namespace AgentManagerFactory {
75
87
  token: symbol | null;
76
88
  }
77
89
  }
78
- export class AgentManagerFactory {
90
+
91
+ /**
92
+ * The agent manager factory.
93
+ */
94
+ export class AgentManagerFactory implements IAgentManagerFactory {
79
95
  constructor(options: AgentManagerFactory.IOptions) {
80
96
  Private.setToken(options.token);
81
97
  this._settingsModel = options.settingsModel;
@@ -104,7 +120,10 @@ export class AgentManagerFactory {
104
120
  }
105
121
  }
106
122
 
107
- createAgent(options: IAgentManagerOptions): AgentManager {
123
+ /**
124
+ * Create a new agent.
125
+ */
126
+ createAgent(options: IAgentManager.IOptions): IAgentManager {
108
127
  const agentManager = new AgentManager({
109
128
  ...options,
110
129
  skillRegistry: this._skillRegistry,
@@ -136,7 +155,7 @@ export class AgentManagerFactory {
136
155
  }
137
156
 
138
157
  /**
139
- * Checks if a specific MCP server is connected by server name.
158
+ * Checks whether a specific MCP server is connected.
140
159
  * @param serverName The name of the MCP server to check
141
160
  * @returns True if the server is connected, false otherwise
142
161
  */
@@ -253,8 +272,8 @@ export class AgentManagerFactory {
253
272
  });
254
273
  }
255
274
 
256
- private _agentManagers: AgentManager[] = [];
257
- private _settingsModel: AISettingsModel;
275
+ private _agentManagers: IAgentManager[] = [];
276
+ private _settingsModel: IAISettingsModel;
258
277
  private _skillRegistry?: ISkillRegistry;
259
278
  private _secretsManager?: ISecretsManager;
260
279
  private _mcpClients: IMCPClientWrapper[];
@@ -268,60 +287,6 @@ export class AgentManagerFactory {
268
287
  const DEFAULT_TEMPERATURE = 0.7;
269
288
  const DEFAULT_MAX_TURNS = 25;
270
289
 
271
- /**
272
- * Event type mapping for type safety with inlined interface definitions
273
- */
274
- export interface IAgentEventTypeMap {
275
- message_start: {
276
- messageId: string;
277
- };
278
- message_chunk: {
279
- messageId: string;
280
- chunk: string;
281
- fullContent: string;
282
- };
283
- message_complete: {
284
- messageId: string;
285
- content: string;
286
- };
287
- tool_call_start: {
288
- callId: string;
289
- toolName: string;
290
- input: string;
291
- };
292
- tool_call_complete: {
293
- callId: string;
294
- toolName: string;
295
- outputData: unknown;
296
- isError: boolean;
297
- };
298
- tool_approval_request: {
299
- approvalId: string;
300
- toolCallId: string;
301
- toolName: string;
302
- args: unknown;
303
- };
304
- tool_approval_resolved: {
305
- approvalId: string;
306
- approved: boolean;
307
- };
308
- error: {
309
- error: Error;
310
- };
311
- }
312
-
313
- /**
314
- * Events emitted by the AgentManager
315
- */
316
- export type IAgentEvent<
317
- T extends keyof IAgentEventTypeMap = keyof IAgentEventTypeMap
318
- > = T extends keyof IAgentEventTypeMap
319
- ? {
320
- type: T;
321
- data: IAgentEventTypeMap[T];
322
- }
323
- : never;
324
-
325
290
  /**
326
291
  * Cached configuration used to (re)build the agent.
327
292
  */
@@ -335,63 +300,18 @@ interface IAgentConfig {
335
300
  shouldUseTools: boolean;
336
301
  }
337
302
 
338
- /**
339
- * Configuration options for the AgentManager
340
- */
341
- export interface IAgentManagerOptions {
342
- /**
343
- * AI settings model for configuration
344
- */
345
- settingsModel: AISettingsModel;
346
-
347
- /**
348
- * Optional tool registry for managing available tools
349
- */
350
- toolRegistry?: IToolRegistry;
351
-
352
- /**
353
- * Optional provider registry for model creation
354
- */
355
- providerRegistry?: IProviderRegistry;
356
-
357
- /**
358
- * The skill registry for discovering skills.
359
- */
360
- skillRegistry?: ISkillRegistry;
361
-
362
- /**
363
- * The secrets manager.
364
- */
365
- secretsManager?: ISecretsManager;
366
-
367
- /**
368
- * The active provider to use with this agent.
369
- */
370
- activeProvider?: string;
371
-
372
- /**
373
- * Initial token usage.
374
- */
375
- tokenUsage?: ITokenUsage;
376
-
377
- /**
378
- * JupyterLab render mime registry for discovering supported MIME types.
379
- */
380
- renderMimeRegistry?: IRenderMimeRegistry;
381
- }
382
-
383
303
  /**
384
304
  * Manages the AI agent lifecycle and execution loop.
385
305
  * Provides agent initialization, tool management, MCP server integration,
386
306
  * and handles the complete agent execution cycle.
387
307
  * Emits events for UI updates instead of directly manipulating the chat interface.
388
308
  */
389
- export class AgentManager {
309
+ export class AgentManager implements IAgentManager {
390
310
  /**
391
311
  * Creates a new AgentManager instance.
392
312
  * @param options Configuration options for the agent manager
393
313
  */
394
- constructor(options: IAgentManagerOptions) {
314
+ constructor(options: IAgentManager.IOptions) {
395
315
  this._settingsModel = options.settingsModel;
396
316
  this._toolRegistry = options.toolRegistry;
397
317
  this._providerRegistry = options.providerRegistry;
@@ -402,7 +322,7 @@ export class AgentManager {
402
322
  this._history = [];
403
323
  this._mcpTools = {};
404
324
  this._controller = null;
405
- this._agentEvent = new Signal<this, IAgentEvent>(this);
325
+ this._agentEvent = new Signal<this, IAgentManager.IAgentEvent>(this);
406
326
  this._tokenUsage = options.tokenUsage ?? {
407
327
  inputTokens: 0,
408
328
  outputTokens: 0
@@ -411,6 +331,7 @@ export class AgentManager {
411
331
  this._skills = [];
412
332
  this._agentConfig = null;
413
333
  this._renderMimeRegistry = options.renderMimeRegistry;
334
+ this._streaming.resolve();
414
335
 
415
336
  this.activeProvider =
416
337
  options.activeProvider ?? this._settingsModel.config.defaultProvider;
@@ -424,7 +345,7 @@ export class AgentManager {
424
345
  /**
425
346
  * Signal emitted when agent events occur
426
347
  */
427
- get agentEvent(): ISignal<this, IAgentEvent> {
348
+ get agentEvent(): ISignal<this, IAgentManager.IAgentEvent> {
428
349
  return this._agentEvent;
429
350
  }
430
351
 
@@ -471,7 +392,17 @@ export class AgentManager {
471
392
  return this._activeProvider;
472
393
  }
473
394
  set activeProvider(value: string) {
395
+ const previousProvider = this._activeProvider;
474
396
  this._activeProvider = value;
397
+
398
+ // Reset request-level context estimate only when switching between providers.
399
+ if (previousProvider && previousProvider !== value) {
400
+ this._tokenUsage.lastRequestInputTokens = undefined;
401
+ }
402
+
403
+ this._tokenUsage.contextWindow = this._getActiveContextWindow();
404
+
405
+ this._tokenUsageChanged.emit(this._tokenUsage);
475
406
  this.initializeAgent();
476
407
  this._activeProviderChanged.emit(this._activeProvider);
477
408
  }
@@ -491,12 +422,12 @@ export class AgentManager {
491
422
  * Gets the currently selected tools as a record.
492
423
  * @returns Record of selected tools
493
424
  */
494
- get selectedAgentTools(): Record<string, ITool> {
425
+ get selectedAgentTools(): ToolMap {
495
426
  if (!this._toolRegistry) {
496
427
  return {};
497
428
  }
498
429
 
499
- const result: Record<string, ITool> = {};
430
+ const result: ToolMap = {};
500
431
  for (const name of this._selectedToolNames) {
501
432
  const tool: ITool | null = this._toolRegistry.get(name);
502
433
  if (tool) {
@@ -539,13 +470,32 @@ export class AgentManager {
539
470
  /**
540
471
  * Clears conversation history and resets agent state.
541
472
  */
542
- clearHistory(): void {
473
+ async clearHistory(): Promise<void> {
543
474
  // Stop any ongoing streaming
475
+ this.stopStreaming('Chat cleared');
476
+
477
+ await this._streaming.promise;
478
+
479
+ // Clear history and token usage
480
+ this._history = [];
481
+ this._tokenUsage = {
482
+ inputTokens: 0,
483
+ outputTokens: 0,
484
+ contextWindow: this._getActiveContextWindow()
485
+ };
486
+ this._tokenUsageChanged.emit(this._tokenUsage);
487
+ }
488
+
489
+ /**
490
+ * Sets the history with a list of messages from the chat.
491
+ * @param messages The chat messages to set as history
492
+ */
493
+ setHistory(messages: IMessageContent[]): void {
494
+ // Stop any ongoing streaming and reject awaiting approvals
544
495
  this.stopStreaming();
545
496
 
546
- // Reject any pending approvals
547
497
  for (const [approvalId, pending] of this._pendingApprovals) {
548
- pending.resolve(false, 'Chat cleared');
498
+ pending.resolve(false, 'Chat history changed');
549
499
  this._agentEvent.emit({
550
500
  type: 'tool_approval_resolved',
551
501
  data: { approvalId, approved: false }
@@ -553,17 +503,33 @@ export class AgentManager {
553
503
  }
554
504
  this._pendingApprovals.clear();
555
505
 
556
- // Clear history and token usage
557
- this._history = [];
558
- this._tokenUsage = { inputTokens: 0, outputTokens: 0 };
559
- this._tokenUsageChanged.emit(this._tokenUsage);
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);
560
515
  }
561
516
 
562
517
  /**
563
518
  * Stops the current streaming response by aborting the request.
519
+ * Resolve any pending approval.
564
520
  */
565
- stopStreaming(): void {
521
+ stopStreaming(reason?: string): void {
566
522
  this._controller?.abort();
523
+
524
+ // Reject any pending approvals
525
+ for (const [approvalId, pending] of this._pendingApprovals) {
526
+ pending.resolve(false, reason ?? 'Stream ended by user');
527
+ this._agentEvent.emit({
528
+ type: 'tool_approval_resolved',
529
+ data: { approvalId, approved: false }
530
+ });
531
+ }
532
+ this._pendingApprovals.clear();
567
533
  }
568
534
 
569
535
  /**
@@ -606,8 +572,9 @@ export class AgentManager {
606
572
  * @param message The user message to respond to (may include processed attachment content)
607
573
  */
608
574
  async generateResponse(message: string): Promise<void> {
575
+ this._streaming = new PromiseDelegate();
609
576
  this._controller = new AbortController();
610
-
577
+ const responseHistory: ModelMessage[] = [];
611
578
  try {
612
579
  // Ensure we have an agent
613
580
  if (!this._agent) {
@@ -619,7 +586,7 @@ export class AgentManager {
619
586
  }
620
587
 
621
588
  // Add user message to history
622
- this._history.push({
589
+ responseHistory.push({
623
590
  role: 'user',
624
591
  content: message
625
592
  });
@@ -627,27 +594,38 @@ export class AgentManager {
627
594
  let continueLoop = true;
628
595
  while (continueLoop) {
629
596
  const result = await this._agent.stream({
630
- messages: this._history,
597
+ messages: [...this._history, ...responseHistory],
631
598
  abortSignal: this._controller.signal
632
599
  });
633
600
 
634
601
  const streamResult = await this._processStreamResult(result);
635
602
 
636
- // Get response messages and update token usage
603
+ if (streamResult.aborted) {
604
+ try {
605
+ const responseMessages = await result.response;
606
+ if (responseMessages.messages?.length) {
607
+ this._history.push(
608
+ ...Private.sanitizeModelMessages(responseMessages.messages)
609
+ );
610
+ }
611
+ } catch {
612
+ // Aborting before a step finishes leaves no completed response to persist.
613
+ }
614
+ break;
615
+ }
616
+
617
+ // Get response messages for completed steps.
637
618
  const responseMessages = await result.response;
638
- this._updateTokenUsage(await result.usage);
639
619
 
640
620
  // Add response messages to history
641
621
  if (responseMessages.messages?.length) {
642
- this._history.push(
643
- ...Private.sanitizeModelMessages(responseMessages.messages)
644
- );
622
+ responseHistory.push(...responseMessages.messages);
645
623
  }
646
624
 
647
625
  // Add approval response if processed
648
626
  if (streamResult.approvalResponse) {
649
627
  // Check if the last message is a tool message we can append to
650
- const lastMsg = this._history[this._history.length - 1];
628
+ const lastMsg = responseHistory[responseHistory.length - 1];
651
629
  if (
652
630
  lastMsg &&
653
631
  lastMsg.role === 'tool' &&
@@ -658,12 +636,15 @@ export class AgentManager {
658
636
  toolContent.push(...streamResult.approvalResponse.content);
659
637
  } else {
660
638
  // Add as separate message
661
- this._history.push(streamResult.approvalResponse);
639
+ responseHistory.push(streamResult.approvalResponse);
662
640
  }
663
641
  }
664
642
 
665
643
  continueLoop = streamResult.approvalProcessed;
666
644
  }
645
+
646
+ // Add the messages to the history only if the response ended without error.
647
+ this._history.push(...Private.sanitizeModelMessages(responseHistory));
667
648
  } catch (error) {
668
649
  if ((error as Error).name !== 'AbortError') {
669
650
  this._agentEvent.emit({
@@ -671,25 +652,45 @@ export class AgentManager {
671
652
  data: { error: error as Error }
672
653
  });
673
654
  }
674
- // After an error (including AbortError), sanitize the history
675
- // to remove any trailing assistant messages without tool results
676
- this._sanitizeHistory();
677
655
  } finally {
678
656
  this._controller = null;
657
+ this._streaming.resolve();
679
658
  }
680
659
  }
681
660
 
682
661
  /**
683
- * Updates token usage statistics.
662
+ * Updates cumulative token usage statistics from a completed model step.
684
663
  */
685
664
  private _updateTokenUsage(
686
- usage: { inputTokens?: number; outputTokens?: number } | undefined
665
+ usage: { inputTokens?: number; outputTokens?: number } | undefined,
666
+ lastRequestInputTokens?: number
687
667
  ): void {
668
+ const contextWindow = this._getActiveContextWindow();
669
+ const estimatedRequestInputTokens =
670
+ lastRequestInputTokens ?? usage?.inputTokens;
671
+
688
672
  if (usage) {
689
673
  this._tokenUsage.inputTokens += usage.inputTokens ?? 0;
690
674
  this._tokenUsage.outputTokens += usage.outputTokens ?? 0;
691
- this._tokenUsageChanged.emit(this._tokenUsage);
692
675
  }
676
+
677
+ this._tokenUsage.lastRequestInputTokens = estimatedRequestInputTokens;
678
+ this._tokenUsage.contextWindow = contextWindow;
679
+
680
+ this._tokenUsageChanged.emit(this._tokenUsage);
681
+ }
682
+
683
+ /**
684
+ * Gets the configured context window for the active provider.
685
+ */
686
+ private _getActiveContextWindow(): number | undefined {
687
+ const activeProviderConfig = this._settingsModel.getProvider(
688
+ this._activeProvider
689
+ );
690
+ return getEffectiveContextWindow(
691
+ activeProviderConfig,
692
+ this._providerRegistry
693
+ );
693
694
  }
694
695
 
695
696
  /**
@@ -752,6 +753,13 @@ export class AgentManager {
752
753
  activeProviderConfig && this._providerRegistry
753
754
  ? this._providerRegistry.getProviderInfo(activeProviderConfig.provider)
754
755
  : null;
756
+ const contextWindow = getEffectiveContextWindow(
757
+ activeProviderConfig,
758
+ this._providerRegistry
759
+ );
760
+
761
+ this._tokenUsage.contextWindow = contextWindow;
762
+ this._tokenUsageChanged.emit(this._tokenUsage);
755
763
 
756
764
  const temperature =
757
765
  activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
@@ -859,7 +867,10 @@ ${richOutputWorkflowInstruction}`;
859
867
  ): Promise<IStreamProcessResult> {
860
868
  let fullResponse = '';
861
869
  let currentMessageId: string | null = null;
862
- const processResult: IStreamProcessResult = { approvalProcessed: false };
870
+ const processResult: IStreamProcessResult = {
871
+ approvalProcessed: false,
872
+ aborted: false
873
+ };
863
874
 
864
875
  for await (const part of result.fullStream) {
865
876
  switch (part.type) {
@@ -921,6 +932,14 @@ ${richOutputWorkflowInstruction}`;
921
932
  await this._handleApprovalRequest(part, processResult);
922
933
  break;
923
934
 
935
+ case 'finish-step':
936
+ this._updateTokenUsage(part.usage, part.usage.inputTokens);
937
+ break;
938
+
939
+ case 'abort':
940
+ processResult.aborted = true;
941
+ break;
942
+
924
943
  // Ignore: text-start, text-end, finish, error, and others
925
944
  default:
926
945
  break;
@@ -1208,98 +1227,8 @@ WEB RETRIEVAL POLICY:
1208
1227
  return `Supported MIME types in this session: ${safeMimeTypes.join(', ')}`;
1209
1228
  }
1210
1229
 
1211
- /**
1212
- * Sanitizes history to ensure it's in a valid state in case of abort or error.
1213
- */
1214
- private _sanitizeHistory(): void {
1215
- if (this._history.length === 0) {
1216
- return;
1217
- }
1218
-
1219
- const newHistory: ModelMessage[] = [];
1220
- for (let i = 0; i < this._history.length; i++) {
1221
- const msg = this._history[i];
1222
-
1223
- if (msg.role === 'assistant') {
1224
- const toolCallIds = this._getToolCallIds(msg);
1225
- if (toolCallIds.length > 0) {
1226
- // Find if there's a following tool message with results for these calls
1227
- const nextMsg = this._history[i + 1];
1228
- if (
1229
- nextMsg &&
1230
- nextMsg.role === 'tool' &&
1231
- this._matchesAllToolCalls(nextMsg, toolCallIds)
1232
- ) {
1233
- newHistory.push(msg);
1234
- } else {
1235
- // Message has unmatched tool calls drop it and everything after it
1236
- break;
1237
- }
1238
- } else {
1239
- newHistory.push(msg);
1240
- }
1241
- } else if (msg.role === 'tool') {
1242
- // Tool messages are valid if they were preceded by a valid assistant message
1243
- newHistory.push(msg);
1244
- } else {
1245
- newHistory.push(msg);
1246
- }
1247
- }
1248
-
1249
- this._history = newHistory;
1250
- }
1251
-
1252
- /**
1253
- * Extracts tool call IDs from a message
1254
- */
1255
- private _getToolCallIds(message: ModelMessage): string[] {
1256
- const ids: string[] = [];
1257
-
1258
- // Check content array for tool-call parts
1259
- if (Array.isArray(message.content)) {
1260
- for (const part of message.content) {
1261
- if (
1262
- typeof part === 'object' &&
1263
- part !== null &&
1264
- 'type' in part &&
1265
- part.type === 'tool-call'
1266
- ) {
1267
- ids.push(part.toolCallId);
1268
- }
1269
- }
1270
- }
1271
-
1272
- return ids;
1273
- }
1274
-
1275
- /**
1276
- * Checks if a tool message contains results for all specified tool call IDs
1277
- */
1278
- private _matchesAllToolCalls(
1279
- message: ModelMessage,
1280
- callIds: string[]
1281
- ): boolean {
1282
- if (message.role !== 'tool' || !Array.isArray(message.content)) {
1283
- return false;
1284
- }
1285
-
1286
- const resultIds = new Set<string>();
1287
- for (const part of message.content) {
1288
- if (
1289
- typeof part === 'object' &&
1290
- part !== null &&
1291
- 'type' in part &&
1292
- part.type === 'tool-result'
1293
- ) {
1294
- resultIds.add(part.toolCallId);
1295
- }
1296
- }
1297
-
1298
- return callIds.every(id => resultIds.has(id));
1299
- }
1300
-
1301
1230
  // Private attributes
1302
- private _settingsModel: AISettingsModel;
1231
+ private _settingsModel: IAISettingsModel;
1303
1232
  private _toolRegistry?: IToolRegistry;
1304
1233
  private _providerRegistry?: IProviderRegistry;
1305
1234
  private _skillRegistry?: ISkillRegistry;
@@ -1309,7 +1238,7 @@ WEB RETRIEVAL POLICY:
1309
1238
  private _history: ModelMessage[];
1310
1239
  private _mcpTools: ToolMap;
1311
1240
  private _controller: AbortController | null;
1312
- private _agentEvent: Signal<this, IAgentEvent>;
1241
+ private _agentEvent: Signal<this, IAgentManager.IAgentEvent>;
1313
1242
  private _tokenUsage: ITokenUsage;
1314
1243
  private _tokenUsageChanged: Signal<this, ITokenUsage>;
1315
1244
  private _activeProvider: string = '';
@@ -1322,25 +1251,123 @@ WEB RETRIEVAL POLICY:
1322
1251
  string,
1323
1252
  { resolve: (approved: boolean, reason?: string) => void }
1324
1253
  > = new Map();
1254
+ private _streaming: PromiseDelegate<void> = new PromiseDelegate();
1325
1255
  }
1326
1256
 
1327
1257
  namespace Private {
1328
1258
  /**
1329
- * Keep only serializable messages by doing a JSON round-trip.
1330
- * Messages that cannot be serialized are dropped.
1259
+ * Sanitize the messages before adding them to the history.
1260
+ *
1261
+ * 1- Make sure the message sequence is not altered:
1262
+ * - tool-call messages should have a corresponding tool-result (and vice-versa)
1263
+ * - tool-approval-request should have a tool-approval-response (and vice-versa)
1264
+ *
1265
+ * 2- Keep only serializable messages by doing a JSON round-trip.
1266
+ * Messages that cannot be serialized are dropped.
1331
1267
  */
1332
1268
  export const sanitizeModelMessages = (
1333
1269
  messages: ModelMessage[]
1334
1270
  ): ModelMessage[] => {
1335
1271
  const sanitized: ModelMessage[] = [];
1336
1272
  for (const message of messages) {
1337
- try {
1338
- sanitized.push(JSON.parse(JSON.stringify(message)));
1339
- } catch {
1340
- // Drop messages that cannot be serialized
1273
+ if (message.role === 'assistant') {
1274
+ let newMessage: AssistantModelMessage | undefined;
1275
+ if (!Array.isArray(message.content)) {
1276
+ newMessage = message;
1277
+ } else {
1278
+ // Remove assistant message content without a required response.
1279
+ const newContent: typeof message.content = [];
1280
+ for (const assistantContent of message.content) {
1281
+ let isContentValid = true;
1282
+ if (assistantContent.type === 'tool-call') {
1283
+ const toolCallId = assistantContent.toolCallId;
1284
+ isContentValid = !!messages.find(
1285
+ msg =>
1286
+ msg.role === 'tool' &&
1287
+ Array.isArray(msg.content) &&
1288
+ msg.content.find(
1289
+ content =>
1290
+ content.type === 'tool-result' &&
1291
+ content.toolCallId === toolCallId
1292
+ )
1293
+ );
1294
+ } else if (assistantContent.type === 'tool-approval-request') {
1295
+ const approvalId = assistantContent.approvalId;
1296
+ isContentValid = !!messages.find(
1297
+ msg =>
1298
+ msg.role === 'tool' &&
1299
+ Array.isArray(msg.content) &&
1300
+ msg.content.find(
1301
+ content =>
1302
+ content.type === 'tool-approval-response' &&
1303
+ content.approvalId === approvalId
1304
+ )
1305
+ );
1306
+ }
1307
+ if (isContentValid) {
1308
+ newContent.push(assistantContent);
1309
+ }
1310
+ }
1311
+ if (newContent.length) {
1312
+ newMessage = { ...message, content: newContent };
1313
+ }
1314
+ }
1315
+ if (newMessage) {
1316
+ try {
1317
+ sanitized.push(JSON.parse(JSON.stringify(newMessage)));
1318
+ } catch {
1319
+ // Drop messages that cannot be serialized
1320
+ }
1321
+ }
1322
+ } else if (message.role === 'tool') {
1323
+ // Remove tool message content without request.
1324
+ const newContent: typeof message.content = [];
1325
+ for (const toolContent of message.content) {
1326
+ let isContentValid = true;
1327
+ if (toolContent.type === 'tool-result') {
1328
+ const toolCallId = toolContent.toolCallId;
1329
+ isContentValid = !!sanitized.find(
1330
+ msg =>
1331
+ msg.role === 'assistant' &&
1332
+ Array.isArray(msg.content) &&
1333
+ msg.content.find(
1334
+ content =>
1335
+ content.type === 'tool-call' &&
1336
+ content.toolCallId === toolCallId
1337
+ )
1338
+ );
1339
+ } else if (toolContent.type === 'tool-approval-response') {
1340
+ const approvalId = toolContent.approvalId;
1341
+ isContentValid = !!sanitized.find(
1342
+ msg =>
1343
+ msg.role === 'assistant' &&
1344
+ Array.isArray(msg.content) &&
1345
+ msg.content.find(
1346
+ content =>
1347
+ content.type === 'tool-approval-request' &&
1348
+ content.approvalId === approvalId
1349
+ )
1350
+ );
1351
+ }
1352
+ if (isContentValid) {
1353
+ newContent.push(toolContent);
1354
+ }
1355
+ }
1356
+ if (newContent.length) {
1357
+ try {
1358
+ sanitized.push(
1359
+ JSON.parse(JSON.stringify({ ...message, content: newContent }))
1360
+ );
1361
+ } catch {
1362
+ // Drop messages that cannot be serialized
1363
+ }
1364
+ }
1365
+ } else {
1366
+ // Message is a system or user message.
1367
+ sanitized.push(message);
1341
1368
  }
1342
1369
  }
1343
- return sanitized;
1370
+ return sanitized.length === messages.length ? sanitized : [];
1344
1371
  };
1345
1372
 
1346
1373
  /**