fix(hooks): record OMP tool calls through the Pi tool schema

OMP was left out of the closed-tool agents introduced with the safe
tool-context capture (#190), so every OMP pre-tool-use/post-tool-use
observation was stored with the event name as its title and an empty
body: session pages, handoffs and the auto-improve reviewer only saw
the prompts.

The generated Pi extension is the OMP extension with its AGENT
constant renamed (build_pi_extension), so OMP posts the exact
tool/callID/args/output/isError payload Pi does. Route it through the
same metadata schema, closed-summary path and isError outcome; unknown
tool families still keep no output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Igor Sant'Ana
2026-09-25 13:37:17 -03:00
co-authored by Claude Opus 5.5
parent 1417119eec
commit 85edc2a5af
3 changed files with 77 additions and 36 deletions
+4 -2
View File
@@ -174,7 +174,9 @@ pub(crate) fn tool_observation_metadata(
object.get("tool")?.as_str()?,
object.get("callID").and_then(Value::as_str),
),
AgentKind::Pi => (
// The Pi extension is generated from the OMP one (`build_pi_extension`),
// so both post the same `tool` / `callID` / `args` payload.
AgentKind::Pi | AgentKind::Omp => (
object.get("tool")?.as_str()?,
object.get("callID").and_then(Value::as_str),
),
@@ -230,7 +232,7 @@ pub(crate) fn tool_observation_metadata(
/// Extracts an outcome only where the adapter protocol proves its meaning.
pub(crate) fn tool_observation_outcome(agent: AgentKind, raw: &Value) -> ToolOutcome {
match agent {
AgentKind::Pi => match raw.get("isError").and_then(Value::as_bool) {
AgentKind::Pi | AgentKind::Omp => match raw.get("isError").and_then(Value::as_bool) {
Some(true) => ToolOutcome::Error,
Some(false) => ToolOutcome::Success,
None => ToolOutcome::Unknown,
+72 -33
View File
@@ -628,6 +628,7 @@ const fn closed_tool_agent(agent: AgentKind) -> bool {
| AgentKind::Codex
| AgentKind::OpenCode
| AgentKind::Pi
| AgentKind::Omp
| AgentKind::AntigravityCli
| AgentKind::Hermes
| AgentKind::Pool
@@ -2747,40 +2748,78 @@ mod tests {
}
#[test]
fn pi_post_outcomes_and_stable_id_are_rendered() {
for (is_error, outcome) in [
(Some(false), "success"),
(Some(true), "error"),
(None, "unknown"),
] {
let mut raw = serde_json::json!({"tool":"bash","args":{},"callID":"pi-stable-190","output":"result"});
if let Some(is_error) = is_error {
raw["isError"] = serde_json::json!(is_error);
fn pi_and_omp_post_outcomes_and_stable_id_are_rendered() {
for agent in ["pi", "omp"] {
for (is_error, outcome) in [
(Some(false), "success"),
(Some(true), "error"),
(None, "unknown"),
] {
let mut raw = serde_json::json!({"tool":"bash","args":{},"callID":"pi-stable-190","output":"result"});
if let Some(is_error) = is_error {
raw["isError"] = serde_json::json!(is_error);
}
let pre = HookEnvelope::from_query_and_body(
HookQuery {
event: "pre-tool-use".into(),
agent: Some(agent.into()),
..Default::default()
},
raw.clone(),
);
let post = HookEnvelope::from_query_and_body(
HookQuery {
event: "post-tool-use".into(),
agent: Some(agent.into()),
..Default::default()
},
raw,
);
assert!(
pre.body_excerpt
.unwrap()
.contains("tool_call_id: pi-stable-190")
);
let body = post.body_excerpt.unwrap();
assert!(body.contains("tool_call_id: pi-stable-190"));
assert!(
body.contains(&format!("outcome: {outcome}")),
"{agent}: {body}"
);
}
let pre = HookEnvelope::from_query_and_body(
HookQuery {
event: "pre-tool-use".into(),
agent: Some("pi".into()),
..Default::default()
},
raw.clone(),
);
let post = HookEnvelope::from_query_and_body(
HookQuery {
event: "post-tool-use".into(),
agent: Some("pi".into()),
..Default::default()
},
raw,
);
assert!(
pre.body_excerpt
.unwrap()
.contains("tool_call_id: pi-stable-190")
);
let body = post.body_excerpt.unwrap();
assert!(body.contains("tool_call_id: pi-stable-190"));
assert!(body.contains(&format!("outcome: {outcome}")));
}
}
#[test]
fn omp_tool_events_keep_family_call_id_and_output_but_not_arguments() {
// `build_pi_extension` in the CLI derives the Pi extension from the OMP
// one, so OMP posts this exact tool payload shape.
let pre = HookEnvelope::from_query_and_body(
HookQuery {
event: "pre-tool-use".into(),
agent: Some("omp".into()),
..Default::default()
},
serde_json::json!({"tool":"bash","callID":"omp-1","args":{"command":"SENTINEL_COMMAND"}}),
);
assert_eq!(pre.title_hint.as_deref(), Some("tool non-file"));
assert_eq!(
pre.body_excerpt.as_deref(),
Some("tool_family: non-file\ntool_call_id: omp-1")
);
let post = HookEnvelope::from_query_and_body(
HookQuery {
event: "post-tool-use".into(),
agent: Some("omp".into()),
..Default::default()
},
serde_json::json!({"tool":"bash","callID":"omp-1","args":{"command":"SENTINEL_COMMAND"},"output":"tests passed","details":{"diff":"SENTINEL_DETAILS"}}),
);
assert_eq!(post.title_hint.as_deref(), Some("tool non-file"));
assert_eq!(
post.body_excerpt.as_deref(),
Some("tool_family: non-file\ntool_call_id: omp-1\noutcome: unknown\n---\ntests passed")
);
}
}
+1 -1
View File
@@ -273,7 +273,7 @@ printf '%s\n' '{"session_id":"demo","cwd":"/example/workspace","tool_name":"Edit
```
The normal capture contract is intentionally narrow: supported Claude Code,
OpenCode, Pi, and Antigravity tool events retain only canonical tool family,
OpenCode, Pi, OMP, and Antigravity tool events retain only canonical tool family,
an agent-provided validated call ID when their documented schema proves one,
and a PostToolUse outcome class. `PreToolUse` never retains commands,
arguments, paths, input bodies, or arbitrary tool names. `PostToolUse` appends