Callback when a tool completes. ENHANCED: Now includes value binding system for dataflow tracking. - Binds numeric results to $vN identifiers - Tracks per-tool call counts for anti-thrash - Caches results for state tracking
(self, result: CTPToolResult)
| 457 | await self.ui_manager.start_tool_execution(display_name, arguments) |
| 458 | |
| 459 | async def _on_tool_result(self, result: CTPToolResult) -> None: |
| 460 | """Callback when a tool completes. |
| 461 | |
| 462 | ENHANCED: Now includes value binding system for dataflow tracking. |
| 463 | - Binds numeric results to $vN identifiers |
| 464 | - Tracks per-tool call counts for anti-thrash |
| 465 | - Caches results for state tracking |
| 466 | """ |
| 467 | meta = self._call_metadata.get(result.id) |
| 468 | llm_tool_name = meta.llm_tool_name if meta else result.tool |
| 469 | execution_tool_name = meta.execution_tool_name if meta else result.tool |
| 470 | arguments = meta.arguments if meta else {} |
| 471 | |
| 472 | # For dynamic tools, extract the actual tool name for better logging/caching |
| 473 | actual_tool_name = execution_tool_name |
| 474 | actual_arguments = arguments |
| 475 | if execution_tool_name == DYNAMIC_TOOL_PROXY_NAME and "tool_name" in arguments: |
| 476 | actual_tool_name = arguments["tool_name"] |
| 477 | actual_arguments = {k: v for k, v in arguments.items() if k != "tool_name"} |
| 478 | |
| 479 | success = result.is_success |
| 480 | logger.info( |
| 481 | f"Tool result ({actual_tool_name}): success={success}, error='{result.error}'" |
| 482 | ) |
| 483 | |
| 484 | tool_state = get_agent_tool_state(getattr(self.context, "agent_id", "default")) |
| 485 | value_binding = None |
| 486 | |
| 487 | # Cache successful results and create value bindings |
| 488 | if success and result.result is not None: |
| 489 | # Extract the actual value from MCP response structure |
| 490 | actual_result = self._extract_result_value(result.result) |
| 491 | |
| 492 | # Cache result for dedup |
| 493 | tool_state.cache_result(actual_tool_name, actual_arguments, actual_result) |
| 494 | logger.debug(f"Cached result for {actual_tool_name}: {actual_result}") |
| 495 | |
| 496 | # Create value binding ($v1, $v2, etc.) for dataflow tracking |
| 497 | # Only bind "execution" tool results (not discovery tools) |
| 498 | if not tool_state.is_discovery_tool(execution_tool_name): |
| 499 | value_binding = tool_state.bind_value( |
| 500 | actual_tool_name, actual_arguments, actual_result |
| 501 | ) |
| 502 | logger.info( |
| 503 | f"Bound value ${value_binding.id} = {actual_result} from {actual_tool_name}" |
| 504 | ) |
| 505 | |
| 506 | # Record numeric results for runaway detection |
| 507 | if isinstance(actual_result, (int, float)): |
| 508 | tool_state.record_numeric_result(float(actual_result)) |
| 509 | |
| 510 | # Increment tool call counter for budget tracking (with tool name for split budgets) |
| 511 | tool_state.increment_tool_call(execution_tool_name) |
| 512 | |
| 513 | # Record tool use for session-aware search boosting |
| 514 | # Successful tools get boosted in future search results |
| 515 | search_engine = get_search_engine() |
| 516 | search_engine.record_tool_use(actual_tool_name, success=success) |