minion-code 0.1.0__py3-none-any.whl → 0.1.1__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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.1.dist-info/METADATA +475 -0
- minion_code-0.1.1.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
- minion_code-0.1.1.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Built-in Plan subagent configuration."""
|
|
4
|
+
|
|
5
|
+
from ..subagent import SubagentConfig
|
|
6
|
+
|
|
7
|
+
PLAN_SYSTEM_PROMPT = """You are a software architect and planning specialist. Your role is to explore codebases and design implementation plans.
|
|
8
|
+
|
|
9
|
+
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
|
10
|
+
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
|
|
11
|
+
- Creating new files (no file_write, touch, or file creation)
|
|
12
|
+
- Modifying existing files (no file_edit operations)
|
|
13
|
+
- Deleting files (no rm or deletion)
|
|
14
|
+
- Running ANY commands that change system state
|
|
15
|
+
|
|
16
|
+
Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
|
|
17
|
+
|
|
18
|
+
## Your Process
|
|
19
|
+
|
|
20
|
+
1. **Understand Requirements**: Analyze what needs to be built or changed.
|
|
21
|
+
|
|
22
|
+
2. **Explore Thoroughly**:
|
|
23
|
+
- Find existing patterns and conventions using glob, grep, and file_read
|
|
24
|
+
- Understand the current architecture
|
|
25
|
+
- Identify similar features as reference
|
|
26
|
+
- Trace through relevant code paths
|
|
27
|
+
|
|
28
|
+
3. **Design Solution**:
|
|
29
|
+
- Create implementation approach
|
|
30
|
+
- Consider trade-offs and architectural decisions
|
|
31
|
+
- Follow existing patterns where appropriate
|
|
32
|
+
|
|
33
|
+
4. **Detail the Plan**:
|
|
34
|
+
- Provide step-by-step implementation strategy
|
|
35
|
+
- Identify files to create/modify with specific changes
|
|
36
|
+
- Anticipate potential challenges
|
|
37
|
+
|
|
38
|
+
## Output Format
|
|
39
|
+
|
|
40
|
+
End your response with:
|
|
41
|
+
|
|
42
|
+
### Critical Files for Implementation
|
|
43
|
+
List 3-5 files most critical for implementing this plan:
|
|
44
|
+
- path/to/file1.ts - [Brief reason]
|
|
45
|
+
- path/to/file2.ts - [Brief reason]
|
|
46
|
+
|
|
47
|
+
REMEMBER: You can ONLY explore and plan. You CANNOT modify any files."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_plan_subagent() -> SubagentConfig:
|
|
51
|
+
"""Get the Plan subagent configuration."""
|
|
52
|
+
return SubagentConfig(
|
|
53
|
+
name="Plan",
|
|
54
|
+
description="Software architect agent for designing implementation plans by exploring codebases and identifying patterns",
|
|
55
|
+
when_to_use="When you need to design an implementation plan, understand how to approach a complex feature, or analyze architecture before making changes",
|
|
56
|
+
tools=["glob", "grep", "file_read", "ls", "web_fetch", "web_search"],
|
|
57
|
+
system_prompt=PLAN_SYSTEM_PROMPT,
|
|
58
|
+
model_name="inherit",
|
|
59
|
+
location="builtin",
|
|
60
|
+
readonly=True,
|
|
61
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Subagent configuration dataclass representing a loaded subagent type.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Dict, Any, List
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SubagentConfig:
|
|
15
|
+
"""Represents a configured subagent type with its metadata and settings."""
|
|
16
|
+
|
|
17
|
+
name: str # Unique identifier (e.g., "Explore", "Plan")
|
|
18
|
+
description: str # Short description of what the agent does
|
|
19
|
+
when_to_use: str # When the user should use this agent
|
|
20
|
+
tools: List[str] = field(default_factory=lambda: ["*"]) # Tool filter: ["*"] = all
|
|
21
|
+
system_prompt: Optional[str] = None # Custom system prompt for this agent type
|
|
22
|
+
model_name: str = "inherit" # "inherit" = use parent, or specific model name
|
|
23
|
+
|
|
24
|
+
# Additional metadata
|
|
25
|
+
path: Optional[Path] = None # Path to config file (for file-based configs)
|
|
26
|
+
location: str = "builtin" # builtin, project, user
|
|
27
|
+
readonly: bool = False # If True, agent only gets read-only tools
|
|
28
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_yaml(
|
|
32
|
+
cls, yaml_path: Path, location: str = "project"
|
|
33
|
+
) -> Optional["SubagentConfig"]:
|
|
34
|
+
"""
|
|
35
|
+
Parse a SUBAGENT.yaml file and create a SubagentConfig instance.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
yaml_path: Path to the SUBAGENT.yaml file
|
|
39
|
+
location: Where the subagent was found (project, user)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
SubagentConfig instance or None if parsing fails
|
|
43
|
+
"""
|
|
44
|
+
if not yaml_path.exists():
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
content = yaml_path.read_text(encoding="utf-8")
|
|
49
|
+
data = yaml.safe_load(content)
|
|
50
|
+
except (yaml.YAMLError, IOError):
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
if not data:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
name = data.get("name")
|
|
57
|
+
description = data.get("description")
|
|
58
|
+
when_to_use = data.get("when_to_use")
|
|
59
|
+
|
|
60
|
+
if not name or not description or not when_to_use:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# Load system_prompt from separate file if specified
|
|
64
|
+
system_prompt = data.get("system_prompt")
|
|
65
|
+
if (
|
|
66
|
+
system_prompt
|
|
67
|
+
and isinstance(system_prompt, str)
|
|
68
|
+
and system_prompt.startswith("file:")
|
|
69
|
+
):
|
|
70
|
+
prompt_file = yaml_path.parent / system_prompt[5:]
|
|
71
|
+
if prompt_file.exists():
|
|
72
|
+
system_prompt = prompt_file.read_text(encoding="utf-8")
|
|
73
|
+
else:
|
|
74
|
+
system_prompt = None
|
|
75
|
+
|
|
76
|
+
return cls(
|
|
77
|
+
name=name,
|
|
78
|
+
description=description,
|
|
79
|
+
when_to_use=when_to_use,
|
|
80
|
+
tools=data.get("tools", ["*"]),
|
|
81
|
+
system_prompt=system_prompt,
|
|
82
|
+
model_name=data.get("model_name", "inherit"),
|
|
83
|
+
path=yaml_path.parent,
|
|
84
|
+
location=location,
|
|
85
|
+
readonly=data.get("readonly", False),
|
|
86
|
+
metadata=data.get("metadata", {}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def to_xml(self) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Format subagent as XML for inclusion in prompts.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
XML formatted subagent entry
|
|
95
|
+
"""
|
|
96
|
+
tools_str = ", ".join(self.tools) if self.tools != ["*"] else "All tools"
|
|
97
|
+
return f"""<subagent>
|
|
98
|
+
<name>{self.name}</name>
|
|
99
|
+
<description>{self.description}</description>
|
|
100
|
+
<when_to_use>{self.when_to_use}</when_to_use>
|
|
101
|
+
<tools>{tools_str}</tools>
|
|
102
|
+
<location>{self.location}</location>
|
|
103
|
+
</subagent>"""
|
|
104
|
+
|
|
105
|
+
def to_prompt_line(self) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Format subagent as a single line for tool description.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Formatted prompt line like: "- Explore: Fast codebase exploration... (Tools: glob, grep)"
|
|
111
|
+
"""
|
|
112
|
+
tools_str = ", ".join(self.tools) if self.tools != ["*"] else "All tools"
|
|
113
|
+
return f"- {self.name}: {self.description} (Tools: {tools_str})"
|
|
114
|
+
|
|
115
|
+
def __repr__(self) -> str:
|
|
116
|
+
return f"SubagentConfig(name={self.name!r}, location={self.location!r})"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Subagent Loader - discovers and loads subagent configurations from standard directories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from .subagent import SubagentConfig
|
|
12
|
+
from .subagent_registry import SubagentRegistry, get_subagent_registry
|
|
13
|
+
from .builtin import get_all_builtin_subagents
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SubagentLoader:
|
|
19
|
+
"""
|
|
20
|
+
Discovers and loads subagent configurations.
|
|
21
|
+
|
|
22
|
+
Search paths (in priority order - lower priority registered first):
|
|
23
|
+
1. builtin (code-defined)
|
|
24
|
+
2. ~/.claude/subagents or ~/.minion/subagents (user-level)
|
|
25
|
+
3. .claude/subagents or .minion/subagents (project-level)
|
|
26
|
+
|
|
27
|
+
Project-level overrides user-level, which overrides builtin.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
SUBAGENT_DIRS = [
|
|
31
|
+
".claude/agents", # Claude Code compatible
|
|
32
|
+
".minion/agents", # Minion compatible
|
|
33
|
+
".claude/subagents", # Legacy/alternative
|
|
34
|
+
".minion/subagents", # Legacy/alternative
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
SUBAGENT_FILE = "SUBAGENT.yaml"
|
|
38
|
+
|
|
39
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
40
|
+
"""Initialize the subagent loader."""
|
|
41
|
+
self.project_root = Path(project_root) if project_root else Path.cwd()
|
|
42
|
+
self.home_dir = Path.home()
|
|
43
|
+
|
|
44
|
+
def get_search_paths(self) -> List[tuple[Path, str]]:
|
|
45
|
+
"""
|
|
46
|
+
Get all subagent search paths with their location type.
|
|
47
|
+
Returns paths in priority order (lowest priority first).
|
|
48
|
+
"""
|
|
49
|
+
paths = []
|
|
50
|
+
|
|
51
|
+
# User-level paths (lower priority than project)
|
|
52
|
+
for subagent_dir in self.SUBAGENT_DIRS:
|
|
53
|
+
user_path = self.home_dir / subagent_dir
|
|
54
|
+
paths.append((user_path, "user"))
|
|
55
|
+
|
|
56
|
+
# Project-level paths (highest priority)
|
|
57
|
+
for subagent_dir in self.SUBAGENT_DIRS:
|
|
58
|
+
project_path = self.project_root / subagent_dir
|
|
59
|
+
paths.append((project_path, "project"))
|
|
60
|
+
|
|
61
|
+
return paths
|
|
62
|
+
|
|
63
|
+
def discover_subagents(self, subagents_dir: Path) -> List[Path]:
|
|
64
|
+
"""
|
|
65
|
+
Discover all subagent directories within a subagents directory.
|
|
66
|
+
A subagent directory must contain a SUBAGENT.yaml file.
|
|
67
|
+
"""
|
|
68
|
+
if not subagents_dir.exists() or not subagents_dir.is_dir():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
subagent_files = []
|
|
72
|
+
|
|
73
|
+
for item in subagents_dir.iterdir():
|
|
74
|
+
if item.is_dir():
|
|
75
|
+
subagent_yaml = item / self.SUBAGENT_FILE
|
|
76
|
+
if subagent_yaml.exists():
|
|
77
|
+
subagent_files.append(subagent_yaml)
|
|
78
|
+
|
|
79
|
+
return subagent_files
|
|
80
|
+
|
|
81
|
+
def load_subagent(self, yaml_path: Path, location: str) -> Optional[SubagentConfig]:
|
|
82
|
+
"""Load a single subagent from its SUBAGENT.yaml file."""
|
|
83
|
+
try:
|
|
84
|
+
subagent = SubagentConfig.from_yaml(yaml_path, location)
|
|
85
|
+
if subagent:
|
|
86
|
+
logger.debug(f"Loaded subagent: {subagent.name} from {yaml_path}")
|
|
87
|
+
else:
|
|
88
|
+
logger.warning(f"Failed to parse subagent: {yaml_path}")
|
|
89
|
+
return subagent
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Error loading subagent from {yaml_path}: {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def load_all(self, registry: Optional[SubagentRegistry] = None) -> SubagentRegistry:
|
|
95
|
+
"""
|
|
96
|
+
Load all subagents (builtin + custom) into the registry.
|
|
97
|
+
Builtin is registered first (lowest priority), then user, then project.
|
|
98
|
+
"""
|
|
99
|
+
if registry is None:
|
|
100
|
+
registry = get_subagent_registry()
|
|
101
|
+
|
|
102
|
+
# 1. Register builtin subagents first (lowest priority)
|
|
103
|
+
for subagent in get_all_builtin_subagents():
|
|
104
|
+
registered = registry.register(subagent)
|
|
105
|
+
if registered:
|
|
106
|
+
logger.debug(f"Registered builtin subagent: {subagent.name}")
|
|
107
|
+
|
|
108
|
+
# 2. Load from file system (user then project)
|
|
109
|
+
for search_path, location in self.get_search_paths():
|
|
110
|
+
subagent_files = self.discover_subagents(search_path)
|
|
111
|
+
|
|
112
|
+
for yaml_path in subagent_files:
|
|
113
|
+
subagent = self.load_subagent(yaml_path, location)
|
|
114
|
+
if subagent:
|
|
115
|
+
registered = registry.register(subagent)
|
|
116
|
+
if registered:
|
|
117
|
+
logger.debug(f"Registered {location} subagent: {subagent.name}")
|
|
118
|
+
else:
|
|
119
|
+
logger.debug(
|
|
120
|
+
f"Skipped subagent {subagent.name} - already registered from higher priority"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return registry
|
|
124
|
+
|
|
125
|
+
def reload(self, registry: Optional[SubagentRegistry] = None) -> SubagentRegistry:
|
|
126
|
+
"""Reload all subagents, clearing the existing registry first."""
|
|
127
|
+
if registry is None:
|
|
128
|
+
registry = get_subagent_registry()
|
|
129
|
+
|
|
130
|
+
registry.clear()
|
|
131
|
+
return self.load_all(registry)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_subagents(project_root: Optional[Path] = None) -> SubagentRegistry:
|
|
135
|
+
"""Convenience function to load all subagents."""
|
|
136
|
+
loader = SubagentLoader(project_root)
|
|
137
|
+
return loader.load_all()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_available_subagents() -> List[SubagentConfig]:
|
|
141
|
+
"""Get list of all available subagents."""
|
|
142
|
+
registry = get_subagent_registry()
|
|
143
|
+
|
|
144
|
+
if len(registry) == 0:
|
|
145
|
+
load_subagents()
|
|
146
|
+
|
|
147
|
+
return registry.list_all()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Subagent Registry - manages loaded subagent configurations and provides lookup functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Optional, List
|
|
8
|
+
from .subagent import SubagentConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SubagentRegistry:
|
|
12
|
+
"""
|
|
13
|
+
Registry for managing loaded subagent configurations.
|
|
14
|
+
|
|
15
|
+
Subagents are organized by name and can be looked up for task execution.
|
|
16
|
+
The registry handles subagent deduplication based on priority.
|
|
17
|
+
Priority: builtin < user < project (project overrides all)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
PRIORITY_ORDER = {"builtin": 2, "user": 1, "project": 0}
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._subagents: Dict[str, SubagentConfig] = {}
|
|
24
|
+
self._subagents_by_location: Dict[str, List[SubagentConfig]] = {
|
|
25
|
+
"builtin": [],
|
|
26
|
+
"project": [],
|
|
27
|
+
"user": [],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def register(self, subagent: SubagentConfig) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Register a subagent in the registry.
|
|
33
|
+
|
|
34
|
+
Project subagents take precedence over user subagents,
|
|
35
|
+
which take precedence over builtin.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
subagent: SubagentConfig instance to register
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if the subagent was registered, False if it was skipped
|
|
42
|
+
"""
|
|
43
|
+
existing = self._subagents.get(subagent.name)
|
|
44
|
+
|
|
45
|
+
if existing:
|
|
46
|
+
existing_priority = self.PRIORITY_ORDER.get(existing.location, 99)
|
|
47
|
+
new_priority = self.PRIORITY_ORDER.get(subagent.location, 99)
|
|
48
|
+
|
|
49
|
+
if new_priority >= existing_priority:
|
|
50
|
+
# Skip - existing subagent has higher or equal priority
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
self._subagents[subagent.name] = subagent
|
|
54
|
+
self._subagents_by_location[subagent.location].append(subagent)
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def get(self, name: str) -> Optional[SubagentConfig]:
|
|
58
|
+
"""Get a subagent by name."""
|
|
59
|
+
return self._subagents.get(name)
|
|
60
|
+
|
|
61
|
+
def exists(self, name: str) -> bool:
|
|
62
|
+
"""Check if a subagent exists in the registry."""
|
|
63
|
+
return name in self._subagents
|
|
64
|
+
|
|
65
|
+
def list_all(self) -> List[SubagentConfig]:
|
|
66
|
+
"""Get all registered subagents."""
|
|
67
|
+
return list(self._subagents.values())
|
|
68
|
+
|
|
69
|
+
def list_names(self) -> List[str]:
|
|
70
|
+
"""Get all registered subagent names."""
|
|
71
|
+
return list(self._subagents.keys())
|
|
72
|
+
|
|
73
|
+
def list_by_location(self, location: str) -> List[SubagentConfig]:
|
|
74
|
+
"""Get subagents by location type."""
|
|
75
|
+
return self._subagents_by_location.get(location, [])
|
|
76
|
+
|
|
77
|
+
def clear(self):
|
|
78
|
+
"""Clear all registered subagents."""
|
|
79
|
+
self._subagents.clear()
|
|
80
|
+
for location in self._subagents_by_location:
|
|
81
|
+
self._subagents_by_location[location].clear()
|
|
82
|
+
|
|
83
|
+
def generate_subagents_prompt(self, char_budget: int = 10000) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Generate a prompt listing all available subagents in XML format.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
char_budget: Maximum characters for subagents list
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Formatted subagents prompt in XML format
|
|
92
|
+
"""
|
|
93
|
+
subagents = self.list_all()
|
|
94
|
+
|
|
95
|
+
if not subagents:
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
entries = []
|
|
99
|
+
total_chars = 0
|
|
100
|
+
|
|
101
|
+
for subagent in subagents:
|
|
102
|
+
entry = subagent.to_xml()
|
|
103
|
+
if total_chars + len(entry) > char_budget:
|
|
104
|
+
break
|
|
105
|
+
entries.append(entry)
|
|
106
|
+
total_chars += len(entry)
|
|
107
|
+
|
|
108
|
+
if not entries:
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
subagents_xml = "\n".join(entries)
|
|
112
|
+
return f"""<available_subagents>
|
|
113
|
+
{subagents_xml}
|
|
114
|
+
</available_subagents>"""
|
|
115
|
+
|
|
116
|
+
def generate_tool_description_lines(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Generate description lines for the Task tool.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Multi-line string with each subagent's prompt line
|
|
122
|
+
"""
|
|
123
|
+
subagents = self.list_all()
|
|
124
|
+
return "\n".join(s.to_prompt_line() for s in subagents)
|
|
125
|
+
|
|
126
|
+
def __len__(self) -> int:
|
|
127
|
+
return len(self._subagents)
|
|
128
|
+
|
|
129
|
+
def __contains__(self, name: str) -> bool:
|
|
130
|
+
return name in self._subagents
|
|
131
|
+
|
|
132
|
+
def __iter__(self):
|
|
133
|
+
return iter(self._subagents.values())
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Global subagent registry instance
|
|
137
|
+
_subagent_registry: Optional[SubagentRegistry] = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_subagent_registry() -> SubagentRegistry:
|
|
141
|
+
"""Get the global subagent registry instance."""
|
|
142
|
+
global _subagent_registry
|
|
143
|
+
if _subagent_registry is None:
|
|
144
|
+
_subagent_registry = SubagentRegistry()
|
|
145
|
+
return _subagent_registry
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def reset_subagent_registry():
|
|
149
|
+
"""Reset the global subagent registry."""
|
|
150
|
+
global _subagent_registry
|
|
151
|
+
_subagent_registry = None
|
minion_code/tools/__init__.py
CHANGED
|
@@ -19,9 +19,11 @@ from .glob_tool import GlobTool
|
|
|
19
19
|
from .ls_tool import LsTool
|
|
20
20
|
from .python_interpreter_tool import PythonInterpreterTool
|
|
21
21
|
from .user_input_tool import UserInputTool
|
|
22
|
+
from .task_tool import TaskTool
|
|
22
23
|
|
|
23
24
|
from .todo_write_tool import TodoWriteTool
|
|
24
25
|
from .todo_read_tool import TodoReadTool
|
|
26
|
+
from .skill_tool import SkillTool
|
|
25
27
|
|
|
26
28
|
# Tool mapping
|
|
27
29
|
TOOL_MAPPING = {
|
|
@@ -37,9 +39,10 @@ TOOL_MAPPING = {
|
|
|
37
39
|
LsTool,
|
|
38
40
|
PythonInterpreterTool,
|
|
39
41
|
UserInputTool,
|
|
40
|
-
|
|
42
|
+
TaskTool,
|
|
41
43
|
TodoWriteTool,
|
|
42
44
|
TodoReadTool,
|
|
45
|
+
SkillTool,
|
|
43
46
|
]
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -57,13 +60,16 @@ __all__ = [
|
|
|
57
60
|
"LsTool",
|
|
58
61
|
# Execution tools
|
|
59
62
|
"PythonInterpreterTool",
|
|
63
|
+
# Task tools
|
|
64
|
+
"TaskTool",
|
|
60
65
|
# Web tools
|
|
61
66
|
# Interactive tools
|
|
62
67
|
"UserInputTool",
|
|
63
|
-
|
|
64
68
|
# Todo tools
|
|
65
69
|
"TodoWriteTool",
|
|
66
70
|
"TodoReadTool",
|
|
71
|
+
# Skill tools
|
|
72
|
+
"SkillTool",
|
|
67
73
|
# Utilities
|
|
68
74
|
"TOOL_MAPPING",
|
|
69
75
|
]
|
minion_code/tools/bash_tool.py
CHANGED
|
@@ -6,8 +6,9 @@ Bash command execution tool
|
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
import subprocess
|
|
9
|
-
from typing import Optional
|
|
9
|
+
from typing import Optional, Any
|
|
10
10
|
from minion.tools import BaseTool
|
|
11
|
+
from ..utils.output_truncator import truncate_output
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class BashTool(BaseTool):
|
|
@@ -26,6 +27,10 @@ class BashTool(BaseTool):
|
|
|
26
27
|
}
|
|
27
28
|
output_type = "string"
|
|
28
29
|
|
|
30
|
+
def __init__(self, workdir: Optional[str] = None, *args, **kwargs):
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
32
|
+
self.workdir = workdir
|
|
33
|
+
|
|
29
34
|
def forward(self, command: str, timeout: Optional[int] = 30) -> str:
|
|
30
35
|
"""Execute bash command"""
|
|
31
36
|
try:
|
|
@@ -34,13 +39,15 @@ class BashTool(BaseTool):
|
|
|
34
39
|
if any(dangerous in command.lower() for dangerous in dangerous_commands):
|
|
35
40
|
return f"Error: Dangerous command prohibited - {command}"
|
|
36
41
|
|
|
42
|
+
# Use injected workdir if available, otherwise fallback to cwd
|
|
43
|
+
cwd = self.workdir if self.workdir else os.getcwd()
|
|
37
44
|
result = subprocess.run(
|
|
38
45
|
command,
|
|
39
46
|
shell=True,
|
|
40
47
|
capture_output=True,
|
|
41
48
|
text=True,
|
|
42
49
|
timeout=timeout,
|
|
43
|
-
cwd=
|
|
50
|
+
cwd=cwd,
|
|
44
51
|
)
|
|
45
52
|
|
|
46
53
|
output = ""
|
|
@@ -50,9 +57,15 @@ class BashTool(BaseTool):
|
|
|
50
57
|
output += f"Standard error:\n{result.stderr}\n"
|
|
51
58
|
output += f"Exit code: {result.returncode}"
|
|
52
59
|
|
|
53
|
-
return output
|
|
60
|
+
return self.format_for_observation(output)
|
|
54
61
|
|
|
55
62
|
except subprocess.TimeoutExpired:
|
|
56
63
|
return f"Command execution timeout ({timeout} seconds)"
|
|
57
64
|
except Exception as e:
|
|
58
65
|
return f"Error executing command: {str(e)}"
|
|
66
|
+
|
|
67
|
+
def format_for_observation(self, output: Any) -> str:
|
|
68
|
+
"""格式化输出,自动截断过大内容"""
|
|
69
|
+
if isinstance(output, str):
|
|
70
|
+
return truncate_output(output, tool_name=self.name)
|
|
71
|
+
return str(output)
|