minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.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.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
minion_code/agents/code_agent.py
CHANGED
|
@@ -23,6 +23,9 @@ from typing import List, Optional, Union, Any
|
|
|
23
23
|
import sys
|
|
24
24
|
|
|
25
25
|
from minion.agents import CodeAgent
|
|
26
|
+
from minion.main.tool_hooks import HookConfig
|
|
27
|
+
from minion.types import AgentState
|
|
28
|
+
from minion.types.history import History
|
|
26
29
|
|
|
27
30
|
# Import all minion_code tools
|
|
28
31
|
from ..tools import (
|
|
@@ -36,20 +39,117 @@ from ..tools import (
|
|
|
36
39
|
LsTool,
|
|
37
40
|
PythonInterpreterTool,
|
|
38
41
|
UserInputTool,
|
|
39
|
-
|
|
40
42
|
TodoWriteTool,
|
|
41
43
|
TodoReadTool,
|
|
44
|
+
SkillTool,
|
|
42
45
|
TOOL_MAPPING,
|
|
43
46
|
)
|
|
44
47
|
|
|
48
|
+
# Import web tools from minion
|
|
49
|
+
from minion.tools import WebFetchTool, WebSearchTool
|
|
50
|
+
|
|
45
51
|
logger = logging.getLogger(__name__)
|
|
46
52
|
|
|
53
|
+
|
|
54
|
+
async def query_quick(
|
|
55
|
+
agent: "MinionCodeAgent",
|
|
56
|
+
user_prompt: str,
|
|
57
|
+
system_prompt: Optional[Union[str, List[str]]] = None,
|
|
58
|
+
assistant_prompt: Optional[str] = None,
|
|
59
|
+
enable_prompt_caching: bool = False,
|
|
60
|
+
llm: Optional[str] = None,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Simplified query function for quick LLM interactions without agent overhead.
|
|
64
|
+
|
|
65
|
+
This function bypasses the agent's complex routing and tool execution,
|
|
66
|
+
providing direct access to the LLM for simple queries. It uses brain.step
|
|
67
|
+
with route='raw' to avoid additional processing.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
agent: MinionCodeAgent instance to use for the query
|
|
71
|
+
user_prompt: The user's message/question
|
|
72
|
+
system_prompt: Optional system prompt(s) - can be a string or list of strings
|
|
73
|
+
assistant_prompt: Optional assistant prompt to prefill the response
|
|
74
|
+
enable_prompt_caching: Whether to enable prompt caching (default: False)
|
|
75
|
+
llm: Optional LLM model to use (defaults to agent's quick LLM)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The LLM's response as a string
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> agent = await MinionCodeAgent.create(name="Assistant", llm="sonnet")
|
|
82
|
+
>>> response = await query_quick(
|
|
83
|
+
... agent,
|
|
84
|
+
... user_prompt="What is 2+2?",
|
|
85
|
+
... system_prompt="You are a helpful math assistant."
|
|
86
|
+
... )
|
|
87
|
+
>>> print(response)
|
|
88
|
+
"4"
|
|
89
|
+
"""
|
|
90
|
+
# Use quick LLM by default
|
|
91
|
+
if llm is None:
|
|
92
|
+
llm = agent.get_llm_for_task("quick")
|
|
93
|
+
|
|
94
|
+
# Build messages list
|
|
95
|
+
messages = [{"role": "user", "content": user_prompt}]
|
|
96
|
+
|
|
97
|
+
# Add assistant prefill if provided
|
|
98
|
+
if assistant_prompt:
|
|
99
|
+
messages.append({"role": "assistant", "content": assistant_prompt})
|
|
100
|
+
|
|
101
|
+
# Build system prompt list
|
|
102
|
+
system_messages = []
|
|
103
|
+
if system_prompt:
|
|
104
|
+
if isinstance(system_prompt, list):
|
|
105
|
+
system_messages = system_prompt
|
|
106
|
+
else:
|
|
107
|
+
system_messages = [system_prompt]
|
|
108
|
+
|
|
109
|
+
# Create a minimal state with empty history
|
|
110
|
+
state = AgentState(history=History())
|
|
111
|
+
|
|
112
|
+
# Prepare kwargs for brain.step
|
|
113
|
+
step_kwargs = {
|
|
114
|
+
"messages": messages,
|
|
115
|
+
"route": "raw", # Use raw route to bypass agent processing and avoid extra overhead
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Add system prompt if provided
|
|
119
|
+
if system_messages:
|
|
120
|
+
step_kwargs["system_prompt"] = system_messages
|
|
121
|
+
|
|
122
|
+
# Add LLM if specified
|
|
123
|
+
if llm:
|
|
124
|
+
step_kwargs["llm"] = llm
|
|
125
|
+
|
|
126
|
+
# Add prompt caching if enabled
|
|
127
|
+
if enable_prompt_caching:
|
|
128
|
+
step_kwargs["enable_prompt_caching"] = enable_prompt_caching
|
|
129
|
+
|
|
130
|
+
# Call brain.step with route='raw' to bypass agent processing
|
|
131
|
+
try:
|
|
132
|
+
response = await agent.brain.step(state=state, **step_kwargs)
|
|
133
|
+
|
|
134
|
+
# Extract the text response
|
|
135
|
+
if hasattr(response, "answer"):
|
|
136
|
+
return response.answer
|
|
137
|
+
elif hasattr(response, "content"):
|
|
138
|
+
return response.content
|
|
139
|
+
else:
|
|
140
|
+
return str(response)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Error in query_quick: {e}")
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
|
|
47
147
|
# Todo reminder constants
|
|
48
148
|
INITIAL_REMINDER = (
|
|
49
149
|
'<reminder source="system" topic="todos">'
|
|
50
150
|
"System message: complex work should be tracked with the Todo tool. "
|
|
51
151
|
"Do not respond to this reminder and do not mention it to the user."
|
|
52
|
-
|
|
152
|
+
"</reminder>"
|
|
53
153
|
)
|
|
54
154
|
|
|
55
155
|
NAG_REMINDER = (
|
|
@@ -57,29 +157,99 @@ NAG_REMINDER = (
|
|
|
57
157
|
"System notice: more than ten rounds passed without Todo usage. "
|
|
58
158
|
"Update the Todo board if the task still requires multiple steps. "
|
|
59
159
|
"Do not reply to or mention this reminder to the user."
|
|
60
|
-
|
|
160
|
+
"</reminder>"
|
|
61
161
|
)
|
|
62
162
|
|
|
163
|
+
|
|
63
164
|
@dataclass
|
|
64
165
|
class MinionCodeAgent(CodeAgent):
|
|
65
166
|
"""
|
|
66
167
|
Enhanced CodeAgent with pre-configured minion_code tools.
|
|
67
|
-
|
|
168
|
+
|
|
68
169
|
This class wraps the Minion CodeAgent with all minion_code tools
|
|
69
170
|
and provides optimized system prompts for coding tasks.
|
|
70
171
|
"""
|
|
71
|
-
|
|
172
|
+
|
|
72
173
|
DEFAULT_SYSTEM_PROMPT = (
|
|
73
|
-
"You are
|
|
74
|
-
"
|
|
174
|
+
"You are Minion Code, an interactive CLI coding agent that helps users with software engineering tasks.\n"
|
|
175
|
+
"Use the instructions below and the tools available to you to assist the user.\n"
|
|
176
|
+
"\n"
|
|
177
|
+
"Working directory: {workdir}\n"
|
|
178
|
+
"\n"
|
|
179
|
+
"IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code that may be used maliciously.\n"
|
|
180
|
+
"Do not assist with credential discovery or harvesting, including bulk crawling for SSH keys, browser cookies, or cryptocurrency wallets.\n"
|
|
181
|
+
"Allow security analysis, detection rules, vulnerability explanations, defensive tools, and security documentation.\n"
|
|
182
|
+
"\n"
|
|
183
|
+
"# Tone and style\n"
|
|
184
|
+
"- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.\n"
|
|
185
|
+
"- Your output will be displayed on a command line interface. Your responses should be short and concise.\n"
|
|
186
|
+
"- Output text to communicate with the user; all text you output outside of tool use is displayed to the user.\n"
|
|
187
|
+
"- Only use tools to complete tasks. Never use tools like bash or code comments as means to communicate with the user.\n"
|
|
188
|
+
"- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one.\n"
|
|
189
|
+
"\n"
|
|
190
|
+
"# Professional objectivity\n"
|
|
191
|
+
"Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving,\n"
|
|
192
|
+
"providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation.\n"
|
|
193
|
+
"It is best for the user if you honestly apply the same rigorous standards to all ideas and disagree when necessary,\n"
|
|
194
|
+
"even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement.\n"
|
|
195
|
+
"Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.\n"
|
|
196
|
+
"\n"
|
|
197
|
+
"# Task Management\n"
|
|
198
|
+
"You have access to the todo_write and todo_read tools to help you manage and plan tasks. Use these tools VERY frequently\n"
|
|
199
|
+
"to ensure that you are tracking your tasks and giving the user visibility into your progress.\n"
|
|
200
|
+
"These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps.\n"
|
|
201
|
+
"If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.\n"
|
|
202
|
+
"\n"
|
|
203
|
+
"It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.\n"
|
|
204
|
+
"\n"
|
|
205
|
+
"Example:\n"
|
|
206
|
+
"user: Run the build and fix any type errors\n"
|
|
207
|
+
"assistant: I'm going to use the todo_write tool to write the following items to the todo list:\n"
|
|
208
|
+
"- Run the build\n"
|
|
209
|
+
"- Fix any type errors\n"
|
|
210
|
+
"\n"
|
|
211
|
+
"I'm now going to run the build using bash.\n"
|
|
212
|
+
"Looks like I found 10 type errors. I'm going to use the todo_write tool to write 10 items to the todo list.\n"
|
|
213
|
+
"marking the first todo as in_progress\n"
|
|
214
|
+
"Let me start working on the first item...\n"
|
|
215
|
+
"The first item has been fixed, let me mark the first todo as completed, and move on to the second item...\n"
|
|
216
|
+
"\n"
|
|
217
|
+
"# Doing tasks\n"
|
|
218
|
+
"The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality,\n"
|
|
219
|
+
"refactoring code, explaining code, and more. For these tasks the following steps are recommended:\n"
|
|
220
|
+
"- Use the todo_write tool to plan the task if required\n"
|
|
221
|
+
"- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities.\n"
|
|
222
|
+
"\n"
|
|
223
|
+
"# Tool usage policy\n"
|
|
224
|
+
"- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them,\n"
|
|
225
|
+
" make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency.\n"
|
|
226
|
+
"- However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially.\n"
|
|
227
|
+
"- Never use placeholders or guess missing parameters in tool calls.\n"
|
|
228
|
+
"- Use specialized tools instead of bash commands when possible. For file operations, use dedicated tools:\n"
|
|
229
|
+
" file_read for reading files instead of cat/head/tail, file_edit for editing instead of sed/awk,\n"
|
|
230
|
+
" and file_write for creating files instead of cat with heredoc or echo redirection.\n"
|
|
231
|
+
"- Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution.\n"
|
|
232
|
+
"- NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user.\n"
|
|
233
|
+
" Output all communication directly in your response text instead.\n"
|
|
234
|
+
"\n"
|
|
235
|
+
"# Code References\n"
|
|
236
|
+
"When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user\n"
|
|
237
|
+
"to easily navigate to the source code location.\n"
|
|
238
|
+
"\n"
|
|
239
|
+
"Example:\n"
|
|
240
|
+
"user: Where are errors from the client handled?\n"
|
|
241
|
+
"assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.py:712.\n"
|
|
242
|
+
"\n"
|
|
243
|
+
"IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the conversation.\n"
|
|
244
|
+
"\n"
|
|
75
245
|
"Rules:\n"
|
|
76
246
|
"- Prefer taking actions with tools (read/write/edit/bash) over long prose.\n"
|
|
77
247
|
"- Keep outputs terse. Use bullet lists / checklists when summarizing.\n"
|
|
78
248
|
"- Never invent file paths. Ask via reads or list directories first if unsure.\n"
|
|
79
|
-
"- For edits, choose the right tool:
|
|
249
|
+
"- For edits, choose the right tool: file_edit for single string replacements, multi_edit for multiple changes to same file or large edits, file_write for complete rewrites.\n"
|
|
250
|
+
"- For large string edits (>2000 chars), prefer multi_edit tool or break into smaller chunks for better reliability.\n"
|
|
80
251
|
"- Always read files before editing to establish freshness tracking.\n"
|
|
81
252
|
"- For bash, avoid destructive or privileged commands; stay inside the workspace.\n"
|
|
82
|
-
"- Use the Todo tool to maintain multi-step plans when needed.\n"
|
|
83
253
|
"- After finishing, summarize what changed and how to run or test."
|
|
84
254
|
)
|
|
85
255
|
|
|
@@ -87,55 +257,60 @@ class MinionCodeAgent(CodeAgent):
|
|
|
87
257
|
"""Initialize the CodeAgent with thinking capabilities and optional state tracking."""
|
|
88
258
|
super().__post_init__()
|
|
89
259
|
self.conversation_history = []
|
|
90
|
-
|
|
260
|
+
# Note: Auto-compact is handled by minion's BaseAgent
|
|
261
|
+
|
|
91
262
|
async def pre_step(self, input_data, kwargs):
|
|
92
|
-
"""Override pre_step to track iterations without todo usage.
|
|
93
|
-
|
|
263
|
+
"""Override pre_step to track iterations without todo usage.
|
|
264
|
+
|
|
265
|
+
Note: Auto-compact logic is handled by BaseAgent. This method only handles
|
|
266
|
+
iteration tracking and nag reminders.
|
|
267
|
+
"""
|
|
268
|
+
# Call parent pre_step first (BaseAgent handles auto-compact)
|
|
94
269
|
result = await super().pre_step(input_data, kwargs)
|
|
95
|
-
|
|
270
|
+
|
|
96
271
|
# Initialize metadata if not exists
|
|
97
|
-
if not hasattr(self.state,
|
|
272
|
+
if not hasattr(self.state, "metadata"):
|
|
98
273
|
self.state.metadata = {}
|
|
99
274
|
if "iteration_without_todos" not in self.state.metadata:
|
|
100
275
|
self.state.metadata["iteration_without_todos"] = 0
|
|
101
|
-
|
|
276
|
+
|
|
102
277
|
# Increment iteration counter
|
|
103
278
|
self.state.metadata["iteration_without_todos"] += 1
|
|
104
|
-
|
|
279
|
+
|
|
105
280
|
# Add nag reminder if more than 10 iterations without todo usage
|
|
106
281
|
if self.state.metadata["iteration_without_todos"] > 10:
|
|
107
|
-
self.state.history.append({
|
|
108
|
-
'role': 'user',
|
|
109
|
-
'content': NAG_REMINDER
|
|
110
|
-
})
|
|
282
|
+
self.state.history.append({"role": "user", "content": NAG_REMINDER})
|
|
111
283
|
# Reset counter to avoid spamming reminders
|
|
112
284
|
self.state.metadata["iteration_without_todos"] = 0
|
|
113
|
-
|
|
114
|
-
return result
|
|
115
|
-
|
|
285
|
+
|
|
116
286
|
return result
|
|
117
|
-
|
|
287
|
+
|
|
118
288
|
@classmethod
|
|
119
289
|
async def create(
|
|
120
290
|
cls,
|
|
121
291
|
name: str = "Minion Code Assistant",
|
|
122
|
-
llm: str = "sonnet",
|
|
292
|
+
llm: str = "claude-sonnet-4-5",
|
|
293
|
+
llms: Optional[dict] = None,
|
|
123
294
|
system_prompt: Optional[str] = None,
|
|
124
295
|
workdir: Optional[Union[str, Path]] = None,
|
|
125
296
|
additional_tools: Optional[List[Any]] = None,
|
|
126
|
-
|
|
297
|
+
hooks: Optional["HookConfig"] = None,
|
|
298
|
+
**kwargs,
|
|
127
299
|
) -> "MinionCodeAgent":
|
|
128
300
|
"""
|
|
129
301
|
Create a new MinionCodeAgent with all minion_code tools.
|
|
130
|
-
|
|
302
|
+
|
|
131
303
|
Args:
|
|
132
304
|
name: Agent name
|
|
133
|
-
llm: LLM model to use
|
|
305
|
+
llm: Main LLM model to use (default for all tasks)
|
|
306
|
+
llms: Optional dict with specialized LLMs: {'quick': 'haiku', 'task': 'sonnet', 'reasoning': 'o4-mini'}
|
|
307
|
+
If not provided, uses smart defaults based on main llm
|
|
134
308
|
system_prompt: Custom system prompt (uses default if None)
|
|
135
309
|
workdir: Working directory (uses current if None)
|
|
136
310
|
additional_tools: Extra tools to add beyond minion_code tools
|
|
311
|
+
hooks: Optional HookConfig for pre-tool-use hooks (permission control)
|
|
137
312
|
**kwargs: Additional arguments passed to CodeAgent.create()
|
|
138
|
-
|
|
313
|
+
|
|
139
314
|
Returns:
|
|
140
315
|
Configured MinionCodeAgent instance
|
|
141
316
|
"""
|
|
@@ -143,178 +318,416 @@ class MinionCodeAgent(CodeAgent):
|
|
|
143
318
|
workdir = Path.cwd()
|
|
144
319
|
else:
|
|
145
320
|
workdir = Path(workdir)
|
|
146
|
-
|
|
321
|
+
|
|
322
|
+
# Set up specialized LLMs with fallback to main llm
|
|
323
|
+
if llms is None:
|
|
324
|
+
llms = {}
|
|
325
|
+
|
|
326
|
+
llm_quick = llms.get("quick")
|
|
327
|
+
llm_task = llms.get("task")
|
|
328
|
+
llm_reasoning = llms.get("reasoning")
|
|
329
|
+
|
|
330
|
+
if llm_quick is None:
|
|
331
|
+
llm_quick = "haiku" if llm == "sonnet" else llm
|
|
332
|
+
if llm_task is None:
|
|
333
|
+
llm_task = "sonnet" if llm != "sonnet" else llm
|
|
334
|
+
if llm_reasoning is None:
|
|
335
|
+
llm_reasoning = "o4-mini" if llm not in ["o4-mini", "o1-mini"] else llm
|
|
336
|
+
|
|
147
337
|
# Use default system prompt if none provided
|
|
148
338
|
if system_prompt is None:
|
|
149
339
|
system_prompt = cls.DEFAULT_SYSTEM_PROMPT.format(workdir=workdir)
|
|
150
|
-
|
|
151
|
-
#
|
|
340
|
+
|
|
341
|
+
# Append skills prompt if skills are available
|
|
342
|
+
from ..tools.skill_tool import generate_skill_tool_prompt
|
|
343
|
+
|
|
344
|
+
skills_prompt = generate_skill_tool_prompt()
|
|
345
|
+
if skills_prompt and "<available_skills>" in skills_prompt:
|
|
346
|
+
system_prompt += "\n\n# Skills\n" + skills_prompt
|
|
347
|
+
|
|
348
|
+
# Get all minion_code tools (inject workdir for path-aware tools)
|
|
349
|
+
workdir_str = str(workdir)
|
|
152
350
|
minion_tools = [
|
|
153
|
-
FileReadTool(),
|
|
154
|
-
FileWriteTool(),
|
|
155
|
-
FileEditTool(),
|
|
156
|
-
MultiEditTool(),
|
|
157
|
-
BashTool(),
|
|
158
|
-
GrepTool(),
|
|
159
|
-
GlobTool(),
|
|
160
|
-
LsTool(),
|
|
351
|
+
FileReadTool(workdir=workdir_str),
|
|
352
|
+
FileWriteTool(workdir=workdir_str),
|
|
353
|
+
FileEditTool(workdir=workdir_str),
|
|
354
|
+
MultiEditTool(), # TODO: Add workdir support if needed
|
|
355
|
+
BashTool(workdir=workdir_str),
|
|
356
|
+
GrepTool(workdir=workdir_str),
|
|
357
|
+
GlobTool(workdir=workdir_str),
|
|
358
|
+
LsTool(workdir=workdir_str),
|
|
161
359
|
PythonInterpreterTool(),
|
|
162
360
|
UserInputTool(),
|
|
163
361
|
TodoWriteTool(),
|
|
164
362
|
TodoReadTool(),
|
|
363
|
+
SkillTool(),
|
|
364
|
+
# Web tools from minion
|
|
365
|
+
WebFetchTool(),
|
|
366
|
+
WebSearchTool(),
|
|
165
367
|
]
|
|
166
|
-
|
|
368
|
+
|
|
369
|
+
# Add TaskTool if available (avoid circular import)
|
|
370
|
+
try:
|
|
371
|
+
from ..tools.task_tool import TaskTool
|
|
372
|
+
|
|
373
|
+
minion_tools.append(TaskTool(workdir=str(workdir)))
|
|
374
|
+
except ImportError:
|
|
375
|
+
pass
|
|
376
|
+
|
|
167
377
|
# Add any additional tools
|
|
168
378
|
all_tools = minion_tools[:]
|
|
169
379
|
if additional_tools:
|
|
170
380
|
all_tools.extend(additional_tools)
|
|
171
|
-
|
|
381
|
+
|
|
172
382
|
logger.info(f"Creating MinionCodeAgent with {len(all_tools)} tools")
|
|
173
|
-
|
|
174
|
-
|
|
383
|
+
logger.info(
|
|
384
|
+
f"LLM config - main: {llm}, quick: {llm_quick}, task: {llm_task}, reasoning: {llm_reasoning}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Create the underlying CodeAgent (hooks applied in BaseAgent.setup())
|
|
175
388
|
agent = await super().create(
|
|
176
389
|
name=name,
|
|
177
390
|
llm=llm,
|
|
178
391
|
system_prompt=system_prompt,
|
|
179
392
|
tools=all_tools,
|
|
180
|
-
|
|
393
|
+
hooks=hooks, # Pass hooks to parent - applied in BaseAgent.setup()
|
|
394
|
+
**kwargs,
|
|
181
395
|
)
|
|
182
|
-
|
|
396
|
+
|
|
397
|
+
# Store specialized LLM configurations in a dict
|
|
398
|
+
agent.llms = {
|
|
399
|
+
"main": agent.llm, # The actual provider object
|
|
400
|
+
"quick": llm_quick,
|
|
401
|
+
"task": llm_task,
|
|
402
|
+
"reasoning": llm_reasoning,
|
|
403
|
+
}
|
|
404
|
+
|
|
183
405
|
# Initialize todo tracking metadata
|
|
184
|
-
if not hasattr(agent.state,
|
|
406
|
+
if not hasattr(agent.state, "metadata"):
|
|
185
407
|
agent.state.metadata = {}
|
|
186
408
|
agent.state.metadata["iteration_without_todos"] = 0
|
|
187
|
-
|
|
409
|
+
|
|
188
410
|
# Add initial todo reminder to history
|
|
189
|
-
agent.state.history.append({
|
|
190
|
-
|
|
191
|
-
'content': INITIAL_REMINDER
|
|
192
|
-
})
|
|
193
|
-
|
|
411
|
+
agent.state.history.append({"role": "user", "content": INITIAL_REMINDER})
|
|
412
|
+
|
|
194
413
|
return agent
|
|
195
|
-
|
|
196
|
-
async def run_async(self, message: str, **kwargs) -> Any:
|
|
414
|
+
|
|
415
|
+
async def run_async(self, message: str, stream: bool = False, **kwargs) -> Any:
|
|
197
416
|
"""
|
|
198
417
|
Run agent asynchronously and track conversation history.
|
|
199
|
-
|
|
418
|
+
|
|
200
419
|
Args:
|
|
201
420
|
message: User message
|
|
421
|
+
stream: If True, return streaming generator
|
|
202
422
|
**kwargs: Additional arguments passed to agent.run_async()
|
|
203
|
-
|
|
423
|
+
|
|
204
424
|
Returns:
|
|
205
|
-
Agent response
|
|
425
|
+
Agent response or async generator for streaming
|
|
206
426
|
"""
|
|
207
427
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
428
|
+
if stream:
|
|
429
|
+
# For streaming, await parent to get async generator, then wrap it
|
|
430
|
+
stream_gen = await super().run_async(message, stream=True, **kwargs)
|
|
431
|
+
return self._wrap_stream_with_history(message, stream_gen)
|
|
432
|
+
else:
|
|
433
|
+
# For non-streaming, await the result
|
|
434
|
+
result = await super().run_async(message, stream=False, **kwargs)
|
|
435
|
+
|
|
436
|
+
# Track conversation history for non-streaming
|
|
437
|
+
self.conversation_history.append(
|
|
438
|
+
{
|
|
439
|
+
"user_message": message,
|
|
440
|
+
"agent_response": (
|
|
441
|
+
result.answer if hasattr(result, "answer") else str(result)
|
|
442
|
+
),
|
|
443
|
+
"timestamp": asyncio.get_event_loop().time(),
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return result
|
|
448
|
+
|
|
219
449
|
except Exception as e:
|
|
220
450
|
logger.error(f"Error in run_async: {e}")
|
|
221
451
|
traceback.print_exc()
|
|
222
452
|
raise
|
|
223
|
-
|
|
453
|
+
|
|
454
|
+
async def _wrap_stream_with_history(self, message: str, stream):
|
|
455
|
+
"""Wrap stream generator to track conversation history."""
|
|
456
|
+
final_response = None
|
|
457
|
+
async for chunk in stream:
|
|
458
|
+
final_response = chunk
|
|
459
|
+
yield chunk
|
|
460
|
+
|
|
461
|
+
# Track conversation history after streaming completes
|
|
462
|
+
if final_response:
|
|
463
|
+
self.conversation_history.append(
|
|
464
|
+
{
|
|
465
|
+
"user_message": message,
|
|
466
|
+
"agent_response": (
|
|
467
|
+
final_response.answer
|
|
468
|
+
if hasattr(final_response, "answer")
|
|
469
|
+
else str(final_response)
|
|
470
|
+
),
|
|
471
|
+
"timestamp": asyncio.get_event_loop().time(),
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
224
475
|
def run(self, message: str, **kwargs) -> Any:
|
|
225
476
|
"""
|
|
226
477
|
Run agent synchronously.
|
|
227
|
-
|
|
478
|
+
|
|
228
479
|
Args:
|
|
229
480
|
message: User message
|
|
230
481
|
**kwargs: Additional arguments
|
|
231
|
-
|
|
482
|
+
|
|
232
483
|
Returns:
|
|
233
484
|
Agent response
|
|
234
485
|
"""
|
|
235
486
|
return asyncio.run(self.run_async(message, **kwargs))
|
|
236
|
-
|
|
487
|
+
|
|
237
488
|
def get_conversation_history(self) -> List[dict]:
|
|
238
489
|
"""Get conversation history."""
|
|
239
490
|
return self.conversation_history.copy()
|
|
240
|
-
|
|
491
|
+
|
|
241
492
|
def clear_conversation_history(self):
|
|
242
493
|
"""Clear conversation history."""
|
|
243
494
|
self.conversation_history.clear()
|
|
244
|
-
|
|
495
|
+
|
|
245
496
|
def get_tools_info(self) -> List[dict]:
|
|
246
497
|
"""
|
|
247
498
|
Get information about available tools.
|
|
248
|
-
|
|
499
|
+
|
|
249
500
|
Returns:
|
|
250
501
|
List of tool information dictionaries
|
|
251
502
|
"""
|
|
252
503
|
tools_info = []
|
|
253
504
|
for tool in self.tools:
|
|
254
505
|
readonly_status = getattr(tool, "readonly", None)
|
|
255
|
-
tools_info.append(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
506
|
+
tools_info.append(
|
|
507
|
+
{
|
|
508
|
+
"name": tool.name,
|
|
509
|
+
"description": tool.description,
|
|
510
|
+
"readonly": readonly_status,
|
|
511
|
+
"type": type(tool).__name__,
|
|
512
|
+
}
|
|
513
|
+
)
|
|
261
514
|
return tools_info
|
|
262
|
-
|
|
515
|
+
|
|
263
516
|
def print_tools_summary(self):
|
|
264
517
|
"""Print a summary of available tools."""
|
|
265
518
|
tools_info = self.get_tools_info()
|
|
266
|
-
|
|
519
|
+
|
|
267
520
|
print(f"\n🛠️ Available Tools ({len(tools_info)} total):")
|
|
268
|
-
|
|
521
|
+
|
|
269
522
|
# Group tools by category
|
|
270
523
|
categories = {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
524
|
+
"File & Directory": ["file", "read", "write", "grep", "glob", "ls"],
|
|
525
|
+
"System & Execution": ["bash", "python", "calc", "system"],
|
|
526
|
+
"Web & Search": ["web", "search", "wikipedia", "visit"],
|
|
527
|
+
"Other": [],
|
|
275
528
|
}
|
|
276
|
-
|
|
529
|
+
|
|
277
530
|
categorized_tools = {cat: [] for cat in categories}
|
|
278
|
-
|
|
531
|
+
|
|
279
532
|
for tool in tools_info:
|
|
280
533
|
categorized = False
|
|
281
534
|
for category, keywords in categories.items():
|
|
282
|
-
if category ==
|
|
535
|
+
if category == "Other":
|
|
283
536
|
continue
|
|
284
|
-
if any(keyword in tool[
|
|
537
|
+
if any(keyword in tool["name"].lower() for keyword in keywords):
|
|
285
538
|
categorized_tools[category].append(tool)
|
|
286
539
|
categorized = True
|
|
287
540
|
break
|
|
288
|
-
|
|
541
|
+
|
|
289
542
|
if not categorized:
|
|
290
|
-
categorized_tools[
|
|
291
|
-
|
|
543
|
+
categorized_tools["Other"].append(tool)
|
|
544
|
+
|
|
292
545
|
# Print categorized tools
|
|
293
546
|
for category, tools in categorized_tools.items():
|
|
294
547
|
if tools:
|
|
295
548
|
print(f"\n📁 {category} Tools:")
|
|
296
549
|
for tool in tools:
|
|
297
|
-
readonly_icon = "🔒" if tool[
|
|
550
|
+
readonly_icon = "🔒" if tool["readonly"] else "✏️"
|
|
298
551
|
print(f" {readonly_icon} {tool['name']}: {tool['description']}")
|
|
299
|
-
|
|
552
|
+
|
|
300
553
|
print(f"\n🔒 = readonly tool, ✏️ = read/write tool")
|
|
301
554
|
|
|
555
|
+
def get_context_stats(self) -> dict:
|
|
556
|
+
"""Get current context usage statistics.
|
|
557
|
+
|
|
558
|
+
Note: Auto-compact is handled by minion's BaseAgent. This method
|
|
559
|
+
delegates to the parent class if available.
|
|
560
|
+
"""
|
|
561
|
+
# Delegate to parent class (BaseAgent) methods
|
|
562
|
+
if hasattr(self, "_calculate_current_tokens") and hasattr(
|
|
563
|
+
self, "_get_context_window_limit"
|
|
564
|
+
):
|
|
565
|
+
if not hasattr(self.state, "history") or not self.state.history:
|
|
566
|
+
context_limit = self._get_context_window_limit()
|
|
567
|
+
return {
|
|
568
|
+
"total_tokens": 0,
|
|
569
|
+
"usage_percentage": 0.0,
|
|
570
|
+
"needs_compacting": False,
|
|
571
|
+
"remaining_tokens": context_limit,
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
current_tokens = self._calculate_current_tokens(self.state.history)
|
|
575
|
+
context_limit = self._get_context_window_limit()
|
|
576
|
+
usage_percentage = (
|
|
577
|
+
current_tokens / context_limit if context_limit > 0 else 0.0
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
"total_tokens": current_tokens,
|
|
582
|
+
"usage_percentage": usage_percentage,
|
|
583
|
+
"needs_compacting": (
|
|
584
|
+
self._should_compact(self.state.history)
|
|
585
|
+
if hasattr(self, "_should_compact")
|
|
586
|
+
else False
|
|
587
|
+
),
|
|
588
|
+
"remaining_tokens": context_limit - current_tokens,
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
# Fallback if parent methods not available
|
|
592
|
+
return {
|
|
593
|
+
"total_tokens": 0,
|
|
594
|
+
"usage_percentage": 0.0,
|
|
595
|
+
"needs_compacting": False,
|
|
596
|
+
"remaining_tokens": (
|
|
597
|
+
self.default_context_window
|
|
598
|
+
if hasattr(self, "default_context_window")
|
|
599
|
+
else 128000
|
|
600
|
+
),
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async def force_compact_history(self) -> bool:
|
|
604
|
+
"""Manually trigger history compaction. Returns True if compaction occurred.
|
|
605
|
+
|
|
606
|
+
Note: Delegates to minion's BaseAgent.compact_now() method.
|
|
607
|
+
"""
|
|
608
|
+
if hasattr(self, "compact_now"):
|
|
609
|
+
await self.compact_now()
|
|
610
|
+
logger.info("Manual compaction triggered via BaseAgent.compact_now()")
|
|
611
|
+
return True
|
|
612
|
+
|
|
613
|
+
logger.warning("compact_now() not available on parent class")
|
|
614
|
+
return False
|
|
615
|
+
|
|
616
|
+
def get_llm_for_task(self, task_type: str = "main"):
|
|
617
|
+
"""
|
|
618
|
+
Get the appropriate LLM for a specific task type.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
task_type: Type of task - "main", "quick", "task", or "reasoning"
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
LLM model name or provider for the specified task type
|
|
625
|
+
"""
|
|
626
|
+
if not hasattr(self, "llms"):
|
|
627
|
+
return self.llm
|
|
628
|
+
|
|
629
|
+
return self.llms.get(task_type, self.llm)
|
|
630
|
+
|
|
631
|
+
def get_llm_config(self) -> dict:
|
|
632
|
+
"""
|
|
633
|
+
Get all LLM configurations.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Dictionary with all LLM configurations
|
|
637
|
+
"""
|
|
638
|
+
if not hasattr(self, "llms"):
|
|
639
|
+
return {
|
|
640
|
+
"main": self.llm,
|
|
641
|
+
"quick": self.llm,
|
|
642
|
+
"task": self.llm,
|
|
643
|
+
"reasoning": self.llm,
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return self.llms.copy()
|
|
647
|
+
|
|
648
|
+
def update_llm_config(self, **kwargs) -> None:
|
|
649
|
+
"""
|
|
650
|
+
Update LLM configurations dynamically.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
**kwargs: LLM configurations to update (quick, task, reasoning)
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
agent.update_llm_config(quick='haiku', reasoning='o1-mini')
|
|
657
|
+
"""
|
|
658
|
+
if not hasattr(self, "llms"):
|
|
659
|
+
self.llms = {
|
|
660
|
+
"main": self.llm,
|
|
661
|
+
"quick": self.llm,
|
|
662
|
+
"task": self.llm,
|
|
663
|
+
"reasoning": self.llm,
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
for key, value in kwargs.items():
|
|
667
|
+
if key in ["quick", "task", "reasoning"]:
|
|
668
|
+
self.llms[key] = value
|
|
669
|
+
logger.info(f"Updated LLM config: {key} = {value}")
|
|
670
|
+
else:
|
|
671
|
+
logger.warning(
|
|
672
|
+
f"Invalid LLM config key: {key}. Valid keys: quick, task, reasoning"
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
async def query_quick(
|
|
676
|
+
self,
|
|
677
|
+
user_prompt: str,
|
|
678
|
+
system_prompt: Optional[Union[str, List[str]]] = None,
|
|
679
|
+
assistant_prompt: Optional[str] = None,
|
|
680
|
+
enable_prompt_caching: bool = False,
|
|
681
|
+
llm: Optional[str] = None,
|
|
682
|
+
) -> str:
|
|
683
|
+
"""
|
|
684
|
+
Quick query method for simple LLM interactions without agent overhead.
|
|
685
|
+
|
|
686
|
+
This is a convenience wrapper around the query_quick function that uses
|
|
687
|
+
this agent instance. It bypasses tool execution and complex routing.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
user_prompt: The user's message/question
|
|
691
|
+
system_prompt: Optional system prompt(s) - can be a string or list of strings
|
|
692
|
+
assistant_prompt: Optional assistant prompt to prefill the response
|
|
693
|
+
enable_prompt_caching: Whether to enable prompt caching (default: False)
|
|
694
|
+
llm: Optional LLM model to use (defaults to agent's quick LLM)
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
The LLM's response as a string
|
|
698
|
+
|
|
699
|
+
Example:
|
|
700
|
+
>>> agent = await MinionCodeAgent.create(name="Assistant", llm="sonnet")
|
|
701
|
+
>>> response = await agent.query_quick(
|
|
702
|
+
... user_prompt="What is 2+2?",
|
|
703
|
+
... system_prompt="You are a helpful math assistant."
|
|
704
|
+
... )
|
|
705
|
+
>>> print(response)
|
|
706
|
+
"4"
|
|
707
|
+
"""
|
|
708
|
+
return await query_quick(
|
|
709
|
+
agent=self,
|
|
710
|
+
user_prompt=user_prompt,
|
|
711
|
+
system_prompt=system_prompt,
|
|
712
|
+
assistant_prompt=assistant_prompt,
|
|
713
|
+
enable_prompt_caching=enable_prompt_caching,
|
|
714
|
+
llm=llm,
|
|
715
|
+
)
|
|
716
|
+
|
|
302
717
|
|
|
303
718
|
# Convenience function for quick setup
|
|
304
719
|
async def create_minion_code_agent(
|
|
305
|
-
name: str = "Minion Code Assistant",
|
|
306
|
-
llm: str = "gpt-4o-mini",
|
|
307
|
-
**kwargs
|
|
720
|
+
name: str = "Minion Code Assistant", llm: str = "claude-sonnet-4-5", **kwargs
|
|
308
721
|
) -> MinionCodeAgent:
|
|
309
722
|
"""
|
|
310
723
|
Convenience function to create a MinionCodeAgent.
|
|
311
|
-
|
|
724
|
+
|
|
312
725
|
Args:
|
|
313
726
|
name: Agent name
|
|
314
727
|
llm: LLM model to use
|
|
315
728
|
**kwargs: Additional arguments passed to MinionCodeAgent.create()
|
|
316
|
-
|
|
729
|
+
|
|
317
730
|
Returns:
|
|
318
731
|
Configured MinionCodeAgent instance
|
|
319
732
|
"""
|
|
320
|
-
return await MinionCodeAgent.create(name=name, llm=llm, **kwargs)
|
|
733
|
+
return await MinionCodeAgent.create(name=name, llm=llm, **kwargs)
|