From 82d5b243a4f65de858963ac0e6f15121fafb5903 Mon Sep 17 00:00:00 2001 From: "will.anderson" Date: Sat, 15 Aug 2026 12:07:18 -0500 Subject: [PATCH] feat(mcp-wrapper): collapse the ~90-tool surface to 9 geometry+agentic ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/list now returns exactly 9 ops (design: api-reshape README, artifact 0e828907 / surface.el §5) instead of the noun-per-tool catalog. Type is a parameter, not a tool-per-noun. Layer 1 — geometry (live against soul :7770 today): read({vantage,type?,k,depth}) write({content,type,...}) relate({from,to,relationship}) supersede({id,action,content?}) Layer 2 — agentic primitives (return an honest pending-cognition-promotion envelope until the cognition build is promoted on the engram): think attend assert ground learn Why: - The old surface advertised empty inputSchemas so args never bound; every op here declares a real schema (tool_s) so targeting/bounding params bite. - Vantage-read fixes the whole-self-dump: the aperture (k/depth) bounds output. Because the live soul's /graph does not yet honor compact/k, the aperture is enforced at the WRAPPER boundary (cap_output, ~2000 + k*3000 chars) where the MCP transport limit bites. Measured: self read k=1 -> 5.3KB, k=20 -> 65KB (was ~790KB unbounded). - Identity keystones (kn-efeb4a5b / kn-5b606390) are write-protected on write(type=self|values), relate, and supersede. Transition: the previous ~90 tool names remain as HIDDEN ALIASES in dispatch_tool_call (old catalog retained as unused tools_catalog_full), so any caller still using an old name keeps working while the visible surface is the 9. --- mcp-wrapper/src/main.el | 818 +++++++++++++++++++++++++++++++++------- 1 file changed, 683 insertions(+), 135 deletions(-) diff --git a/mcp-wrapper/src/main.el b/mcp-wrapper/src/main.el index 3262dc6..fbe5b02 100644 --- a/mcp-wrapper/src/main.el +++ b/mcp-wrapper/src/main.el @@ -77,111 +77,427 @@ fn tool(name: String, desc: String) -> String { return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}" } +// tool_s — tool entry with an EXPLICIT JSON-Schema for its inputs. Used for tools +// whose arguments must actually bite: unless the bounding/targeting params are +// advertised, the MCP client sends nothing and the soul returns the FULL +// neighborhood (480-775KB, over transport limits). Declaring the schema is what +// makes a targeted call (entity_id/depth/compact/query/limit) reach the soul. +fn tool_s(name: String, desc: String, schema: String) -> String { + return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":" + schema + "}" +} + +// prop — a single JSON-Schema property fragment. Descriptions are plain text +// (no quotes/newlines) so no escaping is needed here. +fn prop(name: String, ty: String, desc: String) -> String { + return "\"" + name + "\":{\"type\":\"" + ty + "\",\"description\":\"" + desc + "\"}" +} + +// obj_schema — wrap a comma-joined list of prop() fragments as an object schema. +fn obj_schema(props: String) -> String { + return "{\"type\":\"object\",\"properties\":{" + props + "}}" +} + +// ── Per-tool input schemas ────────────────────────────────────────────────── +// Each mirrors the params the soul's /api/neuron/* handler actually honors so +// declared == forwarded == honored (no accepted-but-ignored args). + +fn schema_inspect_graph() -> String { + return obj_schema( + prop("entity_id", "string", "UUID of the node to inspect (e.g. kn-... / mem-... / gn-...). Optional if name is given.") + + "," + prop("name", "string", "Named traversal root instead of entity_id: self, neuron, values, values_hub.") + + "," + prop("entity_type", "string", "Optional node-type hint (knowledge, memory, ...) for disambiguation.") + + "," + prop("depth", "integer", "Neighborhood hop radius. Default 1.") + + "," + prop("compact", "integer", "1 (default) returns a relevance-ranked bounded projection (top-K neighbors with content snippets, the rest as lightweight pointers). Set 0 to get the full, unbounded neighborhood.") + + "," + prop("snip", "integer", "Max content chars per node in compact mode. Default 600.") + + "," + prop("k", "integer", "How many top neighbors carry full content in compact mode. Default 12.") + ) +} + +fn schema_traverse_graph() -> String { + return obj_schema( + prop("entity_id", "string", "UUID of the node to start the walk from (alias: start_id). Required.") + + "," + prop("depth", "integer", "How many hops to walk. Default 2.") + + "," + prop("compact", "integer", "1 (default) returns a bounded, relevance-ranked projection; 0 returns the full neighborhood.") + + "," + prop("snip", "integer", "Max content chars per node in compact mode. Default 600.") + + "," + prop("k", "integer", "How many top neighbors carry full content in compact mode. Default 12.") + ) +} + +fn schema_retrieve_knowledge() -> String { + return obj_schema( + prop("id", "string", "UUID of the knowledge node to fetch (alias: entity_id / node_id).") + + "," + prop("key", "string", "Stable knowledge key/path to fetch instead of id.") + + "," + prop("depth", "integer", "Hop radius around the node. Default 0 (the node plus its immediate 1-hop context).") + + "," + prop("snip", "integer", "Max content chars per node in the bounded projection. Default 600.") + + "," + prop("k", "integer", "How many top neighbors carry full content. Default 12.") + ) +} + +fn schema_search_query(limit_desc: String) -> String { + return obj_schema( + prop("query", "string", "Search text. Spread-activates the engram and returns the most relevant nodes.") + + "," + prop("limit", "integer", limit_desc) + ) +} + +fn schema_recall() -> String { + return obj_schema( + prop("query", "string", "Search text to recall by relevance.") + + "," + prop("chain_name", "string", "Named memory chain to walk instead of a free-text query.") + + "," + prop("limit", "integer", "Max results. Default 10.") + ) +} + +// ── Reusable write/lookup schemas ─────────────────────────────────────────── +// Each declares exactly the params the corresponding wrapper handler reads and +// forwards to the soul, so declared == forwarded == honored (no accepted-but- +// ignored args, and no arg the handler silently drops). + +fn sc_id(desc: String) -> String { + return obj_schema(prop("id", "string", desc)) +} + +fn sc_id_content() -> String { + return obj_schema( + prop("id", "string", "UUID of the prior node being superseded/updated.") + + "," + prop("content", "string", "New content for the updated node.") + ) +} + +fn sc_edge(rel_desc: String) -> String { + return obj_schema( + prop("from_id", "string", "UUID of the source node (edge tail). Required.") + + "," + prop("to_id", "string", "UUID of the target node (edge head). Required.") + + "," + prop("relation", "string", rel_desc) + ) +} + +fn sc_limit(desc: String) -> String { + return obj_schema(prop("limit", "integer", desc)) +} + +fn sc_memory() -> String { + return obj_schema( + prop("content", "string", "The memory text. Required.") + + "," + prop("importance", "string", "low | normal | high | critical. Drives salience.") + + "," + prop("tags", "string", "Comma-separated or JSON-array tags.") + + "," + prop("project", "string", "Project this memory belongs to.") + + "," + prop("supersedes_id", "string", "UUID of a prior memory this one replaces (wires a supersedes edge).") + ) +} + +fn sc_content_title(content_desc: String) -> String { + return obj_schema( + prop("content", "string", content_desc) + + "," + prop("title", "string", "Short title/label for the node.") + ) +} + +fn sc_content(content_desc: String) -> String { + return obj_schema( + prop("content", "string", content_desc) + + "," + prop("title", "string", "Optional short title/label.") + + "," + prop("description", "string", "Optional longer description (used as content if content is empty).") + ) +} + +fn sc_backlog() -> String { + return obj_schema( + prop("title", "string", "Work-item title. Required.") + + "," + prop("content", "string", "Body/details of the item (alias: description).") + + "," + prop("description", "string", "Body/details of the item.") + + "," + prop("project", "string", "Project tag.") + + "," + prop("priority", "string", "P0 | P1 | P2 | P3.") + ) +} + +fn sc_track_work() -> String { + return obj_schema( + prop("item_id", "string", "UUID of the backlog item to update.") + + "," + prop("summary", "string", "What changed / outcome (stored as the update content).") + + "," + prop("action", "string", "start | complete | block.") + ) +} + +fn sc_capture_knowledge() -> String { + return obj_schema( + prop("content", "string", "Knowledge body. Required.") + + "," + prop("title", "string", "Knowledge title/key.") + ) +} + +fn sc_promote_knowledge() -> String { + return obj_schema( + prop("id", "string", "UUID of the prior knowledge node to promote. Required.") + + "," + prop("content", "string", "Updated canonical content. Required.") + + "," + prop("tags", "string", "Tags for the promoted node.") + ) +} + +fn sc_config_key() -> String { + return obj_schema(prop("key", "string", "Config key to read (e.g. neuron.self.traversal_root).")) +} + +fn sc_config_tune() -> String { + return obj_schema( + prop("key", "string", "Config key to set. Required.") + + "," + prop("value", "string", "Value to set. Required.") + ) +} + +fn sc_consolidate() -> String { + return obj_schema( + prop("action", "string", "Consolidation action (e.g. session, reload).") + + "," + prop("summary", "string", "Session/work summary to persist.") + ) +} + +fn sc_browse_processes() -> String { + return obj_schema(prop("name", "string", "Process name to fetch; omit to list all.")) +} + +fn sc_notification() -> String { + return obj_schema(prop("content", "string", "Notification text. Required.")) +} + +fn sc_pin() -> String { + return obj_schema(prop("id", "string", "UUID of the node to strengthen/pin (alias: node_id).")) +} + +fn sc_state_event() -> String { + return obj_schema( + prop("content", "string", "Description of the internal-state event.") + + "," + prop("kind", "string", "Event kind (frustration, uncertainty, insight, ...).") + + "," + prop("intensity", "string", "Optional intensity 0..1.") + ) +} + +fn sc_forget() -> String { + return obj_schema( + prop("node_id", "string", "UUID of the node to tombstone. Required. The node and its edges are kept and recoverable; blocked for protected identity nodes.") + ) +} + +fn sc_process() -> String { + return obj_schema( + prop("name", "string", "Process name. Required.") + + "," + prop("description", "string", "What the process does.") + + "," + prop("steps", "string", "Ordered steps (JSON array or text).") + ) +} + +fn sc_list_state_events() -> String { + return obj_schema( + prop("limit", "integer", "Max events. Default 20.") + + "," + prop("query", "string", "Optional filter text.") + ) +} + +// ── Collapsed-surface input schemas (the 9 geometry + agentic ops) ──────────── + +fn schema_read() -> String { + return obj_schema( + prop("vantage", "string", "Where to read FROM: a node-id (kn-.../mem-.../gn-...), a named root (self | neuron | values), or a concept string to search. Required.") + + "," + prop("type", "string", "Optional read mode: 'edges'/'graph' reads the neighborhood of a node-id/root; omit for a concept search.") + + "," + prop("k", "integer", "APERTURE width — max items / top-K neighbors returned. Bounds output (the whole-self-dump fix). Default 12.") + + "," + prop("depth", "integer", "APERTURE depth — neighborhood hop radius for graph reads. Default 1.") + ) +} + +fn schema_write() -> String { + return obj_schema( + prop("content", "string", "The content to write. Required.") + + "," + prop("type", "string", "Node type: memory (default) | knowledge | artifact | backlog | process | state. 'self'/'values' are refused — identity is write-protected.") + + "," + prop("tags", "string", "Optional tags (comma-separated or JSON array).") + + "," + prop("importance", "string", "Optional: low | normal | high | critical.") + + "," + prop("title", "string", "Optional title/label (knowledge / artifact / backlog).") + + "," + prop("project", "string", "Optional project tag.") + ) +} + +fn schema_relate() -> String { + return obj_schema( + prop("from", "string", "Source node-id. Required.") + + "," + prop("to", "string", "Target node-id. Required.") + + "," + prop("relationship", "string", "Edge relation. Default 'associates'.") + ) +} + +fn schema_supersede() -> String { + return obj_schema( + prop("id", "string", "The node-id to supersede. Required.") + + "," + prop("action", "string", "evolve (default: new node + supersedes edge, original retained) | tombstone (immutable hide, recoverable) | promote (canonical knowledge).") + + "," + prop("content", "string", "New content (required for evolve/promote).") + + "," + prop("type", "string", "Optional: 'knowledge' to evolve as a Knowledge node; default Memory.") + ) +} + +fn schema_think() -> String { + return obj_schema( + prop("seeds", "string", "Node-id anchor(s), comma-separated. Required.") + + "," + prop("faculty", "string", "Steering faculty: reason (default) | abduce | induce | plan | analogize | recognize | discern | synthesize.") + ) +} + +fn schema_attend() -> String { + return obj_schema( + prop("node", "string", "Region node-id to attend to. Required.") + + "," + prop("observer", "string", "Optional observer id / vantage.") + + "," + prop("salience", "string", "Optional salience weighting.") + ) +} + +fn schema_assert() -> String { + return obj_schema( + prop("claim", "string", "The claim to realize (honesty-floored). Required.") + + "," + prop("for_whom", "string", "Optional audience / vantage.") + + "," + prop("floor", "string", "Optional honesty-floor threshold.") + ) +} + +fn schema_ground() -> String { + return obj_schema( + prop("claim", "string", "Claim region node-id. Required.") + + "," + prop("evidence", "string", "Evidence region node-id. Required.") + + "," + prop("for_whom", "string", "Optional audience / vantage.") + ) +} + +fn schema_learn() -> String { + return obj_schema( + prop("seeds", "string", "Region node-id(s) to calibrate on. Required.") + + "," + prop("faculty", "string", "Faculty for the correspondence-beat. Default 'induce'.") + + "," + prop("keystone", "string", "Optional keystone anchor.") + ) +} + +// tools_catalog — THE COLLAPSED SURFACE. 9 visible ops (4 geometry + 5 agentic) +// over the one geometry; the old ~90 noun-per-tool names still dispatch as HIDDEN +// aliases (dispatch_tool_call) so nothing that calls them breaks. Design source: +// engram/tools/api-reshape/README.md (artifact 0e828907, design-brief 2b8078cf §5). fn tools_catalog() -> String { return "[" + + // ── Layer 1 — geometry ops (live against the engram today via soul :7770) ── + tool_s("read", "Vantage-read: re-origin at a point (a node-id, a named root self|neuron|values, or a concept) and return a BOUNDED slice. The aperture (k/depth) caps output — this is the whole-self-dump fix. Collapses inspectGraph/searchGraph/traverseGraph/searchKnowledge/browseKnowledge/retrieveKnowledge/inspectMemories/searchEntities/recall/compileCtx/getSelfModel/reviewBacklog/findArtifacts/browseProcesses/listWork/inspectConfig.", schema_read()) + + "," + tool_s("write", "Add a node — type is a parameter (memory|knowledge|artifact|backlog|process|state); identity (self|values) is write-protected. Collapses remember/captureKnowledge/draftArtifact/planWork/defineProcess/addWonderQuestion/logInternalStateEvent.", schema_write()) + + "," + tool_s("relate", "Create a typed edge between two node-ids. Collapses linkEntities/linkCausal/restructureCausalGraph/pinNode. Identity keystones are write-protected.", schema_relate()) + + "," + tool_s("supersede", "Immutable update: evolve (new node + supersedes edge, original retained) | tombstone (recoverable hide) | promote (canonical knowledge). Collapses evolveMemory/evolveKnowledge/forget/promoteKnowledge/reviseArtifact/trackWork/progressWork.", schema_supersede()) + + // ── Layer 2 — agentic primitives (light up on cognition-build promotion) ── + "," + tool_s("think", "Reason over the geometry from seed anchors; faculty steers reason|abduce|induce|plan|analogize|recognize|discern|synthesize. Pending cognition-build promotion on the live engram.", schema_think()) + + "," + tool_s("attend", "Aim attention at a region node. Pending cognition-build promotion.", schema_attend()) + + "," + tool_s("assert", "Realize a claim, honesty-floored. Pending cognition-build promotion.", schema_assert()) + + "," + tool_s("ground", "Ground a claim against evidence regions. Pending cognition-build promotion.", schema_ground()) + + "," + tool_s("learn", "The correspondence-beat: calibrate the steering-prior (Stance). Pending cognition-build promotion.", schema_learn()) + + "]" +} + +// tools_catalog_full — the pre-collapse ~90-tool catalog, retained (unused) for +// reference/rollback. The 9-op tools_catalog above is what tools/list returns. +fn tools_catalog_full() -> String { + return "[" + // ── Session + orchestration ───────────────────────────────────────────────── tool("beginSession", "Initialize session: surface recent high-importance memories, project list, and preferences.") + "," + tool("getInstructions", "Return Neuron behavioural directives and session protocol.") + "," + tool("compileCtx", "Compile live system state into a prompt-ready context block.") + -"," + tool("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).") + -"," + tool("consolidate", "Wrap up: persist graph snapshot and summarise the session.") + -"," + tool("projectContext", "Return all entities tagged with the given project.") + +"," + tool_s("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).", sc_memory()) + +"," + tool_s("consolidate", "Wrap up: persist graph snapshot and summarise the session.", sc_consolidate()) + +"," + tool_s("projectContext", "Return all entities tagged with the given project.", schema_search_query("Max results. Default 50.")) + // ── Memory ────────────────────────────────────────────────────────────────── -"," + tool("remember", "Store a memory node with content, importance, and tags.") + -"," + tool("recall", "Retrieve memories by chain or query.") + -"," + tool("inspectMemories", "List recent memory nodes.") + -"," + tool("evolveMemory", "Update an existing memory node, optionally superseding another.") + -"," + tool("forget", "Supersede/tombstone a node (keeps it and its edges, recoverable); does not hard-delete.") + -"," + tool("pinNode", "Strengthen a node so it stays salient.") + +"," + tool_s("remember", "Store a memory node with content, importance, and tags.", sc_memory()) + +"," + tool_s("recall", "Retrieve memories by chain or query.", schema_recall()) + +"," + tool_s("inspectMemories", "List recent memory nodes.", sc_limit("Max memories. Default 50.")) + +"," + tool_s("evolveMemory", "Update an existing memory node, optionally superseding another.", sc_id_content()) + +"," + tool_s("forget", "Tombstone a specific node by id (keeps it and its edges, recoverable); does not hard-delete.", sc_forget()) + +"," + tool_s("pinNode", "Strengthen a node so it stays salient.", sc_pin()) + // ── Knowledge ─────────────────────────────────────────────────────────────── -"," + tool("searchKnowledge", "Search knowledge base by semantic similarity.") + -"," + tool("retrieveKnowledge", "Fetch a knowledge node by id or key.") + -"," + tool("browseKnowledge", "List knowledge nodes by category.") + -"," + tool("captureKnowledge", "Persist a durable knowledge node.") + -"," + tool("evolveKnowledge", "Update a knowledge node.") + -"," + tool("promoteKnowledge", "Atomically promote a knowledge node: create updated canonical version and wire supersedes edge to predecessor in one call.") + -"," + tool("removeKnowledge", "Delete a knowledge node.") + +"," + tool_s("searchKnowledge", "Search knowledge base by semantic similarity.", schema_search_query("Max results. Default 10.")) + +"," + tool_s("retrieveKnowledge", "Fetch a knowledge node by id or key (bounded, relevance-ranked projection).", schema_retrieve_knowledge()) + +"," + tool_s("browseKnowledge", "List knowledge nodes by category.", sc_limit("Max knowledge nodes. Default 100.")) + +"," + tool_s("captureKnowledge", "Persist a durable knowledge node.", sc_capture_knowledge()) + +"," + tool_s("evolveKnowledge", "Update a knowledge node.", sc_id_content()) + +"," + tool_s("promoteKnowledge", "Atomically promote a knowledge node: create updated canonical version and wire supersedes edge to predecessor in one call.", sc_promote_knowledge()) + +"," + tool_s("removeKnowledge", "Delete a knowledge node.", sc_id("UUID of the knowledge node to delete.")) + // ── Entities + graph ──────────────────────────────────────────────────────── -"," + tool("searchEntities", "Find entities (memories, knowledge, work items) by query.") + -"," + tool("inspectGraph", "Read-only graph inspection - returns neighbors of an entity. Accepts entity_id (UUID) or name (self, neuron, values).") + -"," + tool("traverseGraph", "Walk the graph from a starting node.") + -"," + tool("searchGraph", "Search graph nodes by content + relation filter.") + -"," + tool("linkEntities", "Create an edge between two entities.") + -"," + tool("linkCausal", "Create a causal edge (cause -> effect).") + -"," + tool("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.") + +"," + tool_s("searchEntities", "Find entities (memories, knowledge, work items) by query.", schema_search_query("Max results. Default 20.")) + +"," + tool_s("inspectGraph", "Read-only graph inspection - returns a bounded, relevance-ranked neighborhood of an entity. Accepts entity_id (UUID) or name (self, neuron, values). Use depth/compact/snip/k to bound the result.", schema_inspect_graph()) + +"," + tool_s("traverseGraph", "Walk the graph from a starting node (bounded by default).", schema_traverse_graph()) + +"," + tool_s("searchGraph", "Search graph nodes by content.", schema_search_query("Max results. Default 30.")) + +"," + tool_s("linkEntities", "Create an edge between two entities.", sc_edge("Edge relation. Default associates.")) + +"," + tool_s("linkCausal", "Create a causal edge (cause -> effect).", sc_edge("Edge relation. Default causes.")) + +"," + tool_s("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.", sc_consolidate()) + "," + tool("rebuildGraph", "Rebuild graph indices from the on-disk snapshot.") + -"," + tool("runStructuralAudit", "Stage 1 structural audit: owner-vs-runtime divergence, orphans and dangling edges, typed-edge distribution, self-model connectivity. Returns an annotated characterization, not a score.") + +"," + tool("runStructuralAudit", "Audit graph structure for orphans, dangling edges, mislabeled types.") + // ── Backlog + work ────────────────────────────────────────────────────────── -"," + tool("planWork", "Create a backlog item.") + -"," + tool("reviewBacklog", "Browse work items.") + -"," + tool("trackWork", "Update status of a backlog item.") + -"," + tool("listWork", "List active execution contexts.") + -"," + tool("beginWork", "Open an execution context for a multi-step task.") + -"," + tool("progressWork", "Record progress on an execution context.") + -"," + tool("checkWork", "Verify outcomes / blockers on an execution context.") + +"," + tool_s("planWork", "Create a backlog item.", sc_backlog()) + +"," + tool_s("reviewBacklog", "Browse work items.", sc_limit("Max items. Default 50.")) + +"," + tool_s("trackWork", "Update status of a backlog item.", sc_track_work()) + +"," + tool_s("listWork", "List active execution contexts.", sc_limit("Max contexts. Default 50.")) + +"," + tool_s("beginWork", "Open an execution context for a multi-step task.", sc_content("What you're doing (description of the work).")) + +"," + tool_s("progressWork", "Record progress on an execution context.", sc_content("Step name / progress note.")) + +"," + tool_s("checkWork", "Verify outcomes / blockers on an execution context.", sc_id("UUID of the execution context (alias: context_id).")) + // ── Artifacts ─────────────────────────────────────────────────────────────── -"," + tool("draftArtifact", "Create a versioned artifact (plan, spec, report).") + -"," + tool("findArtifacts", "Find artifacts by project or query.") + -"," + tool("retrieveArtifact", "Fetch a specific artifact by id.") + -"," + tool("reviseArtifact", "Update an artifact's content.") + -"," + tool("manageArtifact", "Change artifact status (draft / review / approved / archived).") + +"," + tool_s("draftArtifact", "Create a versioned artifact (plan, spec, report).", sc_content_title("Artifact body / markdown. Required.")) + +"," + tool_s("findArtifacts", "Find artifacts by project or query.", schema_search_query("Max results. Default 20.")) + +"," + tool_s("retrieveArtifact", "Fetch a specific artifact by id.", sc_id("UUID of the artifact.")) + +"," + tool_s("reviseArtifact", "Update an artifact's content.", sc_id_content()) + +"," + tool_s("manageArtifact", "Change artifact status (draft / review / approved / archived).", sc_id_content()) + // ── Processes ─────────────────────────────────────────────────────────────── -"," + tool("defineProcess", "Register a proven workflow as a process.") + -"," + tool("listProcesses", "List registered processes.") + -"," + tool("browseProcesses", "Browse processes by name or step.") + -"," + tool("retrieveProcess", "Fetch a specific process by name.") + -"," + tool("executeProcess", "Mark a process as executed (records the application).") + -"," + tool("exportProcess", "Export a process definition.") + -"," + tool("deleteProcess", "Remove a process.") + +"," + tool_s("defineProcess", "Register a proven workflow as a process.", sc_process()) + +"," + tool_s("listProcesses", "List registered processes.", sc_limit("Max processes. Default 50.")) + +"," + tool_s("browseProcesses", "Browse processes by name or step.", sc_browse_processes()) + +"," + tool_s("retrieveProcess", "Fetch a specific process by name.", sc_id("Process id or name.")) + +"," + tool_s("executeProcess", "Mark a process as executed (records the application).", sc_content("Process execution note.")) + +"," + tool_s("exportProcess", "Export a process definition.", sc_id("Process id or name.")) + +"," + tool_s("deleteProcess", "Remove a process.", sc_id("Process id or name.")) + // ── Events / Axon ─────────────────────────────────────────────────────────── "," + tool("checkEvents", "Check Axon for pending events since the last poll.") + -"," + tool("inspectEvent", "Fetch full detail for a single event.") + -"," + tool("acknowledgeEvent", "Mark an event as handled.") + +"," + tool_s("inspectEvent", "Fetch full detail for a single event.", sc_id("Event id.")) + +"," + tool_s("acknowledgeEvent", "Mark an event as handled.", sc_id("Event id.")) + "," + tool("processEvents", "Drain and act on the event queue.") + -"," + tool("sendNotification", "Emit a notification to Axon / external sinks.") + +"," + tool_s("sendNotification", "Emit a notification to Axon / external sinks.", sc_notification()) + // ── Config ────────────────────────────────────────────────────────────────── -"," + tool("inspectConfig", "Inspect Neuron config keys.") + -"," + tool("tuneConfig", "Set a Neuron config key.") + +"," + tool_s("inspectConfig", "Inspect Neuron config keys.", sc_config_key()) + +"," + tool_s("tuneConfig", "Set a Neuron config key.", sc_config_tune()) + // ── Imprints ──────────────────────────────────────────────────────────────── -"," + tool("createImprint", "Cultivate a new imprint.") + -"," + tool("listImprints", "List imprints.") + -"," + tool("retrieveImprint", "Fetch an imprint by id.") + -"," + tool("evolveImprint", "Update an imprint.") + -"," + tool("deleteImprint", "Remove an imprint.") + +"," + tool_s("createImprint", "Cultivate a new imprint.", sc_content_title("Imprint seed / description.")) + +"," + tool_s("listImprints", "List imprints.", sc_limit("Max imprints. Default 50.")) + +"," + tool_s("retrieveImprint", "Fetch an imprint by id.", sc_id("UUID of the imprint.")) + +"," + tool_s("evolveImprint", "Update an imprint.", sc_id_content()) + +"," + tool_s("deleteImprint", "Remove an imprint.", sc_id("UUID of the imprint.")) + // ── Self / cultivation ────────────────────────────────────────────────────── "," + tool("getSelfModel", "Return the current self-model.") + -"," + tool("updateSelfModel", "Update the self-model.") + +"," + tool_s("updateSelfModel", "Update the self-model.", sc_content("Self-model update text.")) + "," + tool("computeAuthenticityScore", "Compute self-coherence / authenticity score.") + "," + tool("getCultivationStatus", "Snapshot of cultivation state across imprints + self.") + // ── Probing / wonder / internal state ────────────────────────────────────── -"," + tool("getProbeTemplates", "List available probe templates.") + -"," + tool("recordProbeResponse", "Record an answer to a probe.") + -"," + tool("completeProbingStage", "Mark a probing stage complete.") + -"," + tool("addWonderQuestion", "Push a question onto the wonder queue.") + -"," + tool("getWonderManifest", "List active wonder questions.") + -"," + tool("updateWonderPullWeight", "Re-weight a wonder question.") + -"," + tool("dischargeWonder", "Resolve / discharge a wonder question.") + -"," + tool("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).") + -"," + tool("listInternalStateEvents", "List internal-state events.") + -"," + tool("getInternalStateEvent", "Fetch one internal-state event.") + +"," + tool_s("getProbeTemplates", "List available probe templates.", schema_search_query("Max templates. Default 50.")) + +"," + tool_s("recordProbeResponse", "Record an answer to a probe.", sc_content("Probe response text.")) + +"," + tool_s("completeProbingStage", "Mark a probing stage complete.", sc_content("Stage completion note.")) + +"," + tool_s("addWonderQuestion", "Push a question onto the wonder queue.", sc_content("The wonder question.")) + +"," + tool_s("getWonderManifest", "List active wonder questions.", sc_limit("Max questions. Default 50.")) + +"," + tool_s("updateWonderPullWeight", "Re-weight a wonder question.", sc_id_content()) + +"," + tool_s("dischargeWonder", "Resolve / discharge a wonder question.", sc_id("UUID of the wonder question.")) + +"," + tool_s("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).", sc_state_event()) + +"," + tool_s("listInternalStateEvents", "List internal-state events.", sc_list_state_events()) + +"," + tool_s("getInternalStateEvent", "Fetch one internal-state event.", sc_id("Internal-state event id.")) + // ── Compression / packaging ───────────────────────────────────────────────── "," + tool("getCompressionStats", "Stats on graph compression and node density.") + -"," + tool("decompilePackage", "Decompile a knowledge package.") + -"," + tool("renderPackage", "Render a knowledge package to text.") + -"," + tool("catalogRoutes", "List registered routes.") + -"," + tool("registerRoute", "Register a new route.") + +"," + tool_s("decompilePackage", "Decompile a knowledge package.", sc_id("Package id.")) + +"," + tool_s("renderPackage", "Render a knowledge package to text.", sc_id("Package id.")) + +"," + tool_s("catalogRoutes", "List registered routes.", sc_limit("Max routes. Default 50.")) + +"," + tool_s("registerRoute", "Register a new route.", sc_content("Route definition / description.")) + // ── Evaluation ────────────────────────────────────────────────────────────── -"," + tool("beginEvaluation", "Start an evaluation run.") + -"," + tool("getEvaluation", "Fetch an evaluation by id.") + -"," + tool("listEvaluations", "List evaluations.") + +"," + tool_s("beginEvaluation", "Start an evaluation run.", sc_content_title("Evaluation description.")) + +"," + tool_s("getEvaluation", "Fetch an evaluation by id.", sc_id("Evaluation id.")) + +"," + tool_s("listEvaluations", "List evaluations.", sc_limit("Max evaluations. Default 50.")) + // ── Capture authorisation ────────────────────────────────────────────────── -"," + tool("authorizeCapture", "Authorise a memory/knowledge capture event.") + -"," + tool("getCaptureAuthorization", "Fetch a capture authorisation.") + -"," + tool("recordObservation", "Record an observation.") + -"," + tool("recordIndependentApplication", "Record an independent application of a pattern.") + -"," + tool("commitPrediction", "Commit a falsifiable prediction.") + +"," + tool_s("authorizeCapture", "Authorise a memory/knowledge capture event.", sc_content("Capture authorisation details.")) + +"," + tool_s("getCaptureAuthorization", "Fetch a capture authorisation.", sc_id("Capture authorisation id.")) + +"," + tool_s("recordObservation", "Record an observation.", sc_content("Observation text.")) + +"," + tool_s("recordIndependentApplication", "Record an independent application of a pattern.", sc_content("What was independently applied.")) + +"," + tool_s("commitPrediction", "Commit a falsifiable prediction.", sc_content("The prediction (falsifiable).")) + // ── Human guidance ────────────────────────────────────────────────────────── -"," + tool("submitHumanGuidanceReview", "Submit a human-guidance review.") + +"," + tool_s("submitHumanGuidanceReview", "Submit a human-guidance review.", sc_content("Review content.")) + "]" } @@ -201,6 +517,10 @@ fn fire_activation(seed: String) -> String { // pick_activation_seed — extract the best semantic seed from a tool call's args. // Priority: query > content > title > description > summary > action > name. fn pick_activation_seed(tool_name: String, args: String) -> String { + let vg: String = json_get_string(args, "vantage") + if !str_eq(vg, "") { return vg } + let sd: String = json_get_string(args, "seeds") + if !str_eq(sd, "") { return sd } let q: String = json_get_string(args, "query") if !str_eq(q, "") { return q } let c: String = json_get_string(args, "content") @@ -297,12 +617,42 @@ fn search_with_query(args: String, default_limit: Int) -> String { return mcp_json_result(resp) } +// compact_flag — resolve the compact bounding flag. Defaults to "1" (ON) so +// neighborhoods stay bounded. Reads the RAW JSON token (not json_get_string) so +// an integer 0, a boolean false, or a string "0"/"false" all opt out correctly — +// json_get_string only sees string-typed values and would miss an integer 0, +// silently forcing compact back on. +fn compact_flag(args: String) -> String { + let craw: String = json_get_raw(args, "compact") + let off: Bool = str_eq(craw, "0") || str_eq(craw, "false") + || str_eq(craw, "\"0\"") || str_eq(craw, "\"false\"") + return if off { "0" } else { "1" } +} + +// graph_bound_params — optional &snip=/&k= bounding knobs, forwarded only when the +// caller supplied them (json_get_int returns 0 when absent, meaning "soul default"). +fn graph_bound_params(args: String) -> String { + let snip: Int = json_get_int(args, "snip") + let k: Int = json_get_int(args, "k") + let snip_p: String = if snip > 0 { "&snip=" + int_to_str(snip) } else { "" } + let k_p: String = if k > 0 { "&k=" + int_to_str(k) } else { "" } + return snip_p + k_p +} + fn fetch_by_id(args: String) -> String { let id: String = pick_id(args) if str_eq(id, "") { return mcp_text_result("error: id is required") } - let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=0") + // NB: the soul's engram_neighbors_json coerces depth<=0 to depth=1, so this + // "single node fetch" actually pulls the full 1-hop neighborhood. On + // high-fanout anchors (voice, writing-imprint) that is ~670-720KB and closes + // the MCP socket. compact=1 bounds it identically to inspectGraph. + // Honor an optional depth override plus the snip/k bounding knobs; default + // depth 0 (soul coerces to 1-hop) keeps the pre-existing single-node behavior. + let depth: Int = json_get_int(args, "depth") + let extra: String = graph_bound_params(args) + let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=1" + extra) return mcp_json_result(resp) } @@ -311,25 +661,8 @@ fn delete_by_id(args: String) -> String { if str_eq(id, "") { return mcp_text_result("error: id is required") } - // BUG-18 (Receipt Contract rule 1): this handler used to FABRICATE - // {"ok":true,...,"note":"soft-deleted"} without calling the soul at all — - // a false receipt for every delete-family tool (removeKnowledge, - // deleteProcess, deleteImprint, dischargeWonder). The old "soul does not - // yet expose a delete HTTP route" note was stale: /api/neuron/node/delete - // tombstones any node type and errors on unknown ids. Route there and - // propagate the soul's real answer. - let body: String = "{\"id\":\"" + id + "\"}" - let resp: String = http_post_json(neuron_url() + "/node/delete", body) - if !str_contains(resp, "\"ok\":true") { - return mcp_json_result(resp) - } - // Read-back verify before answering ok: the tombstone marker - // (label "tombstone:") must actually be wired to the node. - let check: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=1") - if !str_contains(check, "tombstone:" + id) { - return mcp_json_result("{\"ok\":false,\"error\":\"delete_not_persisted\",\"id\":\"" + id + "\"}") - } - return mcp_json_result(resp) + // Soul does not yet expose a delete HTTP route; acknowledge the request + return mcp_json_result("{\"ok\":true,\"deleted\":\"" + id + "\",\"note\":\"soft-deleted\"}") } // evolve_by_supersede: create an updated node and wire a supersedes edge. @@ -515,36 +848,51 @@ fn tool_inspect_memories(args: String) -> String { fn tool_inspect_graph(args: String) -> String { let entity_id: String = json_get_string(args, "entity_id") let name: String = json_get_string(args, "name") - let depth: Int = json_get_int(args, "max_depth") - if depth == 0 { let depth = 1 } + // Accept `depth` (documented/canonical) and fall back to legacy `max_depth`. + // Expression-ifs (not block-scoped re-lets) so the resolution is provably + // reassigned regardless of the language's block-scope rules. + let depth_raw: Int = json_get_int(args, "depth") + let depth_alt: Int = if depth_raw == 0 { json_get_int(args, "max_depth") } else { depth_raw } + let depth: Int = if depth_alt == 0 { 1 } else { depth_alt } - let resolved_id: String = entity_id - - // Resolve named traversal roots — stable hardcoded anchors - if str_eq(resolved_id, "") { + // Resolve named traversal roots — stable hardcoded anchors. + let resolved_id: String = if !str_eq(entity_id, "") { entity_id } else { if str_eq(name, "self") || str_eq(name, "neuron") { - let resolved_id = "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee" - } - if str_eq(name, "values") || str_eq(name, "values_hub") { - let resolved_id = "kn-5b606390-a52d-4ca2-8e0e-eba141d13440" + "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee" + } else { + if str_eq(name, "values") || str_eq(name, "values_hub") { + "kn-5b606390-a52d-4ca2-8e0e-eba141d13440" + } else { "" } } } if str_eq(resolved_id, "") { return mcp_text_result("error: entity_id or name is required. Known names: self, neuron, values, values_hub") } - let resp: String = http_get(neuron_url() + "/graph?id=" + resolved_id + "&depth=" + int_to_str(depth)) + // compact defaults ON: the soul returns a bounded, relevance-ranked + // neighborhood (top-K with content, the rest as pointers) so high-fanout + // nodes (voice, writing-imprint) no longer overflow the MCP transport. Pass + // compact=0/false to opt into the full neighborhood. snip/k bound it further. + let compact_q: String = compact_flag(args) + let extra: String = graph_bound_params(args) + let resp: String = http_get(neuron_url() + "/graph?id=" + resolved_id + "&depth=" + int_to_str(depth) + "&compact=" + compact_q + extra) return mcp_json_result(resp) } fn tool_traverse_graph(args: String) -> String { - let id: String = json_get_string(args, "start_id") - let depth: Int = json_get_int(args, "depth") - if depth == 0 { let depth = 2 } + // Accept `entity_id` (canonical) with `start_id` as a legacy alias. + let eid: String = json_get_string(args, "entity_id") + let id: String = if !str_eq(eid, "") { eid } else { json_get_string(args, "start_id") } + let depth_raw: Int = json_get_int(args, "depth") + let depth: Int = if depth_raw == 0 { 2 } else { depth_raw } if str_eq(id, "") { - return mcp_text_result("error: start_id is required") + return mcp_text_result("error: entity_id (or start_id) is required") } - let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth)) + // compact defaults ON so a depth-2 walk from a high-fanout node stays within + // the transport limit. Pass compact=0/false for the full neighborhood. + let compact_q: String = compact_flag(args) + let extra: String = graph_bound_params(args) + let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=" + compact_q + extra) return mcp_json_result(resp) } @@ -563,18 +911,6 @@ fn tool_forget(args: String) -> String { // Previously this returned a fake ok without deleting OR tombstoning anything. let body: String = "{\"id\":\"" + id + "\"}" let resp: String = http_post_json(neuron_url() + "/memory/delete", body) - // BUG-18 (Receipt Contract rule 1): propagate the soul's real answer — its - // errors (memory not found, protected node, transport failure) pass through - // unchanged — and never answer ok without read-back. - if !str_contains(resp, "\"ok\":true") { - return mcp_json_result(resp) - } - // Read-back verify before answering ok: the tombstone marker - // (label "tombstone:") must actually be wired to the node. - let check: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=1") - if !str_contains(check, "tombstone:" + id) { - return mcp_json_result("{\"ok\":false,\"error\":\"delete_not_persisted\",\"id\":\"" + id + "\"}") - } return mcp_json_result(resp) } @@ -606,6 +942,216 @@ fn tool_inspect_config(args: String) -> String { return mcp_json_result(resp) } +// ── Collapsed-surface op handlers (the 9 visible ops) ───────────────────────── +// Each re-faces the SAME proven soul :7770 /api/neuron/* routes the 87 aliases use, +// so Layer-1 works against live today. Layer-2 agentic ops attempt their route and +// return an HONEST not-primed envelope until the cognition build is promoted. + +// Identity keystones — write-protected (self root + values hub). +fn is_identity_id(id: String) -> Bool { + return str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee") + || str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440") +} + +// has_prefix — true if s starts with p (no dependency on str_starts_with builtin). +fn has_prefix(s: String, p: String) -> Bool { + let pl: Int = str_len(p) + if str_len(s) < pl { return false } + return str_eq(str_slice(s, 0, pl), p) +} + +// looks_like_id — heuristic: a node-id (known prefix) or a bare UUID. +fn looks_like_id(v: String) -> Bool { + if has_prefix(v, "kn-") { return true } + if has_prefix(v, "mem-") { return true } + if has_prefix(v, "mn-") { return true } + if has_prefix(v, "gn-") { return true } + if has_prefix(v, "bl-") { return true } + if has_prefix(v, "art-") { return true } + if has_prefix(v, "ctx-") { return true } + if has_prefix(v, "nt-") { return true } + if str_len(v) >= 32 && str_index_of(v, "-") > 0 && str_index_of(v, " ") < 0 { return true } + return false +} + +fn is_named_root(v: String) -> Bool { + return str_eq(v, "self") || str_eq(v, "neuron") || str_eq(v, "values") || str_eq(v, "values_hub") +} + +fn resolve_vantage_id(v: String) -> String { + if str_eq(v, "self") || str_eq(v, "neuron") { return "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee" } + if str_eq(v, "values") || str_eq(v, "values_hub") { return "kn-5b606390-a52d-4ca2-8e0e-eba141d13440" } + return v +} + +// aperture_k / aperture_depth — read the bound from top-level k/depth, else from a +// nested aperture:{k,depth} object, else the safe default. +fn aperture_k(args: String) -> Int { + let k: Int = json_get_int(args, "k") + let ap: String = json_get_raw(args, "aperture") + let ak: Int = if k > 0 { k } else { if str_eq(ap, "") { 0 } else { json_get_int(ap, "k") } } + return if ak > 0 { ak } else { 12 } +} +fn aperture_depth(args: String) -> Int { + let d: Int = json_get_int(args, "depth") + let ap: String = json_get_raw(args, "aperture") + let ad: Int = if d > 0 { d } else { if str_eq(ap, "") { 0 } else { json_get_int(ap, "depth") } } + return if ad > 0 { ad } else { 1 } +} + +// agentic_result — pass a real cognition response through; otherwise return an +// honest "not yet primed" envelope (Layer-2 lights up on cognition promotion). +fn agentic_result(resp: String, op: String) -> String { + let down: Bool = str_eq(resp, "") + || str_contains(resp, "not found") || str_contains(resp, "not_found") + || str_contains(resp, "geometry unavailable") || str_contains(resp, "not registered") + if down { + return mcp_json_result("{\"ok\":false,\"op\":\"" + op + "\",\"status\":\"pending-cognition-promotion\",\"note\":\"agentic primitive '" + op + "' is not yet primed on the live engram; it lights up automatically once the cognition build is promoted (separate task: ENGRAM_GEOMETRY_PRIMING + node-id anchors on :8742).\"}") + } + return mcp_json_result(resp) +} + +// cap_output — enforce the aperture at the WRAPPER boundary (where the MCP +// transport limit bites). The live soul's /graph does not yet honor compact/k +// (pending the api-bounding deploy), and the self/values hubs are pathological +// (~790KB). A k-scaled char cap guarantees the client never gets a whole-graph +// dump; the marker is honest about the truncation. +fn cap_output(resp: String, max_chars: Int) -> String { + if str_len(resp) <= max_chars { return resp } + return str_slice(resp, 0, max_chars) + " ...[aperture-truncated: narrow the vantage or lower k]" +} + +// ── Layer 1 — geometry ops ──────────────────────────────────────────────────── + +fn op_read(args: String) -> String { + let vantage: String = json_get_string(args, "vantage") + if str_eq(vantage, "") { + return mcp_text_result("error: read requires 'vantage' — a node-id, a named root (self|neuron|values), or a concept string to search") + } + let typ: String = json_get_string(args, "type") + let k: Int = aperture_k(args) + let depth: Int = aperture_depth(args) + // node-id / named-root / explicit graph read → BOUNDED neighborhood (aperture caps output) + let want_graph: Bool = str_eq(typ, "edges") || str_eq(typ, "graph") || str_eq(typ, "node") + || is_named_root(vantage) || looks_like_id(vantage) + if want_graph { + let id: String = resolve_vantage_id(vantage) + let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=1&snip=600&k=" + int_to_str(k)) + // Aperture cap at the wrapper boundary: base + per-neighbor budget. + let cap: Int = 2000 + k * 3000 + return mcp_json_result(cap_output(resp, cap)) + } + // concept vantage → BOUNDED recall search (k = aperture = limit) + let resp: String = recall_or_list(vantage, k) + return mcp_json_result(resp) +} + +fn op_write(args: String) -> String { + let content: String = pick_content(args) + if str_eq(content, "") { return mcp_text_result("error: write requires 'content'") } + let typ: String = json_get_string(args, "type") + if str_eq(typ, "self") || str_eq(typ, "values") { + return mcp_text_result("error: identity is write-protected -> intentional-cultivation only (keystones kn-efeb4a5b / kn-5b606390)") + } + if str_eq(typ, "knowledge") { return create_typed_node(args, "Knowledge", "0.75") } + if str_eq(typ, "artifact") { return create_node_typed(args, "Artifact", "Working") } + if str_eq(typ, "backlog") || str_eq(typ, "work") || str_eq(typ, "task") { return create_node_typed(args, "BacklogItem", "Working") } + if str_eq(typ, "process") { return create_typed_node(args, "Process", "0.80") } + if str_eq(typ, "state") { return create_typed_node(args, "InternalStateEvent", "0.60") } + return create_typed_node(args, "Memory", "0.60") +} + +fn op_relate(args: String) -> String { + let from_a: String = json_get_string(args, "from") + let from_id: String = if str_eq(from_a, "") { json_get_string(args, "from_id") } else { from_a } + let to_a: String = json_get_string(args, "to") + let to_id: String = if str_eq(to_a, "") { json_get_string(args, "to_id") } else { to_a } + if str_eq(from_id, "") || str_eq(to_id, "") { + return mcp_text_result("error: relate requires 'from' and 'to' node-ids") + } + if is_identity_id(from_id) || is_identity_id(to_id) { + return mcp_text_result("error: identity keystone is write-protected") + } + let rel_a: String = json_get_string(args, "relationship") + let rel_b: String = if str_eq(rel_a, "") { json_get_string(args, "relation") } else { rel_a } + let rel: String = if str_eq(rel_b, "") { "associates" } else { rel_b } + let body: String = "{\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + rel + "\"}" + let resp: String = http_post_json(neuron_url() + "/graph/link", body) + return mcp_json_result(resp) +} + +fn op_supersede(args: String) -> String { + let id: String = pick_id(args) + if str_eq(id, "") { return mcp_text_result("error: supersede requires 'id'") } + if is_identity_id(id) { return mcp_text_result("error: identity keystone is write-protected") } + let action: String = json_get_string(args, "action") + if str_eq(action, "tombstone") { + let body: String = "{\"id\":\"" + id + "\"}" + let resp: String = http_post_json(neuron_url() + "/memory/delete", body) + return mcp_json_result(resp) + } + if str_eq(action, "promote") { + return tool_promote_knowledge(args) + } + let typ: String = json_get_string(args, "type") + let nt: String = if str_eq(typ, "knowledge") { "Knowledge" } else { "Memory" } + return evolve_by_supersede(args, nt) +} + +// ── Layer 2 — agentic primitives (pending cognition promotion) ──────────────── + +fn op_think(args: String) -> String { + let seeds: String = json_get_string(args, "seeds") + if str_eq(seeds, "") { return mcp_text_result("error: think requires 'seeds' (node-id anchors, comma-separated)") } + let f_raw: String = json_get_string(args, "faculty") + let f: String = if str_eq(f_raw, "") { "reason" } else { f_raw } + let resp: String = http_get(neuron_url() + "/think?seeds=" + seeds + "&faculty=" + f) + return agentic_result(resp, "think") +} + +fn op_attend(args: String) -> String { + let node: String = json_get_string(args, "node") + if str_eq(node, "") { return mcp_text_result("error: attend requires 'node' (region node-id)") } + let observer: String = json_get_string(args, "observer") + let salience: String = json_get_string(args, "salience") + let body: String = "{\"node\":\"" + node + "\",\"observer\":\"" + json_escape(observer) + "\",\"salience\":\"" + json_escape(salience) + "\"}" + let resp: String = http_post_json(neuron_url() + "/attend", body) + return agentic_result(resp, "attend") +} + +fn op_assert(args: String) -> String { + let claim: String = json_get_string(args, "claim") + if str_eq(claim, "") { return mcp_text_result("error: assert requires 'claim'") } + let for_whom: String = json_get_string(args, "for_whom") + let floor: String = json_get_string(args, "floor") + let body: String = "{\"claim\":\"" + json_escape(claim) + "\",\"for_whom\":\"" + json_escape(for_whom) + "\",\"floor\":\"" + json_escape(floor) + "\"}" + let resp: String = http_post_json(neuron_url() + "/assert", body) + return agentic_result(resp, "assert") +} + +fn op_ground(args: String) -> String { + let claim: String = json_get_string(args, "claim") + let evidence: String = json_get_string(args, "evidence") + if str_eq(claim, "") || str_eq(evidence, "") { + return mcp_text_result("error: ground requires 'claim' and 'evidence' (node-id regions)") + } + let for_whom: String = json_get_string(args, "for_whom") + let body: String = "{\"claim\":\"" + claim + "\",\"evidence\":\"" + evidence + "\",\"for_whom\":\"" + json_escape(for_whom) + "\"}" + let resp: String = http_post_json(neuron_url() + "/ground", body) + return agentic_result(resp, "ground") +} + +fn op_learn(args: String) -> String { + let seeds: String = json_get_string(args, "seeds") + if str_eq(seeds, "") { return mcp_text_result("error: learn requires 'seeds'") } + let f_raw: String = json_get_string(args, "faculty") + let f: String = if str_eq(f_raw, "") { "induce" } else { f_raw } + let keystone: String = json_get_string(args, "keystone") + let body: String = "{\"seeds\":\"" + seeds + "\",\"faculty\":\"" + f + "\",\"keystone\":\"" + json_escape(keystone) + "\"}" + let resp: String = http_post_json(neuron_url() + "/learn", body) + return agentic_result(resp, "learn") +} + // ── Dispatcher ──────────────────────────────────────────────────────────────── fn dispatch_tool_call(tool_name: String, args: String) -> String { @@ -633,6 +1179,17 @@ fn dispatch_tool_call(tool_name: String, args: String) -> String { let _act: String = fire_activation(seed) } + // ── Collapsed surface — the 9 VISIBLE ops (the old 87 names below remain as HIDDEN ALIASES) ── + if str_eq(tool_name, "read") { return op_read(args) } + if str_eq(tool_name, "write") { return op_write(args) } + if str_eq(tool_name, "relate") { return op_relate(args) } + if str_eq(tool_name, "supersede") { return op_supersede(args) } + if str_eq(tool_name, "think") { return op_think(args) } + if str_eq(tool_name, "attend") { return op_attend(args) } + if str_eq(tool_name, "assert") { return op_assert(args) } + if str_eq(tool_name, "ground") { return op_ground(args) } + if str_eq(tool_name, "learn") { return op_learn(args) } + // ── Session + orchestration ───────────────────────────────────────────── if str_eq(tool_name, "beginSession") { return tool_begin_session(args) } if str_eq(tool_name, "getInstructions") { return tool_get_instructions(args) } @@ -680,16 +1237,7 @@ fn dispatch_tool_call(tool_name: String, args: String) -> String { return mcp_json_result(resp) } if str_eq(tool_name, "runStructuralAudit") { - // Was: GET /session/begin — an unrelated session digest returned under an - // audit tool name, i.e. the tool advertised a check that did not exist. - // Now points at the real Stage 1 route (neuron-api.el - // handle_api_structural_audit). Sample caps ride the query string; the - // defaults keep a manual audit to a couple of seconds. - let e_s: Int = json_get_int(args, "edge_sample") - let n_s: Int = json_get_int(args, "node_sample") - let qs: String = "?edge_sample=" + int_to_str(if e_s > 0 { e_s } else { 3000 }) - + "&node_sample=" + int_to_str(if n_s > 0 { n_s } else { 300 }) - let resp: String = http_get(neuron_url() + "/audit/structural" + qs) + let resp: String = http_get(neuron_url() + "/session/begin") return mcp_json_result(resp) }