-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent_tracker.py
More file actions
127 lines (99 loc) · 3.43 KB
/
agent_tracker.py
File metadata and controls
127 lines (99 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/env python3
"""Track background agents across /clear boundaries.
Runs in TWO modes:
- PreToolUse (Agent): log agent spawn with description + prompt snippet
- SubagentStop: update agent entry with completion status
Tracker file persists across /clear so new sessions can see what was running.
Memory inject hook reads this file to inform new Claude about active/completed agents.
"""
import json
import os
import sys
import time
from pathlib import Path
TRACKER_FILE = Path("/tmp/claude_agent_tracker.json")
MAX_PROMPT_SNIPPET = 150
MAX_AGENTS = 20 # keep last N entries
STALE_HOURS = 2 # remove entries older than this
def _load():
if TRACKER_FILE.exists():
try:
return json.loads(TRACKER_FILE.read_text())
except (json.JSONDecodeError, OSError):
pass
return {"agents": []}
def _save(data):
# Prune stale entries
cutoff = time.time() - STALE_HOURS * 3600
data["agents"] = [a for a in data["agents"] if a.get("started", 0) > cutoff]
# Keep only last N
data["agents"] = data["agents"][-MAX_AGENTS:]
tmp = TRACKER_FILE.with_suffix(".tmp")
tmp.write_text(json.dumps(data, indent=2))
tmp.rename(TRACKER_FILE)
def _handle_spawn(input_data):
"""PreToolUse Agent: log new background agent."""
tool_input = input_data.get("tool_input", {})
# Only track background agents
if not tool_input.get("run_in_background"):
print("{}")
return
desc = tool_input.get("description", "unknown task")
prompt = tool_input.get("prompt", "")[:MAX_PROMPT_SNIPPET]
model = tool_input.get("model", "default")
data = _load()
data["agents"].append({
"description": desc,
"prompt_snippet": prompt,
"model": model,
"started": time.time(),
"status": "running",
"tty": os.environ.get("CLAUDE_TTY_ID", "?"),
})
_save(data)
print("{}")
def _handle_stop(input_data):
"""SubagentStop: mark most recent matching agent as done."""
data = _load()
# Mark the oldest "running" agent as completed
# (SubagentStop doesn't give us much to match on, so FIFO)
for agent in data["agents"]:
if agent.get("status") == "running":
agent["status"] = "completed"
agent["finished"] = time.time()
break
_save(data)
print("{}")
def get_active_agents():
"""Called by memory inject hook to get context about recent agents."""
data = _load()
if not data["agents"]:
return None
lines = []
for a in data["agents"]:
status = a.get("status", "?")
desc = a.get("description", "?")
elapsed = time.time() - a.get("started", time.time())
mins = int(elapsed / 60)
if status == "running":
lines.append(f" - RUNNING ({mins}m): {desc} — {a.get('prompt_snippet', '')}")
elif status == "completed":
lines.append(f" - DONE ({mins}m ago): {desc}")
if not lines:
return None
return "Background agents from previous context:\n" + "\n".join(lines)
def main():
try:
input_data = json.load(sys.stdin)
except (json.JSONDecodeError, EOFError):
print("{}")
return
# Detect mode
tool_name = input_data.get("tool_name", "")
if tool_name == "Agent":
_handle_spawn(input_data)
else:
# SubagentStop mode (no tool_name, or different format)
_handle_stop(input_data)
if __name__ == "__main__":
main()