Skip to content

Commit 44e86cd

Browse files
authored
fix - Subagent progress isolation, persistence, and restoration during session switching (#166)
1 parent 12999ac commit 44e86cd

9 files changed

Lines changed: 559 additions & 59 deletions

File tree

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationAgent;
2626
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCodeCopyParams;
2727
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCreateParams;
28+
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationDestroyParams;
2829
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode;
2930
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams;
3031
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTemplate;
@@ -161,6 +162,12 @@ public interface CopilotLanguageServer extends LanguageServer {
161162
@JsonRequest("conversation/persistence")
162163
CompletableFuture<ChatPersistence> persistence(NullParams param);
163164

165+
/**
166+
* Destroy a conversation, stopping any in-progress processing.
167+
*/
168+
@JsonRequest("conversation/destroy")
169+
CompletableFuture<String> destroy(ConversationDestroyParams param);
170+
164171
/**
165172
* Register agent tools to the language server.
166173
*/

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationAgent;
4343
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCodeCopyParams;
4444
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCreateParams;
45+
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationDestroyParams;
4546
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode;
4647
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams;
4748
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTemplate;
@@ -431,6 +432,21 @@ public CompletableFuture<ChatPersistence> persistence() {
431432
});
432433
}
433434

435+
/**
436+
* Destroy a conversation, stopping any in-progress processing on the server.
437+
*/
438+
public void destroyConversation(String conversationId) {
439+
if (StringUtils.isBlank(conversationId)) {
440+
return;
441+
}
442+
Function<LanguageServer, CompletableFuture<String>> fn = server -> ((CopilotLanguageServer) server)
443+
.destroy(new ConversationDestroyParams(conversationId));
444+
this.languageServerWrapper.execute(fn).exceptionally(ex -> {
445+
CopilotCore.LOGGER.error("Failed to destroy conversation: " + conversationId, ex);
446+
return null;
447+
});
448+
}
449+
434450
/**
435451
* Used to register the tools for the language server.
436452
*/
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol;
5+
6+
import java.util.Objects;
7+
8+
import org.apache.commons.lang3.builder.ToStringBuilder;
9+
10+
/**
11+
* Parameters for destroying a conversation.
12+
*/
13+
public class ConversationDestroyParams {
14+
private String conversationId;
15+
16+
/**
17+
* Creates a new ConversationDestroyParams.
18+
*/
19+
public ConversationDestroyParams(String conversationId) {
20+
this.conversationId = conversationId;
21+
}
22+
23+
public String getConversationId() {
24+
return conversationId;
25+
}
26+
27+
public void setConversationId(String conversationId) {
28+
this.conversationId = conversationId;
29+
}
30+
31+
@Override
32+
public int hashCode() {
33+
return Objects.hash(conversationId);
34+
}
35+
36+
@Override
37+
public boolean equals(Object obj) {
38+
if (this == obj) {
39+
return true;
40+
}
41+
if (obj == null || getClass() != obj.getClass()) {
42+
return false;
43+
}
44+
ConversationDestroyParams other = (ConversationDestroyParams) obj;
45+
return Objects.equals(conversationId, other.conversationId);
46+
}
47+
48+
@Override
49+
public String toString() {
50+
ToStringBuilder builder = new ToStringBuilder(this);
51+
builder.append("conversationId", conversationId);
52+
return builder.toString();
53+
}
54+
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/persistence/ConversationDataFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ public List<Turn> convertToTurns(List<AbstractTurnData> turnDataList) {
212212
if (turnData == null) {
213213
continue;
214214
}
215+
// Skip subagent turns - they are not part of the main conversation history
216+
if (turnData instanceof CopilotTurnData copilotCheck
217+
&& copilotCheck.getParentTurnId() != null) {
218+
continue;
219+
}
215220
if (turnData instanceof UserTurnData userTurnData) {
216221
String requestText = userTurnData.getMessage() != null ? userTurnData.getMessage().getText() : "";
217222
Either<String, List<ChatCompletionContentPart>> request = Either

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/persistence/ConversationPersistenceManager.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ private ConversationData updateConversationProgressInternal(String conversationI
286286
CopilotTurnData copilotTurnData = findOrCreateCopilotTurn(conversationData, progress.getTurnId());
287287
dataFactory.updateReplyFromProgress(copilotTurnData.getReply(), progress);
288288

289+
// Mark subagent turns with their parent turn ID
290+
if (StringUtils.isNotBlank(progress.getParentTurnId())) {
291+
copilotTurnData.setParentTurnId(progress.getParentTurnId());
292+
}
293+
289294
// Update suggested title in CopilotTurnData if present
290295
if (StringUtils.isNotBlank(progress.getSuggestedTitle())) {
291296
copilotTurnData.setSuggestedTitle(progress.getSuggestedTitle());
@@ -543,4 +548,37 @@ public CompletableFuture<Void> persistModelInfo(String conversationId, String tu
543548
public ConversationDataFactory getDataFactory() {
544549
return dataFactory;
545550
}
551+
552+
/**
553+
* Sets the subagentToolCallId on a subagent's CopilotTurnData to associate it with the parent turn's run_subagent
554+
* tool call. This enables precise positioning of subagent content during conversation restoration.
555+
*
556+
* @param conversationId the conversation ID
557+
* @param subagentTurnId the subagent's turn ID
558+
* @param toolCallId the run_subagent tool call ID from the parent turn
559+
* @return a future that completes when the tool call ID has been set
560+
*/
561+
public CompletableFuture<Void> setSubagentToolCallId(String conversationId, String subagentTurnId,
562+
String toolCallId) {
563+
if (toolCallId == null || subagentTurnId == null) {
564+
return CompletableFuture.completedFuture(null);
565+
}
566+
return CompletableFuture.runAsync(() -> {
567+
lock.writeLock().lock();
568+
try {
569+
ConversationData conversation = getConversationFromCacheOrLoadFromDisk(conversationId);
570+
if (conversation == null) {
571+
return;
572+
}
573+
AbstractTurnData turnData = findTurn(conversation, subagentTurnId);
574+
if (turnData instanceof CopilotTurnData turn && turn.getSubagentToolCallId() == null) {
575+
turn.setSubagentToolCallId(toolCallId);
576+
}
577+
} catch (IOException e) {
578+
CopilotCore.LOGGER.error("Failed to set subagent tool call ID: " + conversationId, e);
579+
} finally {
580+
lock.writeLock().unlock();
581+
}
582+
});
583+
}
546584
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/persistence/CopilotTurnData.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
public class CopilotTurnData extends AbstractTurnData {
2020
private ReplyData reply;
2121
private String suggestedTitle;
22+
private String parentTurnId;
23+
private String subagentToolCallId;
2224

2325
/**
2426
* Default constructor initializing default values.
@@ -43,11 +45,27 @@ public void setSuggestedTitle(String suggestedTitle) {
4345
this.suggestedTitle = suggestedTitle;
4446
}
4547

48+
public String getParentTurnId() {
49+
return parentTurnId;
50+
}
51+
52+
public void setParentTurnId(String parentTurnId) {
53+
this.parentTurnId = parentTurnId;
54+
}
55+
56+
public String getSubagentToolCallId() {
57+
return subagentToolCallId;
58+
}
59+
60+
public void setSubagentToolCallId(String subagentToolCallId) {
61+
this.subagentToolCallId = subagentToolCallId;
62+
}
63+
4664
@Override
4765
public int hashCode() {
4866
final int prime = 31;
4967
int result = super.hashCode();
50-
result = prime * result + Objects.hash(reply, suggestedTitle);
68+
result = prime * result + Objects.hash(reply, suggestedTitle, parentTurnId, subagentToolCallId);
5169
return result;
5270
}
5371

@@ -63,7 +81,9 @@ public boolean equals(Object obj) {
6381
return false;
6482
}
6583
CopilotTurnData other = (CopilotTurnData) obj;
66-
return Objects.equals(reply, other.reply) && Objects.equals(suggestedTitle, other.suggestedTitle);
84+
return Objects.equals(reply, other.reply) && Objects.equals(suggestedTitle, other.suggestedTitle)
85+
&& Objects.equals(parentTurnId, other.parentTurnId)
86+
&& Objects.equals(subagentToolCallId, other.subagentToolCallId);
6787
}
6888

6989
@Override
@@ -77,6 +97,8 @@ public String toString() {
7797
// Include CopilotTurnData specific properties
7898
builder.append("reply", reply);
7999
builder.append("suggestedTitle", suggestedTitle);
100+
builder.append("parentTurnId", parentTurnId);
101+
builder.append("subagentToolCallId", subagentToolCallId);
80102
return builder.toString();
81103
}
82104

0 commit comments

Comments
 (0)