Handle a PreToolUse or PostToolUse event. Currently tracks tool usage for informational purposes. Future enhancements may create sub-turn recordings for sub-agent (Task) tools.
(
&mut self,
event: TurnEvent,
)
| 334 | /// Currently tracks tool usage for informational purposes. Future |
| 335 | /// enhancements may create sub-turn recordings for sub-agent (Task) tools. |
| 336 | pub(super) async fn handle_tool_use( |
| 337 | &mut self, |
| 338 | event: TurnEvent, |
| 339 | ) -> AgentResult<DispatchResult> { |
| 340 | let session_id = &event.session_id; |
| 341 | |
| 342 | let session = self.load_or_create_session(session_id, &event)?; |
| 343 | |
| 344 | // Log tool usage |
| 345 | if let Some(ref tool_name) = event.tool_name { |
| 346 | log::info!( |
| 347 | "Session {} tool use: {} ({})", |
| 348 | session_id, |
| 349 | tool_name, |
| 350 | event.event_type, |
| 351 | ); |
| 352 | } |
| 353 | |
| 354 | // Provenance: append tool call nodes on PostToolUse. |
| 355 | // |
| 356 | // PreToolUse doesn't have output or duration yet, so we only |
| 357 | // record on PostToolUse where the full picture is available. |
| 358 | // The classifier uses tool name + input + output to determine |
| 359 | // the node kind (Exploration, Commitment, Verification, etc.). |
| 360 | if event.event_type == HookType::PostToolUse { |
| 361 | if let Some(mut acc) = self.load_accumulator(session_id) { |
| 362 | let tool_name = event.tool_name.as_deref().unwrap_or("unknown"); |
| 363 | let tool_call_id = event.tool_use_id.as_deref(); |
| 364 | |
| 365 | // Extract tool_input, tool_output, status, duration from raw_json. |
| 366 | // |
| 367 | // The enriched OpenCode plugin sends top-level fields alongside |
| 368 | // tool_input: filediff, diagnostics, title, file_path, exit_code. |
| 369 | // We merge these INTO tool_input so the accumulator's classify |
| 370 | // and detail-building functions can find them without changing |
| 371 | // their signature. |
| 372 | let raw = event.raw_json.as_ref(); |
| 373 | let tool_output = raw.and_then(|r| r.get("tool_output").and_then(|v| v.as_str())); |
| 374 | let status = raw.and_then(|r| r.get("status").and_then(|v| v.as_str())); |
| 375 | let duration_ms = raw.and_then(|r| r.get("duration").and_then(|v| v.as_u64())); |
| 376 | |
| 377 | // Build a merged tool_input that includes both the original |
| 378 | // tool_input fields AND the top-level enriched fields. |
| 379 | let merged_input: Option<serde_json::Value> = raw.map(|r| { |
| 380 | let mut merged = r |
| 381 | .get("tool_input") |
| 382 | .and_then(|v| v.as_object().cloned()) |
| 383 | .unwrap_or_default(); |
| 384 | |
| 385 | // Merge enriched top-level fields into tool_input |
| 386 | for key in &[ |
| 387 | "filediff", |
| 388 | "diagnostics", |
| 389 | "title", |
| 390 | "file_path", |
| 391 | "exit_code", |
| 392 | "diff", |
| 393 | ] { |
no test coverage detected