# Tool Name Tracking Fix

## Issue Description

The LangGraph callback handler had a bug where tool names were not properly tracked between `on_tool_start` and `on_tool_end`/`on_tool_error` callbacks.

### Root Cause

In `on_tool_start`, the tool name was extracted from the serialized data:
```python
tool_name = serialized.get("name", "unknown")
```

However, this name was **never stored** for later retrieval.

When `on_tool_end` or `on_tool_error` were called, they couldn't access the tool name because the `serialized` data is not available in those callbacks. As a result, they defaulted to:
```python
tool_name="unknown"  # Hardcoded fallback
```

### Impact

- All tool call events showed `tool_name: "unknown"` in traces
- Lost important debugging information about which tools were being invoked
- Made it impossible to track which tools succeeded vs failed

## Fix Applied

### 1. Added `_tool_names` Dictionary

Similar to how `_graph_names` and `_node_names` are tracked:

```python
def __init__(self, ...):
    # Track run_id -> span_id mapping
    self._run_spans: Dict[str, str] = {}
    
    # Track execution context
    self._graph_names: Dict[str, str] = {}  # run_id -> graph_name
    self._node_names: Dict[str, str] = {}  # run_id -> node_name
    self._tool_names: Dict[str, str] = {}  # run_id -> tool_name  ← ADDED
```

### 2. Store Tool Name in `on_tool_start`

```python
def on_tool_start(self, serialized, ...):
    tool_name = serialized.get("name", "unknown")
    
    # Store tool name for retrieval in on_tool_end/on_tool_error
    self._tool_names[self._get_run_key(run_id)] = tool_name  ← ADDED
    
    evt = evt_tool_start(..., tool_name=tool_name, ...)
    run.emit(evt)
```

### 3. Retrieve Tool Name in `on_tool_end`

```python
def on_tool_end(self, ...):
    # Retrieve stored tool name
    run_key = self._get_run_key(run_id)
    tool_name = self._tool_names.pop(run_key, "unknown")  ← ADDED
    
    evt = evt_tool_end(..., tool_name=tool_name, ...)
    run.emit(evt)
```

### 4. Retrieve Tool Name in `on_tool_error`

```python
def on_tool_error(self, ...):
    # Retrieve stored tool name
    run_key = self._get_run_key(run_id)
    tool_name = self._tool_names.pop(run_key, "unknown")  ← ADDED
    
    evt = evt_tool_end(..., tool_name=tool_name, error=error, ...)
    run.emit(evt)
```

## Key Design Decisions

### Why Use `pop()` Instead of `get()`?

We use `pop()` to **remove** the tool name after retrieval because:
1. **Memory cleanup**: Prevents the dictionary from growing indefinitely
2. **Correctness**: Each `start` should pair with one `end` or `error`
3. **Consistency**: Matches the pattern used for `_graph_names` and `_node_names`

### Why Store by `run_id` Key?

Each callback includes a unique `run_id` (UUID) that identifies that specific execution. By using this as the key:
- Works correctly with concurrent tool invocations
- No collision between different tool calls
- Automatic cleanup on `pop()`

### What About LangChain Handler?

The LangChain handler has a **different design**:
- It passes the full `serialized` data to normalization functions
- Tool name is extracted in `evt_tool_start()` from serialized data
- However, `evt_tool_end()` and `evt_tool_error()` don't receive serialized data

**This means the LangChain handler has a similar issue** - tool names are not tracked in end/error events. This should be fixed separately following the same pattern.

## Testing

Created `test_tool_name_tracking.py` to verify:
1. Tool names are captured in `tool.call.start` events
2. Tool names are captured in `tool.call.end` events (not "unknown")
3. Multiple tools can be tracked correctly
4. No memory leaks from uncleaned entries

## Files Modified

- `src/arzule_ingest/langgraph/callback_handler.py`
  - Added `_tool_names` dictionary
  - Store tool name in `on_tool_start`
  - Retrieve tool name in `on_tool_end`
  - Retrieve tool name in `on_tool_error`

## Before vs After

### Before (Broken)
```json
{
  "event_type": "tool.call.start",
  "attrs_compact": {"tool": "search_tool"},
  "summary": "tool call: search_tool"
}
{
  "event_type": "tool.call.end",
  "attrs_compact": {"tool": "unknown"},  ← Lost the name!
  "summary": "tool completed"
}
```

### After (Fixed)
```json
{
  "event_type": "tool.call.start",
  "attrs_compact": {"tool": "search_tool"},
  "summary": "tool call: search_tool"
}
{
  "event_type": "tool.call.end",
  "attrs_compact": {"tool": "search_tool"},  ← Name preserved!
  "summary": "tool completed: search_tool"
}
```

## Verification

To verify the fix:

```bash
cd arzuleIngestionWrapper
export ARZULE_API_KEY=your-key
export ARZULE_TENANT_ID=your-tenant-id
export ARZULE_PROJECT_ID=your-project-id

python test_tool_name_tracking.py
```

Expected output:
```
[test] Tool start: search_tool
[test] Tool start: calculator_tool
[test] Tool end: search_tool
[test] Tool end: calculator_tool
✓ Tool name tracking test passed
```

## Related Issues

This same pattern should be applied to:
- ✅ LangGraph handler (FIXED)
- ⚠️ LangChain handler (needs similar fix)
- ✅ CrewAI handler (already tracks tool/action names correctly)
- ✅ AutoGen handler (different architecture, doesn't have this issue)

