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
@@ -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
- '</reminder>'
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
- '</reminder>'
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 a coding agent operating INSIDE the user's repository at {workdir}.\n"
74
- "Follow this loop strictly: plan briefly use TOOLS to act directly on files/shell → report concise results.\n"
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: string_edit for single string replacements, multi_edit for multiple changes to same file, file_edit for advanced operations.\n"
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
- # Call parent pre_step first
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, 'metadata'):
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
- **kwargs
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
- # Get all minion_code tools
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
- # Create the underlying CodeAgent
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
- **kwargs
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, 'metadata'):
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
- 'role': 'user',
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
- response = await super().run_async(message, **kwargs)
209
-
210
- # Track conversation history
211
- self.conversation_history.append({
212
- 'user_message': message,
213
- 'agent_response': response.answer if hasattr(response, 'answer') else str(response),
214
- 'timestamp': asyncio.get_event_loop().time()
215
- })
216
-
217
- return response
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
- 'name': tool.name,
257
- 'description': tool.description,
258
- 'readonly': readonly_status,
259
- 'type': type(tool).__name__
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
- 'File & Directory': ['file', 'read', 'write', 'grep', 'glob', 'ls'],
272
- 'System & Execution': ['bash', 'python', 'calc', 'system'],
273
- 'Web & Search': ['web', 'search', 'wikipedia', 'visit'],
274
- 'Other': []
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 == 'Other':
535
+ if category == "Other":
283
536
  continue
284
- if any(keyword in tool['name'].lower() for keyword in keywords):
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['Other'].append(tool)
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['readonly'] else "✏️"
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)