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.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.1.dist-info/METADATA +475 -0
  98. minion_code-0.1.1.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.1.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
  115. {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
@@ -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
  ]
@@ -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=os.getcwd(),
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)