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 @@ -38,11 +38,13 @@
import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger;
import com.microsoft.copilot.eclipse.core.lsp.protocol.AgentRound;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatProgressValue;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatStepStatus;
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel;
import com.microsoft.copilot.eclipse.core.lsp.protocol.Thinking;
import com.microsoft.copilot.eclipse.core.lsp.protocol.Turn;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.EditAgentRoundData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ReplyData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ToolCallData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ThinkingBlockData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ThinkingBlockState;
import com.microsoft.copilot.eclipse.core.persistence.UserTurnData.MessageData;
Expand Down Expand Up @@ -311,6 +313,48 @@ void testPersistConversationProgress_Success() throws Exception {
verify(mockPersistenceService).saveConversation(any(ConversationData.class));
}

@Test
void testMarkRunningToolCallsCancelledAndPersist_UpdatesOnlyRunningToolCalls() throws Exception {
String conversationId = "00000000-0000-0000-0000-000000000000";
ConversationData conversationData = createTestConversationData(conversationId);
CopilotTurnData copilotTurnData = (CopilotTurnData) conversationData.getTurns().get(1);
ToolCallData runningToolCall = createTestToolCallData("tool-1", ChatStepStatus.RUNNING);
ToolCallData completedToolCall = createTestToolCallData("tool-2", ChatStepStatus.COMPLETED);
EditAgentRoundData roundData = new EditAgentRoundData();
roundData.setRoundId(1);
roundData.setToolCalls(List.of(runningToolCall, completedToolCall));
copilotTurnData.getReply().setEditAgentRounds(List.of(roundData));

Map<String, ConversationData> cache = getConversationCache();
cache.put(conversationId, conversationData);

persistenceManager.markRunningToolCallsCancelledAndPersist(conversationId).get();

assertEquals(ChatStepStatus.CANCELLED, runningToolCall.getStatus());
assertEquals(ChatStepStatus.COMPLETED, completedToolCall.getStatus());
verify(mockPersistenceService).saveConversation(conversationData);
}

@Test
void testMarkRunningToolCallsCancelledAndPersist_PersistsWhenNoRunningToolCalls() throws Exception {
String conversationId = "00000000-0000-0000-0000-000000000000";
ConversationData conversationData = createTestConversationData(conversationId);
CopilotTurnData copilotTurnData = (CopilotTurnData) conversationData.getTurns().get(1);
ToolCallData completedToolCall = createTestToolCallData("tool-1", ChatStepStatus.COMPLETED);
EditAgentRoundData roundData = new EditAgentRoundData();
roundData.setRoundId(1);
roundData.setToolCalls(List.of(completedToolCall));
copilotTurnData.getReply().setEditAgentRounds(List.of(roundData));

Map<String, ConversationData> cache = getConversationCache();
cache.put(conversationId, conversationData);

persistenceManager.markRunningToolCallsCancelledAndPersist(conversationId).get();

assertEquals(ChatStepStatus.COMPLETED, completedToolCall.getStatus());
verify(mockPersistenceService).saveConversation(conversationData);
}

@Test
Comment thread
xinyi-gong marked this conversation as resolved.
void testUpdateConversationProgress_NewConversation() throws Exception {
String conversationId = "00000000-0000-0000-0000-000000000001";
Expand Down Expand Up @@ -438,4 +482,19 @@ private void setPrivateField(Object target, String fieldName, Object value) thro
field.setAccessible(true);
field.set(target, value);
}

private ToolCallData createTestToolCallData(String id, String status) {
ToolCallData toolCallData = new ToolCallData();
toolCallData.setId(id);
toolCallData.setName("run_in_terminal");
toolCallData.setProgressMessage("Running command");
toolCallData.setStatus(status);
return toolCallData;
}

private Map<String, ConversationData> getConversationCache() throws Exception {
var cacheField = ConversationPersistenceManager.class.getDeclaredField("conversationCache");
cacheField.setAccessible(true);
return (Map<String, ConversationData>) cacheField.get(persistenceManager);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.microsoft.copilot.eclipse.core.AuthStatusManager;
import com.microsoft.copilot.eclipse.core.CopilotCore;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatProgressValue;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatStepStatus;
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel;
import com.microsoft.copilot.eclipse.core.lsp.protocol.TodoItem;
import com.microsoft.copilot.eclipse.core.lsp.protocol.Turn;
Expand All @@ -28,6 +29,7 @@
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ReplyData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ThinkingBlockData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ThinkingBlockState;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ToolCallData;
import com.microsoft.copilot.eclipse.core.persistence.UserTurnData.MessageData;

/**
Expand Down Expand Up @@ -270,6 +272,41 @@ public CompletableFuture<Void> persistCachedConversation(String conversationId)
});
}

/**
* Marks cached running tool calls as cancelled, then persists the cached conversation to disk.
*
* @param conversationId the ID of the cached conversation to persist
* @return a future that completes when the cached conversation has been persisted
*/
public CompletableFuture<Void> markRunningToolCallsCancelledAndPersist(String conversationId) {
if (StringUtils.isBlank(conversationId)) {
return CompletableFuture.completedFuture(null);
}

ConversationData conversationData;
lock.writeLock().lock();
try {
conversationData = conversationCache.get(conversationId);
if (conversationData == null) {
return CompletableFuture.completedFuture(null);
}
markRunningToolCallsCancelled(conversationData);
} finally {
lock.writeLock().unlock();
}

return CompletableFuture.runAsync(() -> {
lock.writeLock().lock();
try {
persistAndCacheConversation(conversationData);
} catch (IOException e) {
CopilotCore.LOGGER.error("Failed to persist cancelled tool calls for conversation: " + conversationId, e);
} finally {
lock.writeLock().unlock();
}
});
Comment thread
xinyi-gong marked this conversation as resolved.
}

/**
* Updates a conversation with progress data. This method is synchronous and handles all IO operations internally.
*/
Expand Down Expand Up @@ -498,6 +535,28 @@ private void persistAndCacheConversation(ConversationData conversation) throws I
conversationCache.put(conversation.getConversationId(), conversation);
}

private void markRunningToolCallsCancelled(ConversationData conversationData) {
if (conversationData.getTurns() == null) {
return;
}
for (AbstractTurnData turn : conversationData.getTurns()) {
if (!(turn instanceof CopilotTurnData copilotTurnData) || copilotTurnData.getReply() == null
|| copilotTurnData.getReply().getEditAgentRounds() == null) {
continue;
}
for (EditAgentRoundData round : copilotTurnData.getReply().getEditAgentRounds()) {
if (round.getToolCalls() == null) {
continue;
}
for (ToolCallData toolCall : round.getToolCalls()) {
if (toolCall != null && ChatStepStatus.RUNNING.equalsIgnoreCase(toolCall.getStatus())) {
toolCall.setStatus(ChatStepStatus.CANCELLED);
}
}
}
}
}

/**
* Removes a conversation by ID from both disk and in-memory cache.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ public void onCancel() {
this.lastRunSubagentToolCallId = null;

if (persistenceManager != null && StringUtils.isNotBlank(this.conversationId)) {
persistenceManager.persistCachedConversation(this.conversationId);
persistenceManager.markRunningToolCallsCancelledAndPersist(this.conversationId);
}
conversationFutures.forEach(future -> {
future.cancel(false);
Expand Down
Loading