mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(memory): preserve remaining agent id tool inputs
This commit is contained in:
parent
cabff7e498
commit
b1f3332ae1
@ -27,12 +27,13 @@ public class FactQueryTool {
|
||||
|
||||
@Tool(description = "Probe facts about an entity. Returns relevant facts where the entity appears as subject or object.")
|
||||
public String fact_probe(
|
||||
@ToolParam(description = "Agent ID") Long agentId,
|
||||
@ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
|
||||
@ToolParam(description = "Entity name to search for") String entity) {
|
||||
if (!properties.getFact().isProjectionEnabled()) {
|
||||
return "Fact projection is disabled.";
|
||||
}
|
||||
List<FactEntity> facts = queryService.probe(agentId, entity);
|
||||
Long parsedAgentId = parseAgentId(agentId);
|
||||
List<FactEntity> facts = queryService.probe(parsedAgentId, entity);
|
||||
if (facts.isEmpty()) return "No facts found for entity: " + entity;
|
||||
|
||||
// Bump use count
|
||||
@ -45,11 +46,12 @@ public class FactQueryTool {
|
||||
|
||||
@Tool(description = "List unresolved fact contradictions detected during Dream consolidation.")
|
||||
public String fact_list_contradictions(
|
||||
@ToolParam(description = "Agent ID") Long agentId) {
|
||||
@ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId) {
|
||||
if (!properties.getFact().isProjectionEnabled()) {
|
||||
return "Fact projection is disabled.";
|
||||
}
|
||||
List<FactContradictionEntity> contradictions = queryService.listContradictions(agentId);
|
||||
Long parsedAgentId = parseAgentId(agentId);
|
||||
List<FactContradictionEntity> contradictions = queryService.listContradictions(parsedAgentId);
|
||||
if (contradictions.isEmpty()) return "No unresolved contradictions.";
|
||||
|
||||
return contradictions.stream()
|
||||
@ -58,4 +60,16 @@ public class FactQueryTool {
|
||||
c.getDescription() != null ? c.getDescription() : ""))
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private static Long parseAgentId(String agentId) {
|
||||
String trimmed = agentId != null ? agentId.trim() : "";
|
||||
if (trimmed.isEmpty()) {
|
||||
throw new IllegalArgumentException("agentId is required");
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(trimmed);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("agentId must be a numeric string");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,32 +55,32 @@ public class UniversalMemoryTool {
|
||||
如果你需要记录的是结构化条目,优先用 remember_structured。
|
||||
""")
|
||||
public String remember(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "要记住的内容(自由形式)") String content,
|
||||
@ToolParam(description = "可选:来源上下文(skill 名 / conversation id)", required = false) String source,
|
||||
ToolContext toolContext) {
|
||||
|
||||
if (agentId == null) return error("agentId 不能为空");
|
||||
if (content == null || content.isBlank()) return error("content 不能为空");
|
||||
|
||||
try {
|
||||
Long parsedAgentId = parseAgentId(agentId);
|
||||
// Write to the requester's PERSONAL MEMORY.md when per-owner isolation
|
||||
// is active; otherwise the shared file (so the note is not stranded
|
||||
// in an un-read PERSONAL row).
|
||||
String ownerKey = memoryProperties.isLifecycleMediatorEnabled()
|
||||
? memoryOwnerResolver.resolve(ChatOrigin.from(toolContext))
|
||||
: null;
|
||||
WorkspaceFileEntity existing = workspaceFileService.getVisibleFile(agentId, MEMORY_FILENAME, ownerKey);
|
||||
WorkspaceFileEntity existing = workspaceFileService.getVisibleFile(parsedAgentId, MEMORY_FILENAME, ownerKey);
|
||||
String existingContent = existing != null && existing.getContent() != null
|
||||
? existing.getContent() : "";
|
||||
String updated = appendLesson(existingContent, content, source);
|
||||
workspaceFileService.saveVisibleFile(agentId, MEMORY_FILENAME, updated, ownerKey);
|
||||
workspaceFileService.saveVisibleFile(parsedAgentId, MEMORY_FILENAME, updated, ownerKey);
|
||||
|
||||
// RFC-090 §14.3 — universal remember() targets MEMORY.md (the
|
||||
// canonical file), so this IS a MemoryWriteEvent. Skill-local
|
||||
// lessons go through SkillLessonWrittenEvent instead and do
|
||||
// NOT touch this path.
|
||||
eventPublisher.publishEvent(new MemoryWriteEvent(agentId, MEMORY_FILENAME,
|
||||
eventPublisher.publishEvent(new MemoryWriteEvent(parsedAgentId, MEMORY_FILENAME,
|
||||
"remember", content));
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
@ -142,6 +142,18 @@ public class UniversalMemoryTool {
|
||||
return idx < 0 ? -1 : idx + 1; // position of '#' itself
|
||||
}
|
||||
|
||||
private static Long parseAgentId(String agentId) {
|
||||
String trimmed = agentId != null ? agentId.trim() : "";
|
||||
if (trimmed.isEmpty()) {
|
||||
throw new IllegalArgumentException("agentId 不能为空");
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(trimmed);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("agentId 必须是数字字符串");
|
||||
}
|
||||
}
|
||||
|
||||
private static String error(String msg) {
|
||||
JSONObject e = new JSONObject();
|
||||
e.set("success", false);
|
||||
|
||||
@ -42,11 +42,17 @@ public class SkillLessonsTool {
|
||||
public String record_lesson(
|
||||
@ToolParam(description = "skill 的 slug(即 SKILL.md frontmatter 里的 name)") String skillName,
|
||||
@ToolParam(description = "要记录的经验内容") String lesson,
|
||||
@ToolParam(description = "可选:当前 Agent 的 ID", required = false) Long agentId,
|
||||
@ToolParam(description = "可选:当前 Agent 的 ID。传入时必须使用字符串,避免大整数精度丢失", required = false) String agentId,
|
||||
@ToolParam(description = "可选:当前对话 ID", required = false) String conversationId) {
|
||||
|
||||
if (skillName == null || skillName.isBlank()) return error("skillName 不能为空");
|
||||
if (lesson == null || lesson.isBlank()) return error("lesson 不能为空");
|
||||
Long parsedAgentId;
|
||||
try {
|
||||
parsedAgentId = parseOptionalAgentId(agentId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return error(e.getMessage());
|
||||
}
|
||||
|
||||
ResolvedSkill resolved = skillRuntimeService.resolveAllSkillsStatus().stream()
|
||||
.filter(s -> s != null && skillName.equals(s.getName()))
|
||||
@ -69,7 +75,7 @@ public class SkillLessonsTool {
|
||||
int max = manifest != null && manifest.getSelfEvolution() != null
|
||||
? manifest.getSelfEvolution().getLessonsMaxEntries() : 0;
|
||||
|
||||
String lessonId = lessonsService.recordLesson(resolved, agentId, conversationId,
|
||||
String lessonId = lessonsService.recordLesson(resolved, parsedAgentId, conversationId,
|
||||
lesson, max);
|
||||
if (lessonId == null) {
|
||||
return error("Lesson 记录失败:skill 可能仅存在于数据库(无 workspace 目录)。");
|
||||
@ -83,6 +89,18 @@ public class SkillLessonsTool {
|
||||
return JSONUtil.toJsonPrettyStr(result);
|
||||
}
|
||||
|
||||
private static Long parseOptionalAgentId(String agentId) {
|
||||
String trimmed = agentId != null ? agentId.trim() : "";
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(trimmed);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("agentId 必须是数字字符串");
|
||||
}
|
||||
}
|
||||
|
||||
private static String error(String msg) {
|
||||
JSONObject e = new JSONObject();
|
||||
e.set("success", false);
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package vip.mate.memory.fact.tool;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.support.ToolCallbacks;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.memory.fact.query.FactQueryService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class FactQueryToolIdSchemaTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("fact tools publish agentId as a string parameter so LLM tool calls preserve precision")
|
||||
void factToolAgentIdSchemasAreString() throws Exception {
|
||||
FactQueryTool tool = new FactQueryTool(mock(FactQueryService.class), mock(MemoryProperties.class));
|
||||
|
||||
assertAgentIdIsString(tool, "fact_probe");
|
||||
assertAgentIdIsString(tool, "fact_list_contradictions");
|
||||
}
|
||||
|
||||
private static void assertAgentIdIsString(Object tool, String name) throws Exception {
|
||||
JsonNode root = MAPPER.readTree(callback(tool, name).getToolDefinition().inputSchema());
|
||||
|
||||
assertThat(root.at("/properties/agentId/type").asText()).isEqualTo("string");
|
||||
}
|
||||
|
||||
private static ToolCallback callback(Object tool, String name) {
|
||||
for (ToolCallback callback : ToolCallbacks.from(tool)) {
|
||||
if (name.equals(callback.getToolDefinition().name())) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing tool callback: " + name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package vip.mate.memory.tool;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.support.ToolCallbacks;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.memory.identity.MemoryOwnerResolver;
|
||||
import vip.mate.workspace.document.WorkspaceFileService;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class UniversalMemoryToolIdSchemaTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("remember publishes agentId as a string parameter so LLM tool calls preserve precision")
|
||||
void rememberAgentIdSchemaIsString() throws Exception {
|
||||
UniversalMemoryTool tool = new UniversalMemoryTool(
|
||||
mock(WorkspaceFileService.class),
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(MemoryOwnerResolver.class),
|
||||
mock(MemoryProperties.class));
|
||||
|
||||
JsonNode root = MAPPER.readTree(callback(tool, "remember").getToolDefinition().inputSchema());
|
||||
|
||||
assertThat(root.at("/properties/agentId/type").asText()).isEqualTo("string");
|
||||
}
|
||||
|
||||
private static ToolCallback callback(Object tool, String name) {
|
||||
for (ToolCallback callback : ToolCallbacks.from(tool)) {
|
||||
if (name.equals(callback.getToolDefinition().name())) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing tool callback: " + name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package vip.mate.skill.lessons;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.support.ToolCallbacks;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import vip.mate.skill.runtime.SkillRuntimeService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class SkillLessonsToolIdSchemaTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("record_lesson publishes agentId as a string parameter so LLM tool calls preserve precision")
|
||||
void recordLessonAgentIdSchemaIsString() throws Exception {
|
||||
SkillLessonsTool tool = new SkillLessonsTool(mock(SkillRuntimeService.class), mock(SkillLessonsService.class));
|
||||
|
||||
JsonNode root = MAPPER.readTree(callback(tool, "record_lesson").getToolDefinition().inputSchema());
|
||||
|
||||
assertThat(root.at("/properties/agentId/type").asText()).isEqualTo("string");
|
||||
}
|
||||
|
||||
private static ToolCallback callback(Object tool, String name) {
|
||||
for (ToolCallback callback : ToolCallbacks.from(tool)) {
|
||||
if (name.equals(callback.getToolDefinition().name())) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing tool callback: " + name);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user