Skip to main content

agentmux_srv/server/agent_handlers/
history.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4
5use std::sync::Arc;
6
7
8use crate::backend::rpc::engine::WshRpcEngine;
9use crate::backend::rpc_types::{
10    COMMAND_APPEND_AGENT_HISTORY, COMMAND_LIST_AGENT_HISTORY, COMMAND_SEARCH_AGENT_HISTORY,
11    CommandAppendAgentHistoryData, CommandListAgentHistoryData, CommandSearchAgentHistoryData,
12};
13
14use super::super::AppState;
15
16pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
17    // appendagenthistory → append a history entry, broadcast agenthistory:changed
18    let mstore_afh = state.mstore.clone();
19    let broker_afh = state.broker.clone();
20    engine.register_typed(
21        COMMAND_APPEND_AGENT_HISTORY,
22        move |cmd: CommandAppendAgentHistoryData, _ctx| {
23            let mstore = mstore_afh.clone();
24            let broker = broker_afh.clone();
25            async move {
26                let entry = mstore.agent_history_append(&cmd.agent_id, &cmd.entry)
27                    .map_err(|e| format!("appendagenthistory: {e}"))?;
28                broker.publish_event::<crate::backend::mps_events::AgentHistoryChanged>(&(), vec![], 0);
29                Ok(entry)
30            }
31        },
32    );
33
34    // listagenthistory → return history entries with pagination
35    let mstore_lfh = state.mstore.clone();
36    engine.register_typed(
37        COMMAND_LIST_AGENT_HISTORY,
38        move |cmd: CommandListAgentHistoryData, _ctx| {
39            let mstore = mstore_lfh.clone();
40            async move {
41                let entries = mstore.agent_history_list(
42                    &cmd.agent_id,
43                    cmd.session_date.as_deref(),
44                    cmd.limit,
45                    cmd.offset,
46                ).map_err(|e| format!("listagenthistory: {e}"))?;
47                Ok(entries)
48            }
49        },
50    );
51
52    // searchagenthistory → search history entries by query
53    let mstore_sfh = state.mstore.clone();
54    engine.register_typed(
55        COMMAND_SEARCH_AGENT_HISTORY,
56        move |cmd: CommandSearchAgentHistoryData, _ctx| {
57            let mstore = mstore_sfh.clone();
58            async move {
59                let entries = mstore.agent_history_search(&cmd.agent_id, &cmd.query, cmd.limit)
60                    .map_err(|e| format!("searchagenthistory: {e}"))?;
61                Ok(entries)
62            }
63        },
64    );
65
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::backend::rpc_types::{CommandListAgentHistoryData, CommandSearchAgentHistoryData};
72
73    /// All three history commands record their request and response types.
74    #[tokio::test]
75    async fn register_typed_records_every_history_command() {
76        let state = crate::server::tests::test_state();
77        let (engine, _rx) = WshRpcEngine::new();
78        register(&engine, &state);
79        let schema = engine.schema_json();
80        let rows = schema.as_array().unwrap();
81        let find = |cmd: &str| {
82            rows.iter()
83                .find(|r| r["command"] == cmd)
84                .unwrap_or_else(|| panic!("{cmd} missing from the schema"))
85        };
86
87        let append = find(COMMAND_APPEND_AGENT_HISTORY);
88        assert_eq!(append["requestName"], "CommandAppendAgentHistoryData");
89        assert_eq!(append["responseName"], "AgentHistory");
90
91        for (cmd, req) in [
92            (COMMAND_LIST_AGENT_HISTORY, "CommandListAgentHistoryData"),
93            (COMMAND_SEARCH_AGENT_HISTORY, "CommandSearchAgentHistoryData"),
94        ] {
95            let row = find(cmd);
96            assert_eq!(row["requestName"], req, "{cmd} request");
97            let resp = row["responseName"].as_str().unwrap();
98            assert!(
99                resp.starts_with("alloc::vec::Vec<") && resp.contains("AgentHistory"),
100                "{cmd} should answer Vec<AgentHistory>, got {resp:?}",
101            );
102        }
103    }
104
105    /// The pagination fields are `serde(default)` on `i64`, which ts-rs cannot
106    /// mark optional -- the stubs derive `ListAgentHistoryInput` /
107    /// `SearchAgentHistoryInput` to restore it. Pin the server half: drop a
108    /// default and those derived TS types become a lie.
109    #[test]
110    fn the_pagination_fields_may_be_omitted() {
111        let list: CommandListAgentHistoryData =
112            serde_json::from_value(serde_json::json!({ "agent_id": "a1" }))
113                .expect("limit and offset are both defaulted");
114        assert_eq!(list.limit, 50, "listagenthistory defaults limit to 50");
115        assert_eq!(list.offset, 0);
116        assert!(list.session_date.is_none());
117
118        let search: CommandSearchAgentHistoryData =
119            serde_json::from_value(serde_json::json!({ "agent_id": "a1", "query": "x" }))
120                .expect("limit is defaulted");
121        assert_eq!(search.limit, 50, "searchagenthistory shares the same default");
122    }
123}