fix: preserve agent id precision in memory tools

This commit is contained in:
matevip 2026-08-13 03:08:44 -04:00
parent 4d9e7b024f
commit b8a8aac7f9
6 changed files with 113 additions and 25 deletions

View File

@ -111,7 +111,7 @@ public class StructuredMemoryTool {
readOwner(toolContext));
JSONObject result = new JSONObject();
result.set("agentId", agentId);
result.set("agentId", String.valueOf(agentId));
result.set("count", results.size());
result.set("entries", results);
return JSONUtil.toJsonPrettyStr(result);

View File

@ -87,7 +87,7 @@ public class WorkspaceMemoryTool {
}
JSONObject result = new JSONObject();
result.set("agentId", agentId);
result.set("agentId", String.valueOf(agentId));
result.set("count", files.size());
result.set("files", items);
return JSONUtil.toJsonPrettyStr(result);
@ -118,7 +118,7 @@ public class WorkspaceMemoryTool {
memoryRecallTracker.trackActiveRetrieval(agentId, filename, content);
JSONObject result = new JSONObject();
result.set("agentId", agentId);
result.set("agentId", String.valueOf(agentId));
result.set("filename", file.getFilename());
result.set("enabled", Boolean.TRUE.equals(file.getEnabled()));
result.set("fileSize", file.getFileSize());
@ -154,7 +154,7 @@ public class WorkspaceMemoryTool {
WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(agentId, filename, content != null ? content : "", ownerKey);
JSONObject result = new JSONObject();
result.set("agentId", agentId);
result.set("agentId", String.valueOf(agentId));
result.set("filename", saved.getFilename());
result.set("created", before == null);
result.set("overwritten", before != null);
@ -222,7 +222,7 @@ public class WorkspaceMemoryTool {
WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(agentId, filename, updated, ownerKey);
JSONObject result = new JSONObject();
result.set("agentId", agentId);
result.set("agentId", String.valueOf(agentId));
result.set("filename", filename);
result.set("replacements", replacements);
result.set("replaceAll", replaceAllFlag);
@ -296,7 +296,7 @@ public class WorkspaceMemoryTool {
hitsJson.add(h);
}
JSONObject result = new JSONObject();
result.set("agentId", agentId);
result.set("agentId", String.valueOf(agentId));
result.set("query", trimmed);
result.set("scope", scope == null || scope.isBlank() ? "all" : scope);
result.set("totalHits", hits.size());

View File

@ -0,0 +1,34 @@
package vip.mate.memory.tool;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import vip.mate.memory.MemoryProperties;
import vip.mate.memory.identity.MemoryOwnerResolver;
import vip.mate.memory.service.StructuredMemoryService;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class StructuredMemoryToolIdSerializationTest {
@Test
@DisplayName("recall_structured returns agentId as a JSON string to preserve snowflake precision")
void recallStructuredSerializesAgentIdAsString() {
StructuredMemoryService service = mock(StructuredMemoryService.class);
when(service.recall(anyLong(), nullable(String.class), nullable(String.class), anyString()))
.thenReturn(List.of());
StructuredMemoryTool tool = new StructuredMemoryTool(
service,
new MemoryOwnerResolver(),
new MemoryProperties());
String json = tool.recall_structured(2079862124134313986L, "reference", "meeting", null);
assertThat(json).contains("\"agentId\": \"2079862124134313986\"");
assertThat(json).doesNotContain("\"agentId\": 2079862124134313986");
}
}

View File

@ -0,0 +1,36 @@
package vip.mate.tool.builtin;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import vip.mate.memory.MemoryProperties;
import vip.mate.memory.identity.MemoryOwnerResolver;
import vip.mate.memory.service.MemoryRecallTracker;
import vip.mate.workspace.document.WorkspaceFileService;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class WorkspaceMemoryToolIdSerializationTest {
@Test
@DisplayName("search_workspace_memory returns agentId as a JSON string to preserve snowflake precision")
void searchWorkspaceMemorySerializesAgentIdAsString() {
WorkspaceFileService files = mock(WorkspaceFileService.class);
when(files.searchSnippets(anyLong(), anyString(), anySet(), anyInt(), anyString()))
.thenReturn(List.of());
WorkspaceMemoryTool tool = new WorkspaceMemoryTool(
files,
mock(MemoryRecallTracker.class),
new MemoryOwnerResolver(),
new MemoryProperties());
String json = tool.search_workspace_memory(2079862124134313986L, "meeting", "all", 10, null);
assertThat(json).contains("\"agentId\": \"2079862124134313986\"");
assertThat(json).doesNotContain("\"agentId\": 2079862124134313986");
}
}

View File

@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { buildChatStreamRequestBody } from '../useChat'
describe('buildChatStreamRequestBody', () => {
it('serializes agentId as a string before JSON.stringify touches the request body', () => {
const body = buildChatStreamRequestBody('', {
conversationId: 'wecom:2079870010935783426:DeBaDe',
agentId: '2079862124134313986',
contentParts: [],
})
expect(body.agentId).toBe('2079862124134313986')
expect(JSON.stringify(body)).toContain('"agentId":"2079862124134313986"')
})
})

View File

@ -193,6 +193,26 @@ export interface SendMessageOptions {
regenerate?: boolean
}
export function buildChatStreamRequestBody(content: string, options: SendMessageOptions): Record<string, any> {
const body: Record<string, any> = {
agentId: String(options.agentId),
message: content,
conversationId: options.conversationId,
contentParts: options.contentParts || [],
}
if (options.thinkingLevel) {
body.thinkingLevel = options.thinkingLevel
}
if (options.modelProvider && options.modelName) {
body.modelProvider = options.modelProvider
body.modelName = options.modelName
}
if (options.regenerate) {
body.regenerate = true
}
return body
}
export function useChat(options: UseChatOptions): UseChatReturn {
const { baseUrl, token, onStreamEnd } = options
const thinkingLevelRef = options.thinkingLevel
@ -2027,24 +2047,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
currentAssistantId.value = assistantMessage.id as string
// contentParts already includes file entries from buildOutgoingParts — do not re-merge attachments
const body: Record<string, any> = {
agentId,
message: content,
conversationId,
contentParts,
}
if (options.thinkingLevel) {
body.thinkingLevel = options.thinkingLevel
}
// Per-conversation model: the backend pins it onto the conversation row
// so switching the model here never leaks into other conversations.
if (options.modelProvider && options.modelName) {
body.modelProvider = options.modelProvider
body.modelName = options.modelName
}
if (options.regenerate) {
body.regenerate = true
}
const body = buildChatStreamRequestBody(content, { ...options, contentParts })
await stream.connect(body)
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
@ -2071,7 +2074,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
method: 'POST',
body: JSON.stringify({
message: content,
agentId,
agentId: String(agentId),
contentParts: options.contentParts || [],
}),
})