Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private Constants() {
public static final String WORKSPACE_CONTEXT_ENABLED = "workspaceContextEnabled";
public static final String SUB_AGENT_ENABLED = "subAgentEnabled";
public static final String AGENT_MAX_REQUESTS = "agentMaxRequests";
public static final String TRANSCRIPT_SUBDIR = ".copilot/eclipse";
public static final String MCP = "mcp";
public static final String MCP_REGISTRY_URL = "mcpRegistryUrl";
public static final String MCP_REGISTRY_VERSION = "v0.1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ public CompletableFuture<ChatCreateResult> createConversation(String workDoneTok
List<IResource> files, IFile currentFile, Range currentSelection, List<Turn> turns, CopilotModel activeModel,
String chatModeName, String customChatModeId, List<TodoItem> todos, String agentSlug,
String agentJobWorkspaceFolder) {
return createConversation(workDoneToken, message, files, currentFile, currentSelection, turns, activeModel,
chatModeName, customChatModeId, todos, agentSlug, agentJobWorkspaceFolder, null, null);
}

/**
* Create a conversation with the given parameters, including optional conversationId and restoreToTurnId for session
* restoration.
*/
public CompletableFuture<ChatCreateResult> createConversation(String workDoneToken, String message,
List<IResource> files, IFile currentFile, Range currentSelection, List<Turn> turns, CopilotModel activeModel,
Comment thread
jdneo marked this conversation as resolved.
String chatModeName, String customChatModeId, List<TodoItem> todos, String agentSlug,
String agentJobWorkspaceFolder, String conversationId, String restoreToTurnId) {
boolean supportVision = activeModel.getCapabilities().supports().vision();
Either<String, List<ChatCompletionContentPart>> messageWithImages = ChatMessageUtils
.createMessageWithImages(message, FileUtils.filterFilesFrom(files), supportVision);
Expand Down Expand Up @@ -308,6 +320,13 @@ public CompletableFuture<ChatCreateResult> createConversation(String workDoneTok

// TODO: remove needToolCallConfirmation when CLS fully supports it across all IDEs.
param.setNeedToolCallConfirmation(true);

// Set conversationId and restoreToTurnId for session restoration from history
if (conversationId != null) {
param.setConversationId(conversationId);
param.setRestoreToTurnId(restoreToTurnId);
}

if (currentFile != null) {
param.setTextDocument(new TextDocumentIdentifier(FileUtils.getResourceUri(currentFile)));
if (currentSelection != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class ConversationCreateParams {
// TODO: remove needToolCallConfirmation when CLS fully supports it across all IDEs.
private boolean needToolCallConfirmation;
private List<TodoItem> todoList;
private String conversationId;
private String restoreToTurnId;
Comment thread
xinyi-gong marked this conversation as resolved.

/**
* Creates a new ConversationCreateParams.
Expand Down Expand Up @@ -198,6 +200,22 @@ public void setModelProviderName(String modelProviderName) {
this.modelProviderName = modelProviderName;
}

public String getConversationId() {
return conversationId;
}

public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}

public String getRestoreToTurnId() {
return restoreToTurnId;
}

public void setRestoreToTurnId(String restoreToTurnId) {
this.restoreToTurnId = restoreToTurnId;
}

@Override
public int hashCode() {
final int prime = 31;
Expand All @@ -207,7 +225,7 @@ public int hashCode() {
result = prime * result
+ Objects.hash(capabilities, chatMode, computeSuggestions, customChatModeId, model, needToolCallConfirmation,
references, source, textDocument, userLanguage, workDoneToken, workspaceFolder, workspaceFolders,
modelProviderName, todoList);
modelProviderName, todoList, conversationId, restoreToTurnId);
return result;
}

Expand All @@ -233,7 +251,9 @@ public boolean equals(Object obj) {
&& Objects.equals(workDoneToken, other.workDoneToken) && Objects.equals(workspaceFolder, other.workspaceFolder)
&& Objects.equals(workspaceFolders, other.workspaceFolders)
&& Objects.equals(modelProviderName, other.modelProviderName)
&& Objects.equals(todoList, other.todoList);
&& Objects.equals(todoList, other.todoList)
&& Objects.equals(conversationId, other.conversationId)
&& Objects.equals(restoreToTurnId, other.restoreToTurnId);
}

@Override
Expand All @@ -256,6 +276,8 @@ public String toString() {
builder.append("customChatModeId", customChatModeId);
builder.append("needToolCallConfirmation", needToolCallConfirmation);
builder.append("todoList", todoList);
builder.append("conversationId", conversationId);
builder.append("restoreToTurnId", restoreToTurnId);
return builder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class CopilotAgentSettings {
@SerializedName("maxToolCallingLoop")
private int agentMaxRequests;

private String transcriptDirectory;

public int getAgentMaxRequests() {
return agentMaxRequests;
}
Expand All @@ -24,9 +26,17 @@ public void setAgentMaxRequests(int agentMaxRequests) {
this.agentMaxRequests = agentMaxRequests;
}

public String getTranscriptDirectory() {
return transcriptDirectory;
}

public void setTranscriptDirectory(String transcriptDirectory) {
this.transcriptDirectory = transcriptDirectory;
}

@Override
public int hashCode() {
return Objects.hash(agentMaxRequests);
return Objects.hash(agentMaxRequests, transcriptDirectory);
}

@Override
Expand All @@ -38,13 +48,15 @@ public boolean equals(Object obj) {
return false;
}
CopilotAgentSettings other = (CopilotAgentSettings) obj;
return agentMaxRequests == other.agentMaxRequests;
return agentMaxRequests == other.agentMaxRequests
&& Objects.equals(transcriptDirectory, other.transcriptDirectory);
}

@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this);
builder.append("agentMaxRequests", agentMaxRequests);
builder.append("transcriptDirectory", transcriptDirectory);
return builder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class Turn {
Either<String, List<ChatCompletionContentPart>> request;
String response;
String agentSlug;
String turnId;

/**
* Creates a new Turn.
Expand All @@ -28,6 +29,17 @@ public Turn(@NonNull Either<String, List<ChatCompletionContentPart>> request, St
this.agentSlug = agentSlug;
}

/**
* Creates a new Turn with turnId.
*/
public Turn(@NonNull Either<String, List<ChatCompletionContentPart>> request, String response, String agentSlug,
String turnId) {
this.request = request;
this.response = response;
this.agentSlug = agentSlug;
this.turnId = turnId;
}

public Either<String, List<ChatCompletionContentPart>> getRequest() {
return request;
}
Expand All @@ -52,9 +64,17 @@ public void setAgentSlug(String agentSlug) {
this.agentSlug = agentSlug;
}

public String getTurnId() {
return turnId;
}

public void setTurnId(String turnId) {
this.turnId = turnId;
}

@Override
public int hashCode() {
return Objects.hash(request, response, agentSlug);
return Objects.hash(request, response, agentSlug, turnId);
}

@Override
Expand All @@ -67,7 +87,7 @@ public boolean equals(Object o) {
}
Turn turn = (Turn) o;
return Objects.equals(request, turn.request) && Objects.equals(response, turn.response)
&& Objects.equals(agentSlug, turn.agentSlug);
&& Objects.equals(agentSlug, turn.agentSlug) && Objects.equals(turnId, turn.turnId);
}

@Override
Expand All @@ -76,6 +96,7 @@ public String toString() {
builder.append("request", request);
builder.append("response", response);
builder.append("agentSlug", agentSlug);
builder.append("turnId", turnId);
return builder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ToolCallData;
import com.microsoft.copilot.eclipse.core.persistence.UserTurnData.MessageData;


/**
* Factory for creating and transforming conversation data objects. Responsible only for pure data transformation with
* no business logic.
Expand Down Expand Up @@ -208,6 +207,8 @@ public List<Turn> convertToTurns(List<AbstractTurnData> turnDataList) {
// Defensive copy to avoid ConcurrentModificationException if another thread mutates the list while iterating.
List<AbstractTurnData> snapshot = new ArrayList<>(turnDataList);
List<Turn> result = new ArrayList<>(snapshot.size());
Turn unpairedUserTurn = null;

for (AbstractTurnData turnData : snapshot) {
if (turnData == null) {
continue;
Expand All @@ -218,17 +219,31 @@ public List<Turn> convertToTurns(List<AbstractTurnData> turnDataList) {
continue;
}
if (turnData instanceof UserTurnData userTurnData) {
// Flush any unpaired user turn without a response
if (unpairedUserTurn != null) {
result.add(unpairedUserTurn);
}
String requestText = userTurnData.getMessage() != null ? userTurnData.getMessage().getText() : "";
Either<String, List<ChatCompletionContentPart>> request = Either
.forLeft(requestText == null ? "" : requestText);
result.add(new Turn(request, null, null));
unpairedUserTurn = new Turn(request, null, null, turnData.getTurnId());
} else if (turnData instanceof CopilotTurnData copilotTurnData) {
// Assistant turns only contribute the response text; the request field is intentionally empty.
String responseText = extractResponseFromCopilotTurnData(copilotTurnData);
Either<String, List<ChatCompletionContentPart>> request = Either.forLeft("");
result.add(new Turn(request, responseText, null));
if (unpairedUserTurn != null) {
// Pair the response with the unpaired user turn
unpairedUserTurn.setResponse(responseText);
result.add(unpairedUserTurn);
unpairedUserTurn = null;
} else {
// Orphaned copilot turn (no preceding user turn), create a standalone turn
result.add(new Turn(Either.forLeft(""), responseText, null, turnData.getTurnId()));
}
}
}
// Flush any remaining unpaired user turn (user message without a response)
if (unpairedUserTurn != null) {
result.add(unpairedUserTurn);
}
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,47 +299,42 @@ private ConversationData updateConversationProgressInternal(String conversationI
return conversationData;
}

private UserTurnData findOrCreateUserTurn(ConversationData conversation, String turnId) {
if (turnId != null) {
AbstractTurnData existingTurn = findTurn(conversation, turnId);
if (existingTurn != null && existingTurn instanceof UserTurnData userTurnData) {
return userTurnData;
/**
* Finds a turn by ID and type in the conversation.
*/
@SuppressWarnings("unchecked")
private <T extends AbstractTurnData> T findTurn(ConversationData conversation, String turnId, Class<T> type) {
if (turnId == null) {
return null;
}
for (AbstractTurnData t : conversation.getTurns()) {
if (turnId.equals(t.getTurnId()) && type.isInstance(t)) {
return (T) t;
}
}
return null;
}

private UserTurnData findOrCreateUserTurn(ConversationData conversation, String turnId) {
UserTurnData existing = findTurn(conversation, turnId, UserTurnData.class);
if (existing != null) {
return existing;
}
UserTurnData turn = dataFactory.createUserTurnData(conversation.getConversationId(), turnId, "", null, null, null);
conversation.getTurns().add(turn);
return turn;
}

private CopilotTurnData findOrCreateCopilotTurn(ConversationData conversation, String turnId) {
if (turnId != null) {
AbstractTurnData existingTurn = findTurn(conversation, turnId);
if (existingTurn != null && existingTurn instanceof CopilotTurnData copilotTurnData) {
return copilotTurnData;
}
CopilotTurnData existing = findTurn(conversation, turnId, CopilotTurnData.class);
if (existing != null) {
return existing;
}

CopilotTurnData turn = dataFactory.createCopilotTurnData(turnId);
conversation.getTurns().add(turn);
return turn;
}

/**
* Finds a turn by ID in the conversation.
*/
private AbstractTurnData findTurn(ConversationData conversation, String turnId) {
if (conversation == null || turnId == null) {
return null;
}
for (AbstractTurnData t : conversation.getTurns()) {
if (turnId.equals(t.getTurnId())) {
return t;
}
}
return null;
}

private ConversationData getOrCreateNewConversationById(String conversationId) throws IOException {
try {
ConversationData existedConversation = getConversationFromCacheOrLoadFromDisk(conversationId);
Expand Down Expand Up @@ -549,6 +544,42 @@ public ConversationDataFactory getDataFactory() {
return dataFactory;
}

/**
* Sets the CLS-assigned turnId on the last user turn that doesn't have a turnId yet. User turns are initially
* persisted without a turnId (null), and the server-assigned turnId is set when the CLS progress begin event arrives.
*
* @param conversationId the conversation ID
* @param turnId the server-assigned turnId from CLS
*/
public void setUserTurnId(String conversationId, String turnId) {
if (turnId == null) {
return;
}
CompletableFuture.runAsync(() -> {
lock.writeLock().lock();
try {
ConversationData conversation = getConversationFromCacheOrLoadFromDisk(conversationId);
if (conversation == null) {
return;
}
// Find the last UserTurnData with null turnId and set it
List<AbstractTurnData> turns = conversation.getTurns();
for (int i = turns.size() - 1; i >= 0; i--) {
AbstractTurnData t = turns.get(i);
if (t instanceof UserTurnData && t.getTurnId() == null) {
t.setTurnId(turnId);
persistAndCacheConversation(conversation);
break;
}
}
} catch (IOException e) {
CopilotCore.LOGGER.error("Failed to set user turn ID for conversation: " + conversationId, e);
} finally {
lock.writeLock().unlock();
}
});
}

/**
* Sets the subagentToolCallId on a subagent's CopilotTurnData to associate it with the parent turn's run_subagent
* tool call. This enables precise positioning of subagent content during conversation restoration.
Expand All @@ -570,9 +601,9 @@ public CompletableFuture<Void> setSubagentToolCallId(String conversationId, Stri
if (conversation == null) {
return;
}
AbstractTurnData turnData = findTurn(conversation, subagentTurnId);
if (turnData instanceof CopilotTurnData turn && turn.getSubagentToolCallId() == null) {
turn.setSubagentToolCallId(toolCallId);
CopilotTurnData turnData = findTurn(conversation, subagentTurnId, CopilotTurnData.class);
if (turnData != null && turnData.getSubagentToolCallId() == null) {
turnData.setSubagentToolCallId(toolCallId);
}
} catch (IOException e) {
CopilotCore.LOGGER.error("Failed to set subagent tool call ID: " + conversationId, e);
Expand Down
Loading
Loading