add axon-events: typed event schema for the full neuron-technologies stack

This commit is contained in:
2026-04-27 20:01:39 -05:00
parent 745278c902
commit 7a81bb11a7
17 changed files with 2115 additions and 36 deletions
+1
View File
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
neuron-domain = { path = "../neuron-domain" }
neuron-store = { path = "../neuron-store" }
axon-events = { path = "../axon-events" }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
+186 -11
View File
@@ -1,25 +1,118 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// An Axon protocol message — wraps a tool call or other method invocation.
// ------------------------------------------------------------------
// AxonMethod
// ------------------------------------------------------------------
/// Typed method discriminant for Axon protocol messages.
///
/// Replaces the raw `method: String` field — callers that need a string
/// representation can use [`AxonMethod::as_str`] or the `Display` impl.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AxonMethod {
/// Invoke a named tool.
ToolCall,
/// Return value from a tool invocation.
ToolResult,
/// Deliver a typed Axon event envelope.
Event,
/// Liveness check.
Ping,
/// Liveness reply.
Pong,
/// Subscribe to an event stream.
Subscribe,
/// Cancel a subscription.
Unsubscribe,
}
impl AxonMethod {
/// Return the canonical lowercase string for this method.
pub fn as_str(self) -> &'static str {
match self {
Self::ToolCall => "tool_call",
Self::ToolResult => "tool_result",
Self::Event => "event",
Self::Ping => "ping",
Self::Pong => "pong",
Self::Subscribe => "subscribe",
Self::Unsubscribe => "unsubscribe",
}
}
/// Parse a string into an `AxonMethod`, returning `None` on mismatch.
pub fn from_str(s: &str) -> Option<Self> {
match s {
"tool_call" => Some(Self::ToolCall),
"tool_result" => Some(Self::ToolResult),
"event" => Some(Self::Event),
"ping" => Some(Self::Ping),
"pong" => Some(Self::Pong),
"subscribe" => Some(Self::Subscribe),
"unsubscribe" => Some(Self::Unsubscribe),
_ => None,
}
}
}
impl std::fmt::Display for AxonMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
// ------------------------------------------------------------------
// AxonMessage
// ------------------------------------------------------------------
/// An Axon protocol message — wraps a tool call, event delivery, or
/// other method invocation.
///
/// The `method` field is now typed as [`AxonMethod`]. For backward
/// compatibility the raw string representation is available via
/// [`AxonMessage::method_str`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AxonMessage {
/// Unique message ID
/// Unique message ID.
pub id: String,
/// Method name: "tool_call", "tool_result", "ping", etc.
pub method: String,
/// Method-specific parameters
/// Typed method discriminant.
pub method: AxonMethod,
/// Method-specific parameters.
pub params: Value,
}
impl AxonMessage {
/// Return the canonical string representation of the method (e.g.
/// `"tool_call"`, `"ping"`). Useful when serialising to a transport
/// that expects a raw string rather than a tagged enum.
pub fn method_str(&self) -> &'static str {
self.method.as_str()
}
/// Construct a ping message.
pub fn ping(id: impl Into<String>) -> Self {
Self {
id: id.into(),
method: AxonMethod::Ping,
params: Value::Object(Default::default()),
}
}
}
// ------------------------------------------------------------------
// AxonResponse
// ------------------------------------------------------------------
/// An Axon protocol response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AxonResponse {
/// Echo of the request ID
/// Echo of the request ID.
pub id: String,
/// Whether the call succeeded
/// Whether the call succeeded.
pub success: bool,
/// The result payload (on success) or error message (on failure)
/// The result payload (on success) or error message (on failure).
pub result: Value,
}
@@ -41,6 +134,10 @@ impl AxonResponse {
}
}
// ------------------------------------------------------------------
// AxonEvent (legacy SSE event — kept for backward compat)
// ------------------------------------------------------------------
/// An Axon SSE event — sent over the streaming channel.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AxonEvent {
@@ -59,6 +156,10 @@ impl AxonEvent {
}
}
// ------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
@@ -78,14 +179,88 @@ mod tests {
}
#[test]
fn axon_message_roundtrip() {
fn axon_message_roundtrip_with_method_enum() {
let msg = AxonMessage {
id: "m1".into(),
method: "tool_call".into(),
method: AxonMethod::ToolCall,
params: serde_json::json!({ "tool": "remember", "content": "hello" }),
};
let json = serde_json::to_string(&msg).unwrap();
let back: AxonMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back.method, "tool_call");
assert_eq!(back.method, AxonMethod::ToolCall);
assert_eq!(back.method_str(), "tool_call");
}
#[test]
fn axon_method_as_str_all_variants() {
let cases = [
(AxonMethod::ToolCall, "tool_call"),
(AxonMethod::ToolResult, "tool_result"),
(AxonMethod::Event, "event"),
(AxonMethod::Ping, "ping"),
(AxonMethod::Pong, "pong"),
(AxonMethod::Subscribe, "subscribe"),
(AxonMethod::Unsubscribe, "unsubscribe"),
];
for (method, expected) in cases {
assert_eq!(method.as_str(), expected);
assert_eq!(method.to_string(), expected);
}
}
#[test]
fn axon_method_from_str_roundtrip() {
let variants = [
AxonMethod::ToolCall,
AxonMethod::ToolResult,
AxonMethod::Event,
AxonMethod::Ping,
AxonMethod::Pong,
AxonMethod::Subscribe,
AxonMethod::Unsubscribe,
];
for v in variants {
let s = v.as_str();
assert_eq!(AxonMethod::from_str(s), Some(v));
}
}
#[test]
fn axon_method_from_str_unknown() {
assert_eq!(AxonMethod::from_str("mystery"), None);
assert_eq!(AxonMethod::from_str(""), None);
}
#[test]
fn axon_method_serde_roundtrip() {
let m = AxonMethod::Subscribe;
let json = serde_json::to_string(&m).unwrap();
let back: AxonMethod = serde_json::from_str(&json).unwrap();
assert_eq!(back, AxonMethod::Subscribe);
}
#[test]
fn axon_message_ping_helper() {
let msg = AxonMessage::ping("p-1");
assert_eq!(msg.method, AxonMethod::Ping);
assert_eq!(msg.id, "p-1");
}
#[test]
fn axon_method_all_variants_have_unique_str() {
use std::collections::HashSet;
let strs: HashSet<&str> = [
AxonMethod::ToolCall,
AxonMethod::ToolResult,
AxonMethod::Event,
AxonMethod::Ping,
AxonMethod::Pong,
AxonMethod::Subscribe,
AxonMethod::Unsubscribe,
]
.iter()
.map(|m| m.as_str())
.collect();
assert_eq!(strs.len(), 7);
}
}
+14 -10
View File
@@ -1,4 +1,4 @@
use crate::axon::{AxonMessage, AxonResponse};
use crate::axon::{AxonMessage, AxonMethod, AxonResponse};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
@@ -30,8 +30,8 @@ impl AxonHandler {
/// Dispatch an AxonMessage to the appropriate handler.
pub fn dispatch(&self, msg: AxonMessage) -> AxonResponse {
match msg.method.as_str() {
"tool_call" => {
match msg.method {
AxonMethod::ToolCall => {
let tool_name = msg
.params
.get("tool")
@@ -50,8 +50,12 @@ impl AxonHandler {
AxonResponse::err(msg.id, format!("unknown tool: {}", tool_name))
}
}
"ping" => AxonResponse::ok(msg.id, serde_json::json!({ "pong": true })),
other => AxonResponse::err(msg.id, format!("unknown method: {}", other)),
AxonMethod::Ping => {
AxonResponse::ok(msg.id, serde_json::json!({ "pong": true }))
}
other => {
AxonResponse::err(msg.id, format!("unknown method: {}", other.as_str()))
}
}
}
@@ -78,7 +82,7 @@ mod tests {
let handler = AxonHandler::new();
let msg = AxonMessage {
id: "1".into(),
method: "ping".into(),
method: AxonMethod::Ping,
params: serde_json::json!({}),
};
let resp = handler.dispatch(msg);
@@ -95,7 +99,7 @@ mod tests {
let msg = AxonMessage {
id: "2".into(),
method: "tool_call".into(),
method: AxonMethod::ToolCall,
params: serde_json::json!({ "tool": "remember", "params": { "content": "hello" } }),
};
let resp = handler.dispatch(msg);
@@ -108,7 +112,7 @@ mod tests {
let handler = AxonHandler::new();
let msg = AxonMessage {
id: "3".into(),
method: "tool_call".into(),
method: AxonMethod::ToolCall,
params: serde_json::json!({ "tool": "nonexistent", "params": {} }),
};
let resp = handler.dispatch(msg);
@@ -116,11 +120,11 @@ mod tests {
}
#[test]
fn dispatch_unknown_method() {
fn dispatch_unhandled_method() {
let handler = AxonHandler::new();
let msg = AxonMessage {
id: "4".into(),
method: "mystery".into(),
method: AxonMethod::Subscribe,
params: serde_json::json!({}),
};
let resp = handler.dispatch(msg);
+2 -1
View File
@@ -3,5 +3,6 @@ pub mod handler;
pub mod sse;
pub mod tools;
pub use axon::{AxonMessage, AxonResponse};
pub use axon::{AxonEvent, AxonMessage, AxonMethod, AxonResponse};
pub use handler::AxonHandler;
pub use tools::ProjectType;
+182 -14
View File
@@ -1,45 +1,165 @@
/// Tool name constants — must match the existing MCP tool names so Claude Code
/// Tool name constants — must match the existing MCP tool names so services
/// can switch over without configuration changes.
// ------------------------------------------------------------------
// Memory tools
// ------------------------------------------------------------------
pub const REMEMBER: &str = "remember";
pub const RECALL: &str = "recall";
pub const SEARCH_ENTITIES: &str = "search_entities";
pub const INSPECT_MEMORIES: &str = "inspect_memories";
pub const EVOLVE_MEMORY: &str = "evolve_memory";
pub const FORGET: &str = "forget";
pub const PIN_NODE: &str = "pin_node";
pub const INSPECT_GRAPH: &str = "inspect_graph";
pub const SEARCH_GRAPH: &str = "search_graph";
pub const TRAVERSE_GRAPH: &str = "traverse_graph";
pub const REBUILD_GRAPH: &str = "rebuild_graph";
pub const LINK_ENTITIES: &str = "link_entities";
pub const LINK_CAUSAL: &str = "link_causal";
// ------------------------------------------------------------------
// Knowledge tools
// ------------------------------------------------------------------
pub const CAPTURE_KNOWLEDGE: &str = "capture_knowledge";
pub const SEARCH_KNOWLEDGE: &str = "search_knowledge";
pub const RETRIEVE_KNOWLEDGE: &str = "retrieve_knowledge";
pub const BROWSE_KNOWLEDGE: &str = "browse_knowledge";
pub const EVOLVE_KNOWLEDGE: &str = "evolve_knowledge";
pub const REMOVE_KNOWLEDGE: &str = "remove_knowledge";
// ------------------------------------------------------------------
// Backlog / work tools
// ------------------------------------------------------------------
pub const PLAN_WORK: &str = "plan_work";
pub const REVIEW_BACKLOG: &str = "review_backlog";
pub const TRACK_WORK: &str = "track_work";
pub const BEGIN_WORK: &str = "begin_work";
pub const PROGRESS_WORK: &str = "progress_work";
pub const CHECK_WORK: &str = "check_work";
pub const LIST_WORK: &str = "list_work";
// ------------------------------------------------------------------
// Session / context tools
// ------------------------------------------------------------------
pub const BEGIN_SESSION: &str = "begin_session";
pub const COMPILE_CTX: &str = "compile_ctx";
pub const CONSOLIDATE: &str = "consolidate";
pub const PROJECT_CONTEXT: &str = "project_context";
// ------------------------------------------------------------------
// Artifact tools
// ------------------------------------------------------------------
pub const DRAFT_ARTIFACT: &str = "draft_artifact";
pub const FIND_ARTIFACTS: &str = "find_artifacts";
pub const RETRIEVE_ARTIFACT: &str = "retrieve_artifact";
pub const REVISE_ARTIFACT: &str = "revise_artifact";
pub const MANAGE_ARTIFACT: &str = "manage_artifact";
// ------------------------------------------------------------------
// Process tools
// ------------------------------------------------------------------
pub const BROWSE_PROCESSES: &str = "browse_processes";
pub const DEFINE_PROCESS: &str = "define_process";
pub const RETRIEVE_PROCESS: &str = "retrieve_process";
pub const EXECUTE_PROCESS: &str = "execute_process";
pub const EXPORT_PROCESS: &str = "export_process";
pub const DELETE_PROCESS: &str = "delete_process";
pub const LIST_PROCESSES: &str = "list_processes";
pub const CATALOG_ROUTES: &str = "catalog_routes";
pub const REGISTER_ROUTE: &str = "register_route";
// ------------------------------------------------------------------
// Event / ISE tools
// ------------------------------------------------------------------
pub const LOG_INTERNAL_STATE_EVENT: &str = "log_internal_state_event";
pub const LIST_INTERNAL_STATE_EVENTS: &str = "list_internal_state_events";
pub const GET_INTERNAL_STATE_EVENT: &str = "get_internal_state_event";
pub const CHECK_EVENTS: &str = "check_events";
pub const ACKNOWLEDGE_EVENT: &str = "acknowledge_event";
pub const INSPECT_EVENT: &str = "inspect_event";
pub const PROCESS_EVENTS: &str = "process_events";
pub const SEND_NOTIFICATION: &str = "send_notification";
/// All registered tool names in canonical order.
// ------------------------------------------------------------------
// Config tools
// ------------------------------------------------------------------
pub const INSPECT_CONFIG: &str = "inspect_config";
pub const TUNE_CONFIG: &str = "tune_config";
pub const GET_INSTRUCTIONS: &str = "get_instructions";
// ------------------------------------------------------------------
// All tools in canonical alphabetical order
// ------------------------------------------------------------------
pub const ALL_TOOLS: &[&str] = &[
REMEMBER,
RECALL,
SEARCH_ENTITIES,
CAPTURE_KNOWLEDGE,
SEARCH_KNOWLEDGE,
RETRIEVE_KNOWLEDGE,
PLAN_WORK,
REVIEW_BACKLOG,
TRACK_WORK,
ACKNOWLEDGE_EVENT,
BEGIN_SESSION,
BEGIN_WORK,
PROGRESS_WORK,
BROWSE_KNOWLEDGE,
BROWSE_PROCESSES,
CAPTURE_KNOWLEDGE,
CATALOG_ROUTES,
CHECK_EVENTS,
CHECK_WORK,
LOG_INTERNAL_STATE_EVENT,
COMPILE_CTX,
CONSOLIDATE,
DEFINE_PROCESS,
DELETE_PROCESS,
DRAFT_ARTIFACT,
EVOLVE_KNOWLEDGE,
EVOLVE_MEMORY,
EXECUTE_PROCESS,
EXPORT_PROCESS,
FIND_ARTIFACTS,
FORGET,
GET_INSTRUCTIONS,
GET_INTERNAL_STATE_EVENT,
INSPECT_CONFIG,
INSPECT_EVENT,
INSPECT_GRAPH,
INSPECT_MEMORIES,
LINK_CAUSAL,
LINK_ENTITIES,
LIST_INTERNAL_STATE_EVENTS,
LIST_PROCESSES,
LIST_WORK,
LOG_INTERNAL_STATE_EVENT,
MANAGE_ARTIFACT,
PIN_NODE,
PLAN_WORK,
PROCESS_EVENTS,
PROGRESS_WORK,
PROJECT_CONTEXT,
REBUILD_GRAPH,
RECALL,
REGISTER_ROUTE,
REMEMBER,
REMOVE_KNOWLEDGE,
RETRIEVE_ARTIFACT,
RETRIEVE_KNOWLEDGE,
RETRIEVE_PROCESS,
REVIEW_BACKLOG,
REVISE_ARTIFACT,
SEARCH_ENTITIES,
SEARCH_GRAPH,
SEARCH_KNOWLEDGE,
SEND_NOTIFICATION,
TRACK_WORK,
TRAVERSE_GRAPH,
TUNE_CONFIG,
];
// Re-export ProjectType from axon-events for convenient protocol-level access.
pub use axon_events::ProjectType;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_tools_count() {
assert_eq!(ALL_TOOLS.len(), 14);
// Ensure the list has grown to cover the full Neuron MCP surface.
assert!(ALL_TOOLS.len() >= 50, "expected 50+ tools, got {}", ALL_TOOLS.len());
}
#[test]
@@ -48,4 +168,52 @@ mod tests {
assert!(!name.is_empty(), "tool name must not be empty");
}
}
#[test]
fn all_tools_are_unique() {
use std::collections::HashSet;
let set: HashSet<&&str> = ALL_TOOLS.iter().collect();
assert_eq!(set.len(), ALL_TOOLS.len(), "duplicate tool name detected");
}
#[test]
fn all_tools_are_sorted() {
let mut sorted = ALL_TOOLS.to_vec();
sorted.sort_unstable();
assert_eq!(ALL_TOOLS, sorted.as_slice(), "ALL_TOOLS must be in alphabetical order");
}
#[test]
fn original_14_tools_still_present() {
let original = [
REMEMBER,
RECALL,
SEARCH_ENTITIES,
CAPTURE_KNOWLEDGE,
SEARCH_KNOWLEDGE,
RETRIEVE_KNOWLEDGE,
PLAN_WORK,
REVIEW_BACKLOG,
TRACK_WORK,
BEGIN_WORK,
PROGRESS_WORK,
CHECK_WORK,
LOG_INTERNAL_STATE_EVENT,
LIST_INTERNAL_STATE_EVENTS,
];
for name in original {
assert!(
ALL_TOOLS.contains(&name),
"original tool '{}' missing from ALL_TOOLS",
name
);
}
}
#[test]
fn project_type_importable_from_tools() {
// Ensure the re-export compiles and is usable.
let _ = ProjectType::NeuronRs;
let _ = ProjectType::EngramLang;
}
}