use serde_json::Value;
use trusty_mpm::core::agent::TOOL_RESPONSE_KEYS;
const PASSTHROUGH_FIELDS: &[&str] = &[
"transcript_path",
"tool_use_id",
"agent_id",
"agent_type",
"agent_transcript_path",
];
fn compact_tool_response(response: &Value) -> Option<Value> {
let obj = response.as_object()?;
let mut out = serde_json::Map::new();
for key in TOOL_RESPONSE_KEYS {
if let Some(v) = obj.get(*key) {
out.insert((*key).to_string(), v.clone());
}
}
(!out.is_empty()).then_some(Value::Object(out))
}
pub(crate) fn build_hook_payload(
cwd: &str,
stdin_payload: Option<&Value>,
idle_parking_phrase: Option<&str>,
) -> Value {
let mut payload = serde_json::json!({ "cwd": cwd });
if let Some(phrase) = idle_parking_phrase {
payload["idle_parking"] = Value::Bool(true);
payload["idle_parking_phrase"] = Value::String(phrase.to_string());
}
let Some(stdin) = stdin_payload else {
return payload;
};
let tool_name = stdin.get("tool_name").and_then(Value::as_str);
if let Some(name) = tool_name {
payload["tool"] = Value::String(name.to_string());
}
if let Some(input) = stdin.get("tool_input") {
payload["input"] = input.clone();
}
for field in PASSTHROUGH_FIELDS {
if let Some(v) = stdin.get(*field)
&& !v.is_null()
{
payload[*field] = v.clone();
}
}
if tool_name.is_some_and(trusty_mpm::core::agent::is_subagent_dispatch_tool)
&& let Some(response) = stdin.get("tool_response")
&& let Some(compact) = compact_tool_response(response)
{
payload["tool_response"] = compact;
}
payload
}
#[cfg(test)]
mod tests {
use super::*;
fn pre_tool_use_agent() -> Value {
serde_json::json!({
"session_id": "d6066d66-8c7f-41f1-8914-8d0710d563aa",
"transcript_path": "/tmp/proj/d6066d66.jsonl",
"cwd": "/tmp/proj",
"prompt_id": "31fd3d1b-d586-412f-9772-2baf48d56acf",
"permission_mode": "bypassPermissions",
"hook_event_name": "PreToolUse",
"tool_name": "Agent",
"tool_input": {
"description": "probe alpha",
"prompt": "Reply with ALPHA.",
"subagent_type": "general-purpose"
},
"tool_use_id": "toolu_01DAvdgnCx8jYZQmn8NYpUge"
})
}
#[test]
fn forwards_pre_tool_use_correlation_keys() {
let p = build_hook_payload("/tmp/proj", Some(&pre_tool_use_agent()), None);
assert_eq!(p["cwd"], "/tmp/proj");
assert_eq!(p["tool"], "Agent");
assert_eq!(p["input"]["subagent_type"], "general-purpose");
assert_eq!(p["input"]["description"], "probe alpha");
assert_eq!(p["tool_use_id"], "toolu_01DAvdgnCx8jYZQmn8NYpUge");
assert_eq!(p["transcript_path"], "/tmp/proj/d6066d66.jsonl");
}
#[test]
fn forwards_subagent_stop_correlation_keys() {
let stdin = serde_json::json!({
"hook_event_name": "SubagentStop",
"session_id": "d6066d66-8c7f-41f1-8914-8d0710d563aa",
"transcript_path": "/tmp/proj/d6066d66.jsonl",
"agent_id": "a403cdbc078b5c474",
"agent_type": "general-purpose",
"agent_transcript_path": "/tmp/proj/d6066d66/subagents/agent-a403cdbc078b5c474.jsonl",
"last_assistant_message": "ALPHA"
});
let p = build_hook_payload("/tmp/proj", Some(&stdin), None);
assert_eq!(p["agent_id"], "a403cdbc078b5c474");
assert_eq!(p["agent_type"], "general-purpose");
assert_eq!(
p["agent_transcript_path"],
"/tmp/proj/d6066d66/subagents/agent-a403cdbc078b5c474.jsonl"
);
assert_eq!(p["transcript_path"], "/tmp/proj/d6066d66.jsonl");
assert!(p.get("tool").is_none());
}
#[test]
fn compacts_tool_response_to_correlation_keys() {
let stdin = serde_json::json!({
"hook_event_name": "PostToolUse",
"tool_name": "Agent",
"tool_use_id": "toolu_01DAvdgnCx8jYZQmn8NYpUge",
"tool_response": {
"isAsync": true,
"status": "async_launched",
"agentId": "a403cdbc078b5c474",
"resolvedModel": "claude-haiku-4-5-20251001",
"description": "probe alpha",
"prompt": "Reply with ALPHA.",
"outputFile": "/tmp/out.txt",
"canReadOutputFile": true
}
});
let p = build_hook_payload("/tmp/proj", Some(&stdin), None);
assert_eq!(p["tool_response"]["agentId"], "a403cdbc078b5c474");
assert_eq!(p["tool_response"]["status"], "async_launched");
assert_eq!(p["tool_response"]["isAsync"], true);
assert_eq!(
p["tool_response"]["resolvedModel"],
"claude-haiku-4-5-20251001"
);
assert!(p["tool_response"].get("prompt").is_none());
assert!(p["tool_response"].get("outputFile").is_none());
}
#[test]
fn compact_tool_response_drops_bulk_output() {
let huge = "x".repeat(200_000);
let stdin = serde_json::json!({
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_use_id": "toolu_bash",
"tool_response": { "stdout": huge, "is_error": false }
});
let p = build_hook_payload("/tmp/proj", Some(&stdin), None);
assert!(
p.get("tool_response").is_none(),
"non-dispatch tool_response must not be forwarded"
);
assert!(p.to_string().len() < 1000, "payload must stay bounded");
}
#[test]
fn preserves_idle_parking_fields() {
let stdin = serde_json::json!({
"hook_event_name": "SubagentStop",
"agent_id": "a403cdbc078b5c474"
});
let p = build_hook_payload("/w", Some(&stdin), Some("waiting for the monitor"));
assert_eq!(p["idle_parking"], true);
assert_eq!(p["idle_parking_phrase"], "waiting for the monitor");
assert_eq!(p["agent_id"], "a403cdbc078b5c474");
}
#[test]
fn no_stdin_payload_yields_cwd_only() {
let p = build_hook_payload("/w", None, None);
assert_eq!(p["cwd"], "/w");
assert_eq!(p.as_object().expect("object").len(), 1);
}
#[test]
fn absent_fields_are_omitted_not_nulled() {
let stdin = serde_json::json!({
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "ls" },
"agent_id": Value::Null
});
let p = build_hook_payload("/w", Some(&stdin), None);
assert!(p.get("agent_id").is_none());
assert!(p.get("tool_use_id").is_none());
assert!(p.get("transcript_path").is_none());
}
}