@theia/ai-chat 1.76.0-next.18 → 1.76.0-next.23

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.
@@ -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 does not have access to previous conversation context ' +
61
- ' or external systems. Therefore, it is crucial to provide all necessary context and detailed information directly within your request to ensure accurate ' +
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 newSession;
136
+ let session: ChatSession;
125
137
  let childModelDisposable: Disposable | undefined;
126
- try {
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
- // Store the current active session to restore it after delegation
130
- const currentActiveSession = chatService.getActiveSession();
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
+ }
131
177
 
132
- newSession = chatService.createSession(
133
- undefined,
134
- { focus: false },
135
- agent
136
- );
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();
137
185
 
138
- // Set root session ID to enable task context sharing across delegation chains
139
- // Root is either the current root (for nested delegation) or current session (for first-level delegation)
140
- const rootId = ctx.rootSessionId || ctx.request.session.id;
141
- newSession.rootSessionId = rootId;
142
- newSession.model.rootSessionId = rootId;
186
+ // Store the current active session to restore it after delegation
187
+ const currentActiveSession = chatService.getActiveSession();
143
188
 
144
- // Track the immediate parent (the delegating session) so the UI can render the full
145
- // delegation hierarchy, not just a flat list under the root.
146
- const parentId = ctx.request.session.id;
147
- newSession.parentSessionId = parentId;
148
- newSession.model.parentSessionId = parentId;
189
+ const newSession = chatService.createSession(
190
+ undefined,
191
+ { focus: false },
192
+ agent
193
+ );
149
194
 
150
- if (taskContextId) {
151
- newSession.model.context.addVariables({
152
- variable: TASK_CONTEXT_VARIABLE,
153
- arg: taskContextId
154
- });
155
- }
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
+ }
156
213
 
157
- // Immediately restore the original active session to avoid confusing the user
158
- if (currentActiveSession) {
159
- chatService.setActiveSession(currentActiveSession.id, { focus: false });
160
- }
214
+ // Immediately restore the original active session to avoid confusing the user
215
+ if (currentActiveSession) {
216
+ chatService.setActiveSession(currentActiveSession.id, { focus: false });
217
+ }
161
218
 
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;
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;
226
+ }
168
227
  }
169
228
 
170
- // Send the request
171
- const chatRequest: ChatRequest = {
172
- text: `@${agentId} ${prompt}`,
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
- if (ctx.cancellationToken?.isCancellationRequested) {
178
- return 'Operation cancelled by user';
179
- }
233
+ const chatRequest: ChatRequest = {
234
+ text: `@${agentId} ${prompt}`,
235
+ };
180
236
 
181
- const chatService = this.getChatService();
182
- response = await chatService.sendRequest(
183
- newSession.id,
184
- chatRequest
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
- const toolCallId = ctx.toolCallId;
212
- const sessionEventListener = chatService.onSessionEvent(event => {
213
- if (event.type === 'deleted' && event.sessionId === newSession.id) {
214
- this.pendingDelegations.delete(toolCallId);
215
- sessionEventListener.dispose();
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
- try {
221
- // Wait for completion to return the final result as tool output
222
- const result = await response.responseCompleted;
223
- const filteredContent = result.response.content.filter(c => !ThinkingChatResponseContent.is(c) && !ToolCallChatResponseContent.is(c));
224
- const stringResult = filteredContent
225
- .map(c => ChatResponseContent.hasAsString(c) ? c.asString() : undefined)
226
- .filter((text): text is string => text !== undefined && text !== '')
227
- .join('\n\n');
228
-
229
- // Clean up the parent-child link after completion (event bubbling is no longer needed)
230
- childModelDisposable?.dispose();
231
-
232
- // Return the raw text to the top-level Agent, as a tool result
233
- return stringResult;
234
- } catch (completionError) {
235
- if (
236
- completionError instanceof Error &&
237
- completionError.message.includes('cancelled')
238
- ) {
239
- return 'Operation cancelled by user';
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
+ });
240
280
  }
241
- const errorMsg = `Failed to complete response from agent '${agentId}': ${completionError instanceof Error ? completionError.message : completionError}`;
242
- this.logger.error(errorMsg, completionError);
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;
305
+ }
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
- } else {
246
- const errorMsg = `Delegation to agent '${agentId}' has failed: no response returned.`;
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