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,
)
| 370 | /// Currently tracks tool usage for informational purposes. Future |
| 371 | /// enhancements may create sub-turn recordings for sub-agent (Task) tools. |
| 372 | pub(super) async fn handle_tool_use( |
| 373 | &mut self, |
| 374 | event: TurnEvent, |
| 375 | ) -> AgentResult<DispatchResult> { |
| 376 | let session_id = &event.session_id; |
| 377 | |
| 378 | let session = self.load_or_create_session(session_id, &event)?; |
| 379 | |
| 380 | // Log tool usage |
| 381 | if let Some(ref tool_name) = event.tool_name { |
| 382 | log::info!( |
| 383 | "Session {} tool use: {} ({})", |
| 384 | session_id, |
| 385 | tool_name, |
| 386 | event.event_type, |
| 387 | ); |
| 388 | } |
| 389 | |
| 390 | // Provenance: append tool call nodes on PostToolUse. |
| 391 | // |
| 392 | // PreToolUse doesn't have output or duration yet, so we only |
| 393 | // record on PostToolUse where the full picture is available. |
| 394 | // The classifier uses tool name + input + output to determine |
| 395 | // the node kind (Exploration, Commitment, Verification, etc.). |
| 396 | if event.event_type == HookType::PostToolUse { |
| 397 | if let Some(mut acc) = self.load_accumulator(session_id) { |
| 398 | let tool_name = event.tool_name.as_deref().unwrap_or("unknown"); |
| 399 | let tool_call_id = event.tool_use_id.as_deref(); |
| 400 | |
| 401 | // Extract tool_input, tool_output, status, duration from raw_json. |
| 402 | // |
| 403 | // The enriched OpenCode plugin sends top-level fields alongside |
| 404 | // tool_input: filediff, diagnostics, title, file_path, exit_code. |
| 405 | // We merge these INTO tool_input so the accumulator's classify |
| 406 | // and detail-building functions can find them without changing |
| 407 | // their signature. |
| 408 | let raw = event.raw_json.as_ref(); |
| 409 | let tool_output = raw.and_then(|r| r.get("tool_output").and_then(|v| v.as_str())); |
| 410 | let status = raw.and_then(|r| r.get("status").and_then(|v| v.as_str())); |
| 411 | let duration_ms = raw.and_then(|r| r.get("duration").and_then(|v| v.as_u64())); |
| 412 | |
| 413 | // Build a merged tool_input that includes both the original |
| 414 | // tool_input fields AND the top-level enriched fields. |
| 415 | let merged_input: Option<serde_json::Value> = raw.map(|r| { |
| 416 | let mut merged = r |
| 417 | .get("tool_input") |
| 418 | .and_then(|v| v.as_object().cloned()) |
| 419 | .unwrap_or_default(); |
| 420 | |
| 421 | // Merge enriched top-level fields into tool_input |
| 422 | for key in &[ |
| 423 | "filediff", |
| 424 | "diagnostics", |
| 425 | "title", |
| 426 | "file_path", |
| 427 | "exit_code", |
| 428 | "diff", |
| 429 | ] { |
no test coverage detected