Skip to main content

agentmux_mcp/tools/
history.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Conversation history search and transcripts.
5
6use super::*;
7
8pub(super) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
9    let ToolCtx { local_url, auth_key, client, .. } = *cx;
10    match name {
11        "SearchHistory" => {
12            // Whose history this is, srv takes from this process's
13            // `X-Agent-Token` (sent on every request, see `main`), never from a
14            // name; the tool exposes no `agent` parameter on purpose — see
15            // SEARCH_HISTORY_TOOL's comment. The name is sent only for srv's
16            // actor counters, so a process without one still searches.
17            let slug = std::env::var("AGENTMUX_AGENT_ID").unwrap_or_default();
18            let query = arguments
19                .get("query")
20                .and_then(|v| v.as_str())
21                .unwrap_or_default()
22                .to_string();
23            let tool = arguments.get("tool").and_then(|v| v.as_str());
24            if query.trim().is_empty() && tool.is_none() {
25                anyhow::bail!(
26                    "query must be non-empty unless `tool` is given (an empty query with no \
27                     tool filter would match everything and return a truncated firehose)"
28                );
29            }
30
31            if local_url.is_empty() || auth_key.is_empty() {
32                anyhow::bail!(
33                    "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
34                     Is this agent pane opened via AgentMux?"
35                );
36            }
37
38            let url = format!(
39                "{}/agentmux/reactive/history/search",
40                local_url.trim_end_matches('/')
41            );
42            let mut query_params: Vec<(&str, String)> = vec![("query", query)];
43            if !slug.trim().is_empty() {
44                query_params.push(("agent", slug));
45            }
46            if let Some(t) = tool {
47                query_params.push(("tool", t.to_string()));
48            }
49            if let Some(r) = arguments.get("role").and_then(|v| v.as_str()) {
50                query_params.push(("role", r.to_string()));
51            }
52            for (key, arg) in [
53                ("since", "since"),
54                ("until", "until"),
55                ("max_sessions", "max_sessions"),
56                ("limit", "limit"),
57            ] {
58                if let Some(n) = arguments.get(arg).and_then(|v| v.as_i64()) {
59                    query_params.push((key, n.to_string()));
60                }
61            }
62            if arguments.get("include_inferred").and_then(|v| v.as_bool()) == Some(true) {
63                query_params.push(("include_inferred", "true".to_string()));
64            }
65
66            let body = srv_get_json(client, &url, auth_key, &query_params, "history search").await?;
67            Ok(serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string()))
68        }
69        "GetAgentTranscript" => {
70            let agent = arguments
71                .get("agent")
72                .and_then(|v| v.as_str())
73                .filter(|s| !s.is_empty())
74                .ok_or_else(|| anyhow::anyhow!("missing required parameter: agent"))?;
75            let max_lines = arguments.get("max_lines").and_then(|v| v.as_u64());
76
77            if local_url.is_empty() || auth_key.is_empty() {
78                anyhow::bail!(
79                    "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
80                     Is this agent pane opened via AgentMux?"
81                );
82            }
83
84            let url = format!(
85                "{}/agentmux/reactive/transcript",
86                local_url.trim_end_matches('/')
87            );
88            let mut query: Vec<(&str, String)> = vec![("agent", agent.to_string())];
89            if let Some(n) = max_lines {
90                query.push(("max_lines", n.to_string()));
91            }
92
93            let resp = client
94                .get(&url)
95                .header(AUTH_KEY_HEADER, auth_key)
96                .query(&query)
97                .send()
98                .await
99                .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
100
101            if !resp.status().is_success() {
102                let status = resp.status();
103                let text = resp.text().await.unwrap_or_default();
104                anyhow::bail!("transcript fetch failed: HTTP {status} — {text}");
105            }
106
107            let result: Value = resp
108                .json()
109                .await
110                .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
111
112            Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
113        }
114        "ListConversations" => {
115            if local_url.is_empty() || auth_key.is_empty() {
116                anyhow::bail!(
117                    "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
118                     Is this agent pane opened via AgentMux?"
119                );
120            }
121
122            let url = format!(
123                "{}/api/v1/muxspect/conversations",
124                local_url.trim_end_matches('/')
125            );
126            let resp = client
127                .get(&url)
128                .header(AUTH_KEY_HEADER, auth_key)
129                .send()
130                .await
131                .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
132
133            if !resp.status().is_success() {
134                let status = resp.status();
135                let text = resp.text().await.unwrap_or_default();
136                anyhow::bail!("conversations fetch failed: HTTP {status} — {text}");
137            }
138
139            let result: Value = resp
140                .json()
141                .await
142                .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
143
144            Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
145        }
146        _ => Err(not_in_family()),
147    }
148}