@theia/ai-chat 1.76.0-next.7 → 1.76.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.d.ts +6 -0
- package/lib/browser/agent-delegation-tool.d.ts.map +1 -1
- package/lib/browser/agent-delegation-tool.js +177 -92
- package/lib/browser/agent-delegation-tool.js.map +1 -1
- package/lib/browser/agent-delegation-tool.spec.js +170 -7
- package/lib/browser/agent-delegation-tool.spec.js.map +1 -1
- package/lib/common/chat-model.d.ts +39 -0
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +54 -3
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-response-model.spec.js +157 -0
- package/lib/common/chat-response-model.spec.js.map +1 -1
- package/lib/common/chat-service.d.ts +12 -0
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +26 -7
- package/lib/common/chat-service.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/agent-delegation-tool.spec.ts +237 -7
- package/src/browser/agent-delegation-tool.ts +200 -109
- package/src/common/chat-model.ts +87 -3
- package/src/common/chat-response-model.spec.ts +212 -1
- package/src/common/chat-service.ts +29 -6
|
@@ -24,8 +24,11 @@ import {
|
|
|
24
24
|
ChatChangeEvent,
|
|
25
25
|
ChatRequest,
|
|
26
26
|
ChatResponseContent,
|
|
27
|
+
ChatContextManager,
|
|
27
28
|
ChatService,
|
|
28
29
|
ChatServiceFactory,
|
|
30
|
+
ChatSession,
|
|
31
|
+
ChatSessionStatus,
|
|
29
32
|
ChatToolContext,
|
|
30
33
|
MutableChatModel,
|
|
31
34
|
MutableChatRequestModel,
|
|
@@ -57,9 +60,12 @@ export class AgentDelegationTool implements ToolProvider {
|
|
|
57
60
|
name: AgentDelegationTool.ID,
|
|
58
61
|
description:
|
|
59
62
|
'Delegate a task or question to a specific AI agent. IMPORTANT: When you delegate a task or question to a specific AI agent using this tool, ' +
|
|
60
|
-
'remember that each sub-agent operates solely within its specialized capabilities and tools and
|
|
61
|
-
' or external systems. Therefore, it is crucial to provide all necessary context and detailed
|
|
62
|
-
'and effective task completion. ' +
|
|
63
|
+
'remember that each sub-agent operates solely within its specialized capabilities and tools and, unless you resume a previous delegation via sessionId, ' +
|
|
64
|
+
'does not have access to previous conversation context or external systems. Therefore, it is crucial to provide all necessary context and detailed ' +
|
|
65
|
+
'information directly within your request to ensure accurate and effective task completion. ' +
|
|
66
|
+
'Each result ends with a "[delegation sessionId: <id>]" line. To send a follow-up request to the same agent with its previous conversation retained ' +
|
|
67
|
+
'(e.g. to request fixes after reviewing its result), pass that id as sessionId. Prefer resuming for follow-ups on the same task; ' +
|
|
68
|
+
'start a fresh session (omit sessionId) for unrelated tasks, as a resumed session grows with every round. ' +
|
|
63
69
|
'You may optionally pass a taskContextId to make a specific task context (e.g. a plan) available to the delegated agent via its system prompt.',
|
|
64
70
|
parameters: {
|
|
65
71
|
type: 'object',
|
|
@@ -78,6 +84,12 @@ export class AgentDelegationTool implements ToolProvider {
|
|
|
78
84
|
type: 'string',
|
|
79
85
|
description: 'Optional task context ID to make available to the delegated agent. The agent will see the task context in its system prompt.',
|
|
80
86
|
},
|
|
87
|
+
sessionId: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Optional ID of a previous delegation session to continue, as reported in the "[delegation sessionId: <id>]" line of an earlier result. ' +
|
|
90
|
+
'The delegated agent keeps its previous conversation context, so you do not need to repeat context you already sent it. ' +
|
|
91
|
+
'Only a session that you delegated yourself can be resumed, and the agentId must match the original delegation.',
|
|
92
|
+
},
|
|
81
93
|
},
|
|
82
94
|
required: ['agentId', 'prompt'],
|
|
83
95
|
},
|
|
@@ -102,7 +114,7 @@ export class AgentDelegationTool implements ToolProvider {
|
|
|
102
114
|
|
|
103
115
|
try {
|
|
104
116
|
const args = JSON.parse(arg_string);
|
|
105
|
-
const { agentId, prompt, taskContextId } = args;
|
|
117
|
+
const { agentId, prompt, taskContextId, sessionId } = args;
|
|
106
118
|
|
|
107
119
|
if (!agentId || !prompt) {
|
|
108
120
|
const errorMsg = 'Both agentId and prompt parameters are required.';
|
|
@@ -121,131 +133,183 @@ export class AgentDelegationTool implements ToolProvider {
|
|
|
121
133
|
return errorMsg;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
let
|
|
136
|
+
let session: ChatSession;
|
|
125
137
|
let childModelDisposable: Disposable | undefined;
|
|
126
|
-
|
|
138
|
+
if (sessionId) {
|
|
127
139
|
const chatService = this.getChatService();
|
|
140
|
+
// Restore from storage if necessary: delegated sessions are auto-saved, so after a reload the
|
|
141
|
+
// delegating session's history still carries the id while the session itself is no longer in memory.
|
|
142
|
+
const existingSession = await chatService.getOrRestoreSession(sessionId);
|
|
143
|
+
if (!existingSession) {
|
|
144
|
+
const errorMsg = `Delegation session '${sessionId}' not found. It may have been deleted. ` +
|
|
145
|
+
'Send the request again without sessionId to start a fresh session — the agent will not have any prior context.';
|
|
146
|
+
// warn, not error: a stale session id is expected in a normal agentic loop (e.g. the user deleted the session)
|
|
147
|
+
this.logger.warn(errorMsg);
|
|
148
|
+
return errorMsg;
|
|
149
|
+
}
|
|
150
|
+
if (existingSession.parentSessionId !== ctx.request.session.id) {
|
|
151
|
+
// Without this, any session id would be accepted, including an unrelated top-level user chat,
|
|
152
|
+
// and the delegated request would be injected into it.
|
|
153
|
+
const errorMsg = `Delegation session '${sessionId}' was not delegated by this session and cannot be resumed here. ` +
|
|
154
|
+
'Only pass a sessionId reported by one of your own earlier delegations, or omit it to start a fresh session.';
|
|
155
|
+
this.logger.error(errorMsg);
|
|
156
|
+
return errorMsg;
|
|
157
|
+
}
|
|
158
|
+
if (ChatSessionStatus.isInProgress(existingSession.model.status)) {
|
|
159
|
+
const errorMsg = `Delegation session '${sessionId}' is still processing a previous request. ` +
|
|
160
|
+
'Wait for its result before sending a follow-up, or omit sessionId to start a fresh session.';
|
|
161
|
+
// warn, not error: expected when the caller issues parallel requests against the same session
|
|
162
|
+
this.logger.warn(errorMsg);
|
|
163
|
+
return errorMsg;
|
|
164
|
+
}
|
|
165
|
+
if (existingSession.pinnedAgent && existingSession.pinnedAgent.id !== agentId) {
|
|
166
|
+
const errorMsg = `Delegation session '${sessionId}' belongs to agent '${existingSession.pinnedAgent.id}', not '${agentId}'. ` +
|
|
167
|
+
'Pass the matching agentId, or omit sessionId to start a fresh session.';
|
|
168
|
+
this.logger.error(errorMsg);
|
|
169
|
+
return errorMsg;
|
|
170
|
+
}
|
|
128
171
|
|
|
129
|
-
//
|
|
130
|
-
|
|
172
|
+
// Deliberately keep rootSessionId/parentSessionId as set at creation time; a resumed
|
|
173
|
+
// session stays attached to its original delegation hierarchy.
|
|
174
|
+
if (taskContextId) {
|
|
175
|
+
this.setTaskContext(existingSession.model.context, taskContextId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
session = existingSession;
|
|
179
|
+
// Re-establish bubbling for this request; the previous disposable was disposed when the
|
|
180
|
+
// original delegation completed, and the parent request/response are new objects now.
|
|
181
|
+
childModelDisposable = this.setupChildSessionBubbling(existingSession.model as MutableChatModel, ctx.request.session, ctx.response);
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
const chatService = this.getChatService();
|
|
131
185
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{ focus: false },
|
|
135
|
-
agent
|
|
136
|
-
);
|
|
186
|
+
// Store the current active session to restore it after delegation
|
|
187
|
+
const currentActiveSession = chatService.getActiveSession();
|
|
137
188
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
const newSession = chatService.createSession(
|
|
190
|
+
undefined,
|
|
191
|
+
{ focus: false },
|
|
192
|
+
agent
|
|
193
|
+
);
|
|
143
194
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
195
|
+
// Set root session ID to enable task context sharing across delegation chains
|
|
196
|
+
// Root is either the current root (for nested delegation) or current session (for first-level delegation)
|
|
197
|
+
const rootId = ctx.rootSessionId || ctx.request.session.id;
|
|
198
|
+
newSession.rootSessionId = rootId;
|
|
199
|
+
newSession.model.rootSessionId = rootId;
|
|
200
|
+
|
|
201
|
+
// Track the immediate parent (the delegating session) so the UI can render the full
|
|
202
|
+
// delegation hierarchy, not just a flat list under the root.
|
|
203
|
+
const parentId = ctx.request.session.id;
|
|
204
|
+
newSession.parentSessionId = parentId;
|
|
205
|
+
newSession.model.parentSessionId = parentId;
|
|
206
|
+
|
|
207
|
+
if (taskContextId) {
|
|
208
|
+
newSession.model.context.addVariables({
|
|
209
|
+
variable: TASK_CONTEXT_VARIABLE,
|
|
210
|
+
arg: taskContextId
|
|
211
|
+
});
|
|
212
|
+
}
|
|
149
213
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
}
|
|
214
|
+
// Immediately restore the original active session to avoid confusing the user
|
|
215
|
+
if (currentActiveSession) {
|
|
216
|
+
chatService.setActiveSession(currentActiveSession.id, { focus: false });
|
|
217
|
+
}
|
|
156
218
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
219
|
+
// Setup bubbling of child session events to parent session
|
|
220
|
+
childModelDisposable = this.setupChildSessionBubbling(newSession.model as MutableChatModel, ctx.request.session, ctx.response);
|
|
221
|
+
session = newSession;
|
|
222
|
+
} catch (sessionError) {
|
|
223
|
+
const errorMsg = `Failed to create chat session for agent '${agentId}': ${sessionError instanceof Error ? sessionError.message : sessionError}`;
|
|
224
|
+
this.logger.error(errorMsg, sessionError);
|
|
225
|
+
return errorMsg;
|
|
160
226
|
}
|
|
161
|
-
|
|
162
|
-
// Setup bubbling of child session events to parent session
|
|
163
|
-
childModelDisposable = this.setupChildSessionBubbling(newSession.model as MutableChatModel, ctx.request.session, ctx.response);
|
|
164
|
-
} catch (sessionError) {
|
|
165
|
-
const errorMsg = `Failed to create chat session for agent '${agentId}': ${sessionError instanceof Error ? sessionError.message : sessionError}`;
|
|
166
|
-
this.logger.error(errorMsg, sessionError);
|
|
167
|
-
return errorMsg;
|
|
168
227
|
}
|
|
169
228
|
|
|
170
|
-
// Send the request
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
let response: ChatRequestInvocation | undefined;
|
|
229
|
+
// Send the request. The `finally` releases the event bubbling on every outcome: a resumed
|
|
230
|
+
// session is long-lived, so leaking a listener per failed round would forward its events
|
|
231
|
+
// into stale parent responses.
|
|
176
232
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
233
|
+
const chatRequest: ChatRequest = {
|
|
234
|
+
text: `@${agentId} ${prompt}`,
|
|
235
|
+
};
|
|
180
236
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (ctx.cancellationToken) {
|
|
188
|
-
ctx.cancellationToken.onCancellationRequested(
|
|
189
|
-
async () => {
|
|
190
|
-
if (response) {
|
|
191
|
-
((await response?.requestCompleted) as MutableChatRequestModel).cancel();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
} catch (sendError) {
|
|
197
|
-
const errorMsg = `Failed to send request to agent '${agentId}': ${sendError instanceof Error ? sendError.message : sendError}`;
|
|
198
|
-
this.logger.error(errorMsg, sendError);
|
|
199
|
-
return errorMsg;
|
|
200
|
-
}
|
|
237
|
+
let response: ChatRequestInvocation | undefined;
|
|
238
|
+
try {
|
|
239
|
+
if (ctx.cancellationToken?.isCancellationRequested) {
|
|
240
|
+
return 'Operation cancelled by user';
|
|
241
|
+
}
|
|
201
242
|
|
|
202
|
-
if (response) {
|
|
203
|
-
// Store the invocation in the registry so the renderer can access it
|
|
204
|
-
if (ctx.toolCallId) {
|
|
205
|
-
this.pendingDelegations.set(ctx.toolCallId, {
|
|
206
|
-
prompt,
|
|
207
|
-
invocation: response
|
|
208
|
-
});
|
|
209
|
-
// Clean up when the delegated session is deleted
|
|
210
243
|
const chatService = this.getChatService();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
244
|
+
response = await chatService.sendRequest(
|
|
245
|
+
session.id,
|
|
246
|
+
chatRequest
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (ctx.cancellationToken) {
|
|
250
|
+
ctx.cancellationToken.onCancellationRequested(
|
|
251
|
+
async () => {
|
|
252
|
+
if (response) {
|
|
253
|
+
((await response?.requestCompleted) as MutableChatRequestModel).cancel();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
} catch (sendError) {
|
|
259
|
+
const errorMsg = `Failed to send request to agent '${agentId}': ${sendError instanceof Error ? sendError.message : sendError}`;
|
|
260
|
+
this.logger.error(errorMsg, sendError);
|
|
261
|
+
return errorMsg;
|
|
218
262
|
}
|
|
219
263
|
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return
|
|
264
|
+
if (response) {
|
|
265
|
+
// Store the invocation in the registry so the renderer can access it
|
|
266
|
+
if (ctx.toolCallId) {
|
|
267
|
+
this.pendingDelegations.set(ctx.toolCallId, {
|
|
268
|
+
prompt,
|
|
269
|
+
invocation: response
|
|
270
|
+
});
|
|
271
|
+
// Clean up when the delegated session is deleted
|
|
272
|
+
const chatService = this.getChatService();
|
|
273
|
+
const toolCallId = ctx.toolCallId;
|
|
274
|
+
const sessionEventListener = chatService.onSessionEvent(event => {
|
|
275
|
+
if (event.type === 'deleted' && event.sessionId === session.id) {
|
|
276
|
+
this.pendingDelegations.delete(toolCallId);
|
|
277
|
+
sessionEventListener.dispose();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Wait for completion to return the final result as tool output
|
|
284
|
+
const result = await response.responseCompleted;
|
|
285
|
+
const filteredContent = result.response.content.filter(c => !ThinkingChatResponseContent.is(c) && !ToolCallChatResponseContent.is(c));
|
|
286
|
+
const stringResult = filteredContent
|
|
287
|
+
.map(c => ChatResponseContent.hasAsString(c) ? c.asString() : undefined)
|
|
288
|
+
.filter((text): text is string => text !== undefined && text !== '')
|
|
289
|
+
.join('\n\n');
|
|
290
|
+
|
|
291
|
+
// Return the raw text to the top-level Agent, as a tool result. The session id
|
|
292
|
+
// allows the caller to send follow-up requests into the same session.
|
|
293
|
+
const sessionIdSuffix = `[delegation sessionId: ${session.id}]`;
|
|
294
|
+
return stringResult ? `${stringResult}\n\n${sessionIdSuffix}` : sessionIdSuffix;
|
|
295
|
+
} catch (completionError) {
|
|
296
|
+
if (
|
|
297
|
+
completionError instanceof Error &&
|
|
298
|
+
completionError.message.includes('cancelled')
|
|
299
|
+
) {
|
|
300
|
+
return 'Operation cancelled by user';
|
|
301
|
+
}
|
|
302
|
+
const errorMsg = `Failed to complete response from agent '${agentId}': ${completionError instanceof Error ? completionError.message : completionError}`;
|
|
303
|
+
this.logger.error(errorMsg, completionError);
|
|
304
|
+
return errorMsg;
|
|
240
305
|
}
|
|
241
|
-
|
|
242
|
-
|
|
306
|
+
} else {
|
|
307
|
+
const errorMsg = `Delegation to agent '${agentId}' has failed: no response returned.`;
|
|
308
|
+
this.logger.error(errorMsg);
|
|
243
309
|
return errorMsg;
|
|
244
310
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.logger.error(errorMsg);
|
|
248
|
-
return errorMsg;
|
|
311
|
+
} finally {
|
|
312
|
+
childModelDisposable?.dispose();
|
|
249
313
|
}
|
|
250
314
|
} catch (error) {
|
|
251
315
|
this.logger.error('Failed to delegate to agent', error);
|
|
@@ -255,6 +319,24 @@ export class AgentDelegationTool implements ToolProvider {
|
|
|
255
319
|
}
|
|
256
320
|
}
|
|
257
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Makes the given task context the session's task context, replacing a previously set one.
|
|
324
|
+
* `addVariables` deduplicates on variable id *and* argument, so adding a different task context
|
|
325
|
+
* id would otherwise leave the session with both in its system prompt.
|
|
326
|
+
*/
|
|
327
|
+
private setTaskContext(context: ChatContextManager, taskContextId: string): void {
|
|
328
|
+
const staleIndices = context.getVariables().reduce<number[]>((indices, variable, index) => {
|
|
329
|
+
if (variable.variable.id === TASK_CONTEXT_VARIABLE.id && variable.arg !== taskContextId) {
|
|
330
|
+
indices.push(index);
|
|
331
|
+
}
|
|
332
|
+
return indices;
|
|
333
|
+
}, []);
|
|
334
|
+
if (staleIndices.length > 0) {
|
|
335
|
+
context.deleteVariables(...staleIndices);
|
|
336
|
+
}
|
|
337
|
+
context.addVariables({ variable: TASK_CONTEXT_VARIABLE, arg: taskContextId });
|
|
338
|
+
}
|
|
339
|
+
|
|
258
340
|
/**
|
|
259
341
|
* Sets up all event bubbling from a delegated child session to the parent session:
|
|
260
342
|
* - Interaction forwarding: child interactionNeeded events are forwarded to the parent response
|
|
@@ -273,6 +355,15 @@ export class AgentDelegationTool implements ToolProvider {
|
|
|
273
355
|
if (ChatChangeEvent.isInteractionNeededEvent(event)) {
|
|
274
356
|
parentResponse.fireInteractionNeeded(event.contentPart);
|
|
275
357
|
event.contentPart.whenResolved.then(() => parentResponse.notifyChanged());
|
|
358
|
+
// A tool call's confirmation settles long before the call resolves (the
|
|
359
|
+
// tool may run for minutes after being confirmed); notify the parent at
|
|
360
|
+
// settle time so its UI re-derives pending interactions immediately.
|
|
361
|
+
if (ToolCallChatResponseContent.is(event.contentPart)) {
|
|
362
|
+
event.contentPart.confirmed.then(
|
|
363
|
+
() => parentResponse.notifyChanged(),
|
|
364
|
+
() => parentResponse.notifyChanged()
|
|
365
|
+
);
|
|
366
|
+
}
|
|
276
367
|
}
|
|
277
368
|
});
|
|
278
369
|
|
package/src/common/chat-model.ts
CHANGED
|
@@ -286,6 +286,19 @@ export type ChatSessionStatus =
|
|
|
286
286
|
| 'failed';
|
|
287
287
|
|
|
288
288
|
export namespace ChatSessionStatus {
|
|
289
|
+
/**
|
|
290
|
+
* All {@link ChatSessionStatus} values, e.g. for schema declarations. Derived from an
|
|
291
|
+
* exhaustive record so that adding a status without listing it here fails to compile.
|
|
292
|
+
*/
|
|
293
|
+
export const VALUES: readonly ChatSessionStatus[] = Object.keys({
|
|
294
|
+
idle: true,
|
|
295
|
+
running: true,
|
|
296
|
+
awaitingApproval: true,
|
|
297
|
+
awaitingToolCall: true,
|
|
298
|
+
awaitingInput: true,
|
|
299
|
+
failed: true
|
|
300
|
+
} satisfies Record<ChatSessionStatus, true>) as ChatSessionStatus[];
|
|
301
|
+
|
|
289
302
|
/**
|
|
290
303
|
* Whether a request is in progress in this status, including the states waiting on the user or a tool.
|
|
291
304
|
*/
|
|
@@ -502,6 +515,13 @@ export interface InteractiveContent {
|
|
|
502
515
|
readonly isResolved: boolean;
|
|
503
516
|
/** Resolves when the interaction is resolved. Used for cleanup in delegation chains. */
|
|
504
517
|
readonly whenResolved: Promise<void>;
|
|
518
|
+
/**
|
|
519
|
+
* Whether the interaction currently requires user action. Unlike {@link isResolved},
|
|
520
|
+
* this reflects the momentary state: e.g. a tool call whose confirmation was granted
|
|
521
|
+
* is not yet resolved (the tool is still executing) but no longer awaits interaction.
|
|
522
|
+
* When `undefined`, consumers should fall back to `!isResolved`.
|
|
523
|
+
*/
|
|
524
|
+
readonly isAwaitingInteraction?: boolean;
|
|
505
525
|
}
|
|
506
526
|
|
|
507
527
|
export namespace InteractiveContent {
|
|
@@ -706,6 +726,16 @@ export interface ToolCallChatResponseContent extends Required<ChatResponseConten
|
|
|
706
726
|
cancelConfirmation(reason?: unknown): void;
|
|
707
727
|
/** Signal that this tool call needs user confirmation. Resolves the needsUserConfirmation promise. */
|
|
708
728
|
requestUserConfirmation(): void;
|
|
729
|
+
/**
|
|
730
|
+
* Whether the tool execution is currently blocked waiting for user input provided
|
|
731
|
+
* through the tool's own UI (e.g. an interactive wizard). Cleared when the tool
|
|
732
|
+
* call finishes or {@link userInputHandled} is called.
|
|
733
|
+
*/
|
|
734
|
+
readonly isAwaitingUserInput: boolean;
|
|
735
|
+
/** Signal that the tool execution is blocked waiting for user input. */
|
|
736
|
+
requestUserInput(): void;
|
|
737
|
+
/** Signal that the tool execution is no longer waiting for user input. */
|
|
738
|
+
userInputHandled(): void;
|
|
709
739
|
/**
|
|
710
740
|
* Update the tool call's result without marking it finished. Use this to persist
|
|
711
741
|
* intermediate state for long-running tools (e.g. the user-interaction wizard) so
|
|
@@ -1073,6 +1103,13 @@ export interface ChatResponseModel {
|
|
|
1073
1103
|
* The content part that needs interaction is provided as the event payload.
|
|
1074
1104
|
*/
|
|
1075
1105
|
readonly onInteractionNeeded: Event<InteractiveContent & ChatResponseContent>;
|
|
1106
|
+
/**
|
|
1107
|
+
* Content parts announced via {@link onInteractionNeeded} that currently await user
|
|
1108
|
+
* interaction (see {@link InteractiveContent.isAwaitingInteraction}). Lets late
|
|
1109
|
+
* subscribers (e.g. a remounted delegation renderer) rebuild pending interaction
|
|
1110
|
+
* state instead of relying solely on the push event.
|
|
1111
|
+
*/
|
|
1112
|
+
readonly pendingInteractions: ReadonlyArray<InteractiveContent & ChatResponseContent>;
|
|
1076
1113
|
/**
|
|
1077
1114
|
* The unique identifier of the response model
|
|
1078
1115
|
*/
|
|
@@ -2578,6 +2615,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
|
|
|
2578
2615
|
protected _needsUserConfirmation: Promise<void>;
|
|
2579
2616
|
protected _needsUserConfirmationResolver?: () => void;
|
|
2580
2617
|
protected _isAwaitingUserConfirmation = false;
|
|
2618
|
+
protected _isAwaitingUserInput = false;
|
|
2581
2619
|
protected _confirmed: Promise<boolean>;
|
|
2582
2620
|
protected _confirmationResolver?: (value: boolean) => void;
|
|
2583
2621
|
protected _confirmationRejecter?: (reason?: unknown) => void;
|
|
@@ -2659,6 +2697,14 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
|
|
|
2659
2697
|
return this._isAwaitingUserConfirmation && !this.finished;
|
|
2660
2698
|
}
|
|
2661
2699
|
|
|
2700
|
+
get isAwaitingUserInput(): boolean {
|
|
2701
|
+
return this._isAwaitingUserInput && !this.finished;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
get isAwaitingInteraction(): boolean {
|
|
2705
|
+
return this.isAwaitingUserConfirmation || this.isAwaitingUserInput;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2662
2708
|
get whenFinished(): Promise<void> {
|
|
2663
2709
|
return this._whenFinished;
|
|
2664
2710
|
}
|
|
@@ -2717,6 +2763,14 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
|
|
|
2717
2763
|
}
|
|
2718
2764
|
}
|
|
2719
2765
|
|
|
2766
|
+
requestUserInput(): void {
|
|
2767
|
+
this._isAwaitingUserInput = true;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
userInputHandled(): void {
|
|
2771
|
+
this._isAwaitingUserInput = false;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2720
2774
|
updateResult(result: ToolCallResult): void {
|
|
2721
2775
|
this._result = result;
|
|
2722
2776
|
this._onDidChangeEmitter.fire();
|
|
@@ -3104,6 +3158,12 @@ export class QuestionResponseContentImpl implements QuestionResponseContent, Int
|
|
|
3104
3158
|
return this.selectedOption !== undefined;
|
|
3105
3159
|
}
|
|
3106
3160
|
|
|
3161
|
+
get isAwaitingInteraction(): boolean {
|
|
3162
|
+
// A skipped question resolves with an empty selection: isResolved stays false,
|
|
3163
|
+
// but the question no longer awaits interaction.
|
|
3164
|
+
return !this.isReadOnly && this._selectedOptions === undefined;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3107
3167
|
set selectedOption(option: { text: string; value?: string } | undefined) {
|
|
3108
3168
|
this._selectedOptions = option ? [option] : undefined;
|
|
3109
3169
|
this._resolvedResolver?.();
|
|
@@ -3292,6 +3352,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
|
|
|
3292
3352
|
protected readonly _onInteractionNeededEmitter = new Emitter<InteractiveContent & ChatResponseContent>();
|
|
3293
3353
|
readonly onInteractionNeeded: Event<InteractiveContent & ChatResponseContent> = this._onInteractionNeededEmitter.event;
|
|
3294
3354
|
|
|
3355
|
+
protected _pendingInteractions: (InteractiveContent & ChatResponseContent)[] = [];
|
|
3356
|
+
|
|
3295
3357
|
data = {};
|
|
3296
3358
|
|
|
3297
3359
|
protected _id: string;
|
|
@@ -3474,16 +3536,22 @@ export class MutableChatResponseModel implements ChatResponseModel {
|
|
|
3474
3536
|
this._agentId = agentId;
|
|
3475
3537
|
}
|
|
3476
3538
|
|
|
3539
|
+
/** Reset all pending-input state; the response no longer accepts user interaction. */
|
|
3540
|
+
protected resetPendingInput(): void {
|
|
3541
|
+
this._waitingForInputCount = 0;
|
|
3542
|
+
this._pendingInteractions = [];
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3477
3545
|
complete(): void {
|
|
3478
3546
|
this._isComplete = true;
|
|
3479
|
-
this.
|
|
3547
|
+
this.resetPendingInput();
|
|
3480
3548
|
this._onDidChangeEmitter.fire();
|
|
3481
3549
|
}
|
|
3482
3550
|
|
|
3483
3551
|
cancel(): void {
|
|
3484
3552
|
this._cancellationToken.cancel();
|
|
3485
3553
|
this._isComplete = true;
|
|
3486
|
-
this.
|
|
3554
|
+
this.resetPendingInput();
|
|
3487
3555
|
|
|
3488
3556
|
// Ensure any pending tool confirmations are canceled when the chat is canceled
|
|
3489
3557
|
try {
|
|
@@ -3516,7 +3584,23 @@ export class MutableChatResponseModel implements ChatResponseModel {
|
|
|
3516
3584
|
this._onDidChangeEmitter.fire();
|
|
3517
3585
|
}
|
|
3518
3586
|
|
|
3587
|
+
get pendingInteractions(): ReadonlyArray<InteractiveContent & ChatResponseContent> {
|
|
3588
|
+
// Filter by the momentary awaiting state so consumers never see interactions
|
|
3589
|
+
// whose actionable phase has passed (e.g. a confirmed tool call that is still
|
|
3590
|
+
// executing and therefore not yet resolved).
|
|
3591
|
+
return this._pendingInteractions.filter(part => part.isAwaitingInteraction ?? !part.isResolved);
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3519
3594
|
fireInteractionNeeded(contentPart: InteractiveContent & ChatResponseContent): void {
|
|
3595
|
+
if (!this._isComplete && !contentPart.isResolved && !this._pendingInteractions.includes(contentPart)) {
|
|
3596
|
+
this._pendingInteractions = [...this._pendingInteractions, contentPart];
|
|
3597
|
+
// Stop tracking on settlement either way: custom InteractiveContent implementations
|
|
3598
|
+
// may reject whenResolved, and the rejection must not escape the model unhandled.
|
|
3599
|
+
const stopTracking = (): void => {
|
|
3600
|
+
this._pendingInteractions = this._pendingInteractions.filter(part => part !== contentPart);
|
|
3601
|
+
};
|
|
3602
|
+
contentPart.whenResolved.then(stopTracking, stopTracking);
|
|
3603
|
+
}
|
|
3520
3604
|
this._onInteractionNeededEmitter.fire(contentPart);
|
|
3521
3605
|
}
|
|
3522
3606
|
|
|
@@ -3526,7 +3610,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
|
|
|
3526
3610
|
|
|
3527
3611
|
error(error: Error): void {
|
|
3528
3612
|
this._isComplete = true;
|
|
3529
|
-
this.
|
|
3613
|
+
this.resetPendingInput();
|
|
3530
3614
|
this._isError = true;
|
|
3531
3615
|
this._errorObject = error;
|
|
3532
3616
|
this._onDidChangeEmitter.fire();
|