@theia/ai-chat 1.74.0-next.8 → 1.74.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/browser/agent-delegation-tool.js +6 -7
- package/lib/browser/agent-delegation-tool.js.map +1 -1
- package/lib/browser/agent-delegation-tool.spec.js +13 -0
- package/lib/browser/agent-delegation-tool.spec.js.map +1 -1
- package/lib/browser/chat-session-store-impl.d.ts.map +1 -1
- package/lib/browser/chat-session-store-impl.js +2 -0
- package/lib/browser/chat-session-store-impl.js.map +1 -1
- package/lib/browser/chat-session-store-impl.spec.js +51 -2
- package/lib/browser/chat-session-store-impl.spec.js.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.d.ts +18 -8
- package/lib/browser/chat-tool-preference-bindings.d.ts.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.js +27 -28
- package/lib/browser/chat-tool-preference-bindings.js.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.spec.js +43 -46
- package/lib/browser/chat-tool-preference-bindings.spec.js.map +1 -1
- package/lib/common/chat-agents.d.ts +8 -0
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +35 -1
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-agents.spec.js +94 -0
- package/lib/common/chat-agents.spec.js.map +1 -1
- package/lib/common/chat-auto-save.spec.js +13 -0
- package/lib/common/chat-auto-save.spec.js.map +1 -1
- package/lib/common/chat-content-deserializer.d.ts.map +1 -1
- package/lib/common/chat-content-deserializer.js +4 -0
- package/lib/common/chat-content-deserializer.js.map +1 -1
- package/lib/common/chat-model-serialization.d.ts +12 -1
- package/lib/common/chat-model-serialization.d.ts.map +1 -1
- package/lib/common/chat-model-serialization.js.map +1 -1
- package/lib/common/chat-model-serialization.spec.js +29 -0
- package/lib/common/chat-model-serialization.spec.js.map +1 -1
- package/lib/common/chat-model.d.ts +104 -2
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +151 -2
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-model.spec.d.ts +2 -0
- package/lib/common/chat-model.spec.d.ts.map +1 -0
- package/lib/common/chat-model.spec.js +42 -0
- package/lib/common/chat-model.spec.js.map +1 -0
- package/lib/common/chat-request-parser.d.ts +4 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +43 -3
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-request-parser.spec.js +59 -0
- package/lib/common/chat-request-parser.spec.js.map +1 -1
- package/lib/common/chat-service-deletion.spec.js +43 -0
- package/lib/common/chat-service-deletion.spec.js.map +1 -1
- package/lib/common/chat-service-pinned-agent.spec.js +52 -2
- package/lib/common/chat-service-pinned-agent.spec.js.map +1 -1
- package/lib/common/chat-service-session-events.spec.d.ts +2 -0
- package/lib/common/chat-service-session-events.spec.d.ts.map +1 -0
- package/lib/common/chat-service-session-events.spec.js +130 -0
- package/lib/common/chat-service-session-events.spec.js.map +1 -0
- package/lib/common/chat-service.d.ts +3 -0
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +60 -6
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-service.spec.d.ts +2 -0
- package/lib/common/chat-service.spec.d.ts.map +1 -0
- package/lib/common/chat-service.spec.js +177 -0
- package/lib/common/chat-service.spec.js.map +1 -0
- package/lib/common/chat-session-status.spec.d.ts +2 -0
- package/lib/common/chat-session-status.spec.d.ts.map +1 -0
- package/lib/common/chat-session-status.spec.js +213 -0
- package/lib/common/chat-session-status.spec.js.map +1 -0
- package/lib/common/chat-session-store.d.ts +8 -0
- package/lib/common/chat-session-store.d.ts.map +1 -1
- package/lib/common/tool-call-response-content.spec.js +36 -0
- package/lib/common/tool-call-response-content.spec.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/agent-delegation-tool.spec.ts +16 -0
- package/src/browser/agent-delegation-tool.ts +7 -7
- package/src/browser/chat-session-store-impl.spec.ts +62 -2
- package/src/browser/chat-session-store-impl.ts +2 -0
- package/src/browser/chat-tool-preference-bindings.spec.ts +43 -48
- package/src/browser/chat-tool-preference-bindings.ts +42 -30
- package/src/common/chat-agents.spec.ts +112 -2
- package/src/common/chat-agents.ts +44 -1
- package/src/common/chat-auto-save.spec.ts +18 -1
- package/src/common/chat-content-deserializer.ts +11 -0
- package/src/common/chat-model-serialization.spec.ts +38 -0
- package/src/common/chat-model-serialization.ts +12 -1
- package/src/common/chat-model.spec.ts +42 -0
- package/src/common/chat-model.ts +233 -2
- package/src/common/chat-request-parser.spec.ts +69 -1
- package/src/common/chat-request-parser.ts +51 -6
- package/src/common/chat-service-deletion.spec.ts +52 -0
- package/src/common/chat-service-pinned-agent.spec.ts +58 -2
- package/src/common/chat-service-session-events.spec.ts +161 -0
- package/src/common/chat-service.spec.ts +211 -0
- package/src/common/chat-service.ts +64 -8
- package/src/common/chat-session-status.spec.ts +280 -0
- package/src/common/chat-session-store.ts +8 -0
- package/src/common/tool-call-response-content.spec.ts +42 -0
|
@@ -73,6 +73,8 @@ export interface ChatSession {
|
|
|
73
73
|
pinnedAgent?: ChatAgent;
|
|
74
74
|
/** ID of the root session in the delegation chain. For delegated sessions, this points to the topmost session where task contexts are stored. */
|
|
75
75
|
rootSessionId?: string;
|
|
76
|
+
/** ID of the immediate parent session that delegated this one. Undefined for top-level sessions. */
|
|
77
|
+
parentSessionId?: string;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
export interface ActiveSessionChangedEvent {
|
|
@@ -224,6 +226,43 @@ export class ChatServiceImpl implements ChatService {
|
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
async deleteSession(sessionId: string): Promise<void> {
|
|
229
|
+
await this.deleteSessionAndChildren(sessionId, new Set<string>());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected async deleteSessionAndChildren(sessionId: string, visited: Set<string>): Promise<void> {
|
|
233
|
+
if (visited.has(sessionId)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
visited.add(sessionId);
|
|
237
|
+
|
|
238
|
+
// Delete children first. A child is any session whose immediate parent (parentSessionId) or root
|
|
239
|
+
// (rootSessionId) points to this session. Matching parentSessionId is what lets us cascade through
|
|
240
|
+
// intermediate levels: deleting B in A -> B -> C reaches C via its parentSessionId even though C's
|
|
241
|
+
// rootSessionId still points at A. Children may live only in memory, only in persisted storage
|
|
242
|
+
// (e.g. after a reload, not yet restored), or both, so collect ids from both sources. The visited
|
|
243
|
+
// set guards against cycles and re-deleting a child reachable from more than one ancestor.
|
|
244
|
+
const childIds = new Set<string>();
|
|
245
|
+
for (const s of this._sessions) {
|
|
246
|
+
if (s.parentSessionId === sessionId || s.rootSessionId === sessionId) {
|
|
247
|
+
childIds.add(s.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (this.sessionStore) {
|
|
251
|
+
try {
|
|
252
|
+
const index = await this.sessionStore.getSessionIndex();
|
|
253
|
+
for (const metadata of Object.values(index)) {
|
|
254
|
+
if (metadata.parentSessionId === sessionId || metadata.rootSessionId === sessionId) {
|
|
255
|
+
childIds.add(metadata.sessionId);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.logger.error('Failed to read session index for cascade delete', { sessionId, error });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const childId of childIds) {
|
|
263
|
+
await this.deleteSessionAndChildren(childId, visited);
|
|
264
|
+
}
|
|
265
|
+
|
|
227
266
|
const sessionIndex = this._sessions.findIndex(candidate => candidate.id === sessionId);
|
|
228
267
|
|
|
229
268
|
// If session is in memory, remove it
|
|
@@ -351,6 +390,7 @@ export class ChatServiceImpl implements ChatService {
|
|
|
351
390
|
}
|
|
352
391
|
const requestText = request.request.displayText ?? request.request.text;
|
|
353
392
|
session.title = requestText;
|
|
393
|
+
this.onSessionEventEmitter.fire({ type: 'renamed', sessionId: session.id });
|
|
354
394
|
if (this.chatSessionNamingService) {
|
|
355
395
|
const otherSessionNames = this._sessions.map(s => s.title).filter((title): title is string => title !== undefined);
|
|
356
396
|
const namingService = this.chatSessionNamingService;
|
|
@@ -362,6 +402,7 @@ export class ChatServiceImpl implements ChatService {
|
|
|
362
402
|
session.title = name;
|
|
363
403
|
// Trigger persistence when title changes
|
|
364
404
|
this.saveSession(session.id);
|
|
405
|
+
this.onSessionEventEmitter.fire({ type: 'renamed', sessionId: session.id });
|
|
365
406
|
}
|
|
366
407
|
didGenerateName = true;
|
|
367
408
|
}).catch(error => this.logger.error('Failed to generate chat session name', error));
|
|
@@ -411,8 +452,12 @@ export class ChatServiceImpl implements ChatService {
|
|
|
411
452
|
return mentionedAgent;
|
|
412
453
|
} else if (session.pinnedAgent) {
|
|
413
454
|
// If we have a valid pinned agent, use it (pinned agent may become stale
|
|
414
|
-
// if it was disabled; so we always need to recheck)
|
|
415
|
-
|
|
455
|
+
// if it was disabled; so we always need to recheck). A pin is an explicit choice —
|
|
456
|
+
// e.g. AgentDelegationTool pins the delegated agent on the session it creates, and
|
|
457
|
+
// worker agents are often hidden (showInChat: false) — so hidden agents are honored
|
|
458
|
+
// here, while plain @-mentions above stay hidden-excluding. Without this, a request
|
|
459
|
+
// in a session pinned to a hidden agent silently falls back to the default agent.
|
|
460
|
+
const pinnedAgent = this.chatAgentService.getAgent(session.pinnedAgent.id, true);
|
|
416
461
|
if (pinnedAgent) {
|
|
417
462
|
return pinnedAgent;
|
|
418
463
|
}
|
|
@@ -458,9 +503,15 @@ export class ChatServiceImpl implements ChatService {
|
|
|
458
503
|
// Store session with title, pinned agent info, last interaction timestamp, and error state
|
|
459
504
|
const lastRequest = session.model.getRequests().at(-1);
|
|
460
505
|
const hasError = lastRequest?.response.isComplete === true && lastRequest?.response.isError === true;
|
|
461
|
-
return this.sessionStore.storeSessions(
|
|
462
|
-
|
|
463
|
-
|
|
506
|
+
return this.sessionStore.storeSessions({
|
|
507
|
+
model: session.model,
|
|
508
|
+
title: session.title,
|
|
509
|
+
pinnedAgentId: session.pinnedAgent?.id,
|
|
510
|
+
lastInteraction: session.lastInteraction?.getTime(),
|
|
511
|
+
hasError,
|
|
512
|
+
rootSessionId: session.rootSessionId,
|
|
513
|
+
parentSessionId: session.parentSessionId
|
|
514
|
+
}).catch(error => {
|
|
464
515
|
this.logger.error('Failed to store chat sessions', error);
|
|
465
516
|
});
|
|
466
517
|
}
|
|
@@ -505,9 +556,10 @@ export class ChatServiceImpl implements ChatService {
|
|
|
505
556
|
const model = new MutableChatModel(serialized.model);
|
|
506
557
|
await this.restoreSessionData(model, serialized.model);
|
|
507
558
|
|
|
508
|
-
// Determine pinned agent
|
|
559
|
+
// Determine pinned agent (hidden agents included — a restored pin, like a live one,
|
|
560
|
+
// is an explicit choice; see getPinnedAgent)
|
|
509
561
|
const pinnedAgent = serialized.pinnedAgentId
|
|
510
|
-
? this.chatAgentService.getAgent(serialized.pinnedAgentId)
|
|
562
|
+
? this.chatAgentService.getAgent(serialized.pinnedAgentId, true)
|
|
511
563
|
: undefined;
|
|
512
564
|
|
|
513
565
|
// Register as session
|
|
@@ -517,8 +569,12 @@ export class ChatServiceImpl implements ChatService {
|
|
|
517
569
|
lastInteraction: new Date(serialized.saveDate),
|
|
518
570
|
model,
|
|
519
571
|
isActive: false,
|
|
520
|
-
pinnedAgent
|
|
572
|
+
pinnedAgent,
|
|
573
|
+
rootSessionId: serialized.rootSessionId,
|
|
574
|
+
parentSessionId: serialized.parentSessionId
|
|
521
575
|
};
|
|
576
|
+
session.model.rootSessionId = serialized.rootSessionId;
|
|
577
|
+
session.model.parentSessionId = serialized.parentSessionId;
|
|
522
578
|
this._sessions.push(session);
|
|
523
579
|
this.setupAutoSaveForSession(session);
|
|
524
580
|
this.onSessionEventEmitter.fire({ type: 'created', sessionId: session.id });
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import {
|
|
19
|
+
ChatChangeEvent,
|
|
20
|
+
ChatSessionStatus,
|
|
21
|
+
MutableChatModel,
|
|
22
|
+
MutableChatRequestModel,
|
|
23
|
+
QuestionResponseContentImpl,
|
|
24
|
+
TextChatResponseContentImpl,
|
|
25
|
+
ToolCallChatResponseContentImpl
|
|
26
|
+
} from './chat-model';
|
|
27
|
+
import { ParsedChatRequest, ParsedChatRequestTextPart } from './parsed-chat-request';
|
|
28
|
+
|
|
29
|
+
describe('ChatSessionStatus', () => {
|
|
30
|
+
|
|
31
|
+
function parsedRequest(text: string = 'hello'): ParsedChatRequest {
|
|
32
|
+
return {
|
|
33
|
+
request: { text },
|
|
34
|
+
parts: [new ParsedChatRequestTextPart({ start: 0, endExclusive: text.length }, text)],
|
|
35
|
+
toolRequests: new Map(),
|
|
36
|
+
variables: []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function trackStatusChanges(model: MutableChatModel): ChatSessionStatus[] {
|
|
41
|
+
const statuses: ChatSessionStatus[] = [];
|
|
42
|
+
model.onDidChange(event => {
|
|
43
|
+
if (ChatChangeEvent.isStatusChangedEvent(event)) {
|
|
44
|
+
statuses.push(event.status);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return statuses;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Simulates the tool request service's confirmation flow up to the point where the user must decide. */
|
|
51
|
+
function requestConfirmation(request: MutableChatRequestModel, toolCall: ToolCallChatResponseContentImpl): void {
|
|
52
|
+
toolCall.requestUserConfirmation();
|
|
53
|
+
request.response.fireInteractionNeeded(toolCall);
|
|
54
|
+
request.response.waitForInput();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('fromRequest', () => {
|
|
58
|
+
it('should be idle without a request', () => {
|
|
59
|
+
expect(ChatSessionStatus.fromRequest(undefined)).to.equal('idle');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('isInProgress', () => {
|
|
64
|
+
it('should be true for all non-terminal statuses', () => {
|
|
65
|
+
expect(ChatSessionStatus.isInProgress('running')).to.be.true;
|
|
66
|
+
expect(ChatSessionStatus.isInProgress('awaitingApproval')).to.be.true;
|
|
67
|
+
expect(ChatSessionStatus.isInProgress('awaitingToolCall')).to.be.true;
|
|
68
|
+
expect(ChatSessionStatus.isInProgress('awaitingInput')).to.be.true;
|
|
69
|
+
expect(ChatSessionStatus.isInProgress('idle')).to.be.false;
|
|
70
|
+
expect(ChatSessionStatus.isInProgress('failed')).to.be.false;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('requiresUserAction', () => {
|
|
75
|
+
it('should be true only when blocked on the user', () => {
|
|
76
|
+
expect(ChatSessionStatus.requiresUserAction('awaitingApproval')).to.be.true;
|
|
77
|
+
expect(ChatSessionStatus.requiresUserAction('awaitingInput')).to.be.true;
|
|
78
|
+
expect(ChatSessionStatus.requiresUserAction('running')).to.be.false;
|
|
79
|
+
expect(ChatSessionStatus.requiresUserAction('awaitingToolCall')).to.be.false;
|
|
80
|
+
expect(ChatSessionStatus.requiresUserAction('idle')).to.be.false;
|
|
81
|
+
expect(ChatSessionStatus.requiresUserAction('failed')).to.be.false;
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('MutableChatModel.status', () => {
|
|
86
|
+
it('should be idle for an empty session', () => {
|
|
87
|
+
const model = new MutableChatModel();
|
|
88
|
+
expect(model.status).to.equal('idle');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should be running while a request is in progress', () => {
|
|
92
|
+
const model = new MutableChatModel();
|
|
93
|
+
model.addRequest(parsedRequest());
|
|
94
|
+
expect(model.status).to.equal('running');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should fire a statusChanged event with the model already updated', () => {
|
|
98
|
+
const model = new MutableChatModel();
|
|
99
|
+
let statusInEvent: ChatSessionStatus | undefined;
|
|
100
|
+
let statusOnModel: ChatSessionStatus | undefined;
|
|
101
|
+
model.onDidChange(event => {
|
|
102
|
+
if (ChatChangeEvent.isStatusChangedEvent(event)) {
|
|
103
|
+
statusInEvent = event.status;
|
|
104
|
+
statusOnModel = model.status;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
model.addRequest(parsedRequest());
|
|
109
|
+
|
|
110
|
+
expect(statusInEvent).to.equal('running');
|
|
111
|
+
expect(statusOnModel).to.equal('running');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not fire statusChanged for changes that keep the status', () => {
|
|
115
|
+
const model = new MutableChatModel();
|
|
116
|
+
const request = model.addRequest(parsedRequest());
|
|
117
|
+
const statuses = trackStatusChanges(model);
|
|
118
|
+
|
|
119
|
+
request.response.response.addContent(new TextChatResponseContentImpl('first'));
|
|
120
|
+
request.response.response.addContent(new TextChatResponseContentImpl('second'));
|
|
121
|
+
|
|
122
|
+
expect(model.status).to.equal('running');
|
|
123
|
+
expect(statuses).to.deep.equal([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return to idle when the last request completes', () => {
|
|
127
|
+
const model = new MutableChatModel();
|
|
128
|
+
const request = model.addRequest(parsedRequest());
|
|
129
|
+
const statuses = trackStatusChanges(model);
|
|
130
|
+
|
|
131
|
+
request.response.complete();
|
|
132
|
+
|
|
133
|
+
expect(model.status).to.equal('idle');
|
|
134
|
+
expect(statuses).to.deep.equal(['idle']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should be idle when the last request was canceled', () => {
|
|
138
|
+
const model = new MutableChatModel();
|
|
139
|
+
const request = model.addRequest(parsedRequest());
|
|
140
|
+
|
|
141
|
+
request.response.cancel();
|
|
142
|
+
|
|
143
|
+
expect(model.status).to.equal('idle');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should be failed when the last request ended in an error', () => {
|
|
147
|
+
const model = new MutableChatModel();
|
|
148
|
+
const request = model.addRequest(parsedRequest());
|
|
149
|
+
|
|
150
|
+
request.response.error(new Error('boom'));
|
|
151
|
+
|
|
152
|
+
expect(model.status).to.equal('failed');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should be running again when a new request is added after a failure', () => {
|
|
156
|
+
const model = new MutableChatModel();
|
|
157
|
+
const failed = model.addRequest(parsedRequest());
|
|
158
|
+
failed.response.error(new Error('boom'));
|
|
159
|
+
|
|
160
|
+
model.addRequest(parsedRequest('try again'));
|
|
161
|
+
|
|
162
|
+
expect(model.status).to.equal('running');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should be awaitingToolCall while a tool call is executing', () => {
|
|
166
|
+
const model = new MutableChatModel();
|
|
167
|
+
const request = model.addRequest(parsedRequest());
|
|
168
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'myTool', '{}', false);
|
|
169
|
+
|
|
170
|
+
request.response.response.addContent(toolCall);
|
|
171
|
+
expect(model.status).to.equal('awaitingToolCall');
|
|
172
|
+
|
|
173
|
+
toolCall.complete('done');
|
|
174
|
+
expect(model.status).to.equal('running');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should be awaitingApproval while a tool call waits for user confirmation', () => {
|
|
178
|
+
const model = new MutableChatModel();
|
|
179
|
+
const request = model.addRequest(parsedRequest());
|
|
180
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'myTool', '{}', false);
|
|
181
|
+
request.response.response.addContent(toolCall);
|
|
182
|
+
|
|
183
|
+
requestConfirmation(request, toolCall);
|
|
184
|
+
|
|
185
|
+
expect(model.status).to.equal('awaitingApproval');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should move to awaitingToolCall when the user approves the tool call', () => {
|
|
189
|
+
const model = new MutableChatModel();
|
|
190
|
+
const request = model.addRequest(parsedRequest());
|
|
191
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'myTool', '{}', false);
|
|
192
|
+
request.response.response.addContent(toolCall);
|
|
193
|
+
requestConfirmation(request, toolCall);
|
|
194
|
+
|
|
195
|
+
toolCall.confirm();
|
|
196
|
+
request.response.stopWaitingForInput();
|
|
197
|
+
|
|
198
|
+
expect(model.status).to.equal('awaitingToolCall');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should move back to running when the user denies the tool call', () => {
|
|
202
|
+
const model = new MutableChatModel();
|
|
203
|
+
const request = model.addRequest(parsedRequest());
|
|
204
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'myTool', '{}', false);
|
|
205
|
+
request.response.response.addContent(toolCall);
|
|
206
|
+
requestConfirmation(request, toolCall);
|
|
207
|
+
|
|
208
|
+
toolCall.deny('not allowed');
|
|
209
|
+
request.response.stopWaitingForInput();
|
|
210
|
+
|
|
211
|
+
expect(model.status).to.equal('running');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should be awaitingInput while a structured question is unanswered', () => {
|
|
215
|
+
const model = new MutableChatModel();
|
|
216
|
+
const request = model.addRequest(parsedRequest());
|
|
217
|
+
const question = new QuestionResponseContentImpl(
|
|
218
|
+
'Proceed?',
|
|
219
|
+
[{ text: 'Yes' }, { text: 'No' }],
|
|
220
|
+
request,
|
|
221
|
+
() => { /* no-op */ }
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
request.response.response.addContent(question);
|
|
225
|
+
expect(model.status).to.equal('awaitingInput');
|
|
226
|
+
|
|
227
|
+
question.selectedOption = { text: 'Yes' };
|
|
228
|
+
expect(model.status).to.equal('running');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should be awaitingInput while waiting for input without a question part', () => {
|
|
232
|
+
// E.g. the user-interaction tool keeps its tool call unfinished while the
|
|
233
|
+
// user completes a wizard; only the waiting-for-input flag is set.
|
|
234
|
+
const model = new MutableChatModel();
|
|
235
|
+
const request = model.addRequest(parsedRequest());
|
|
236
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'userInteraction', '{}', false);
|
|
237
|
+
request.response.response.addContent(toolCall);
|
|
238
|
+
|
|
239
|
+
request.response.waitForInput();
|
|
240
|
+
expect(model.status).to.equal('awaitingInput');
|
|
241
|
+
|
|
242
|
+
request.response.stopWaitingForInput();
|
|
243
|
+
expect(model.status).to.equal('awaitingToolCall');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should prefer awaitingApproval over awaitingInput', () => {
|
|
247
|
+
const model = new MutableChatModel();
|
|
248
|
+
const request = model.addRequest(parsedRequest());
|
|
249
|
+
const question = new QuestionResponseContentImpl(
|
|
250
|
+
'Proceed?',
|
|
251
|
+
[{ text: 'Yes' }],
|
|
252
|
+
request,
|
|
253
|
+
() => { /* no-op */ }
|
|
254
|
+
);
|
|
255
|
+
request.response.response.addContent(question);
|
|
256
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'myTool', '{}', false);
|
|
257
|
+
request.response.response.addContent(toolCall);
|
|
258
|
+
|
|
259
|
+
requestConfirmation(request, toolCall);
|
|
260
|
+
|
|
261
|
+
expect(model.status).to.equal('awaitingApproval');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should record the full transition sequence of a confirmed tool call', () => {
|
|
265
|
+
const model = new MutableChatModel();
|
|
266
|
+
const statuses = trackStatusChanges(model);
|
|
267
|
+
const request = model.addRequest(parsedRequest());
|
|
268
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'myTool', '{}', false);
|
|
269
|
+
|
|
270
|
+
request.response.response.addContent(toolCall);
|
|
271
|
+
requestConfirmation(request, toolCall);
|
|
272
|
+
toolCall.confirm();
|
|
273
|
+
request.response.stopWaitingForInput();
|
|
274
|
+
toolCall.complete('done');
|
|
275
|
+
request.response.complete();
|
|
276
|
+
|
|
277
|
+
expect(statuses).to.deep.equal(['running', 'awaitingToolCall', 'awaitingApproval', 'awaitingToolCall', 'running', 'idle']);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -28,6 +28,10 @@ export interface ChatModelWithMetadata {
|
|
|
28
28
|
lastInteraction?: number;
|
|
29
29
|
/** Whether the last request ended with an error. */
|
|
30
30
|
hasError?: boolean;
|
|
31
|
+
/** ID of the root session in the delegation chain. */
|
|
32
|
+
rootSessionId?: string;
|
|
33
|
+
/** ID of the immediate parent session that delegated this one. */
|
|
34
|
+
parentSessionId?: string;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface ChatSessionStore {
|
|
@@ -75,4 +79,8 @@ export interface ChatSessionMetadata {
|
|
|
75
79
|
pinnedAgentId?: string;
|
|
76
80
|
/** Whether the last request ended with an error. */
|
|
77
81
|
hasError?: boolean;
|
|
82
|
+
/** ID of the root session in the delegation chain. */
|
|
83
|
+
rootSessionId?: string;
|
|
84
|
+
/** ID of the immediate parent session that delegated this one. */
|
|
85
|
+
parentSessionId?: string;
|
|
78
86
|
}
|
|
@@ -290,6 +290,48 @@ describe('ToolCallChatResponseContent - confirmation state transitions', () => {
|
|
|
290
290
|
expect(result).to.be.false;
|
|
291
291
|
expect(content.result).to.deep.equal({ denied: true, reason: undefined });
|
|
292
292
|
});
|
|
293
|
+
|
|
294
|
+
describe('isAwaitingUserConfirmation', () => {
|
|
295
|
+
it('should be false initially', () => {
|
|
296
|
+
const content = createToolCallContent();
|
|
297
|
+
expect(content.isAwaitingUserConfirmation).to.be.false;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should be true after requestUserConfirmation()', () => {
|
|
301
|
+
const content = createToolCallContent();
|
|
302
|
+
content.requestUserConfirmation();
|
|
303
|
+
expect(content.isAwaitingUserConfirmation).to.be.true;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should be false after the user confirms', () => {
|
|
307
|
+
const content = createToolCallContent();
|
|
308
|
+
content.requestUserConfirmation();
|
|
309
|
+
content.confirm();
|
|
310
|
+
expect(content.isAwaitingUserConfirmation).to.be.false;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should be false after the user denies', () => {
|
|
314
|
+
const content = createToolCallContent();
|
|
315
|
+
content.requestUserConfirmation();
|
|
316
|
+
content.deny('rejected');
|
|
317
|
+
expect(content.isAwaitingUserConfirmation).to.be.false;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should be false after the confirmation is canceled', () => {
|
|
321
|
+
const content = createToolCallContent();
|
|
322
|
+
content.confirmed.catch(() => undefined);
|
|
323
|
+
content.requestUserConfirmation();
|
|
324
|
+
content.cancelConfirmation(new Error('request canceled'));
|
|
325
|
+
expect(content.isAwaitingUserConfirmation).to.be.false;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should be false once the tool call is finished', () => {
|
|
329
|
+
const content = createToolCallContent();
|
|
330
|
+
content.requestUserConfirmation();
|
|
331
|
+
content.complete('result');
|
|
332
|
+
expect(content.isAwaitingUserConfirmation).to.be.false;
|
|
333
|
+
});
|
|
334
|
+
});
|
|
293
335
|
});
|
|
294
336
|
|
|
295
337
|
describe('Tool Confirmation Timeout', () => {
|