use serde_json::Value;
pub fn prompt_of(data: &Value) -> Option<&str> {
field(data, &["prompt"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn tool_name_of(data: &Value) -> Option<&str> {
field(data, &["tool_name", "toolName"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub const TOOL_INPUT_KEYS: &[&str] = &["tool_input", "parameters"];
pub const TOOL_RESULT_KEYS: &[&str] = &["tool_result", "result", "tool_response"];
pub fn tool_input_of(data: &Value) -> Option<&Value> {
field(data, TOOL_INPUT_KEYS)
}
pub fn tool_ok_of(data: &Value) -> Option<bool> {
if let Some(success) = field(data, &["success"]).and_then(Value::as_bool) {
return Some(success);
}
tool_result_of(data)?
.get("is_error")
.and_then(Value::as_bool)
.map(|is_error| !is_error)
}
pub fn duration_ms_of(data: &Value) -> Option<u64> {
field(data, &["duration_ms", "durationMs", "executionTimeMs"])?.as_u64()
}
pub fn native_event_of(data: &Value) -> Option<&str> {
field(data, &["hook_event_name", "hookName"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn turn_id_of(data: &Value) -> Option<&str> {
field(data, &["turn_id", "turnId"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn agent_version_of(data: &Value) -> Option<&str> {
field(data, &["agent_version", "clineVersion"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn tool_result_of(data: &Value) -> Option<&Value> {
field(data, TOOL_RESULT_KEYS)
}
pub fn tool_use_id_of(data: &Value) -> Option<&str> {
field(data, &["tool_use_id", "toolUseId", "toolCallId"])
.or_else(|| data.pointer("/tool_call/id"))
.or_else(|| data.pointer("/tool_result/id"))
.and_then(Value::as_str)
.and_then(non_empty)
}
const TEXT_PARAMS: &[&str] = &[
"command",
"path",
"file_path",
"content",
"diff",
"patch",
"old_text",
"new_text",
"regex",
"file_pattern",
"url",
"uri",
"prompt",
"query",
"question",
"response",
"result",
"text",
"input",
"server_name",
"tool_name",
];
fn reparse_params(params: &Value) -> Value {
let Some(object) = params.as_object() else {
return params.clone();
};
let reparsed = object
.iter()
.map(|(key, value)| {
let parsed = match value.as_str() {
Some(raw) if !TEXT_PARAMS.contains(&key.as_str()) => {
let trimmed = raw.trim_start();
if trimmed.starts_with('{') || trimmed.starts_with('[') {
serde_json::from_str::<Value>(raw)
.ok()
.filter(|v| v.is_object() || v.is_array())
} else {
None
}
}
_ => None,
};
(key.clone(), parsed.unwrap_or_else(|| value.clone()))
})
.collect();
Value::Object(reparsed)
}
fn is_camel_tool_shape(data: &Value) -> bool {
matches!(
located(data, &["tool_name", "toolName"]),
Some((pointer, _)) if pointer.ends_with("/toolName")
)
}
fn camel_tool_input(data: &Value) -> Option<Value> {
["/tool_call/input", "/tool_result/input"]
.iter()
.find_map(|pointer| data.pointer(pointer).filter(|v| v.is_object()))
.cloned()
.or_else(|| tool_input_of(data).map(reparse_params))
}
pub fn common_tool_of(data: &Value) -> Option<(String, Option<Value>)> {
let name = tool_name_of(data)?;
if !is_camel_tool_shape(data) {
return Some((name.to_string(), tool_input_of(data).cloned()));
}
Some(map_camel_tool(name, camel_tool_input(data)))
}
fn map_camel_tool(name: &str, input: Option<Value>) -> (String, Option<Value>) {
let mut args = match input {
Some(Value::Object(map)) => map,
other => {
return (
camel_mcp_name(name).unwrap_or_else(|| name.to_string()),
other,
)
}
};
let rename = |args: &mut serde_json::Map<String, Value>, from: &str, to: &str| {
if let Some(value) = args.remove(from) {
args.entry(to.to_string()).or_insert(value);
}
};
let canonical = match name {
"execute_command" => "Bash",
"run_commands" => {
if let Some(joined) = commands_line(args.get("commands")) {
args.remove("commands");
args.entry("command".to_string())
.or_insert(Value::String(joined));
}
"Bash"
}
"read_file" => {
rename(&mut args, "path", "file_path");
"Read"
}
"read_files" => {
if let Some(first) = first_read_path(&args) {
args.entry("file_path".to_string())
.or_insert(Value::String(first));
}
"Read"
}
"write_to_file" => {
rename(&mut args, "path", "file_path");
"Write"
}
"replace_in_file" | "apply_diff" => {
rename(&mut args, "path", "file_path");
"Edit"
}
"editor" => {
rename(&mut args, "path", "file_path");
let creates = args.get("old_text").is_none_or(Value::is_null)
&& args.get("insert_line").is_none_or(Value::is_null);
if creates {
args.remove("old_text");
args.remove("insert_line");
rename(&mut args, "new_text", "content");
"Write"
} else {
rename(&mut args, "old_text", "old_string");
rename(&mut args, "new_text", "new_string");
"Edit"
}
}
"search_files" => {
rename(&mut args, "regex", "pattern");
rename(&mut args, "file_pattern", "glob");
"Grep"
}
"list_files" => "Glob",
"use_mcp_tool" => {
let server = args.get("server_name").and_then(Value::as_str);
let tool = args.get("tool_name").and_then(Value::as_str);
let Some(name) = server.zip(tool).map(|(s, t)| format!("mcp__{s}__{t}")) else {
return (name.to_string(), Some(Value::Object(args)));
};
let arguments = match args.remove("arguments") {
Some(Value::String(raw)) => serde_json::from_str(&raw)
.ok()
.filter(Value::is_object)
.unwrap_or(Value::String(raw)),
Some(other) => other,
None => Value::Object(serde_json::Map::new()),
};
return (name, Some(arguments));
}
"access_mcp_resource" => {
rename(&mut args, "server_name", "server");
"ReadMcpResourceTool"
}
"web_fetch" => "WebFetch",
"fetch_web_content" => {
let first = args
.get("requests")
.and_then(Value::as_array)
.and_then(|requests| requests.first())
.and_then(Value::as_object)
.cloned();
for (key, value) in first.iter().flatten() {
if key == "url" || key == "prompt" {
args.entry(key.clone()).or_insert(value.clone());
}
}
"WebFetch"
}
other => {
return (
camel_mcp_name(other).unwrap_or_else(|| other.to_string()),
Some(Value::Object(args)),
);
}
};
(canonical.to_string(), Some(Value::Object(args)))
}
fn camel_mcp_name(name: &str) -> Option<String> {
(!name.starts_with("mcp__") && name.contains("__")).then(|| format!("mcp__{name}"))
}
fn commands_line(commands: Option<&Value>) -> Option<String> {
let lines: Vec<&str> = match commands? {
Value::String(one) => vec![one.as_str()],
Value::Array(items) => items
.iter()
.filter_map(|item| {
item.as_str()
.or_else(|| item.get("command").and_then(Value::as_str))
})
.collect(),
_ => return None,
};
(!lines.is_empty()).then(|| lines.join("\n"))
}
fn first_read_path(args: &serde_json::Map<String, Value>) -> Option<String> {
let entry_path = |entry: &Value| -> Option<String> {
entry
.as_str()
.or_else(|| entry.get("path").and_then(Value::as_str))
.map(str::to_string)
};
["files", "paths", "file_paths", "path"]
.iter()
.filter_map(|key| args.get(*key))
.find_map(|value| match value {
Value::Array(items) => items.iter().find_map(entry_path),
other => entry_path(other),
})
}
pub fn common_tool_response_of(data: &Value) -> Option<Value> {
if !is_camel_tool_shape(data) {
return tool_result_of(data).cloned();
}
let content = field(data, &["result"]).cloned();
let ok = field(data, &["success"]).and_then(Value::as_bool);
if content.is_none() && ok.is_none() {
return None;
}
let mut response = serde_json::Map::new();
response.insert("content".to_string(), content.unwrap_or(Value::Null));
if let Some(ok) = ok {
response.insert("is_error".to_string(), Value::Bool(!ok));
}
Some(Value::Object(response))
}
pub fn common_tool_use_id_of(data: &Value) -> Option<String> {
use sha2::{Digest, Sha256};
if let Some(id) = tool_use_id_of(data) {
return Some(id.to_string());
}
if !is_camel_tool_shape(data) {
return None;
}
let (name, input) = common_tool_of(data)?;
let key = Value::Array(vec![
Value::String(session_id_of(data).unwrap_or_default().to_string()),
Value::String(name),
input.unwrap_or(Value::Null),
]);
let canonical = serde_json_canonicalizer::to_string(&key).ok()?;
let digest = Sha256::digest(canonical.as_bytes());
let hex: String = digest[..12].iter().map(|b| format!("{b:02x}")).collect();
Some(format!("ol_{hex}"))
}
pub fn cwd_of(data: &Value) -> Option<&str> {
if let Some(cwd) = field(data, &["cwd"])
.and_then(Value::as_str)
.and_then(non_empty)
{
return Some(cwd);
}
field(data, &["workspaceRoots"])?
.as_array()?
.first()?
.as_str()
.and_then(non_empty)
}
pub fn declared_workspace_root_of(data: &Value) -> Option<&str> {
field(data, &["workspaceRoots"])?
.as_array()?
.first()?
.as_str()
.and_then(non_empty)
}
pub fn session_id_of(data: &Value) -> Option<&str> {
field(data, &["session_id", "sessionId", "taskId"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn prompt_and_mode(data: &Value) -> (Option<&str>, Option<&str>) {
let Some(raw) = prompt_of(data) else {
return (None, None);
};
let Some(rest) = raw.strip_prefix("<user_input") else {
return (Some(raw), None);
};
let Some((attrs, body)) = rest.split_once('>') else {
return (Some(raw), None);
};
let Some(inner) = body.strip_suffix("</user_input>") else {
return (Some(raw), None);
};
let mode = attrs
.split_once("mode=\"")
.and_then(|(_, m)| m.split_once('"'))
.map(|(m, _)| m)
.and_then(non_empty);
(non_empty(inner.trim()).or(Some(raw)), mode)
}
pub const ENRICHMENT_PREFIX: &str = "openlatch.";
pub fn to_common(data: &Value) -> Option<Value> {
let mut common = serde_json::Map::new();
let (prompt, mode) = prompt_and_mode(data);
let mut put = |key: &str, value: Option<Value>| {
if let Some(value) = value {
common.insert(key.to_string(), value);
}
};
let text = |v: Option<&str>| v.map(|s| Value::String(s.to_string()));
put("hook_event_name", text(native_event_of(data)));
put("session_id", text(session_id_of(data)));
put("cwd", text(cwd_of(data)));
put("prompt", text(prompt));
match common_tool_of(data) {
Some((name, input)) => {
put("tool_name", Some(Value::String(name)));
put("tool_input", input);
}
None => put("tool_input", tool_input_of(data).cloned()),
}
put("tool_response", common_tool_response_of(data));
put(
"tool_use_id",
common_tool_use_id_of(data).map(Value::String),
);
put("turn_id", text(turn_id_of(data)));
put("agent_version", text(agent_version_of(data)));
put("duration_ms", duration_ms_of(data).map(Value::from));
put("model", data.get("model").cloned());
if mode.is_none() && common.iter().all(|(k, v)| data.get(k) == Some(v)) {
return None;
}
let mut raw = data.clone();
if let Some(obj) = raw.as_object_mut() {
let ours: Vec<String> = obj
.keys()
.filter(|key| key.starts_with(ENRICHMENT_PREFIX))
.cloned()
.collect();
for key in ours {
if let Some(value) = obj.remove(&key) {
common.insert(key, value);
}
}
}
if let (Some(mode), Some(obj)) = (mode, raw.as_object_mut()) {
obj.insert("mode".to_string(), Value::String(mode.to_string()));
}
common.insert("raw_agent_payload".to_string(), raw);
Some(Value::Object(common))
}
fn field<'a>(data: &'a Value, keys: &[&str]) -> Option<&'a Value> {
located(data, keys).map(|(_, value)| value)
}
pub fn pointer_to(data: &Value, keys: &[&str]) -> Option<String> {
located(data, keys).map(|(pointer, _)| pointer)
}
fn located<'a>(data: &'a Value, keys: &[&str]) -> Option<(String, &'a Value)> {
for key in keys {
if let Some(found) = data.get(key) {
return Some((format!("/{}", escape(key)), found));
}
}
let object = data.as_object()?;
keys.iter().find_map(|key| {
object
.iter()
.filter(|(outer, _)| !is_argument_container(outer))
.find_map(|(outer, nested)| {
nested
.get(key)
.map(|found| (format!("/{}/{}", escape(outer), escape(key)), found))
})
})
}
fn is_argument_container(key: &str) -> bool {
TOOL_INPUT_KEYS.contains(&key) || TOOL_RESULT_KEYS.contains(&key)
}
fn escape(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
pub fn model_of(data: &Value) -> (Option<&str>, Option<&str>) {
let Some(model) = data.get("model") else {
return (None, None);
};
if let Some(slug) = model.as_str() {
return (None, non_empty(slug));
}
(
model
.get("provider")
.and_then(Value::as_str)
.and_then(non_empty),
model
.get("slug")
.and_then(Value::as_str)
.and_then(non_empty),
)
}
fn non_empty(s: &str) -> Option<&str> {
let t = s.trim();
(!t.is_empty()).then_some(s)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_prompt_is_found_by_shape_across_agents() {
assert_eq!(
prompt_of(&json!({"prompt": "hello", "session_id": "abc"})),
Some("hello")
);
assert_eq!(
prompt_of(&json!({
"hookName": "UserPromptSubmit",
"userPromptSubmit": {"prompt": "hey there !!"}
})),
Some("hey there !!")
);
assert_eq!(
prompt_of(&json!({"someFutureHook": {"prompt": "p"}})),
Some("p")
);
assert_eq!(prompt_of(&json!({"toolName": "Bash"})), None);
assert_eq!(prompt_of(&json!({"prompt": " "})), None);
}
#[test]
fn clines_nested_hook_shape_reads_the_same_as_a_flat_one() {
let nested = json!({
"taskId": "conv_1789665246332_em78z12",
"hookName": "PreToolUse",
"clineVersion": "4.1.17",
"workspaceRoots": ["/work/scratch"],
"preToolUse": {
"toolName": "execute_command",
"parameters": {"command": "rm -rf /tmp"}
}
});
assert_eq!(tool_name_of(&nested), Some("execute_command"));
assert_eq!(
tool_input_of(&nested),
Some(&json!({"command": "rm -rf /tmp"}))
);
assert_eq!(cwd_of(&nested), Some("/work/scratch"));
let post = json!({
"taskId": "conv_1",
"postToolUse": {"toolName": "execute_command", "result": "ok", "success": true}
});
assert_eq!(tool_result_of(&post), Some(&json!("ok")));
let flat = json!({"toolName": "execute_command", "parameters": {"command": "ls"}});
assert_eq!(tool_name_of(&flat), Some("execute_command"));
assert_eq!(tool_input_of(&flat), Some(&json!({"command": "ls"})));
}
#[test]
fn the_native_spelling_outranks_every_nested_one() {
let native = json!({
"session_id": "sess_a",
"cwd": "/repo",
"tool_name": "Bash",
"tool_input": {"command": "cargo test"},
"tool_use_id": "toolu_01",
"nested": {"tool_name": "NotThis", "tool_input": {"command": "nor this"}}
});
assert_eq!(tool_name_of(&native), Some("Bash"));
assert_eq!(
tool_input_of(&native),
Some(&json!({"command": "cargo test"}))
);
assert_eq!(tool_use_id_of(&native), Some("toolu_01"));
assert_eq!(cwd_of(&native), Some("/repo"));
let empty = json!({"hookName": "Stop", "taskId": "conv_1"});
assert_eq!(tool_name_of(&empty), None);
assert_eq!(tool_input_of(&empty), None);
assert_eq!(tool_result_of(&empty), None);
assert_eq!(tool_use_id_of(&empty), None);
assert_eq!(cwd_of(&empty), None);
}
#[test]
fn a_pointer_resolves_to_the_value_the_reader_returns() {
for data in [
json!({"tool_input": {"command": "cargo test"}}),
json!({"preToolUse": {"parameters": {"command": "cargo test"}}}),
json!({"parameters": {"command": "cargo test"}}),
] {
let pointer = pointer_to(&data, TOOL_INPUT_KEYS).expect("a pointer");
assert_eq!(
data.pointer(&pointer),
tool_input_of(&data),
"pointer {pointer} and reader disagree for {data}"
);
assert_eq!(
data.pointer(&pointer).and_then(|v| v.get("command")),
Some(&json!("cargo test"))
);
}
assert_eq!(pointer_to(&json!({"prompt": "hi"}), TOOL_INPUT_KEYS), None);
}
#[test]
fn pointer_tokens_are_escaped() {
let data = json!({"a/b": {"tool_input": 1}, "c~d": {"tool_input": 2}});
let pointer = pointer_to(&data, TOOL_INPUT_KEYS).expect("a pointer");
assert!(
pointer == "/a~1b/tool_input" || pointer == "/c~0d/tool_input",
"unexpected pointer: {pointer}"
);
assert!(data.pointer(&pointer).is_some(), "{pointer} must resolve");
}
#[test]
fn the_remaining_facts_read_across_agents() {
let cline_post = json!({
"taskId": "conv_1",
"hookName": "PostToolUse",
"clineVersion": "4.1.17",
"postToolUse": {
"toolName": "execute_command",
"result": "done",
"success": false,
"executionTimeMs": 1234
}
});
assert_eq!(tool_ok_of(&cline_post), Some(false));
assert_eq!(duration_ms_of(&cline_post), Some(1234));
assert_eq!(native_event_of(&cline_post), Some("PostToolUse"));
assert_eq!(agent_version_of(&cline_post), Some("4.1.17"));
assert_eq!(turn_id_of(&cline_post), None);
let claude_post = json!({
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_response": {"is_error": true, "stdout": ""}
});
assert_eq!(tool_ok_of(&claude_post), Some(false));
assert_eq!(
tool_ok_of(&json!({"tool_response": {"is_error": false}})),
Some(true)
);
let codex = json!({"session_id": "s", "turn_id": "t_1", "hook_event_name": "PreToolUse"});
assert_eq!(turn_id_of(&codex), Some("t_1"));
assert_eq!(tool_ok_of(&json!({"tool_name": "Bash"})), None);
assert_eq!(duration_ms_of(&json!({"tool_name": "Bash"})), None);
assert_eq!(agent_version_of(&json!({"tool_name": "Bash"})), None);
}
#[test]
fn the_probe_never_descends_past_one_level() {
let deep = json!({"a": {"b": {"prompt": "buried", "tool_name": "Buried"}}});
assert_eq!(prompt_of(&deep), None);
assert_eq!(tool_name_of(&deep), None);
}
#[test]
fn the_model_is_read_without_inventing_the_missing_half() {
assert_eq!(
model_of(&json!({"model": {"provider": "ollama", "slug": "qwen2.5-coder:7b"}})),
(Some("ollama"), Some("qwen2.5-coder:7b"))
);
assert_eq!(
model_of(&json!({"model": "claude-opus-5"})),
(None, Some("claude-opus-5"))
);
assert_eq!(
model_of(&json!({"model": {"provider": "unknown", "slug": "unknown"}})),
(Some("unknown"), Some("unknown"))
);
assert_eq!(model_of(&json!({})), (None, None));
}
#[test]
fn clines_wrapper_is_removed_and_its_mode_kept() {
let data = json!({
"taskId": "conv_1",
"hookName": "UserPromptSubmit",
"userPromptSubmit": {"prompt": "<user_input mode=\"act\">hey y</user_input>"}
});
assert_eq!(prompt_and_mode(&data), (Some("hey y"), Some("act")));
let mapped = to_common(&data).expect("a wrapped prompt is a rewrite");
assert_eq!(mapped["prompt"], "hey y");
assert_eq!(mapped["raw_agent_payload"]["mode"], "act");
assert_eq!(
mapped["raw_agent_payload"]["userPromptSubmit"]["prompt"],
"<user_input mode=\"act\">hey y</user_input>"
);
}
#[test]
fn a_prompt_that_is_not_wrapped_is_untouched() {
for raw in [
"just a prompt",
"<user_input mode=\"act\">unterminated",
"look at <user_input mode=\"act\">this</user_input> inline",
] {
let data = json!({"prompt": raw});
let (prompt, _) = prompt_and_mode(&data);
assert_eq!(prompt, Some(raw), "must not rewrite {raw:?}");
}
let bare = json!({"prompt": "<user_input>hi</user_input>"});
assert_eq!(prompt_and_mode(&bare), (Some("hi"), None));
}
#[test]
fn clines_tools_map_onto_the_common_vocabulary() {
let common = |tool: &str, params: Value| {
common_tool_of(
&json!({"taskId": "t", "preToolUse": {"toolName": tool, "parameters": params}}),
)
.expect("a tool")
};
let cases = [
(
"execute_command",
json!({"command": "ls", "requires_approval": "false"}),
"Bash",
json!({"command": "ls", "requires_approval": "false"}),
),
(
"run_commands",
json!({"commands": "[\"ls\",\"pwd\"]"}),
"Bash",
json!({"command": "ls\npwd"}),
),
(
"read_file",
json!({"path": "src/a.rs"}),
"Read",
json!({"file_path": "src/a.rs"}),
),
(
"write_to_file",
json!({"path": "p.json", "content": "{\"a\":1}"}),
"Write",
json!({"file_path": "p.json", "content": "{\"a\":1}"}),
),
(
"replace_in_file",
json!({"path": "a.rs", "diff": "---"}),
"Edit",
json!({"file_path": "a.rs", "diff": "---"}),
),
(
"apply_diff",
json!({"path": "a.rs", "diff": "---"}),
"Edit",
json!({"file_path": "a.rs", "diff": "---"}),
),
(
"search_files",
json!({"path": "src", "regex": "fn main", "file_pattern": "*.rs"}),
"Grep",
json!({"path": "src", "pattern": "fn main", "glob": "*.rs"}),
),
(
"list_files",
json!({"path": "src", "recursive": "true"}),
"Glob",
json!({"path": "src", "recursive": "true"}),
),
(
"use_mcp_tool",
json!({"server_name": "github", "tool_name": "create_issue", "arguments": "{\"title\":\"x\"}"}),
"mcp__github__create_issue",
json!({"title": "x"}),
),
(
"access_mcp_resource",
json!({"server_name": "docs", "uri": "file:///a"}),
"ReadMcpResourceTool",
json!({"server": "docs", "uri": "file:///a"}),
),
(
"web_fetch",
json!({"url": "https://example.com"}),
"WebFetch",
json!({"url": "https://example.com"}),
),
(
"editor",
json!({"path": "new.rs", "new_text": "fn x() {}"}),
"Write",
json!({"file_path": "new.rs", "content": "fn x() {}"}),
),
(
"editor",
json!({"path": "a.rs", "old_text": "a", "new_text": "b"}),
"Edit",
json!({"file_path": "a.rs", "old_string": "a", "new_string": "b"}),
),
(
"read_files",
json!({"files": [{"path": "a.rs"}, {"path": "b.rs"}]}),
"Read",
json!({"file_path": "a.rs", "files": [{"path": "a.rs"}, {"path": "b.rs"}]}),
),
(
"fetch_web_content",
json!({"requests": [{"url": "https://e.com", "prompt": "p"}]}),
"WebFetch",
json!({"requests": [{"url": "https://e.com", "prompt": "p"}], "url": "https://e.com", "prompt": "p"}),
),
(
"github__create_issue",
json!({"title": "x"}),
"mcp__github__create_issue",
json!({"title": "x"}),
),
(
"browser_action",
json!({"action": "launch"}),
"browser_action",
json!({"action": "launch"}),
),
(
"attempt_completion",
json!({"result": "{\"done\":true}"}),
"attempt_completion",
json!({"result": "{\"done\":true}"}),
),
];
for (tool, params, name, input) in cases {
assert_eq!(
common(tool, params),
(name.to_string(), Some(input)),
"{tool}"
);
}
}
#[test]
fn the_snake_case_shape_is_never_remapped() {
let native = json!({"tool_name": "read_file", "tool_input": {"path": "a"}});
assert_eq!(
common_tool_of(&native),
Some(("read_file".to_string(), Some(json!({"path": "a"}))))
);
assert_eq!(common_tool_use_id_of(&native), None, "nothing derived");
let claude =
json!({"tool_name": "Bash", "tool_input": {"command": "ls"}, "tool_use_id": "toolu_1"});
assert_eq!(
to_common(&claude),
None,
"a native payload travels untouched"
);
}
#[test]
fn the_sdk_record_supplies_the_input_and_the_id() {
let pre = json!({
"taskId": "conv_1",
"hookName": "tool_call",
"tool_call": {"id": "call_9", "name": "run_commands", "input": {"commands": ["ls"]}},
"preToolUse": {"toolName": "run_commands", "parameters": {"commands": "[\"ls\"]"}}
});
let post = json!({
"taskId": "conv_1",
"hookName": "tool_result",
"tool_result": {"id": "call_9", "name": "run_commands", "input": {"commands": ["ls"]}, "output": "x"},
"postToolUse": {"toolName": "run_commands", "parameters": {"commands": "[\"ls\"]"}, "result": "x", "success": true}
});
let plugin = json!({"taskId": "conv_1", "toolName": "run_commands", "parameters": {"commands": ["ls"]}, "toolCallId": "call_9"});
for data in [&pre, &post, &plugin] {
assert_eq!(
common_tool_use_id_of(data).as_deref(),
Some("call_9"),
"{data}"
);
assert_eq!(
common_tool_of(data),
Some(("Bash".to_string(), Some(json!({"command": "ls"})))),
"{data}"
);
}
assert_eq!(
common_tool_response_of(&post),
Some(json!({"content": "x", "is_error": false}))
);
let mapped = to_common(&post).expect("remapped");
assert_eq!(mapped["tool_response"]["content"], "x");
assert_eq!(
mapped["raw_agent_payload"]["postToolUse"]["toolName"],
"run_commands"
);
}
#[test]
fn a_derived_id_pairs_a_pre_with_its_post() {
let pre = json!({"taskId": "t1", "preToolUse": {"toolName": "write_to_file", "parameters": {"path": "a", "content": "x"}}});
let post = json!({"taskId": "t1", "postToolUse": {"toolName": "write_to_file", "parameters": {"path": "a", "content": "x"}, "result": "ok", "success": true}});
let id = common_tool_use_id_of(&pre).expect("derived");
assert!(id.starts_with("ol_"), "{id}");
assert_eq!(common_tool_use_id_of(&post).as_deref(), Some(id.as_str()));
let other_session = json!({"taskId": "t2", "preToolUse": {"toolName": "write_to_file", "parameters": {"path": "a", "content": "x"}}});
assert_ne!(
common_tool_use_id_of(&other_session).as_deref(),
Some(id.as_str())
);
assert_eq!(tool_use_id_of(&pre), None);
}
#[test]
fn enrichment_stays_top_level_through_the_mapping() {
let enriched = json!({
"taskId": "conv_1",
"hookName": "TaskStart",
"workspaceRoots": ["/work"],
"taskStart": {"taskMetadata": {}},
"openlatch.declared_skills": [],
"openlatch.declared_mcp_sources": [{"source_id": "s"}],
"openlatch.declared_rules_hash": "abc",
"openlatch.cwd": "def"
});
let mapped = to_common(&enriched).expect("a Cline payload is remapped");
for key in [
"openlatch.declared_skills",
"openlatch.declared_mcp_sources",
"openlatch.declared_rules_hash",
"openlatch.cwd",
] {
assert_eq!(mapped[key], enriched[key], "{key} must be top-level");
assert!(
mapped["raw_agent_payload"].get(key).is_none(),
"{key} is not the agent's"
);
}
assert_eq!(mapped["raw_agent_payload"]["taskId"], "conv_1");
assert_eq!(declared_workspace_root_of(&enriched), Some("/work"));
assert_eq!(declared_workspace_root_of(&json!({"cwd": "/w"})), None);
}
#[test]
fn a_tool_argument_is_never_mistaken_for_the_prompt() {
let task = json!({
"hook_event_name": "PreToolUse",
"tool_name": "Task",
"tool_input": {"description": "review", "prompt": "SUBAGENT PROMPT"}
});
assert_eq!(prompt_of(&task), None);
let shadowed = json!({
"tool_name": "Task",
"tool_input": {"cwd": "/tmp/sub", "toolName": "Inner", "prompt": "x"}
});
assert_eq!(cwd_of(&shadowed), None);
assert_eq!(tool_name_of(&shadowed), Some("Task"));
let cline = json!({"userPromptSubmit": {"prompt": "the real prompt"}});
assert_eq!(prompt_of(&cline), Some("the real prompt"));
let flat = json!({"tool_input": {"command": "ls"}, "tool_result": "ok"});
assert_eq!(tool_input_of(&flat), Some(&json!({"command": "ls"})));
assert_eq!(tool_result_of(&flat), Some(&json!("ok")));
}
}