mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-17 04:44:40 +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.")
|
@Tool(description = "Probe facts about an entity. Returns relevant facts where the entity appears as subject or object.")
|
||||||
public String fact_probe(
|
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) {
|
@ToolParam(description = "Entity name to search for") String entity) {
|
||||||
if (!properties.getFact().isProjectionEnabled()) {
|
if (!properties.getFact().isProjectionEnabled()) {
|
||||||
return "Fact projection is disabled.";
|
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;
|
if (facts.isEmpty()) return "No facts found for entity: " + entity;
|
||||||
|
|
||||||
// Bump use count
|
// Bump use count
|
||||||
@ -45,11 +46,12 @@ public class FactQueryTool {
|
|||||||
|
|
||||||
@Tool(description = "List unresolved fact contradictions detected during Dream consolidation.")
|
@Tool(description = "List unresolved fact contradictions detected during Dream consolidation.")
|
||||||
public String fact_list_contradictions(
|
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()) {
|
if (!properties.getFact().isProjectionEnabled()) {
|
||||||
return "Fact projection is disabled.";
|
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.";
|
if (contradictions.isEmpty()) return "No unresolved contradictions.";
|
||||||
|
|
||||||
return contradictions.stream()
|
return contradictions.stream()
|
||||||
@ -58,4 +60,16 @@ public class FactQueryTool {
|
|||||||
c.getDescription() != null ? c.getDescription() : ""))
|
c.getDescription() != null ? c.getDescription() : ""))
|
||||||
.collect(Collectors.joining("\n"));
|
.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。
|
如果你需要记录的是结构化条目,优先用 remember_structured。
|
||||||
""")
|
""")
|
||||||
public String remember(
|
public String remember(
|
||||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||||
@ToolParam(description = "要记住的内容(自由形式)") String content,
|
@ToolParam(description = "要记住的内容(自由形式)") String content,
|
||||||
@ToolParam(description = "可选:来源上下文(skill 名 / conversation id)", required = false) String source,
|
@ToolParam(description = "可选:来源上下文(skill 名 / conversation id)", required = false) String source,
|
||||||
ToolContext toolContext) {
|
ToolContext toolContext) {
|
||||||
|
|
||||||
if (agentId == null) return error("agentId 不能为空");
|
|
||||||
if (content == null || content.isBlank()) return error("content 不能为空");
|
if (content == null || content.isBlank()) return error("content 不能为空");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Long parsedAgentId = parseAgentId(agentId);
|
||||||
// Write to the requester's PERSONAL MEMORY.md when per-owner isolation
|
// Write to the requester's PERSONAL MEMORY.md when per-owner isolation
|
||||||
// is active; otherwise the shared file (so the note is not stranded
|
// is active; otherwise the shared file (so the note is not stranded
|
||||||
// in an un-read PERSONAL row).
|
// in an un-read PERSONAL row).
|
||||||
String ownerKey = memoryProperties.isLifecycleMediatorEnabled()
|
String ownerKey = memoryProperties.isLifecycleMediatorEnabled()
|
||||||
? memoryOwnerResolver.resolve(ChatOrigin.from(toolContext))
|
? memoryOwnerResolver.resolve(ChatOrigin.from(toolContext))
|
||||||
: null;
|
: null;
|
||||||
WorkspaceFileEntity existing = workspaceFileService.getVisibleFile(agentId, MEMORY_FILENAME, ownerKey);
|
WorkspaceFileEntity existing = workspaceFileService.getVisibleFile(parsedAgentId, MEMORY_FILENAME, ownerKey);
|
||||||
String existingContent = existing != null && existing.getContent() != null
|
String existingContent = existing != null && existing.getContent() != null
|
||||||
? existing.getContent() : "";
|
? existing.getContent() : "";
|
||||||
String updated = appendLesson(existingContent, content, source);
|
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
|
// RFC-090 §14.3 — universal remember() targets MEMORY.md (the
|
||||||
// canonical file), so this IS a MemoryWriteEvent. Skill-local
|
// canonical file), so this IS a MemoryWriteEvent. Skill-local
|
||||||
// lessons go through SkillLessonWrittenEvent instead and do
|
// lessons go through SkillLessonWrittenEvent instead and do
|
||||||
// NOT touch this path.
|
// NOT touch this path.
|
||||||
eventPublisher.publishEvent(new MemoryWriteEvent(agentId, MEMORY_FILENAME,
|
eventPublisher.publishEvent(new MemoryWriteEvent(parsedAgentId, MEMORY_FILENAME,
|
||||||
"remember", content));
|
"remember", content));
|
||||||
|
|
||||||
JSONObject result = new JSONObject();
|
JSONObject result = new JSONObject();
|
||||||
@ -142,6 +142,18 @@ public class UniversalMemoryTool {
|
|||||||
return idx < 0 ? -1 : idx + 1; // position of '#' itself
|
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) {
|
private static String error(String msg) {
|
||||||
JSONObject e = new JSONObject();
|
JSONObject e = new JSONObject();
|
||||||
e.set("success", false);
|
e.set("success", false);
|
||||||
|
|||||||
@ -42,11 +42,17 @@ public class SkillLessonsTool {
|
|||||||
public String record_lesson(
|
public String record_lesson(
|
||||||
@ToolParam(description = "skill 的 slug(即 SKILL.md frontmatter 里的 name)") String skillName,
|
@ToolParam(description = "skill 的 slug(即 SKILL.md frontmatter 里的 name)") String skillName,
|
||||||
@ToolParam(description = "要记录的经验内容") String lesson,
|
@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) {
|
@ToolParam(description = "可选:当前对话 ID", required = false) String conversationId) {
|
||||||
|
|
||||||
if (skillName == null || skillName.isBlank()) return error("skillName 不能为空");
|
if (skillName == null || skillName.isBlank()) return error("skillName 不能为空");
|
||||||
if (lesson == null || lesson.isBlank()) return error("lesson 不能为空");
|
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()
|
ResolvedSkill resolved = skillRuntimeService.resolveAllSkillsStatus().stream()
|
||||||
.filter(s -> s != null && skillName.equals(s.getName()))
|
.filter(s -> s != null && skillName.equals(s.getName()))
|
||||||
@ -69,7 +75,7 @@ public class SkillLessonsTool {
|
|||||||
int max = manifest != null && manifest.getSelfEvolution() != null
|
int max = manifest != null && manifest.getSelfEvolution() != null
|
||||||
? manifest.getSelfEvolution().getLessonsMaxEntries() : 0;
|
? manifest.getSelfEvolution().getLessonsMaxEntries() : 0;
|
||||||
|
|
||||||
String lessonId = lessonsService.recordLesson(resolved, agentId, conversationId,
|
String lessonId = lessonsService.recordLesson(resolved, parsedAgentId, conversationId,
|
||||||
lesson, max);
|
lesson, max);
|
||||||
if (lessonId == null) {
|
if (lessonId == null) {
|
||||||
return error("Lesson 记录失败:skill 可能仅存在于数据库(无 workspace 目录)。");
|
return error("Lesson 记录失败:skill 可能仅存在于数据库(无 workspace 目录)。");
|
||||||
@ -83,6 +89,18 @@ public class SkillLessonsTool {
|
|||||||
return JSONUtil.toJsonPrettyStr(result);
|
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) {
|
private static String error(String msg) {
|
||||||
JSONObject e = new JSONObject();
|
JSONObject e = new JSONObject();
|
||||||
e.set("success", false);
|
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