@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.
- package/lib/agent.d.ts +33 -115
- package/lib/agent.js +192 -106
- package/lib/chat-model-handler.d.ts +9 -11
- package/lib/chat-model-handler.js +9 -4
- package/lib/chat-model.d.ts +84 -13
- package/lib/chat-model.js +214 -136
- package/lib/completion/completion-provider.d.ts +2 -3
- package/lib/components/completion-status.d.ts +2 -2
- package/lib/components/index.d.ts +1 -1
- package/lib/components/index.js +1 -1
- package/lib/components/model-select.d.ts +3 -3
- package/lib/components/save-button.d.ts +31 -0
- package/lib/components/save-button.js +41 -0
- package/lib/components/tool-select.d.ts +3 -4
- package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +13 -14
- package/lib/components/usage-display.js +109 -0
- package/lib/diff-manager.d.ts +2 -3
- package/lib/index.d.ts +2 -4
- package/lib/index.js +186 -28
- package/lib/models/settings-model.d.ts +11 -53
- package/lib/models/settings-model.js +38 -22
- package/lib/providers/built-in-providers.js +22 -36
- package/lib/providers/generated-context-windows.d.ts +8 -0
- package/lib/providers/generated-context-windows.js +96 -0
- package/lib/providers/model-info.d.ts +3 -0
- package/lib/providers/model-info.js +58 -0
- package/lib/tokens.d.ts +361 -36
- package/lib/tokens.js +18 -13
- package/lib/tools/commands.d.ts +2 -3
- package/lib/widgets/ai-settings.d.ts +3 -5
- package/lib/widgets/ai-settings.js +12 -0
- package/lib/widgets/main-area-chat.d.ts +2 -3
- package/lib/widgets/main-area-chat.js +12 -12
- package/lib/widgets/provider-config-dialog.d.ts +1 -2
- package/lib/widgets/provider-config-dialog.js +34 -34
- package/package.json +17 -10
- package/schema/settings-model.json +18 -1
- package/src/agent.ts +275 -248
- package/src/chat-model-handler.ts +25 -21
- package/src/chat-model.ts +307 -196
- package/src/completion/completion-provider.ts +7 -4
- package/src/components/completion-status.tsx +3 -3
- package/src/components/index.ts +1 -1
- package/src/components/model-select.tsx +4 -3
- package/src/components/save-button.tsx +84 -0
- package/src/components/tool-select.tsx +10 -4
- package/src/components/usage-display.tsx +208 -0
- package/src/diff-manager.ts +4 -4
- package/src/index.ts +250 -58
- package/src/models/settings-model.ts +46 -88
- package/src/providers/built-in-providers.ts +22 -36
- package/src/providers/generated-context-windows.ts +102 -0
- package/src/providers/model-info.ts +88 -0
- package/src/tokens.ts +438 -58
- package/src/tools/commands.ts +2 -3
- package/src/widgets/ai-settings.tsx +69 -15
- package/src/widgets/main-area-chat.ts +18 -15
- package/src/widgets/provider-config-dialog.tsx +96 -61
- package/style/base.css +17 -195
- package/lib/approval-buttons.d.ts +0 -49
- package/lib/approval-buttons.js +0 -79
- package/lib/components/token-usage-display.js +0 -72
- package/src/approval-buttons.ts +0 -115
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
257
|
-
private _settingsModel:
|
|
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:
|
|
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():
|
|
425
|
+
get selectedAgentTools(): ToolMap {
|
|
495
426
|
if (!this._toolRegistry) {
|
|
496
427
|
return {};
|
|
497
428
|
}
|
|
498
429
|
|
|
499
|
-
const result:
|
|
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
|
|
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
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 = {
|
|
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:
|
|
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
|
-
*
|
|
1330
|
-
*
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
/**
|