From 28520f86804313aeadbbb99c93ac496d1e0dd862 Mon Sep 17 00:00:00 2001 From: srimon12 Date: Mon, 29 Jun 2026 12:24:28 +0530 Subject: [PATCH 1/5] refactor(events): simplify events serialization and deserialization using Serde --- microagents-events/src/lib.rs | 474 +++++----------------------------- 1 file changed, 71 insertions(+), 403 deletions(-) diff --git a/microagents-events/src/lib.rs b/microagents-events/src/lib.rs index c1474b3..cbf3835 100644 --- a/microagents-events/src/lib.rs +++ b/microagents-events/src/lib.rs @@ -183,22 +183,22 @@ pub struct TaskEvent { pub timestamp: DateTime, } +fn serialize_to_jsonrpc(method: &str, params_struct: &T) -> JsonRpcNotification { + let params_value = serde_json::to_value(params_struct).expect("Event params serialization failed"); + let params = match params_value { + Value::Object(m) => m, + other => panic!("Serialized event params must be a JSON Object, found: {:?}", other), + }; + JsonRpcNotification { + jsonrpc: "2.0".to_string(), + method: method.to_string(), + params, + } +} + impl AgentEvent for SessionInitEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("session.init".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("system".into(), Value::from(self.system.clone())) - .add_param("model".into(), Value::from(self.model.clone())) - .add_param("provider".into(), Value::from(self.provider.clone())) - .add_param( - "init_type".into(), - serde_json::to_value(self.init_type.clone()).unwrap(), - ) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("session.init", self) } fn session_id(&self) -> String { @@ -208,21 +208,7 @@ impl AgentEvent for SessionInitEvent { impl AgentEvent for SessionStopEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("session.stop".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("success".into(), Value::from(self.success)) - .add_param("result".into(), Value::from(self.result.clone())) - .add_param("error".into(), Value::from(self.error.clone())) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) - .add_param( - "incomplete_tasks".into(), - Value::from(self.incomplete_tasks.clone()), - ) - .add_param("usage".into(), serde_json::to_value(self.usage).unwrap()) + serialize_to_jsonrpc("session.stop", self) } fn session_id(&self) -> String { @@ -232,15 +218,7 @@ impl AgentEvent for SessionStopEvent { impl AgentEvent for UserPromptSubmitEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("user.prompt.submit".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("prompt".into(), Value::from(self.prompt.clone())) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("user.prompt.submit", self) } fn session_id(&self) -> String { @@ -250,19 +228,7 @@ impl AgentEvent for UserPromptSubmitEvent { impl AgentEvent for StreamDeltaEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("stream.delta".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("delta".into(), Value::from(self.delta.clone())) - .add_param( - "delta_type".into(), - serde_json::to_value(self.delta_type.clone()).unwrap(), - ) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("stream.delta", self) } fn session_id(&self) -> String { @@ -272,16 +238,7 @@ impl AgentEvent for StreamDeltaEvent { impl AgentEvent for ToolCallEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("tool.call".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("name".into(), Value::from(self.name.clone())) - .add_param("input".into(), self.input.clone()) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("tool.call", self) } fn session_id(&self) -> String { @@ -291,19 +248,7 @@ impl AgentEvent for ToolCallEvent { impl AgentEvent for ToolResultEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("tool.result".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("result".into(), serde_json::to_value(&self.result).unwrap()) - .add_param( - "tool_call_id".into(), - Value::from(self.tool_call_id.clone()), - ) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("tool.result", self) } fn session_id(&self) -> String { @@ -313,15 +258,7 @@ impl AgentEvent for ToolResultEvent { impl AgentEvent for SkillLoadEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("skill.load".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("skill_name".into(), Value::from(self.skill_name.clone())) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("skill.load", self) } fn session_id(&self) -> String { @@ -331,23 +268,7 @@ impl AgentEvent for SkillLoadEvent { impl AgentEvent for AssistantResponseEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("assistant.response".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("full_text".into(), Value::from(self.full_text.clone())) - .add_param( - "tool_calls".into(), - Value::from(self.tool_calls.clone().map(|tcs| { - tcs.iter() - .map(|tc| serde_json::to_value(tc).unwrap()) - .collect::>() - })), - ) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("assistant.response", self) } fn session_id(&self) -> String { @@ -357,19 +278,7 @@ impl AgentEvent for AssistantResponseEvent { impl AgentEvent for TaskEvent { fn to_jsonrpc(&self) -> JsonRpcNotification { - JsonRpcNotification::builder() - .method("assistant.task".into()) - .add_param("session_id".into(), Value::from(self.session_id.clone())) - .add_param("turn_id".into(), Value::from(self.turn_id.clone())) - .add_param("task_name".into(), Value::from(self.task_name.clone())) - .add_param( - "task_status".to_string(), - serde_json::to_value(self.task_status).unwrap(), - ) - .add_param( - "timestamp".into(), - serde_json::to_value(self.timestamp).unwrap(), - ) + serialize_to_jsonrpc("assistant.task", self) } fn session_id(&self) -> String { @@ -378,17 +287,27 @@ impl AgentEvent for TaskEvent { } /// A sum type wrapping any agent event. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "method", content = "params", rename_all = "snake_case")] #[non_exhaustive] pub enum AgentEventAny { + #[serde(rename = "session.init")] SessionInit(SessionInitEvent), + #[serde(rename = "session.stop")] SessionStop(SessionStopEvent), + #[serde(rename = "stream.delta")] StreamDelta(StreamDeltaEvent), + #[serde(rename = "tool.call")] ToolCall(ToolCallEvent), + #[serde(rename = "tool.result")] ToolResult(ToolResultEvent), + #[serde(rename = "assistant.response")] AssistantResponse(AssistantResponseEvent), + #[serde(rename = "skill.load")] SkillLoad(SkillLoadEvent), + #[serde(rename = "user.prompt.submit")] UserPromptSubmit(UserPromptSubmitEvent), + #[serde(rename = "assistant.task")] Task(TaskEvent), } @@ -406,20 +325,35 @@ impl AgentEventAny { Self::Task(s) => s.timestamp, } } + + pub fn with_session_id(mut self, sid: String) -> Self { + match &mut self { + Self::SessionInit(s) => s.session_id = sid, + Self::AssistantResponse(s) => s.session_id = sid, + Self::SessionStop(s) => s.session_id = sid, + Self::SkillLoad(s) => s.session_id = sid, + Self::StreamDelta(s) => s.session_id = sid, + Self::UserPromptSubmit(s) => s.session_id = sid, + Self::ToolCall(s) => s.session_id = sid, + Self::ToolResult(s) => s.session_id = sid, + Self::Task(s) => s.session_id = sid, + } + self + } } impl AgentEvent for AgentEventAny { fn to_jsonrpc(&self) -> JsonRpcNotification { match self { - Self::SessionInit(s) => s.to_jsonrpc(), - Self::AssistantResponse(s) => s.to_jsonrpc(), - Self::SessionStop(s) => s.to_jsonrpc(), - Self::ToolCall(s) => s.to_jsonrpc(), - Self::StreamDelta(s) => s.to_jsonrpc(), - Self::UserPromptSubmit(s) => s.to_jsonrpc(), - Self::ToolResult(s) => s.to_jsonrpc(), - Self::SkillLoad(s) => s.to_jsonrpc(), - Self::Task(s) => s.to_jsonrpc(), + Self::SessionInit(e) => e.to_jsonrpc(), + Self::SessionStop(e) => e.to_jsonrpc(), + Self::StreamDelta(e) => e.to_jsonrpc(), + Self::ToolCall(e) => e.to_jsonrpc(), + Self::ToolResult(e) => e.to_jsonrpc(), + Self::AssistantResponse(e) => e.to_jsonrpc(), + Self::SkillLoad(e) => e.to_jsonrpc(), + Self::UserPromptSubmit(e) => e.to_jsonrpc(), + Self::Task(e) => e.to_jsonrpc(), } } @@ -442,287 +376,21 @@ impl TryFrom for AgentEventAny { type Error = AgentEventError; fn try_from(value: JsonRpcNotification) -> Result { - let session_id = value - .params - .get("session_id") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("session_id".to_string()))? - .to_string(); - let turn_id = value - .params - .get("turn_id") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - match value.method.as_str() { - "session.init" => Ok(Self::SessionInit(SessionInitEvent { - session_id, - model: value - .params - .get("model") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("model".to_string()))? - .to_string(), - provider: value - .params - .get("provider") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("provider".to_string()))? - .to_string(), - system: value - .params - .get("system") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("system".to_string()))? - .to_string(), - init_type: { - let raw = value - .params - .get("init_type") - .ok_or_else(|| AgentEventError::MissingField("init_type".to_string()))?; - let init_type: SessionInitType = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("init_type".to_string()))?; - init_type - }, - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - })), - "session.stop" => Ok(Self::SessionStop(SessionStopEvent { - session_id, - success: value - .params - .get("success") - .and_then(|v| v.as_bool()) - .ok_or_else(|| AgentEventError::InvalidFieldType("success".to_string()))?, - result: value - .params - .get("result") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - error: value - .params - .get("error") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - incomplete_tasks: value - .params - .get("incomplete_tasks") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }), - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - usage: { - let raw = value - .params - .get("usage") - .ok_or_else(|| AgentEventError::MissingField("usage".to_string()))?; - let usg: Usage = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("usage".to_string()))?; - usg - }, - })), - "user.prompt.submit" => Ok(Self::UserPromptSubmit(UserPromptSubmitEvent { - session_id, - turn_id, - prompt: value - .params - .get("prompt") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("prompt".to_string()))? - .to_string(), - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - })), - "stream.delta" => Ok(Self::StreamDelta(StreamDeltaEvent { - session_id, - turn_id, - delta: value - .params - .get("delta") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("delta".to_string()))? - .to_string(), - delta_type: { - let raw = value - .params - .get("delta_type") - .ok_or_else(|| AgentEventError::MissingField("delta_type".into()))?; - let dt: DeltaType = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("delta_type".to_string()))?; - dt - }, - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - })), - "tool.call" => Ok(Self::ToolCall(ToolCallEvent { - session_id, - turn_id, - name: value - .params - .get("name") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("name".to_string()))? - .to_string(), - input: value.params.get("input").cloned().unwrap_or(Value::Null), - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - })), - "tool.result" => { - let result = value - .params - .get("result") - .ok_or_else(|| AgentEventError::MissingField("result".to_string()))?; - let tool_result: ToolResult = serde_json::from_value(result.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("result".to_string()))?; - Ok(Self::ToolResult(ToolResultEvent { - session_id, - turn_id, - result: tool_result, - tool_call_id: value - .params - .get("tool_call_id") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("tool_call_id".to_string()))? - .to_string(), - timestamp: { - let raw = value.params.get("timestamp").ok_or_else(|| { - AgentEventError::MissingField("timestamp".to_string()) - })?; - let tms: DateTime = - serde_json::from_value(raw.to_owned()).map_err(|_| { - AgentEventError::InvalidFieldType("timestamp".to_string()) - })?; - tms - }, - })) - } - "skill.load" => Ok(Self::SkillLoad(SkillLoadEvent { - session_id, - turn_id, - skill_name: value - .params - .get("skill_name") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("skill_name".to_string()))? - .to_string(), - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - })), - "assistant.response" => { - let tool_calls = value.params.get("tool_calls").and_then(|v| match v { - Value::Array(arr) => arr - .iter() - .map(|a| { - let tc: Result = - serde_json::from_value(a.to_owned()).map_err(|_| { - AgentEventError::InvalidFieldType("tool_calls".into()) - }); - tc - }) - .collect::, _>>() - .ok(), - _ => None, - }); - Ok(Self::AssistantResponse(AssistantResponseEvent { - session_id, - turn_id, - full_text: value - .params - .get("full_text") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("full_text".to_string()))? - .to_string(), - tool_calls, - timestamp: { - let raw = value.params.get("timestamp").ok_or_else(|| { - AgentEventError::MissingField("timestamp".to_string()) - })?; - let tms: DateTime = - serde_json::from_value(raw.to_owned()).map_err(|_| { - AgentEventError::InvalidFieldType("timestamp".to_string()) - })?; - tms - }, - })) + let mut map = serde_json::Map::new(); + map.insert("method".to_string(), Value::String(value.method.clone())); + map.insert("params".to_string(), Value::Object(value.params)); + + serde_json::from_value::(Value::Object(map)).map_err(|err| { + let msg = err.to_string(); + let field = msg.split('`').nth(1).unwrap_or("unknown").to_string(); + if msg.contains("unknown variant") && field == value.method { + AgentEventError::UnknownMethod(value.method) + } else if msg.contains("missing field") { + AgentEventError::MissingField(field) + } else { + AgentEventError::InvalidFieldType(field) } - "assistant.task" => Ok(Self::Task(TaskEvent { - session_id, - turn_id, - timestamp: { - let raw = value - .params - .get("timestamp") - .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?; - let tms: DateTime = serde_json::from_value(raw.to_owned()) - .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?; - tms - }, - task_name: value - .params - .get("task_name") - .and_then(|v| v.as_str()) - .ok_or_else(|| AgentEventError::MissingField("task_name".to_string()))? - .to_string(), - task_status: { - let raw = value - .params - .get("task_status") - .ok_or_else(|| AgentEventError::MissingField("task_status".to_string()))?; - let ts: TaskStatus = serde_json::from_value(raw.to_owned()).map_err(|_| { - AgentEventError::InvalidFieldType("task_status".to_string()) - })?; - ts - }, - })), - method => Err(AgentEventError::UnknownMethod(method.to_string())), - } + }) } } From 8bbee3bac0f1bda7cd095f59339c66a06ac1c51d Mon Sep 17 00:00:00 2001 From: srimon12 Date: Mon, 29 Jun 2026 12:24:16 +0530 Subject: [PATCH 2/5] feat(cli): support session forking and history loading in CLI --- microagents-cli/src/main.rs | 55 +++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/microagents-cli/src/main.rs b/microagents-cli/src/main.rs index b17a81f..6667d82 100644 --- a/microagents-cli/src/main.rs +++ b/microagents-cli/src/main.rs @@ -46,6 +46,10 @@ struct Args { #[arg(long = "session-id", value_name = "ID")] session_id: Option, + /// Fork a previous session by id, creating a new session with cloned history. + #[arg(long = "fork", value_name = "ID")] + fork: Option, + /// Prompt to run in headless mode. #[arg(long, short, default_value = None)] prompt: Option, @@ -117,14 +121,61 @@ async fn build_agent( Ok(base_builder.build()?) } +async fn fork_session( + parent_id: &str, + storage_opt: Option, +) -> Result { + let storage = build_storage(storage_opt).await?; + let parent_events = storage + .get_session(parent_id) + .await + .map_err(|e| AgentError::SessionLoadError(format!("Failed to load parent session: {}", e)))?; + + if parent_events.is_empty() { + return Err(AgentError::SessionLoadError(format!("Parent session {} has no events or does not exist", parent_id))); + } + + let new_sid = uuid::Uuid::new_v4().to_string(); + + for event in parent_events { + let cloned_event = event.with_session_id(new_sid.clone()); + match cloned_event { + microagents_events::AgentEventAny::SessionInit(init_event) => { + storage.create_session(init_event).await.map_err(|e| { + AgentError::RunError(format!("Failed to write forked session init: {}", e)) + })?; + } + other => { + storage.update_session(other).await.map_err(|e| { + AgentError::RunError(format!("Failed to write forked event: {}", e)) + })?; + } + } + } + + Ok(new_sid) +} + #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); + + let resolved_session_id = if let Some(parent_id) = &args.fork { + let new_sid = fork_session(parent_id, args.storage.clone()).await?; + if args.prompt.is_none() && args.session_id.is_none() { + println!("Cloned session: {}", new_sid); + return Ok(()); + } + Some(new_sid) + } else { + args.session_id.clone() + }; + if let Some(p) = args.prompt { initialize_environment(args.verbose).await?; let agent = build_agent(args.provider, args.model, args.storage, args.skill).await?; let mut stream = agent - .run(p, args.session_id) + .run(p, resolved_session_id) .await .map_err(|e| e.to_string())?; while let Some(ev) = stream.next().await { @@ -151,7 +202,7 @@ async fn main() -> Result<(), Box> { // and that things did not just randomly freeze initialize_environment(true).await?; println!("Launching TUI..."); - let initial_session = args.session_id.clone(); + let initial_session = resolved_session_id.clone(); let load_history_storage = args.storage.clone(); tui::run_with_session( initial_session, From 09bd1efbc808d490f4f14ff4c626426446a5ce7a Mon Sep 17 00:00:00 2001 From: srimon12 Date: Mon, 29 Jun 2026 12:41:56 +0530 Subject: [PATCH 3/5] fix(cli): persist SessionFork event and fix TUI startup on fork --- microagents-cli/src/main.rs | 13 ++++++++---- microagents-events/src/lib.rs | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/microagents-cli/src/main.rs b/microagents-cli/src/main.rs index 6667d82..ab8f3b7 100644 --- a/microagents-cli/src/main.rs +++ b/microagents-cli/src/main.rs @@ -153,6 +153,15 @@ async fn fork_session( } } + let fork_event = microagents_events::types::SessionForkEvent { + session_id: new_sid.clone(), + parent_session_id: parent_id.to_string(), + timestamp: chrono::Utc::now(), + }; + storage.update_session(microagents_events::AgentEventAny::SessionFork(fork_event)).await.map_err(|e| { + AgentError::RunError(format!("Failed to write session fork event: {}", e)) + })?; + Ok(new_sid) } @@ -162,10 +171,6 @@ async fn main() -> Result<(), Box> { let resolved_session_id = if let Some(parent_id) = &args.fork { let new_sid = fork_session(parent_id, args.storage.clone()).await?; - if args.prompt.is_none() && args.session_id.is_none() { - println!("Cloned session: {}", new_sid); - return Ok(()); - } Some(new_sid) } else { args.session_id.clone() diff --git a/microagents-events/src/lib.rs b/microagents-events/src/lib.rs index cbf3835..f99400b 100644 --- a/microagents-events/src/lib.rs +++ b/microagents-events/src/lib.rs @@ -183,6 +183,24 @@ pub struct TaskEvent { pub timestamp: DateTime, } +/// Event emitted when a session is forked from a parent. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionForkEvent { + pub session_id: String, + pub parent_session_id: String, + pub timestamp: DateTime, +} + +impl AgentEvent for SessionForkEvent { + fn to_jsonrpc(&self) -> JsonRpcNotification { + serialize_to_jsonrpc("session.fork", self) + } + + fn session_id(&self) -> String { + self.session_id.clone() + } +} + fn serialize_to_jsonrpc(method: &str, params_struct: &T) -> JsonRpcNotification { let params_value = serde_json::to_value(params_struct).expect("Event params serialization failed"); let params = match params_value { @@ -309,6 +327,8 @@ pub enum AgentEventAny { UserPromptSubmit(UserPromptSubmitEvent), #[serde(rename = "assistant.task")] Task(TaskEvent), + #[serde(rename = "session.fork")] + SessionFork(SessionForkEvent), } impl AgentEventAny { @@ -323,6 +343,7 @@ impl AgentEventAny { Self::ToolCall(s) => s.timestamp, Self::ToolResult(s) => s.timestamp, Self::Task(s) => s.timestamp, + Self::SessionFork(s) => s.timestamp, } } @@ -337,6 +358,7 @@ impl AgentEventAny { Self::ToolCall(s) => s.session_id = sid, Self::ToolResult(s) => s.session_id = sid, Self::Task(s) => s.session_id = sid, + Self::SessionFork(s) => s.session_id = sid, } self } @@ -354,6 +376,7 @@ impl AgentEvent for AgentEventAny { Self::SkillLoad(e) => e.to_jsonrpc(), Self::UserPromptSubmit(e) => e.to_jsonrpc(), Self::Task(e) => e.to_jsonrpc(), + Self::SessionFork(e) => e.to_jsonrpc(), } } @@ -368,6 +391,7 @@ impl AgentEvent for AgentEventAny { Self::ToolResult(s) => s.session_id.clone(), Self::UserPromptSubmit(s) => s.session_id.clone(), Self::Task(s) => s.session_id.clone(), + Self::SessionFork(s) => s.session_id.clone(), } } } @@ -835,6 +859,22 @@ mod tests { ); } + #[test] + fn try_from_jsonrpc_session_fork_ok() { + let rpc = JsonRpcNotification::builder() + .method("session.fork".into()) + .add_param("session_id".into(), Value::from("s1")) + .add_param("parent_session_id".into(), Value::from("parent1")) + .add_param("timestamp".into(), { + let tms = Utc::now(); + serde_json::to_value(tms).expect("Should convert to value") + }); + let any = AgentEventAny::try_from(rpc).unwrap(); + assert!( + matches!(any, AgentEventAny::SessionFork(ref e) if e.session_id == "s1" && e.parent_session_id == "parent1") + ); + } + #[test] fn try_from_jsonrpc_unknown_method() { let rpc = JsonRpcNotification::builder() From efa2a9240b600ce0eff51269b5cd00971bb2b12b Mon Sep 17 00:00:00 2001 From: "Clelia (Astra) Bertelli" Date: Sun, 5 Jul 2026 16:48:52 +0200 Subject: [PATCH 4/5] fix: compile error and vbump --- Cargo.lock | 9 +++++---- microagents-cli/Cargo.toml | 9 +++++---- microagents-cli/src/main.rs | 26 +++++++++++++------------- microagents-core/Cargo.toml | 6 +++--- microagents-events/Cargo.toml | 2 +- microagents-events/src/lib.rs | 10 +++++++--- microagents-storage/Cargo.toml | 4 ++-- search-evals/Cargo.toml | 2 +- 8 files changed, 37 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98433ac..eea2f34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3768,10 +3768,11 @@ dependencies = [ [[package]] name = "microagents-cli" -version = "0.4.2" +version = "0.4.3" dependencies = [ "astchunk", "async-trait", + "chrono", "chunk 0.10.1", "clap", "dirs", @@ -3798,7 +3799,7 @@ dependencies = [ [[package]] name = "microagents-core" -version = "0.4.2" +version = "0.4.3" dependencies = [ "async-stream", "async-trait", @@ -3825,7 +3826,7 @@ dependencies = [ [[package]] name = "microagents-events" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "serde", @@ -3835,7 +3836,7 @@ dependencies = [ [[package]] name = "microagents-storage" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "async-trait", diff --git a/microagents-cli/Cargo.toml b/microagents-cli/Cargo.toml index 0b19bd1..d277160 100644 --- a/microagents-cli/Cargo.toml +++ b/microagents-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microagents-cli" -version = "0.4.2" +version = "0.4.3" edition = "2024" license = "MIT" readme = "README.md" @@ -12,9 +12,9 @@ keywords = ["ai", "agents", "cli", "tui", "rust"] async-trait = "0.1.89" futures-core = "0.3.32" futures-util = "0.3.32" -microagents-core = { path = "../microagents-core", version = "0.4.2" } -microagents-events = { path = "../microagents-events", version = "0.2.0" } -microagents-storage = { path = "../microagents-storage", version = "0.1.1" } +microagents-core = { path = "../microagents-core", version = "0.4.3" } +microagents-events = { path = "../microagents-events", version = "0.3.0" } +microagents-storage = { path = "../microagents-storage", version = "0.1.2" } qdrant-edge = "0.7.1" ratatui = {version = "0.30.0", features = ["serde", "palette"]} serde_json = "1.0.150" @@ -31,6 +31,7 @@ tempfile = "3.27.0" fastembed = "5" dirs = "6.0.0" indicatif = "0.18.4" +chrono = { version = "0.4.45", features = ["serde"] } [target.'cfg(windows)'.dependencies] liteparse = { version = "2.0.2", default-features = false } diff --git a/microagents-cli/src/main.rs b/microagents-cli/src/main.rs index ab8f3b7..669f379 100644 --- a/microagents-cli/src/main.rs +++ b/microagents-cli/src/main.rs @@ -121,18 +121,17 @@ async fn build_agent( Ok(base_builder.build()?) } -async fn fork_session( - parent_id: &str, - storage_opt: Option, -) -> Result { +async fn fork_session(parent_id: &str, storage_opt: Option) -> Result { let storage = build_storage(storage_opt).await?; - let parent_events = storage - .get_session(parent_id) - .await - .map_err(|e| AgentError::SessionLoadError(format!("Failed to load parent session: {}", e)))?; + let parent_events = storage.get_session(parent_id).await.map_err(|e| { + AgentError::SessionLoadError(format!("Failed to load parent session: {}", e)) + })?; if parent_events.is_empty() { - return Err(AgentError::SessionLoadError(format!("Parent session {} has no events or does not exist", parent_id))); + return Err(AgentError::SessionLoadError(format!( + "Parent session {} has no events or does not exist", + parent_id + ))); } let new_sid = uuid::Uuid::new_v4().to_string(); @@ -153,14 +152,15 @@ async fn fork_session( } } - let fork_event = microagents_events::types::SessionForkEvent { + let fork_event = microagents_events::SessionForkEvent { session_id: new_sid.clone(), parent_session_id: parent_id.to_string(), timestamp: chrono::Utc::now(), }; - storage.update_session(microagents_events::AgentEventAny::SessionFork(fork_event)).await.map_err(|e| { - AgentError::RunError(format!("Failed to write session fork event: {}", e)) - })?; + storage + .update_session(microagents_events::AgentEventAny::SessionFork(fork_event)) + .await + .map_err(|e| AgentError::RunError(format!("Failed to write session fork event: {}", e)))?; Ok(new_sid) } diff --git a/microagents-core/Cargo.toml b/microagents-core/Cargo.toml index 51af2cf..f3e2473 100644 --- a/microagents-core/Cargo.toml +++ b/microagents-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microagents-core" -version = "0.4.2" +version = "0.4.3" edition = "2024" license = "MIT" readme = "README.md" @@ -23,8 +23,8 @@ serde_json = "1.0.149" thiserror = "2.0.18" dashmap = "6" ultrafast-models-sdk = { version = "0.1.6", features = ["openai", "anthropic", "groq", "mistral", "cohere", "azure", "bedrock", "standalone", "ollama"] } -microagents-events = { path = "../microagents-events", version = "0.2.0" } -microagents-storage = { path = "../microagents-storage", version = "0.1.1" } +microagents-events = { path = "../microagents-events", version = "0.3.0" } +microagents-storage = { path = "../microagents-storage", version = "0.1.2" } async-stream = "0.3.6" uuid = { version = "1.23.1", features = ["v4"] } tokio = { version = "1.52.3", features = ["full"] } diff --git a/microagents-events/Cargo.toml b/microagents-events/Cargo.toml index c53a20c..0531cab 100644 --- a/microagents-events/Cargo.toml +++ b/microagents-events/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microagents-events" -version = "0.2.0" +version = "0.3.0" edition = "2024" license = "MIT" readme = "README.md" diff --git a/microagents-events/src/lib.rs b/microagents-events/src/lib.rs index f99400b..f586927 100644 --- a/microagents-events/src/lib.rs +++ b/microagents-events/src/lib.rs @@ -8,7 +8,7 @@ use std::{convert::TryFrom, fmt}; use crate::types::{AgentEvent, AgentEventError, JsonRpcNotification, ToolCall, ToolResult}; -pub const EVENTS_PROTOCOL_VERSION: &str = "0.2.0"; +pub const EVENTS_PROTOCOL_VERSION: &str = "0.3.0"; /// Indicates whether a session is being started fresh or resumed. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -202,10 +202,14 @@ impl AgentEvent for SessionForkEvent { } fn serialize_to_jsonrpc(method: &str, params_struct: &T) -> JsonRpcNotification { - let params_value = serde_json::to_value(params_struct).expect("Event params serialization failed"); + let params_value = + serde_json::to_value(params_struct).expect("Event params serialization failed"); let params = match params_value { Value::Object(m) => m, - other => panic!("Serialized event params must be a JSON Object, found: {:?}", other), + other => panic!( + "Serialized event params must be a JSON Object, found: {:?}", + other + ), }; JsonRpcNotification { jsonrpc: "2.0".to_string(), diff --git a/microagents-storage/Cargo.toml b/microagents-storage/Cargo.toml index 6964a42..dd0ac23 100644 --- a/microagents-storage/Cargo.toml +++ b/microagents-storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microagents-storage" -version = "0.1.1" +version = "0.1.2" edition = "2024" license = "MIT" readme = "README.md" @@ -13,7 +13,7 @@ keywords = ["ai", "agents", "session-storage", "rust"] async-trait = "0.1.89" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" -microagents-events = { path = "../microagents-events", version = "0.2.0" } +microagents-events = { path = "../microagents-events", version = "0.3.0" } anyhow = "1.0.102" dirs = "6.0.0" tokio = { version = "1.52.3", features = ["rt-multi-thread", "io-std", "io-util", "sync", "fs", "macros"] } diff --git a/search-evals/Cargo.toml b/search-evals/Cargo.toml index cda7652..a011fc2 100644 --- a/search-evals/Cargo.toml +++ b/search-evals/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -microagents-cli = { path = "../microagents-cli", version = "0.4.2" } +microagents-cli = { path = "../microagents-cli", version = "0.4.3" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" anyhow = "1" From d555fc4adfe673f8f7e14076b9d30e287704a57b Mon Sep 17 00:00:00 2001 From: "Clelia (Astra) Bertelli" Date: Sun, 5 Jul 2026 16:59:16 +0200 Subject: [PATCH 5/5] chore: address coderabbit review --- microagents-cli/src/main.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/microagents-cli/src/main.rs b/microagents-cli/src/main.rs index 669f379..88250fd 100644 --- a/microagents-cli/src/main.rs +++ b/microagents-cli/src/main.rs @@ -137,6 +137,9 @@ async fn fork_session(parent_id: &str, storage_opt: Option) -> Result { @@ -170,6 +173,12 @@ async fn main() -> Result<(), Box> { let args = Args::parse(); let resolved_session_id = if let Some(parent_id) = &args.fork { + if args.session_id.is_some() { + return Err( + "--fork-id and --session-id are mutually exclusive: only provide one of the two." + .into(), + ); + } let new_sid = fork_session(parent_id, args.storage.clone()).await?; Some(new_sid) } else {