claude-mpm 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
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.
- claude_mpm/_version.py +4 -34
- claude_mpm/agents/INSTRUCTIONS.md +1 -16
- claude_mpm/agents/templates/research.json +53 -85
- claude_mpm/cli.py +80 -11
- claude_mpm/core/simple_runner.py +45 -5
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -1
- claude_mpm/services/agent_capabilities_generator.py +182 -0
- claude_mpm/services/agent_deployment.py +228 -37
- claude_mpm/services/deployed_agent_discovery.py +222 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +29 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +29 -7
- claude_mpm/utils/framework_detection.py +39 -0
- {claude_mpm-2.0.0.dist-info → claude_mpm-2.1.0.dist-info}/METADATA +9 -1
- {claude_mpm-2.0.0.dist-info → claude_mpm-2.1.0.dist-info}/RECORD +17 -14
- {claude_mpm-2.0.0.dist-info → claude_mpm-2.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-2.0.0.dist-info → claude_mpm-2.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-2.0.0.dist-info → claude_mpm-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Agent Capabilities Content Generator.
|
|
2
|
+
|
|
3
|
+
This service generates markdown content for agent capabilities section
|
|
4
|
+
from discovered deployed agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from jinja2 import Template
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentCapabilitiesGenerator:
|
|
16
|
+
"""Generates markdown content for agent capabilities section."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize the generator with default template."""
|
|
20
|
+
self.template = self._load_template()
|
|
21
|
+
logger.debug("Initialized AgentCapabilitiesGenerator")
|
|
22
|
+
|
|
23
|
+
def generate_capabilities_section(self, deployed_agents: List[Dict[str, Any]]) -> str:
|
|
24
|
+
"""Generate the complete agent capabilities markdown section.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
deployed_agents: List of agent information dictionaries
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Generated markdown content for agent capabilities
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
# Group agents by source tier for organized display
|
|
34
|
+
agents_by_tier = self._group_by_tier(deployed_agents)
|
|
35
|
+
|
|
36
|
+
# Generate core agent list
|
|
37
|
+
core_agent_list = self._generate_core_agent_list(deployed_agents)
|
|
38
|
+
|
|
39
|
+
# Generate detailed capabilities
|
|
40
|
+
detailed_capabilities = self._generate_detailed_capabilities(deployed_agents)
|
|
41
|
+
|
|
42
|
+
# Render template
|
|
43
|
+
content = self.template.render(
|
|
44
|
+
core_agents=core_agent_list,
|
|
45
|
+
detailed_capabilities=detailed_capabilities,
|
|
46
|
+
agents_by_tier=agents_by_tier,
|
|
47
|
+
total_agents=len(deployed_agents)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger.info(f"Generated capabilities section for {len(deployed_agents)} agents")
|
|
51
|
+
return content
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Failed to generate capabilities section: {e}")
|
|
55
|
+
# Return fallback content on error
|
|
56
|
+
return self._generate_fallback_content()
|
|
57
|
+
|
|
58
|
+
def _group_by_tier(self, agents: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
59
|
+
"""Group agents by their source tier.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
agents: List of agent information dictionaries
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Dictionary mapping tiers to lists of agents
|
|
66
|
+
"""
|
|
67
|
+
tiers = {'system': [], 'user': [], 'project': []}
|
|
68
|
+
|
|
69
|
+
for agent in agents:
|
|
70
|
+
tier = agent.get('source_tier', 'system')
|
|
71
|
+
if tier in tiers:
|
|
72
|
+
tiers[tier].append(agent)
|
|
73
|
+
else:
|
|
74
|
+
# Handle unknown tiers gracefully
|
|
75
|
+
tiers['system'].append(agent)
|
|
76
|
+
logger.warning(f"Unknown source tier '{tier}' for agent {agent.get('id')}, defaulting to system")
|
|
77
|
+
|
|
78
|
+
return tiers
|
|
79
|
+
|
|
80
|
+
def _generate_core_agent_list(self, agents: List[Dict[str, Any]]) -> str:
|
|
81
|
+
"""Generate comma-separated list of core agent IDs.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
agents: List of agent information dictionaries
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Comma-separated string of agent IDs
|
|
88
|
+
"""
|
|
89
|
+
agent_ids = [agent['id'] for agent in agents]
|
|
90
|
+
return ', '.join(sorted(agent_ids))
|
|
91
|
+
|
|
92
|
+
def _generate_detailed_capabilities(self, agents: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
|
93
|
+
"""Generate detailed capability descriptions for each agent.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
agents: List of agent information dictionaries
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of capability dictionaries for template rendering
|
|
100
|
+
"""
|
|
101
|
+
capabilities = []
|
|
102
|
+
|
|
103
|
+
for agent in sorted(agents, key=lambda a: a['id']):
|
|
104
|
+
# Extract key capabilities
|
|
105
|
+
specializations = agent.get('specializations', [])
|
|
106
|
+
when_to_use = agent.get('capabilities', {}).get('when_to_use', [])
|
|
107
|
+
|
|
108
|
+
# Create capability summary
|
|
109
|
+
if when_to_use:
|
|
110
|
+
capability_text = '; '.join(when_to_use[:2]) # First 2 items
|
|
111
|
+
elif specializations:
|
|
112
|
+
capability_text = ', '.join(specializations[:3]) # First 3 specializations
|
|
113
|
+
else:
|
|
114
|
+
capability_text = agent.get('description', 'General purpose agent')
|
|
115
|
+
|
|
116
|
+
# Truncate long capability text
|
|
117
|
+
if len(capability_text) > 100:
|
|
118
|
+
capability_text = capability_text[:97] + '...'
|
|
119
|
+
|
|
120
|
+
capabilities.append({
|
|
121
|
+
'name': agent['name'],
|
|
122
|
+
'id': agent['id'],
|
|
123
|
+
'capability_text': capability_text,
|
|
124
|
+
'tools': ', '.join(agent.get('tools', [])[:5]) # First 5 tools
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return capabilities
|
|
128
|
+
|
|
129
|
+
def _load_template(self) -> Template:
|
|
130
|
+
"""Load the Jinja2 template for agent capabilities.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Configured Jinja2 template
|
|
134
|
+
"""
|
|
135
|
+
template_content = """
|
|
136
|
+
## Agent Names & Capabilities
|
|
137
|
+
**Core Agents**: {{ core_agents }}
|
|
138
|
+
|
|
139
|
+
{% if agents_by_tier.project %}
|
|
140
|
+
### Project-Specific Agents
|
|
141
|
+
{% for agent in agents_by_tier.project %}
|
|
142
|
+
- **{{ agent.name }}** ({{ agent.id }}): {{ agent.description }}
|
|
143
|
+
{% endfor %}
|
|
144
|
+
|
|
145
|
+
{% endif %}
|
|
146
|
+
**Agent Capabilities**:
|
|
147
|
+
{% for cap in detailed_capabilities %}
|
|
148
|
+
- **{{ cap.name }}**: {{ cap.capability_text }}
|
|
149
|
+
{% endfor %}
|
|
150
|
+
|
|
151
|
+
**Agent Name Formats** (both valid):
|
|
152
|
+
- Capitalized: {{ detailed_capabilities | map(attribute='name') | join('", "') }}
|
|
153
|
+
- Lowercase-hyphenated: {{ detailed_capabilities | map(attribute='id') | join('", "') }}
|
|
154
|
+
|
|
155
|
+
*Generated from {{ total_agents }} deployed agents*
|
|
156
|
+
""".strip()
|
|
157
|
+
|
|
158
|
+
return Template(template_content)
|
|
159
|
+
|
|
160
|
+
def _generate_fallback_content(self) -> str:
|
|
161
|
+
"""Generate fallback content when agent discovery fails.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Static fallback markdown content
|
|
165
|
+
"""
|
|
166
|
+
logger.warning("Using fallback content due to generation failure")
|
|
167
|
+
return """
|
|
168
|
+
## Agent Names & Capabilities
|
|
169
|
+
**Core Agents**: research, engineer, qa, documentation, security, ops, version_control, data_engineer
|
|
170
|
+
|
|
171
|
+
**Agent Capabilities**:
|
|
172
|
+
- **Research**: Codebase analysis, best practices, technical investigation
|
|
173
|
+
- **Engineer**: Implementation, refactoring, debugging
|
|
174
|
+
- **QA**: Quality assurance, testing, code review
|
|
175
|
+
- **Documentation**: Technical writing, API docs, user guides
|
|
176
|
+
- **Security**: Security analysis, vulnerability assessment
|
|
177
|
+
- **Ops**: Operations, deployment, infrastructure
|
|
178
|
+
- **Version Control**: Git operations, branch management
|
|
179
|
+
- **Data Engineer**: Data pipelines, ETL, database operations
|
|
180
|
+
|
|
181
|
+
*Note: Unable to dynamically generate agent list. Using default agents.*
|
|
182
|
+
""".strip()
|
|
@@ -63,6 +63,7 @@ class AgentDeploymentService:
|
|
|
63
63
|
"errors": [],
|
|
64
64
|
"skipped": [],
|
|
65
65
|
"updated": [],
|
|
66
|
+
"migrated": [], # Track agents migrated from old format
|
|
66
67
|
"total": 0
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -87,28 +88,37 @@ class AgentDeploymentService:
|
|
|
87
88
|
try:
|
|
88
89
|
import json
|
|
89
90
|
base_agent_data = json.loads(self.base_agent_path.read_text())
|
|
90
|
-
|
|
91
|
-
self.
|
|
91
|
+
# Handle both 'base_version' (new format) and 'version' (old format)
|
|
92
|
+
base_agent_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
|
|
93
|
+
self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
|
|
92
94
|
except Exception as e:
|
|
93
95
|
self.logger.warning(f"Could not load base agent: {e}")
|
|
94
96
|
|
|
95
97
|
# Get all template files
|
|
96
|
-
template_files = list(self.templates_dir.glob("
|
|
98
|
+
template_files = list(self.templates_dir.glob("*.json"))
|
|
99
|
+
# Filter out non-agent files
|
|
100
|
+
template_files = [f for f in template_files if f.stem != "__init__" and not f.stem.startswith(".")]
|
|
97
101
|
results["total"] = len(template_files)
|
|
98
102
|
|
|
99
103
|
for template_file in template_files:
|
|
100
104
|
try:
|
|
101
|
-
agent_name = template_file.stem
|
|
105
|
+
agent_name = template_file.stem
|
|
102
106
|
target_file = target_dir / f"{agent_name}.md"
|
|
103
107
|
|
|
104
108
|
# Check if agent needs update
|
|
105
109
|
needs_update = force_rebuild
|
|
110
|
+
is_migration = False
|
|
106
111
|
if not needs_update and target_file.exists():
|
|
107
112
|
needs_update, reason = self._check_agent_needs_update(
|
|
108
113
|
target_file, template_file, base_agent_version
|
|
109
114
|
)
|
|
110
115
|
if needs_update:
|
|
111
|
-
|
|
116
|
+
# Check if this is a migration from old format
|
|
117
|
+
if "migration needed" in reason:
|
|
118
|
+
is_migration = True
|
|
119
|
+
self.logger.info(f"Migrating agent {agent_name}: {reason}")
|
|
120
|
+
else:
|
|
121
|
+
self.logger.info(f"Agent {agent_name} needs update: {reason}")
|
|
112
122
|
|
|
113
123
|
# Skip if exists and doesn't need update
|
|
114
124
|
if target_file.exists() and not needs_update:
|
|
@@ -123,7 +133,15 @@ class AgentDeploymentService:
|
|
|
123
133
|
is_update = target_file.exists()
|
|
124
134
|
target_file.write_text(agent_md)
|
|
125
135
|
|
|
126
|
-
if
|
|
136
|
+
if is_migration:
|
|
137
|
+
results["migrated"].append({
|
|
138
|
+
"name": agent_name,
|
|
139
|
+
"template": str(template_file),
|
|
140
|
+
"target": str(target_file),
|
|
141
|
+
"reason": reason
|
|
142
|
+
})
|
|
143
|
+
self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
|
|
144
|
+
elif is_update:
|
|
127
145
|
results["updated"].append({
|
|
128
146
|
"name": agent_name,
|
|
129
147
|
"template": str(template_file),
|
|
@@ -146,6 +164,7 @@ class AgentDeploymentService:
|
|
|
146
164
|
self.logger.info(
|
|
147
165
|
f"Deployed {len(results['deployed'])} agents, "
|
|
148
166
|
f"updated {len(results['updated'])}, "
|
|
167
|
+
f"migrated {len(results['migrated'])}, "
|
|
149
168
|
f"skipped {len(results['skipped'])}, "
|
|
150
169
|
f"errors: {len(results['errors'])}"
|
|
151
170
|
)
|
|
@@ -194,9 +213,14 @@ class AgentDeploymentService:
|
|
|
194
213
|
template_data = json.loads(template_path.read_text())
|
|
195
214
|
|
|
196
215
|
# Extract basic info
|
|
197
|
-
agent_version
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
# Handle both 'agent_version' (new format) and 'version' (old format)
|
|
217
|
+
agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
|
|
218
|
+
base_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
|
|
219
|
+
|
|
220
|
+
# Format version string as semantic version
|
|
221
|
+
# Combine base and agent versions for a unified semantic version
|
|
222
|
+
# Use agent version as primary, with base version in metadata
|
|
223
|
+
version_string = self._format_version_display(agent_version)
|
|
200
224
|
|
|
201
225
|
# Build YAML frontmatter
|
|
202
226
|
description = (
|
|
@@ -219,6 +243,10 @@ author: "{template_data.get('author', 'claude-mpm@anthropic.com')}"
|
|
|
219
243
|
created: "{datetime.now().isoformat()}Z"
|
|
220
244
|
updated: "{datetime.now().isoformat()}Z"
|
|
221
245
|
tags: {tags}
|
|
246
|
+
metadata:
|
|
247
|
+
base_version: "{self._format_version_display(base_version)}"
|
|
248
|
+
agent_version: "{self._format_version_display(agent_version)}"
|
|
249
|
+
deployment_type: "system"
|
|
222
250
|
---
|
|
223
251
|
|
|
224
252
|
"""
|
|
@@ -253,11 +281,12 @@ tags: {tags}
|
|
|
253
281
|
template_data = json.loads(template_path.read_text())
|
|
254
282
|
|
|
255
283
|
# Extract versions
|
|
256
|
-
agent_version
|
|
257
|
-
|
|
284
|
+
# Handle both 'agent_version' (new format) and 'version' (old format)
|
|
285
|
+
agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
|
|
286
|
+
base_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
|
|
258
287
|
|
|
259
|
-
#
|
|
260
|
-
version_string =
|
|
288
|
+
# Use semantic version format
|
|
289
|
+
version_string = self._format_version_display(agent_version)
|
|
261
290
|
|
|
262
291
|
# Merge narrative fields (base + agent specific)
|
|
263
292
|
narrative_fields = self._merge_narrative_fields(base_agent_data, template_data)
|
|
@@ -317,8 +346,8 @@ capabilities:
|
|
|
317
346
|
# Agent Metadata
|
|
318
347
|
metadata:
|
|
319
348
|
source: "claude-mpm"
|
|
320
|
-
template_version: {agent_version}
|
|
321
|
-
base_version: {base_version}
|
|
349
|
+
template_version: "{self._format_version_display(agent_version)}"
|
|
350
|
+
base_version: "{self._format_version_display(base_version)}"
|
|
322
351
|
deployment_type: "system"
|
|
323
352
|
|
|
324
353
|
...
|
|
@@ -441,6 +470,7 @@ metadata:
|
|
|
441
470
|
results = {
|
|
442
471
|
"config_dir": str(config_dir),
|
|
443
472
|
"agents_found": [],
|
|
473
|
+
"agents_needing_migration": [],
|
|
444
474
|
"environment": {},
|
|
445
475
|
"warnings": []
|
|
446
476
|
}
|
|
@@ -469,11 +499,19 @@ metadata:
|
|
|
469
499
|
"path": str(agent_file)
|
|
470
500
|
}
|
|
471
501
|
|
|
472
|
-
# Extract name from YAML frontmatter
|
|
502
|
+
# Extract name and version from YAML frontmatter
|
|
503
|
+
version_str = None
|
|
473
504
|
for line in lines:
|
|
474
505
|
if line.startswith("name:"):
|
|
475
506
|
agent_info["name"] = line.split(":", 1)[1].strip().strip('"\'')
|
|
476
|
-
|
|
507
|
+
elif line.startswith("version:"):
|
|
508
|
+
version_str = line.split(":", 1)[1].strip().strip('"\'')
|
|
509
|
+
agent_info["version"] = version_str
|
|
510
|
+
|
|
511
|
+
# Check if agent needs migration
|
|
512
|
+
if version_str and self._is_old_version_format(version_str):
|
|
513
|
+
agent_info["needs_migration"] = True
|
|
514
|
+
results["agents_needing_migration"].append(agent_info["name"])
|
|
477
515
|
|
|
478
516
|
results["agents_found"].append(agent_info)
|
|
479
517
|
|
|
@@ -504,11 +542,13 @@ metadata:
|
|
|
504
542
|
self.logger.warning(f"Templates directory not found: {self.templates_dir}")
|
|
505
543
|
return agents
|
|
506
544
|
|
|
507
|
-
template_files = sorted(self.templates_dir.glob("
|
|
545
|
+
template_files = sorted(self.templates_dir.glob("*.json"))
|
|
546
|
+
# Filter out non-agent files
|
|
547
|
+
template_files = [f for f in template_files if f.stem != "__init__" and not f.stem.startswith(".")]
|
|
508
548
|
|
|
509
549
|
for template_file in template_files:
|
|
510
550
|
try:
|
|
511
|
-
agent_name = template_file.stem
|
|
551
|
+
agent_name = template_file.stem
|
|
512
552
|
agent_info = {
|
|
513
553
|
"name": agent_name,
|
|
514
554
|
"file": template_file.name,
|
|
@@ -521,11 +561,22 @@ metadata:
|
|
|
521
561
|
try:
|
|
522
562
|
import json
|
|
523
563
|
template_data = json.loads(template_file.read_text())
|
|
524
|
-
config_fields = template_data.get('configuration_fields', {})
|
|
525
564
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
565
|
+
# Handle different schema formats
|
|
566
|
+
if 'metadata' in template_data:
|
|
567
|
+
# New schema format
|
|
568
|
+
metadata = template_data.get('metadata', {})
|
|
569
|
+
agent_info["description"] = metadata.get('description', agent_info["description"])
|
|
570
|
+
agent_info["role"] = metadata.get('specializations', [''])[0] if metadata.get('specializations') else ''
|
|
571
|
+
elif 'configuration_fields' in template_data:
|
|
572
|
+
# Old schema format
|
|
573
|
+
config_fields = template_data.get('configuration_fields', {})
|
|
574
|
+
agent_info["role"] = config_fields.get('primary_role', '')
|
|
575
|
+
agent_info["description"] = config_fields.get('description', agent_info["description"])
|
|
576
|
+
|
|
577
|
+
# Handle both 'agent_version' (new format) and 'version' (old format)
|
|
578
|
+
version_tuple = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
|
|
579
|
+
agent_info["version"] = self._format_version_display(version_tuple)
|
|
529
580
|
|
|
530
581
|
except Exception:
|
|
531
582
|
pass # Use defaults if can't parse
|
|
@@ -537,7 +588,7 @@ metadata:
|
|
|
537
588
|
|
|
538
589
|
return agents
|
|
539
590
|
|
|
540
|
-
def _check_agent_needs_update(self, deployed_file: Path, template_file: Path, current_base_version:
|
|
591
|
+
def _check_agent_needs_update(self, deployed_file: Path, template_file: Path, current_base_version: tuple) -> tuple:
|
|
541
592
|
"""
|
|
542
593
|
Check if a deployed agent needs to be updated.
|
|
543
594
|
|
|
@@ -554,32 +605,82 @@ metadata:
|
|
|
554
605
|
deployed_content = deployed_file.read_text()
|
|
555
606
|
|
|
556
607
|
# Check if it's a system agent (authored by claude-mpm)
|
|
557
|
-
if "
|
|
608
|
+
if "claude-mpm" not in deployed_content:
|
|
558
609
|
return (False, "not a system agent")
|
|
559
610
|
|
|
560
611
|
# Extract version info from YAML frontmatter
|
|
561
612
|
import re
|
|
562
613
|
|
|
563
|
-
#
|
|
564
|
-
|
|
565
|
-
|
|
614
|
+
# Check if using old serial format first
|
|
615
|
+
is_old_format = False
|
|
616
|
+
old_version_str = None
|
|
617
|
+
|
|
618
|
+
# Try legacy combined format (e.g., "0002-0005")
|
|
619
|
+
legacy_match = re.search(r'^version:\s*["\']?(\d+)-(\d+)["\']?', deployed_content, re.MULTILINE)
|
|
620
|
+
if legacy_match:
|
|
621
|
+
is_old_format = True
|
|
622
|
+
old_version_str = f"{legacy_match.group(1)}-{legacy_match.group(2)}"
|
|
623
|
+
# Convert legacy format to semantic version
|
|
624
|
+
# Treat the agent version (second number) as minor version
|
|
625
|
+
deployed_agent_version = (0, int(legacy_match.group(2)), 0)
|
|
626
|
+
self.logger.info(f"Detected old serial version format: {old_version_str}")
|
|
627
|
+
else:
|
|
628
|
+
# Try to extract semantic version format (e.g., "2.1.0")
|
|
629
|
+
version_match = re.search(r'^version:\s*["\']?v?(\d+)\.(\d+)\.(\d+)["\']?', deployed_content, re.MULTILINE)
|
|
630
|
+
if version_match:
|
|
631
|
+
deployed_agent_version = (int(version_match.group(1)), int(version_match.group(2)), int(version_match.group(3)))
|
|
632
|
+
else:
|
|
633
|
+
# Fallback: try separate fields (very old format)
|
|
634
|
+
agent_version_match = re.search(r"^agent_version:\s*(\d+)", deployed_content, re.MULTILINE)
|
|
635
|
+
if agent_version_match:
|
|
636
|
+
is_old_format = True
|
|
637
|
+
old_version_str = f"agent_version: {agent_version_match.group(1)}"
|
|
638
|
+
deployed_agent_version = (0, int(agent_version_match.group(1)), 0)
|
|
639
|
+
self.logger.info(f"Detected old separate version format: {old_version_str}")
|
|
640
|
+
else:
|
|
641
|
+
# Check for missing version field
|
|
642
|
+
if "version:" not in deployed_content:
|
|
643
|
+
is_old_format = True
|
|
644
|
+
old_version_str = "missing"
|
|
645
|
+
deployed_agent_version = (0, 0, 0)
|
|
646
|
+
self.logger.info("Detected missing version field")
|
|
647
|
+
else:
|
|
648
|
+
deployed_agent_version = (0, 0, 0)
|
|
566
649
|
|
|
567
|
-
#
|
|
568
|
-
|
|
569
|
-
deployed_base_version = int(base_version_match.group(1)) if base_version_match else 0
|
|
650
|
+
# For base version, we don't need to extract from deployed file anymore
|
|
651
|
+
# as it's tracked in metadata
|
|
570
652
|
|
|
571
653
|
# Read template to get current agent version
|
|
572
654
|
import json
|
|
573
655
|
template_data = json.loads(template_file.read_text())
|
|
574
|
-
|
|
656
|
+
|
|
657
|
+
# Extract agent version from template (handle both numeric and semantic versioning)
|
|
658
|
+
current_agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
|
|
659
|
+
|
|
660
|
+
# Compare semantic versions properly
|
|
661
|
+
# Semantic version comparison: compare major, then minor, then patch
|
|
662
|
+
def compare_versions(v1: tuple, v2: tuple) -> int:
|
|
663
|
+
"""Compare two version tuples. Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
|
|
664
|
+
for a, b in zip(v1, v2):
|
|
665
|
+
if a < b:
|
|
666
|
+
return -1
|
|
667
|
+
elif a > b:
|
|
668
|
+
return 1
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
# If old format detected, always trigger update for migration
|
|
672
|
+
if is_old_format:
|
|
673
|
+
new_version_str = self._format_version_display(current_agent_version)
|
|
674
|
+
return (True, f"migration needed from old format ({old_version_str}) to semantic version ({new_version_str})")
|
|
575
675
|
|
|
576
676
|
# Check if agent template version is newer
|
|
577
|
-
if current_agent_version >
|
|
578
|
-
|
|
677
|
+
if compare_versions(current_agent_version, deployed_agent_version) > 0:
|
|
678
|
+
deployed_str = self._format_version_display(deployed_agent_version)
|
|
679
|
+
current_str = self._format_version_display(current_agent_version)
|
|
680
|
+
return (True, f"agent template updated ({deployed_str} -> {current_str})")
|
|
579
681
|
|
|
580
|
-
#
|
|
581
|
-
|
|
582
|
-
return (True, f"base agent updated (v{deployed_base_version:04d} -> v{current_base_version:04d})")
|
|
682
|
+
# Note: We no longer check base agent version separately since we're using
|
|
683
|
+
# a unified semantic version for the agent
|
|
583
684
|
|
|
584
685
|
return (False, "up to date")
|
|
585
686
|
|
|
@@ -730,6 +831,96 @@ metadata:
|
|
|
730
831
|
# Return specific tools or default set
|
|
731
832
|
return agent_tools.get(agent_name, base_tools + ["Bash", "WebSearch"])
|
|
732
833
|
|
|
834
|
+
def _format_version_display(self, version_tuple: tuple) -> str:
|
|
835
|
+
"""
|
|
836
|
+
Format version tuple for display.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
version_tuple: Tuple of (major, minor, patch)
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
Formatted version string
|
|
843
|
+
"""
|
|
844
|
+
if isinstance(version_tuple, tuple) and len(version_tuple) == 3:
|
|
845
|
+
major, minor, patch = version_tuple
|
|
846
|
+
return f"{major}.{minor}.{patch}"
|
|
847
|
+
else:
|
|
848
|
+
# Fallback for legacy format
|
|
849
|
+
return str(version_tuple)
|
|
850
|
+
|
|
851
|
+
def _is_old_version_format(self, version_str: str) -> bool:
|
|
852
|
+
"""
|
|
853
|
+
Check if a version string is in the old serial format.
|
|
854
|
+
|
|
855
|
+
Old formats include:
|
|
856
|
+
- Serial format: "0002-0005" (contains hyphen, all digits)
|
|
857
|
+
- Missing version field
|
|
858
|
+
- Non-semantic version formats
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
version_str: Version string to check
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
True if old format, False if semantic version
|
|
865
|
+
"""
|
|
866
|
+
if not version_str:
|
|
867
|
+
return True
|
|
868
|
+
|
|
869
|
+
import re
|
|
870
|
+
|
|
871
|
+
# Check for serial format (e.g., "0002-0005")
|
|
872
|
+
if re.match(r'^\d+-\d+$', version_str):
|
|
873
|
+
return True
|
|
874
|
+
|
|
875
|
+
# Check for semantic version format (e.g., "2.1.0")
|
|
876
|
+
if re.match(r'^v?\d+\.\d+\.\d+$', version_str):
|
|
877
|
+
return False
|
|
878
|
+
|
|
879
|
+
# Any other format is considered old
|
|
880
|
+
return True
|
|
881
|
+
|
|
882
|
+
def _parse_version(self, version_value: Any) -> tuple:
|
|
883
|
+
"""
|
|
884
|
+
Parse version from various formats to semantic version tuple.
|
|
885
|
+
|
|
886
|
+
Handles:
|
|
887
|
+
- Integer values: 5 -> (0, 5, 0)
|
|
888
|
+
- String integers: "5" -> (0, 5, 0)
|
|
889
|
+
- Semantic versions: "2.1.0" -> (2, 1, 0)
|
|
890
|
+
- Invalid formats: returns (0, 0, 0)
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
version_value: Version in various formats
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
Tuple of (major, minor, patch) for comparison
|
|
897
|
+
"""
|
|
898
|
+
if isinstance(version_value, int):
|
|
899
|
+
# Legacy integer version - treat as minor version
|
|
900
|
+
return (0, version_value, 0)
|
|
901
|
+
|
|
902
|
+
if isinstance(version_value, str):
|
|
903
|
+
# Try to parse as simple integer
|
|
904
|
+
if version_value.isdigit():
|
|
905
|
+
return (0, int(version_value), 0)
|
|
906
|
+
|
|
907
|
+
# Try to parse semantic version (e.g., "2.1.0" or "v2.1.0")
|
|
908
|
+
import re
|
|
909
|
+
sem_ver_match = re.match(r'^v?(\d+)\.(\d+)\.(\d+)', version_value)
|
|
910
|
+
if sem_ver_match:
|
|
911
|
+
major = int(sem_ver_match.group(1))
|
|
912
|
+
minor = int(sem_ver_match.group(2))
|
|
913
|
+
patch = int(sem_ver_match.group(3))
|
|
914
|
+
return (major, minor, patch)
|
|
915
|
+
|
|
916
|
+
# Try to extract first number from string as minor version
|
|
917
|
+
num_match = re.search(r'(\d+)', version_value)
|
|
918
|
+
if num_match:
|
|
919
|
+
return (0, int(num_match.group(1)), 0)
|
|
920
|
+
|
|
921
|
+
# Default to 0.0.0 for invalid formats
|
|
922
|
+
return (0, 0, 0)
|
|
923
|
+
|
|
733
924
|
def _format_yaml_list(self, items: List[str], indent: int) -> str:
|
|
734
925
|
"""
|
|
735
926
|
Format a list for YAML with proper indentation.
|