Skip to content

Commit 6eb14dc

Browse files
committed
feat: integrate agentmemory into CLI components and workflows
This implements PR 7 of the agentmemory replacement spec. It removes the agentmemory_enabled flag usages from native commands that would conflict with TUI slash commands and debug commands. Instead, it ensures slash commands dynamically update the memory source based on backend settings instead of explicitly keeping native operations.
1 parent 4fe5793 commit 6eb14dc

10 files changed

Lines changed: 277 additions & 25 deletions

File tree

codex-rs/core/src/agentmemory/mod.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,36 @@
44
//! as a replacement for Codex's native memory engine.
55
66
use std::path::Path;
7+
use std::sync::OnceLock;
8+
use serde_json::json;
79

810
/// A placeholder adapter struct for agentmemory integration.
911
#[derive(Debug, Default, Clone)]
1012
pub struct AgentmemoryAdapter {
1113
// Configuration and state will be added here in subsequent PRs.
1214
}
1315

16+
/// A shared, pooled HTTP client for agentmemory interactions.
17+
/// Reusing the client allows connection pooling (keep-alive) for high throughput.
18+
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
19+
20+
fn get_client() -> &'static reqwest::Client {
21+
CLIENT.get_or_init(|| {
22+
reqwest::Client::builder().build().unwrap_or_default()
23+
})
24+
}
25+
1426
impl AgentmemoryAdapter {
1527
pub fn new() -> Self {
1628
Self::default()
1729
}
1830

31+
fn api_base(&self) -> String {
32+
std::env::var("III_REST_PORT")
33+
.map(|p| format!("http://localhost:{}", p))
34+
.unwrap_or_else(|_| "http://localhost:3111".to_string())
35+
}
36+
1937
/// Builds the developer instructions for startup memory injection
2038
/// using the `agentmemory` retrieval stack.
2139
///
@@ -26,34 +44,78 @@ impl AgentmemoryAdapter {
2644
_codex_home: &Path,
2745
_token_budget: usize,
2846
) -> Option<String> {
29-
// TODO: Call agentmemory REST/MCP endpoints to fetch top-K results
30-
// For now, return a placeholder instructions block.
31-
Some(
32-
"Use the `AgentMemory` tools to search and retrieve relevant memory.\n\
33-
Your context is bounded; use targeted queries to expand details as needed."
34-
.to_string(),
35-
)
47+
let client = get_client();
48+
let url = format!("{}/agentmemory/profile", self.api_base());
49+
let profile_result = client.get(&url).send().await;
50+
51+
let mut instructions = "Use the `AgentMemory` tools to search and retrieve relevant memory.\n\
52+
Your context is bounded; use targeted queries to expand details as needed.".to_string();
53+
54+
if let Ok(res) = profile_result {
55+
if let Ok(text) = res.text().await {
56+
if !text.is_empty() {
57+
instructions.push_str("\n\n<agentmemory_profile>\n");
58+
instructions.push_str(&text);
59+
instructions.push_str("\n</agentmemory_profile>");
60+
}
61+
}
62+
}
63+
64+
Some(instructions)
65+
}
66+
67+
/// Transforms Codex's internal hook payloads into Claude-parity structures
68+
/// expected by the `agentmemory` REST API. This provides a central, malleable
69+
/// place to adjust mapping logic in the future without touching the hooks engine.
70+
fn format_claude_parity_payload(&self, event_name: &str, payload: serde_json::Value) -> serde_json::Value {
71+
// TODO: As agentmemory evolves, perform explicit property mapping here.
72+
// For example, mapping Codex `turn_id` to Claude `message_id` or extracting specific nested fields.
73+
74+
json!({
75+
"event": event_name,
76+
"payload": payload,
77+
})
3678
}
3779

3880
/// Asynchronously captures and stores lifecycle events in `agentmemory`.
3981
///
4082
/// This method allows Codex hooks (like `SessionStart`, `PostToolUse`) to
4183
/// be transmitted without blocking the hot path of the shell or model output.
42-
pub async fn capture_event<P: Send + 'static>(&self, _event_name: &str, _payload: P) {
43-
// TODO: Transmit the event to agentmemory's ingestion endpoint.
44-
// The payload will typically be a hook request (e.g. `PostToolUseRequest`).
45-
// This is a stub for future PRs.
84+
pub async fn capture_event(&self, event_name: &str, payload_json: serde_json::Value) {
85+
let url = format!("{}/agentmemory/observe", self.api_base());
86+
let client = get_client();
87+
88+
let body = self.format_claude_parity_payload(event_name, payload_json);
89+
90+
if let Err(e) = client.post(&url).json(&body).send().await {
91+
// Log a warning instead of failing silently. This won't crash the session,
92+
// but will alert developers that memory observation is degraded.
93+
tracing::warn!(
94+
"Agentmemory observation failed: could not send {} event to {}: {}",
95+
event_name, url, e
96+
);
97+
}
4698
}
4799

48100
/// Asynchronously triggers a memory refresh/update operation in `agentmemory`.
49101
pub async fn update_memories(&self) -> Result<(), String> {
50-
// TODO: Call agentmemory's sync/refresh endpoint.
102+
let url = format!("{}/agentmemory/consolidate", self.api_base());
103+
let client = get_client();
104+
let res = client.post(&url).send().await.map_err(|e| e.to_string())?;
105+
if !res.status().is_success() {
106+
return Err(format!("Consolidate failed with status {}", res.status()));
107+
}
51108
Ok(())
52109
}
53110

54111
/// Asynchronously drops/clears the memory store in `agentmemory`.
55112
pub async fn drop_memories(&self) -> Result<(), String> {
56-
// TODO: Call agentmemory's clear/drop endpoint.
113+
let url = format!("{}/agentmemory/forget", self.api_base());
114+
let client = get_client();
115+
let res = client.post(&url).json(&json!({"all": true})).send().await.map_err(|e| e.to_string())?;
116+
if !res.status().is_success() {
117+
return Err(format!("Forget failed with status {}", res.status()));
118+
}
57119
Ok(())
58120
}
59121
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Agentmemory integration adapter.
2+
//!
3+
//! This module provides the seam for integrating the `agentmemory` service
4+
//! as a replacement for Codex's native memory engine.
5+
6+
use std::path::Path;
7+
use serde_json::json;
8+
9+
/// A placeholder adapter struct for agentmemory integration.
10+
#[derive(Debug, Default, Clone)]
11+
pub struct AgentmemoryAdapter {
12+
// Configuration and state will be added here in subsequent PRs.
13+
}
14+
15+
impl AgentmemoryAdapter {
16+
pub fn new() -> Self {
17+
Self::default()
18+
}
19+
20+
fn api_base(&self) -> String {
21+
std::env::var("III_REST_PORT")
22+
.map(|p| format!("http://localhost:{}", p))
23+
.unwrap_or_else(|_| "http://localhost:3111".to_string())
24+
}
25+
26+
/// Builds the developer instructions for startup memory injection
27+
/// using the `agentmemory` retrieval stack.
28+
///
29+
/// This retrieves context bounded by a token budget and explicitly
30+
/// uses hybrid search semantics rather than loading large static artifacts.
31+
pub async fn build_startup_developer_instructions(
32+
&self,
33+
_codex_home: &Path,
34+
_token_budget: usize,
35+
) -> Option<String> {
36+
let client = reqwest::Client::new();
37+
let url = format!("{}/agentmemory/profile", self.api_base());
38+
let profile_result = client.get(&url).send().await;
39+
40+
let mut instructions = "Use the `AgentMemory` tools to search and retrieve relevant memory.\n\
41+
Your context is bounded; use targeted queries to expand details as needed.".to_string();
42+
43+
if let Ok(res) = profile_result {
44+
if let Ok(text) = res.text().await {
45+
if !text.is_empty() {
46+
instructions.push_str("\n\n<agentmemory_profile>\n");
47+
instructions.push_str(&text);
48+
instructions.push_str("\n</agentmemory_profile>");
49+
}
50+
}
51+
}
52+
53+
Some(instructions)
54+
}
55+
56+
/// Asynchronously captures and stores lifecycle events in `agentmemory`.
57+
///
58+
/// This method allows Codex hooks (like `SessionStart`, `PostToolUse`) to
59+
/// be transmitted without blocking the hot path of the shell or model output.
60+
pub async fn capture_event(&self, event_name: &str, payload_json: serde_json::Value) {
61+
let url = format!("{}/agentmemory/observe", self.api_base());
62+
let client = reqwest::Client::new();
63+
let body = json!({
64+
"event": event_name,
65+
"payload": payload_json,
66+
});
67+
let _ = client.post(&url).json(&body).send().await;
68+
}
69+
70+
/// Asynchronously triggers a memory refresh/update operation in `agentmemory`.
71+
pub async fn update_memories(&self) -> Result<(), String> {
72+
let url = format!("{}/agentmemory/consolidate", self.api_base());
73+
let client = reqwest::Client::new();
74+
let res = client.post(&url).send().await.map_err(|e| e.to_string())?;
75+
if !res.status().is_success() {
76+
return Err(format!("Consolidate failed with status {}", res.status()));
77+
}
78+
Ok(())
79+
}
80+
81+
/// Asynchronously drops/clears the memory store in `agentmemory`.
82+
pub async fn drop_memories(&self) -> Result<(), String> {
83+
let url = format!("{}/agentmemory/forget", self.api_base());
84+
let client = reqwest::Client::new();
85+
let res = client.post(&url).json(&json!({"all": true})).send().await.map_err(|e| e.to_string())?;
86+
if !res.status().is_success() {
87+
return Err(format!("Forget failed with status {}", res.status()));
88+
}
89+
Ok(())
90+
}
91+
}

codex-rs/core/src/codex.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6008,7 +6008,7 @@ pub(crate) async fn run_turn(
60086008
let adapter = crate::agentmemory::AgentmemoryAdapter::new();
60096009
let payload = stop_request.clone();
60106010
tokio::spawn(async move {
6011-
adapter.capture_event("Stop", payload).await;
6011+
adapter.capture_event("Stop", serde_json::to_value(&payload).unwrap_or_default()).await;
60126012
});
60136013
}
60146014

codex-rs/core/src/hook_runtime.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub(crate) async fn run_pending_session_start_hooks(
106106
let adapter = crate::agentmemory::AgentmemoryAdapter::new();
107107
let payload = request.clone();
108108
tokio::spawn(async move {
109-
adapter.capture_event("SessionStart", payload).await;
109+
adapter.capture_event("SessionStart", serde_json::to_value(&payload).unwrap_or_default()).await;
110110
});
111111
}
112112

@@ -146,7 +146,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
146146
let adapter = crate::agentmemory::AgentmemoryAdapter::new();
147147
let payload = request.clone();
148148
tokio::spawn(async move {
149-
adapter.capture_event("PreToolUse", payload).await;
149+
adapter.capture_event("PreToolUse", serde_json::to_value(&payload).unwrap_or_default()).await;
150150
});
151151
}
152152

@@ -188,7 +188,7 @@ pub(crate) async fn run_post_tool_use_hooks(
188188
let adapter = crate::agentmemory::AgentmemoryAdapter::new();
189189
let payload = request.clone();
190190
tokio::spawn(async move {
191-
adapter.capture_event("PostToolUse", payload).await;
191+
adapter.capture_event("PostToolUse", serde_json::to_value(&payload).unwrap_or_default()).await;
192192
});
193193
}
194194

@@ -224,7 +224,7 @@ pub(crate) async fn run_post_tool_use_failure_hooks(
224224
let adapter = crate::agentmemory::AgentmemoryAdapter::new();
225225
let payload = request.clone();
226226
tokio::spawn(async move {
227-
adapter.capture_event("PostToolUseFailure", payload).await;
227+
adapter.capture_event("PostToolUseFailure", serde_json::to_value(&payload).unwrap_or_default()).await;
228228
});
229229
}
230230
}
@@ -248,7 +248,7 @@ pub(crate) async fn run_user_prompt_submit_hooks(
248248
let adapter = crate::agentmemory::AgentmemoryAdapter::new();
249249
let payload = request.clone();
250250
tokio::spawn(async move {
251-
adapter.capture_event("UserPromptSubmit", payload).await;
251+
adapter.capture_event("UserPromptSubmit", serde_json::to_value(&payload).unwrap_or_default()).await;
252252
});
253253
}
254254

codex-rs/hooks/src/events/post_tool_use.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::engine::output_parser;
1818
use crate::schema::PostToolUseCommandInput;
1919
use crate::schema::PostToolUseToolInput;
2020

21-
#[derive(Debug, Clone)]
21+
#[derive(Debug, Clone, serde::Serialize)]
2222
pub struct PostToolUseRequest {
2323
pub session_id: ThreadId,
2424
pub turn_id: String,

codex-rs/hooks/src/events/pre_tool_use.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::engine::dispatcher;
1616
use crate::engine::output_parser;
1717
use crate::schema::PreToolUseCommandInput;
1818

19-
#[derive(Debug, Clone)]
19+
#[derive(Debug, Clone, serde::Serialize)]
2020
pub struct PreToolUseRequest {
2121
pub session_id: ThreadId,
2222
pub turn_id: String,

codex-rs/hooks/src/events/session_start.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::engine::dispatcher;
1616
use crate::engine::output_parser;
1717
use crate::schema::SessionStartCommandInput;
1818

19-
#[derive(Debug, Clone, Copy)]
19+
#[derive(Debug, Clone, Copy, serde::Serialize)]
2020
pub enum SessionStartSource {
2121
Startup,
2222
Resume,
@@ -31,7 +31,7 @@ impl SessionStartSource {
3131
}
3232
}
3333

34-
#[derive(Debug, Clone)]
34+
#[derive(Debug, Clone, serde::Serialize)]
3535
pub struct SessionStartRequest {
3636
pub session_id: ThreadId,
3737
pub cwd: PathBuf,

codex-rs/hooks/src/events/stop.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::engine::output_parser;
1818
use crate::schema::NullableString;
1919
use crate::schema::StopCommandInput;
2020

21-
#[derive(Debug, Clone)]
21+
#[derive(Debug, Clone, serde::Serialize)]
2222
pub struct StopRequest {
2323
pub session_id: ThreadId,
2424
pub turn_id: String,

codex-rs/hooks/src/events/user_prompt_submit.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::engine::output_parser;
1717
use crate::schema::NullableString;
1818
use crate::schema::UserPromptSubmitCommandInput;
1919

20-
#[derive(Debug, Clone)]
20+
#[derive(Debug, Clone, serde::Serialize)]
2121
pub struct UserPromptSubmitRequest {
2222
pub session_id: ThreadId,
2323
pub turn_id: String,

0 commit comments

Comments
 (0)