Skip to content

Commit 6fa2a25

Browse files
ericjutacodex
andcommitted
feat(agentmemory): tighten native observe payload contract
Normalize native observe payloads around structured post-tool data, explicit sender metadata, stable event identity, and persistence classes. Expand non-shell post-tool capture for native file/search lanes, fix explicit cwd attribution on secondary events, and pin the behavior with focused core tests and updated runtime docs. Co-authored-by: Codex <noreply@openai.com>
1 parent 70f4106 commit 6fa2a25

16 files changed

Lines changed: 1150 additions & 121 deletions

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

Lines changed: 59 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
//! This module provides the seam for integrating the `agentmemory` service
44
//! as a replacement for Codex's native memory engine.
55
6+
mod observe_payload;
7+
8+
use crate::agentmemory::observe_payload::build_observe_payload;
69
use crate::config::types::AgentmemoryConfig;
710
use crate::config::types::MemoriesConfig;
811
use codex_git_utils::get_git_repo_root;
@@ -81,28 +84,6 @@ pub(crate) fn workspace_project(cwd: &Path) -> PathBuf {
8184
get_git_repo_root(cwd).unwrap_or_else(|| cwd.to_path_buf())
8285
}
8386

84-
fn extract_project_and_cwd(payload: &serde_json::Value) -> (String, String) {
85-
let cwd = payload
86-
.get("cwd")
87-
.and_then(|value| value.as_str())
88-
.map(ToOwned::to_owned)
89-
.or_else(|| {
90-
std::env::current_dir()
91-
.ok()
92-
.map(|path| path.to_string_lossy().into_owned())
93-
})
94-
.unwrap_or_default();
95-
let project = if cwd.is_empty() {
96-
String::new()
97-
} else {
98-
get_git_repo_root(Path::new(&cwd))
99-
.unwrap_or_else(|| Path::new(&cwd).to_path_buf())
100-
.to_string_lossy()
101-
.into_owned()
102-
};
103-
(project, cwd)
104-
}
105-
10687
impl AgentmemoryAdapter {
10788
pub fn new() -> Self {
10889
Self::default()
@@ -220,7 +201,6 @@ impl AgentmemoryAdapter {
220201
fn parse_structured_tool_input(raw: &serde_json::Value) -> serde_json::Value {
221202
if let Some(s) = raw.as_str()
222203
&& let Ok(parsed) = serde_json::from_str::<serde_json::Value>(s)
223-
&& parsed.is_object()
224204
{
225205
return parsed;
226206
}
@@ -278,34 +258,6 @@ impl AgentmemoryAdapter {
278258
(files, search_terms)
279259
}
280260

281-
/// Transforms Codex's internal hook payloads into Claude-parity structures
282-
/// expected by the `agentmemory` REST API. This provides a central, malleable
283-
/// place to adjust mapping logic in the future without touching the hooks engine.
284-
fn format_claude_parity_payload(
285-
&self,
286-
event_name: &str,
287-
payload: serde_json::Value,
288-
) -> serde_json::Value {
289-
let session_id = payload
290-
.get("session_id")
291-
.and_then(|v| v.as_str())
292-
.unwrap_or("unknown")
293-
.to_string();
294-
let (project, cwd) = extract_project_and_cwd(&payload);
295-
296-
let timestamp = chrono::Utc::now().to_rfc3339();
297-
let hook_type = normalize_hook_type(event_name);
298-
299-
json!({
300-
"sessionId": session_id,
301-
"hookType": hook_type,
302-
"project": project,
303-
"cwd": cwd,
304-
"timestamp": timestamp,
305-
"data": payload,
306-
})
307-
}
308-
309261
/// Asynchronously captures and stores lifecycle events in `agentmemory`.
310262
///
311263
/// This method allows Codex hooks (like `SessionStart`, `PostToolUse`) to
@@ -317,7 +269,17 @@ impl AgentmemoryAdapter {
317269
memories: &MemoriesConfig,
318270
) {
319271
let url = format!("{}/agentmemory/observe", self.api_base(memories));
320-
let body = self.format_claude_parity_payload(event_name, payload_json);
272+
let body = match build_observe_payload(event_name, payload_json) {
273+
Ok(body) => body,
274+
Err(err) => {
275+
tracing::warn!(
276+
"Agentmemory observation skipped for unsupported or invalid {} payload: {}",
277+
event_name,
278+
err
279+
);
280+
return;
281+
}
282+
};
321283

322284
match self
323285
.request_builder(reqwest::Method::POST, &url, memories)
@@ -844,23 +806,6 @@ impl AgentmemoryAdapter {
844806
Self::json_or_error(res).await
845807
}
846808
}
847-
fn normalize_hook_type(event_name: &str) -> &str {
848-
match event_name {
849-
"SessionStart" => "session_start",
850-
"UserPromptSubmit" => "prompt_submit",
851-
"PreToolUse" => "pre_tool_use",
852-
"PostToolUse" => "post_tool_use",
853-
"PostToolUseFailure" => "post_tool_failure",
854-
"AssistantResult" => "assistant_result",
855-
"SubagentStart" => "subagent_start",
856-
"SubagentStop" => "subagent_stop",
857-
"Stop" => "stop",
858-
"Notification" => "notification",
859-
"TaskCompleted" => "task_completed",
860-
"SessionEnd" => "session_end",
861-
_ => event_name,
862-
}
863-
}
864809
#[cfg(test)]
865810
#[allow(clippy::await_holding_lock)]
866811
mod tests {
@@ -934,28 +879,6 @@ mod tests {
934879
assert!(instructions.contains("Do not call memory tools on every turn"));
935880
}
936881

937-
#[test]
938-
fn format_claude_parity_payload_normalizes_codex_hook_names() {
939-
let adapter = AgentmemoryAdapter::new();
940-
let payload = json!({ "session_id": "session-123" });
941-
942-
let prompt_submit =
943-
adapter.format_claude_parity_payload("UserPromptSubmit", payload.clone());
944-
assert_eq!(prompt_submit["hookType"], json!("prompt_submit"));
945-
assert_eq!(prompt_submit["sessionId"], json!("session-123"));
946-
947-
let post_tool_failure =
948-
adapter.format_claude_parity_payload("PostToolUseFailure", payload.clone());
949-
assert_eq!(post_tool_failure["hookType"], json!("post_tool_failure"));
950-
951-
let stop = adapter.format_claude_parity_payload("Stop", payload);
952-
assert_eq!(stop["hookType"], json!("stop"));
953-
954-
let session_end = adapter
955-
.format_claude_parity_payload("SessionEnd", json!({ "session_id": "session-1" }));
956-
assert_eq!(session_end["hookType"], json!("session_end"));
957-
}
958-
959882
#[tokio::test]
960883
#[serial_test::serial(agentmemory_env)]
961884
async fn update_memories_returns_consolidate_payload() {
@@ -1216,26 +1139,6 @@ mod tests {
12161139
);
12171140
}
12181141

1219-
#[test]
1220-
fn test_format_claude_parity_payload() {
1221-
let adapter = AgentmemoryAdapter::new();
1222-
let raw_payload = json!({
1223-
"session_id": "1234",
1224-
"turn_id": "turn-5",
1225-
"cwd": "/tmp/project",
1226-
"command": "echo hello"
1227-
});
1228-
1229-
let formatted = adapter.format_claude_parity_payload("PreToolUse", raw_payload.clone());
1230-
1231-
assert_eq!(formatted["sessionId"], "1234");
1232-
assert_eq!(formatted["hookType"], "pre_tool_use");
1233-
assert_eq!(formatted["project"], "/tmp/project");
1234-
assert_eq!(formatted["cwd"], "/tmp/project");
1235-
assert!(formatted.get("timestamp").is_some());
1236-
assert_eq!(formatted["data"], raw_payload);
1237-
}
1238-
12391142
#[tokio::test]
12401143
#[serial_test::serial(agentmemory_env)]
12411144
async fn refresh_context_posts_query_aware_payload() {
@@ -1286,6 +1189,51 @@ mod tests {
12861189
);
12871190
}
12881191

1192+
#[tokio::test]
1193+
#[serial_test::serial(agentmemory_env)]
1194+
async fn recall_context_posts_query_aware_payload() {
1195+
let server = MockServer::start().await;
1196+
let adapter = AgentmemoryAdapter::new();
1197+
let _guard = ENV_LOCK.lock().expect("lock env");
1198+
let _url_guard = EnvVarGuard::set("AGENTMEMORY_URL", "");
1199+
let memories = test_memories(&server);
1200+
1201+
Mock::given(method("POST"))
1202+
.and(path("/agentmemory/context"))
1203+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
1204+
"context": "<agentmemory-context>recall</agentmemory-context>",
1205+
})))
1206+
.expect(1)
1207+
.mount(&server)
1208+
.await;
1209+
1210+
let context = adapter
1211+
.recall_context(
1212+
"session-1",
1213+
Path::new("/tmp/project"),
1214+
Some("debug agentmemory recall semantics"),
1215+
DEFAULT_RUNTIME_RECALL_TOKEN_BUDGET,
1216+
&memories,
1217+
)
1218+
.await
1219+
.expect("recall context should succeed");
1220+
1221+
assert_eq!(context, "<agentmemory-context>recall</agentmemory-context>");
1222+
1223+
let requests = server.received_requests().await.unwrap_or_default();
1224+
let body = serde_json::from_slice::<serde_json::Value>(&requests[0].body)
1225+
.expect("recall request body should be json");
1226+
assert_eq!(
1227+
body,
1228+
json!({
1229+
"sessionId": "session-1",
1230+
"project": "/tmp/project",
1231+
"budget": DEFAULT_RUNTIME_RECALL_TOKEN_BUDGET,
1232+
"query": "debug agentmemory recall semantics",
1233+
})
1234+
);
1235+
}
1236+
12891237
#[tokio::test]
12901238
#[serial_test::serial(agentmemory_env)]
12911239
async fn remember_memory_posts_content_and_returns_json() {

0 commit comments

Comments
 (0)