feat(ai): keep generated edge IDs out of create_diagram_memo Agent input

This commit is contained in:
tianma-if committed 2026-09-21 17:53:44 +08:00
1 parent b41fb366eb
commit 6e0d9c570c
4 files changed
+42 -7

No files matched your search

+1 -1
View File
@@ -54,7 +54,7 @@ const TOOL_HINTS: Record<string, string> = {
search_memos: " Searches note titles and bodies, not notebook names. query is optional. For recently created or added notes, pass createdAfter (YYYY-MM-DD or ISO date-time) and omit query; never put this week/最近/新增 in query. For recently edited notes, use updatedAfter. Do not pass notebookId unless find_notebooks or list_notebooks returned it. For notes in a named notebook, find_notebooks then list_memos. If hasMore is true, say the list is incomplete.",
list_tags: " Use this when the user names a tag.",
create_memo: " For prose Markdown notes only. Never use this for 思维导图/mind maps, 流程图/flowcharts, or 架构图; use create_diagram_memo.",
create_diagram_memo: " Create an editable visual diagram note. kind=mind-map for 思维导图/mind map, flowchart for 流程图, architecture for 架构图. For mind maps, give a root and children with parentId; omit node type. If the user did not name a notebook, use the open notebook id from Focus DATA. Build nodes from the open note body in Focus DATA when the user refers to this note.",
create_diagram_memo: " Create an editable visual diagram note. kind=mind-map for 思维导图/mind map, flowchart for 流程图, architecture for 架构图. Omit edge ids; EdgeEver generates them. For mind maps, give a root and children with parentId; omit node type. If the user did not name a notebook, use the open notebook id from Focus DATA. Build nodes from the open note body in Focus DATA when the user refers to this note.",
get_diagram: " Read an existing editable diagram as a semantic graph. Call this before update_diagram. Do not use get_memo when you only need the diagram structure.",
update_diagram: " Edit an existing diagram after get_diagram. Pass expectedRevision from get_diagram. Use add_node, update_node, remove_node, add_edge, update_edge, or remove_edge. Do not create a new diagram unless the user asked for a new note.",
use_note_template: " Create a new memo from a template. If the user did not name a notebook, use the open notebook id from Focus DATA.",
@@ -219,6 +219,29 @@ describe("shared companion MCP adapter", () => {
.toContain("eval");
expect(f.sqlite.query("SELECT COUNT(*) AS n FROM companion_actions").get().n).toBe(0);
});
test("create_diagram_memo keeps generated edge IDs out of Agent input", async () => {
const f = await setup();
const tools = createCompanionTools({ ...f, scope, signal: new AbortController().signal, assertActive: async () => {}, sources: [] });
const input = {
notebookId: "ideas",
title: "Service architecture",
kind: "architecture",
nodes: [
{ id: "web", label: "Web", type: "frontend" },
{ id: "api", label: "API", type: "service" },
],
edges: [{ source: "web", target: "api", type: "request" }],
};
expect(() => validateCompanionTool("create_diagram_memo", {
...input,
edges: [{ id: "edge-1", source: "web", target: "api", type: "request" }],
})).toThrow();
const created = await tools.create_diagram_memo.execute(input);
expect(created).toMatchObject({ applied: true, diagramKind: "architecture", nodeCount: 2 });
const diagram = parseDiagramDocument((await getMemoDetail(f.db, scope.workspaceId, created.id)).contentMarkdown);
expect(diagram.edges).toMatchObject([{ source: "web", target: "api", kind: "request" }]);
expect(diagram.edges[0].id).toBeTruthy();
});
test("note templates and AI instructions execute immediately including deletes", async () => {
const f = await setup();
const tools = createCompanionTools({ ...f, scope, signal: new AbortController().signal, assertActive: async () => {}, sources: [] });
+3
View File
@@ -66,6 +66,7 @@ describe("MCP tool catalog", () => {
});
expect(byName.get("create_diagram_memo")?.inputSchema.properties.nodes.items.properties.x).toBeUndefined();
expect(byName.get("create_diagram_memo")?.inputSchema.properties.nodes.items.properties.width).toBeUndefined();
expect(byName.get("create_diagram_memo")?.inputSchema.properties.edges.items.properties.id).toBeUndefined();
expect(byName.get("get_diagram")?.annotations).toMatchObject({
readOnlyHint: true,
destructiveHint: false,
@@ -98,6 +99,8 @@ describe("MCP tool catalog", () => {
});
expect(byName.get("update_diagram")?.inputSchema.properties.operations.items.oneOf).toBeUndefined();
expect(byName.get("update_diagram")?.inputSchema.properties.operations.items.properties.op.const).toBeUndefined();
expect(byName.get("update_diagram")?.inputSchema.properties.operations.items.properties.edge.properties.id)
.toEqual({ type: "string", minLength: 1, maxLength: 100 });
expect(byName.get("rename_notebook")?.annotations).toMatchObject({
readOnlyHint: false,
destructiveHint: false,
+15 -6
View File
@@ -33,17 +33,26 @@ const diagramNodeSchema = {
properties: diagramNodeProperties,
};
const diagramEdgeProperties = {
source: { type: "string", minLength: 1, maxLength: 100 },
target: { type: "string", minLength: 1, maxLength: 100 },
label: { type: "string", maxLength: 500 },
type: { type: "string", enum: ["dependency", "request", "async", "data"] },
bidirectional: { type: "boolean" },
};
const diagramEdgeSchema = {
type: "object",
required: ["source", "target"],
additionalProperties: false,
properties: diagramEdgeProperties,
};
const mutableDiagramEdgeSchema = {
...diagramEdgeSchema,
properties: {
id: { type: "string", minLength: 1, maxLength: 100 },
source: { type: "string", minLength: 1, maxLength: 100 },
target: { type: "string", minLength: 1, maxLength: 100 },
label: { type: "string", maxLength: 500 },
type: { type: "string", enum: ["dependency", "request", "async", "data"] },
bidirectional: { type: "boolean" },
...diagramEdgeProperties,
},
};
@@ -201,7 +210,7 @@ const MCP_TOOL_DEFINITIONS = [
},
node: diagramNodeSchema,
nodeId: { type: "string", minLength: 1 },
edge: diagramEdgeSchema,
edge: mutableDiagramEdgeSchema,
edgeId: { type: "string", minLength: 1 },
changes: {
type: "object",