diff --git a/apps/api/src/companion-agent-tools.ts b/apps/api/src/companion-agent-tools.ts index a9d8dca0..c82a0175 100644 --- a/apps/api/src/companion-agent-tools.ts +++ b/apps/api/src/companion-agent-tools.ts @@ -54,7 +54,7 @@ const TOOL_HINTS: Record = { 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.", diff --git a/apps/api/src/companion-tool-actions.test.mjs b/apps/api/src/companion-tool-actions.test.mjs index 7b1120c7..5f47360b 100644 --- a/apps/api/src/companion-tool-actions.test.mjs +++ b/apps/api/src/companion-tool-actions.test.mjs @@ -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: [] }); diff --git a/apps/api/src/mcp-tools.test.mjs b/apps/api/src/mcp-tools.test.mjs index 86cffe8b..467dc456 100644 --- a/apps/api/src/mcp-tools.test.mjs +++ b/apps/api/src/mcp-tools.test.mjs @@ -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, diff --git a/apps/api/src/mcp-tools.ts b/apps/api/src/mcp-tools.ts index d82ebc62..af358563 100644 --- a/apps/api/src/mcp-tools.ts +++ b/apps/api/src/mcp-tools.ts @@ -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",