claude-memory-agent 3.0.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/installer.js +3 -1
- package/bin/lib/steps/advanced.js +1 -1
- package/config.py +10 -2
- package/dashboard.html +88 -38
- package/hooks/auto-detect-response.py +3 -6
- package/hooks/detect-correction.py +8 -7
- package/hooks/extract_memories.py +104 -0
- package/hooks/grounding-hook-v2.py +169 -33
- package/hooks/grounding-hook.py +19 -3
- package/hooks/log-tool-use.py +3 -6
- package/hooks/log-user-request.py +14 -6
- package/hooks/pre-tool-decision.py +3 -6
- package/hooks/pre_compact_hook.py +269 -5
- package/hooks/session_end_hook.py +37 -0
- package/hooks/session_start.py +10 -0
- package/hooks/stop_hook.py +315 -14
- package/install.py +141 -46
- package/main.py +522 -13
- package/mcp_proxy.py +93 -6
- package/package.json +2 -2
- package/services/agent_registry.py +260 -12
- package/services/database.py +453 -1
- package/services/embeddings.py +1 -1
- package/services/retry_queue.py +5 -1
- package/services/soul.py +791 -0
- package/services/vector_index.py +5 -1
- package/update_system.py +34 -8
package/mcp_proxy.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""Slim MCP proxy for Claude Memory.
|
|
2
2
|
|
|
3
|
-
Thin adapter that exposes
|
|
3
|
+
Thin adapter that exposes 4 unified tools over stdio JSON-RPC,
|
|
4
4
|
forwarding all work to the HTTP backend (main.py on port 8102).
|
|
5
5
|
|
|
6
6
|
NO embedding model loaded. NO database connection. Just HTTP calls.
|
|
7
7
|
|
|
8
8
|
Tools:
|
|
9
|
-
memory_ask
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
memory_store
|
|
13
|
-
|
|
9
|
+
memory_ask - Unified search (replaces memory_search, memory_search_patterns,
|
|
10
|
+
memory_context, memory_get_project, memory_active_sessions,
|
|
11
|
+
memory_session_catchup)
|
|
12
|
+
memory_store - Unified store (replaces memory_store, memory_store_pattern,
|
|
13
|
+
memory_store_project)
|
|
14
|
+
memory_resume - Complete catch-up package for session recovery after context
|
|
15
|
+
clear (goal, decisions, entities, workflows, memories, soul)
|
|
14
16
|
memory_status - Quick stats + project info (replaces memory_stats, memory_dashboard)
|
|
15
17
|
|
|
16
18
|
Usage:
|
|
@@ -180,6 +182,12 @@ async def memory_ask(
|
|
|
180
182
|
elif label == "sessions":
|
|
181
183
|
results["active_sessions"] = result.get("sessions", [])
|
|
182
184
|
|
|
185
|
+
# -- Soul context enrichment (lightweight DB read) --
|
|
186
|
+
if project_path:
|
|
187
|
+
soul_data = await _rest_get("/api/soul/brief", {"project_path": project_path})
|
|
188
|
+
if soul_data and soul_data.get("brief"):
|
|
189
|
+
results["soul_context"] = {"brief": soul_data["brief"]}
|
|
190
|
+
|
|
183
191
|
results["success"] = bool(results.get("memories") or results.get("patterns"))
|
|
184
192
|
return json.dumps(results, default=str)
|
|
185
193
|
|
|
@@ -262,6 +270,85 @@ async def memory_store(
|
|
|
262
270
|
return json.dumps(result or {"error": "Memory agent unavailable"}, default=str)
|
|
263
271
|
|
|
264
272
|
|
|
273
|
+
@mcp_server.tool()
|
|
274
|
+
async def memory_resume(
|
|
275
|
+
project_path: Optional[str] = None,
|
|
276
|
+
session_id: Optional[str] = None,
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Resume after context clear — get complete catch-up package in one call.
|
|
279
|
+
|
|
280
|
+
Call this when starting a fresh session or after context was compacted.
|
|
281
|
+
Returns: goal, decisions, entity registry, learned workflows,
|
|
282
|
+
recent memories, and soul brief — everything needed to continue working.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
project_path: Project path to resume context for
|
|
286
|
+
session_id: Optional specific session ID (uses latest for project if omitted)
|
|
287
|
+
"""
|
|
288
|
+
result = await _rest_post("/api/session/resume", {
|
|
289
|
+
"session_id": session_id or "",
|
|
290
|
+
"project_path": project_path or "",
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
if not result:
|
|
294
|
+
return json.dumps({
|
|
295
|
+
"success": False,
|
|
296
|
+
"error": "Memory agent unavailable - is main.py running on port 8102?",
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
# Format for readability
|
|
300
|
+
output: Dict[str, Any] = {"success": True}
|
|
301
|
+
|
|
302
|
+
# Session state
|
|
303
|
+
state = result.get("session_state")
|
|
304
|
+
if state and isinstance(state, dict):
|
|
305
|
+
output["goal"] = state.get("current_goal", "")
|
|
306
|
+
output["decisions"] = state.get("decisions_summary", "")
|
|
307
|
+
output["entity_registry"] = state.get("entity_registry", {})
|
|
308
|
+
output["pending"] = state.get("pending_questions", [])
|
|
309
|
+
|
|
310
|
+
# Checkpoint
|
|
311
|
+
cp = result.get("checkpoint")
|
|
312
|
+
if cp and isinstance(cp, dict):
|
|
313
|
+
output["checkpoint"] = {
|
|
314
|
+
"summary": cp.get("summary", ""),
|
|
315
|
+
"key_facts": cp.get("key_facts", []),
|
|
316
|
+
"decisions": cp.get("decisions", []),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Workflows
|
|
320
|
+
workflows = result.get("workflows")
|
|
321
|
+
if workflows and isinstance(workflows, list):
|
|
322
|
+
output["workflows"] = [
|
|
323
|
+
{
|
|
324
|
+
"name": wf.get("name", ""),
|
|
325
|
+
"commands": wf.get("commands", []),
|
|
326
|
+
"steps": wf.get("steps", []),
|
|
327
|
+
"success_count": wf.get("success_count", 0),
|
|
328
|
+
}
|
|
329
|
+
for wf in workflows[:10]
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
# Recent memories (compact)
|
|
333
|
+
memories = result.get("memories")
|
|
334
|
+
if memories and isinstance(memories, list):
|
|
335
|
+
output["recent_memories"] = [
|
|
336
|
+
{
|
|
337
|
+
"content": m.get("content", "")[:200],
|
|
338
|
+
"type": m.get("type", ""),
|
|
339
|
+
"importance": m.get("importance", 5),
|
|
340
|
+
}
|
|
341
|
+
for m in memories[:10]
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
# Soul brief
|
|
345
|
+
soul = result.get("soul_brief")
|
|
346
|
+
if soul:
|
|
347
|
+
output["soul_brief"] = soul
|
|
348
|
+
|
|
349
|
+
return json.dumps(output, default=str)
|
|
350
|
+
|
|
351
|
+
|
|
265
352
|
@mcp_server.tool()
|
|
266
353
|
async def memory_status(
|
|
267
354
|
project_path: Optional[str] = None,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory-agent",
|
|
3
|
-
"version": "3.0
|
|
4
|
-
"description": "Persistent semantic memory
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Persistent semantic memory for Claude Code with session recovery, self-reported learning, soul layer, and anti-hallucination grounding",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Agent Registry -
|
|
2
|
+
Agent Registry - Discovers real agents from disk + loads MCP/hook catalogs.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Agents are discovered from ~/.claude/agents/ (global) and
|
|
5
|
+
<project>/.claude/agents/ (per-project) by scanning .md files with YAML
|
|
6
|
+
frontmatter. Enabled agents live in the root; disabled agents live in
|
|
7
|
+
_disabled/<category>/ subdirectories.
|
|
8
|
+
|
|
9
|
+
MCP and hook metadata still comes from agent_catalog.json; dynamic loading
|
|
10
|
+
reads Claude Code settings files at runtime to check configured status.
|
|
7
11
|
"""
|
|
8
12
|
|
|
9
13
|
import json
|
|
10
14
|
import logging
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
11
17
|
from pathlib import Path
|
|
12
18
|
from typing import List, Dict, Any, Optional
|
|
13
19
|
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
14
22
|
logger = logging.getLogger(__name__)
|
|
15
23
|
|
|
16
24
|
# ---------------------------------------------------------------------------
|
|
@@ -24,8 +32,11 @@ _GLOBAL_SETTINGS_PATH = _HOME / ".claude" / "settings.json"
|
|
|
24
32
|
_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
25
33
|
_LOCAL_SETTINGS_PATH = _PROJECT_ROOT / ".claude" / "settings.local.json"
|
|
26
34
|
|
|
35
|
+
# Global agents directory
|
|
36
|
+
_GLOBAL_AGENTS_DIR = _HOME / ".claude" / "agents"
|
|
37
|
+
|
|
27
38
|
# ---------------------------------------------------------------------------
|
|
28
|
-
# Load static catalog from JSON
|
|
39
|
+
# Load static catalog from JSON (MCPs + hooks only now)
|
|
29
40
|
# ---------------------------------------------------------------------------
|
|
30
41
|
_CATALOG_PATH = Path(__file__).resolve().parent / "agent_catalog.json"
|
|
31
42
|
|
|
@@ -42,11 +53,249 @@ def _load_catalog() -> dict:
|
|
|
42
53
|
|
|
43
54
|
_catalog = _load_catalog()
|
|
44
55
|
|
|
56
|
+
# Categories are now auto-generated from discovered agents, but keep catalog
|
|
57
|
+
# ones as fallback for MCPs/hooks
|
|
45
58
|
AGENT_CATEGORIES = _catalog.get("categories", {})
|
|
46
|
-
AVAILABLE_AGENTS = _catalog.get("agents", [])
|
|
47
59
|
AVAILABLE_MCPS = _catalog.get("mcps", [])
|
|
48
60
|
AVAILABLE_HOOKS = _catalog.get("hooks", [])
|
|
49
61
|
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Category metadata (colors + icons for known categories)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
_CATEGORY_META = {
|
|
66
|
+
"design": {"name": "Design", "color": "#f778ba", "icon": "palette"},
|
|
67
|
+
"engineering": {"name": "Engineering", "color": "#58a6ff", "icon": "code"},
|
|
68
|
+
"marketing": {"name": "Marketing", "color": "#f0883e", "icon": "bullhorn"},
|
|
69
|
+
"product": {"name": "Product", "color": "#a371f7", "icon": "lightbulb"},
|
|
70
|
+
"project-management": {"name": "Project Management", "color": "#3fb950", "icon": "tasks"},
|
|
71
|
+
"spatial-computing": {"name": "Spatial Computing", "color": "#79c0ff", "icon": "cube"},
|
|
72
|
+
"specialized": {"name": "Specialized", "color": "#d2a8ff", "icon": "star"},
|
|
73
|
+
"support": {"name": "Support", "color": "#56d364", "icon": "headset"},
|
|
74
|
+
"testing": {"name": "Testing", "color": "#e3b341", "icon": "vial"},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Color palette for agent frontmatter colors
|
|
78
|
+
_COLOR_MAP = {
|
|
79
|
+
"blue": "#58a6ff",
|
|
80
|
+
"green": "#3fb950",
|
|
81
|
+
"red": "#f85149",
|
|
82
|
+
"purple": "#a371f7",
|
|
83
|
+
"orange": "#f0883e",
|
|
84
|
+
"yellow": "#e3b341",
|
|
85
|
+
"pink": "#f778ba",
|
|
86
|
+
"cyan": "#79c0ff",
|
|
87
|
+
"teal": "#2ea043",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Agent discovery from disk
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
def _parse_frontmatter(file_path: Path) -> Optional[Dict[str, Any]]:
|
|
95
|
+
"""Parse YAML frontmatter from an agent .md file."""
|
|
96
|
+
try:
|
|
97
|
+
text = file_path.read_text(encoding="utf-8")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(f"Could not read {file_path}: {e}")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Match --- delimited frontmatter
|
|
103
|
+
match = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
104
|
+
if not match:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
data = yaml.safe_load(match.group(1))
|
|
109
|
+
if not isinstance(data, dict):
|
|
110
|
+
return None
|
|
111
|
+
return data
|
|
112
|
+
except yaml.YAMLError as e:
|
|
113
|
+
logger.warning(f"Invalid YAML frontmatter in {file_path}: {e}")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _infer_category(filename_stem: str) -> str:
|
|
118
|
+
"""Infer category from filename prefix like 'engineering-senior-developer'."""
|
|
119
|
+
known = [
|
|
120
|
+
"design", "engineering", "marketing", "product",
|
|
121
|
+
"project-management", "spatial-computing", "specialized",
|
|
122
|
+
"support", "testing",
|
|
123
|
+
]
|
|
124
|
+
for cat in known:
|
|
125
|
+
if filename_stem.startswith(cat):
|
|
126
|
+
return cat
|
|
127
|
+
# Try first word as category
|
|
128
|
+
parts = filename_stem.split("-")
|
|
129
|
+
if parts:
|
|
130
|
+
return parts[0]
|
|
131
|
+
return "uncategorized"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _resolve_color(color_str: Optional[str]) -> str:
|
|
135
|
+
"""Resolve a color name or hex value to a hex color."""
|
|
136
|
+
if not color_str:
|
|
137
|
+
return "#8b949e"
|
|
138
|
+
if color_str.startswith("#"):
|
|
139
|
+
return color_str
|
|
140
|
+
return _COLOR_MAP.get(color_str.lower(), "#8b949e")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse_agent_file(
|
|
144
|
+
file_path: Path,
|
|
145
|
+
enabled: bool,
|
|
146
|
+
scope: str,
|
|
147
|
+
category: Optional[str] = None,
|
|
148
|
+
base_dir: Optional[Path] = None,
|
|
149
|
+
) -> Optional[Dict[str, Any]]:
|
|
150
|
+
"""Parse a single agent .md file into an agent dict."""
|
|
151
|
+
fm = _parse_frontmatter(file_path)
|
|
152
|
+
if fm is None:
|
|
153
|
+
# Minimal entry for files without valid frontmatter
|
|
154
|
+
fm = {}
|
|
155
|
+
|
|
156
|
+
stem = file_path.stem
|
|
157
|
+
agent_id = stem
|
|
158
|
+
|
|
159
|
+
name = fm.get("name", stem.replace("-", " ").replace("_", " ").title())
|
|
160
|
+
description = fm.get("description", "").replace("\\n", " ").strip()
|
|
161
|
+
color = _resolve_color(fm.get("color"))
|
|
162
|
+
|
|
163
|
+
if not category:
|
|
164
|
+
category = _infer_category(stem)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"id": agent_id,
|
|
168
|
+
"name": name,
|
|
169
|
+
"description": description,
|
|
170
|
+
"color": color,
|
|
171
|
+
"category": category,
|
|
172
|
+
"enabled": enabled,
|
|
173
|
+
"scope": scope,
|
|
174
|
+
"file_path": str(file_path),
|
|
175
|
+
"filename": file_path.name,
|
|
176
|
+
"base_dir": str(base_dir or file_path.parent),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _scan_agent_dir(base_dir: Path, scope: str) -> List[Dict[str, Any]]:
|
|
181
|
+
"""Scan an agents directory for enabled and disabled agents."""
|
|
182
|
+
agents = []
|
|
183
|
+
if not base_dir.exists():
|
|
184
|
+
return agents
|
|
185
|
+
|
|
186
|
+
# Enabled agents: .md files directly in base_dir
|
|
187
|
+
for f in sorted(base_dir.glob("*.md")):
|
|
188
|
+
if f.name.lower() in ("readme.md", "readme.txt"):
|
|
189
|
+
continue
|
|
190
|
+
agent = _parse_agent_file(
|
|
191
|
+
f, enabled=True, scope=scope, base_dir=base_dir
|
|
192
|
+
)
|
|
193
|
+
if agent:
|
|
194
|
+
agents.append(agent)
|
|
195
|
+
|
|
196
|
+
# Disabled agents: .md files in _disabled/ subdirectories
|
|
197
|
+
disabled_dir = base_dir / "_disabled"
|
|
198
|
+
if disabled_dir.exists():
|
|
199
|
+
for f in sorted(disabled_dir.rglob("*.md")):
|
|
200
|
+
if f.name.lower() in ("readme.md", "readme.txt"):
|
|
201
|
+
continue
|
|
202
|
+
# Category = parent dir name if it's a subdirectory of _disabled
|
|
203
|
+
if f.parent != disabled_dir:
|
|
204
|
+
category = f.parent.name
|
|
205
|
+
else:
|
|
206
|
+
category = _infer_category(f.stem)
|
|
207
|
+
agent = _parse_agent_file(
|
|
208
|
+
f, enabled=False, scope=scope,
|
|
209
|
+
category=category, base_dir=base_dir,
|
|
210
|
+
)
|
|
211
|
+
if agent:
|
|
212
|
+
agents.append(agent)
|
|
213
|
+
|
|
214
|
+
return agents
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def discover_agents(project_path: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
218
|
+
"""Discover all agents from global and project directories.
|
|
219
|
+
|
|
220
|
+
Returns a list of agent dicts with id, name, description, color,
|
|
221
|
+
category, enabled, scope, file_path, filename, base_dir.
|
|
222
|
+
"""
|
|
223
|
+
agents = []
|
|
224
|
+
|
|
225
|
+
# 1. Global agents: ~/.claude/agents/
|
|
226
|
+
agents += _scan_agent_dir(_GLOBAL_AGENTS_DIR, scope="global")
|
|
227
|
+
|
|
228
|
+
# 2. Project agents: <project>/.claude/agents/
|
|
229
|
+
if project_path:
|
|
230
|
+
project_dir = Path(project_path) / ".claude" / "agents"
|
|
231
|
+
agents += _scan_agent_dir(project_dir, scope="project")
|
|
232
|
+
|
|
233
|
+
return agents
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def discover_categories(agents: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
|
237
|
+
"""Build categories dict from discovered agents."""
|
|
238
|
+
cats: Dict[str, Dict[str, Any]] = {}
|
|
239
|
+
for agent in agents:
|
|
240
|
+
cat_key = agent["category"]
|
|
241
|
+
if cat_key not in cats:
|
|
242
|
+
meta = _CATEGORY_META.get(cat_key, {
|
|
243
|
+
"name": cat_key.replace("-", " ").replace("_", " ").title(),
|
|
244
|
+
"color": "#8b949e",
|
|
245
|
+
"icon": "circle",
|
|
246
|
+
})
|
|
247
|
+
cats[cat_key] = meta
|
|
248
|
+
return cats
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def find_agent_by_id(
|
|
252
|
+
agent_id: str, project_path: Optional[str] = None
|
|
253
|
+
) -> Optional[Dict[str, Any]]:
|
|
254
|
+
"""Find a specific agent by ID across all directories."""
|
|
255
|
+
for agent in discover_agents(project_path):
|
|
256
|
+
if agent["id"] == agent_id:
|
|
257
|
+
return agent
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def toggle_agent(agent_id: str, enabled: bool, project_path: Optional[str] = None) -> Dict[str, Any]:
|
|
262
|
+
"""Toggle an agent between enabled and disabled state.
|
|
263
|
+
|
|
264
|
+
- Enable: move from _disabled/<category>/ to agents/ root
|
|
265
|
+
- Disable: move from agents/ root to _disabled/<category>/
|
|
266
|
+
|
|
267
|
+
Returns the updated agent dict.
|
|
268
|
+
"""
|
|
269
|
+
agent = find_agent_by_id(agent_id, project_path)
|
|
270
|
+
if not agent:
|
|
271
|
+
raise FileNotFoundError(f"Agent '{agent_id}' not found")
|
|
272
|
+
|
|
273
|
+
src = Path(agent["file_path"])
|
|
274
|
+
base = Path(agent["base_dir"])
|
|
275
|
+
|
|
276
|
+
if enabled and not agent["enabled"]:
|
|
277
|
+
# Move from _disabled/<category>/ to agents/ root
|
|
278
|
+
dest = base / agent["filename"]
|
|
279
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
shutil.move(str(src), str(dest))
|
|
281
|
+
agent["file_path"] = str(dest)
|
|
282
|
+
agent["enabled"] = True
|
|
283
|
+
elif not enabled and agent["enabled"]:
|
|
284
|
+
# Move from agents/ root to _disabled/<category>/
|
|
285
|
+
category = agent["category"]
|
|
286
|
+
category_dir = base / "_disabled" / category
|
|
287
|
+
category_dir.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
dest = category_dir / agent["filename"]
|
|
289
|
+
shutil.move(str(src), str(dest))
|
|
290
|
+
agent["file_path"] = str(dest)
|
|
291
|
+
agent["enabled"] = False
|
|
292
|
+
|
|
293
|
+
return agent
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Backward-compatible: AVAILABLE_AGENTS as lazy discovery result
|
|
297
|
+
AVAILABLE_AGENTS = discover_agents()
|
|
298
|
+
|
|
50
299
|
|
|
51
300
|
# ---------------------------------------------------------------------------
|
|
52
301
|
# Helper: derive a hook ID from its command string
|
|
@@ -249,10 +498,12 @@ def load_configured_mcps(
|
|
|
249
498
|
# Helper functions (unchanged interface)
|
|
250
499
|
# ---------------------------------------------------------------------------
|
|
251
500
|
|
|
252
|
-
def get_agents_by_category():
|
|
501
|
+
def get_agents_by_category(agents: Optional[List[Dict[str, Any]]] = None):
|
|
253
502
|
"""Group agents by category."""
|
|
503
|
+
if agents is None:
|
|
504
|
+
agents = discover_agents()
|
|
254
505
|
result = {}
|
|
255
|
-
for agent in
|
|
506
|
+
for agent in agents:
|
|
256
507
|
cat = agent["category"]
|
|
257
508
|
if cat not in result:
|
|
258
509
|
result[cat] = []
|
|
@@ -262,7 +513,4 @@ def get_agents_by_category():
|
|
|
262
513
|
|
|
263
514
|
def get_agent_by_id(agent_id: str):
|
|
264
515
|
"""Get agent by ID."""
|
|
265
|
-
|
|
266
|
-
if agent["id"] == agent_id:
|
|
267
|
-
return agent
|
|
268
|
-
return None
|
|
516
|
+
return find_agent_by_id(agent_id)
|