minion-code 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.1.dist-info/METADATA +475 -0
- minion_code-0.1.1.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
- minion_code-0.1.1.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Task Tool for launching specialized agents to handle complex, multi-step tasks.
|
|
4
|
+
Uses SubagentRegistry to dynamically manage available agent types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, Optional, List, Union
|
|
11
|
+
from minion.tools import AsyncBaseTool
|
|
12
|
+
from minion.types import AgentState
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_task_tool_prompt() -> str:
|
|
16
|
+
"""
|
|
17
|
+
Generate the complete Task tool prompt including available subagents.
|
|
18
|
+
This is used to generate the Task tool description dynamically.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Complete task tool prompt with all available subagents
|
|
22
|
+
"""
|
|
23
|
+
from ..subagents import load_subagents
|
|
24
|
+
|
|
25
|
+
registry = load_subagents()
|
|
26
|
+
subagents = registry.list_all()
|
|
27
|
+
|
|
28
|
+
if not subagents:
|
|
29
|
+
return """Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
30
|
+
|
|
31
|
+
No subagents are currently available."""
|
|
32
|
+
|
|
33
|
+
# Generate subagent descriptions
|
|
34
|
+
subagent_lines = registry.generate_tool_description_lines()
|
|
35
|
+
|
|
36
|
+
return f"""Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
37
|
+
|
|
38
|
+
Available agent types and the tools they have access to:
|
|
39
|
+
{subagent_lines}
|
|
40
|
+
|
|
41
|
+
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use. Default is "general-purpose".
|
|
42
|
+
|
|
43
|
+
When to use the Task tool:
|
|
44
|
+
- For complex, multi-step tasks that require specialized expertise
|
|
45
|
+
- When you need to delegate a complete subtask to a focused agent
|
|
46
|
+
- For exploration tasks (use "Explore" subagent)
|
|
47
|
+
- For planning and architecture design (use "Plan" subagent)
|
|
48
|
+
- For documentation lookup (use "claude-code-guide" subagent)
|
|
49
|
+
|
|
50
|
+
When NOT to use the Task tool:
|
|
51
|
+
- If you want to read a specific file path, use the file_read tool instead
|
|
52
|
+
- For simple grep searches, use the grep tool directly
|
|
53
|
+
- For single bash commands, use the bash tool directly
|
|
54
|
+
- For simple questions you can answer directly without tools
|
|
55
|
+
|
|
56
|
+
Usage notes:
|
|
57
|
+
1. Each agent invocation is stateless and autonomous
|
|
58
|
+
2. Provide detailed task descriptions for best results
|
|
59
|
+
3. Choose the appropriate subagent_type for your specific task
|
|
60
|
+
4. Read-only subagents (Explore, Plan) cannot modify files
|
|
61
|
+
|
|
62
|
+
Example usage:
|
|
63
|
+
- Task(description="Explore auth", prompt="Find all authentication-related files", subagent_type="Explore")
|
|
64
|
+
- Task(description="Plan feature", prompt="Design implementation plan for user settings", subagent_type="Plan")
|
|
65
|
+
- Task(description="Complex refactor", prompt="Refactor the database layer...", subagent_type="general-purpose")
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TaskTool(AsyncBaseTool):
|
|
70
|
+
"""
|
|
71
|
+
A tool for launching specialized agents to handle complex, multi-step tasks autonomously.
|
|
72
|
+
Uses SubagentRegistry to dynamically manage available agent types.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name = "Task"
|
|
76
|
+
# Description will be set dynamically in __init__
|
|
77
|
+
description = "Launch a new agent to handle complex, multi-step tasks autonomously"
|
|
78
|
+
readonly = (
|
|
79
|
+
True # Task execution is read-only from the perspective of the calling agent
|
|
80
|
+
)
|
|
81
|
+
needs_state = True
|
|
82
|
+
|
|
83
|
+
inputs = {
|
|
84
|
+
"description": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "A short (3-5 word) description of the task",
|
|
87
|
+
},
|
|
88
|
+
"prompt": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "The task for the agent to perform",
|
|
91
|
+
},
|
|
92
|
+
"model_name": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "Optional: Specific model name to use for this task",
|
|
95
|
+
"nullable": True,
|
|
96
|
+
},
|
|
97
|
+
"subagent_type": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "The type of specialized agent to use (default: general-purpose)",
|
|
100
|
+
"nullable": True,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
output_type = "string"
|
|
104
|
+
|
|
105
|
+
def __init__(self, workdir: Optional[str] = None):
|
|
106
|
+
super().__init__()
|
|
107
|
+
self._registry = None
|
|
108
|
+
self._workdir = Path(workdir) if workdir else None
|
|
109
|
+
# Set dynamic description
|
|
110
|
+
self.description = generate_task_tool_prompt()
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def registry(self):
|
|
114
|
+
"""Get the subagent registry, loading subagents if needed."""
|
|
115
|
+
if self._registry is None:
|
|
116
|
+
from ..subagents import load_subagents
|
|
117
|
+
|
|
118
|
+
self._registry = load_subagents()
|
|
119
|
+
return self._registry
|
|
120
|
+
|
|
121
|
+
async def forward(
|
|
122
|
+
self,
|
|
123
|
+
description: str,
|
|
124
|
+
prompt: str,
|
|
125
|
+
model_name: Optional[str] = None,
|
|
126
|
+
subagent_type: Optional[str] = None,
|
|
127
|
+
*,
|
|
128
|
+
state: AgentState,
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Execute the task using a specialized agent (async)."""
|
|
131
|
+
start_time = time.time()
|
|
132
|
+
|
|
133
|
+
# Default to general-purpose
|
|
134
|
+
agent_type = subagent_type or "general-purpose"
|
|
135
|
+
|
|
136
|
+
# Get subagent config from registry
|
|
137
|
+
subagent_config = self.registry.get(agent_type)
|
|
138
|
+
|
|
139
|
+
if subagent_config is None:
|
|
140
|
+
available_types = self.registry.list_names()
|
|
141
|
+
return (
|
|
142
|
+
f"Agent type '{agent_type}' not found.\n\nAvailable agents:\n"
|
|
143
|
+
+ "\n".join(f" - {t}" for t in available_types)
|
|
144
|
+
+ "\n\nUse one of the available agent types."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Build effective prompt
|
|
148
|
+
effective_prompt = prompt
|
|
149
|
+
if subagent_config.system_prompt:
|
|
150
|
+
effective_prompt = f"{subagent_config.system_prompt}\n\n{prompt}"
|
|
151
|
+
|
|
152
|
+
# Determine model
|
|
153
|
+
effective_model = model_name or "gpt-4o-mini"
|
|
154
|
+
if not model_name and subagent_config.model_name != "inherit":
|
|
155
|
+
effective_model = subagent_config.model_name
|
|
156
|
+
|
|
157
|
+
# Progress messages
|
|
158
|
+
progress_messages = [
|
|
159
|
+
f"Starting agent: {agent_type}",
|
|
160
|
+
f"Using model: {effective_model}",
|
|
161
|
+
f"Task: {description}",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
from ..agents.code_agent import MinionCodeAgent
|
|
166
|
+
|
|
167
|
+
# Determine working directory
|
|
168
|
+
workdir = self._workdir or Path.cwd()
|
|
169
|
+
|
|
170
|
+
# Create agent with filtered tools
|
|
171
|
+
agent = await MinionCodeAgent.create(
|
|
172
|
+
name=f"Task Agent ({agent_type})",
|
|
173
|
+
llm=effective_model,
|
|
174
|
+
system_prompt=(
|
|
175
|
+
effective_prompt if subagent_config.system_prompt else None
|
|
176
|
+
),
|
|
177
|
+
workdir=workdir,
|
|
178
|
+
additional_tools=self._get_filtered_tools(subagent_config.tools),
|
|
179
|
+
# History decay: save large outputs to file after N steps
|
|
180
|
+
decay_enabled=True,
|
|
181
|
+
decay_ttl_steps=3,
|
|
182
|
+
decay_min_size=100_000, # 100KB
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Execute
|
|
186
|
+
response = await agent.run_async(prompt)
|
|
187
|
+
|
|
188
|
+
# Extract response
|
|
189
|
+
if hasattr(response, "answer"):
|
|
190
|
+
result_text = response.answer
|
|
191
|
+
elif hasattr(response, "content"):
|
|
192
|
+
result_text = response.content
|
|
193
|
+
else:
|
|
194
|
+
result_text = str(response)
|
|
195
|
+
|
|
196
|
+
duration = time.time() - start_time
|
|
197
|
+
completion_message = f"Task completed ({self._format_duration(duration)})"
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
"\n".join(progress_messages)
|
|
201
|
+
+ f"\n\n{result_text}\n\n{completion_message}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return (
|
|
206
|
+
"\n".join(progress_messages)
|
|
207
|
+
+ f"\n\nError during task execution: {str(e)}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _get_filtered_tools(self, tool_filter: Union[str, List[str]]) -> Optional[List]:
|
|
211
|
+
"""Get filtered tools based on subagent configuration."""
|
|
212
|
+
if tool_filter == "*" or (isinstance(tool_filter, list) and "*" in tool_filter):
|
|
213
|
+
return None # Use all default tools
|
|
214
|
+
|
|
215
|
+
# TODO: Implement actual tool filtering based on tool names
|
|
216
|
+
# For now, return None to use all tools
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def _format_duration(self, seconds: float) -> str:
|
|
220
|
+
"""Format duration in a human-readable way."""
|
|
221
|
+
if seconds < 1:
|
|
222
|
+
return f"{int(seconds * 1000)}ms"
|
|
223
|
+
elif seconds < 60:
|
|
224
|
+
return f"{seconds:.1f}s"
|
|
225
|
+
else:
|
|
226
|
+
minutes = int(seconds // 60)
|
|
227
|
+
remaining_seconds = seconds % 60
|
|
228
|
+
return f"{minutes}m {remaining_seconds:.1f}s"
|
|
229
|
+
|
|
230
|
+
def _validate_input(
|
|
231
|
+
self,
|
|
232
|
+
description: str,
|
|
233
|
+
prompt: str,
|
|
234
|
+
model_name: Optional[str] = None,
|
|
235
|
+
subagent_type: Optional[str] = None,
|
|
236
|
+
) -> Dict[str, Any]:
|
|
237
|
+
"""Validate input parameters."""
|
|
238
|
+
|
|
239
|
+
if not description or not isinstance(description, str):
|
|
240
|
+
return {
|
|
241
|
+
"valid": False,
|
|
242
|
+
"message": "Description is required and must be a string",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if not prompt or not isinstance(prompt, str):
|
|
246
|
+
return {
|
|
247
|
+
"valid": False,
|
|
248
|
+
"message": "Prompt is required and must be a string",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Validate subagent_type if provided
|
|
252
|
+
if subagent_type and not self.registry.exists(subagent_type):
|
|
253
|
+
available_types = self.registry.list_names()
|
|
254
|
+
return {
|
|
255
|
+
"valid": False,
|
|
256
|
+
"message": f"Agent type '{subagent_type}' does not exist. Available types: {', '.join(available_types)}",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {"valid": True}
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def get_available_agent_types(cls) -> List[str]:
|
|
263
|
+
"""Get list of available agent types."""
|
|
264
|
+
from ..subagents import get_available_subagents
|
|
265
|
+
|
|
266
|
+
return [s.name for s in get_available_subagents()]
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def get_agent_description(cls, agent_type: str) -> Optional[Dict[str, Any]]:
|
|
270
|
+
"""Get description for a specific agent type."""
|
|
271
|
+
from ..subagents import get_subagent_registry
|
|
272
|
+
|
|
273
|
+
registry = get_subagent_registry()
|
|
274
|
+
subagent = registry.get(agent_type)
|
|
275
|
+
if subagent:
|
|
276
|
+
return {
|
|
277
|
+
"description": subagent.description,
|
|
278
|
+
"when_to_use": subagent.when_to_use,
|
|
279
|
+
"tools": subagent.tools,
|
|
280
|
+
"readonly": subagent.readonly,
|
|
281
|
+
}
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def get_prompt_text(cls) -> str:
|
|
286
|
+
"""Get the tool prompt text for agent instructions."""
|
|
287
|
+
return generate_task_tool_prompt()
|
|
@@ -11,17 +11,17 @@ def format_todos_display(todos):
|
|
|
11
11
|
"""Format todos for display."""
|
|
12
12
|
if not todos:
|
|
13
13
|
return "No todos currently"
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
# Sort: [completed, in_progress, pending]
|
|
16
16
|
order = [TodoStatus.COMPLETED, TodoStatus.IN_PROGRESS, TodoStatus.PENDING]
|
|
17
17
|
sorted_todos = sorted(todos, key=lambda t: (order.index(t.status), t.content))
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
# Find the next pending task
|
|
20
20
|
next_pending_index = next(
|
|
21
21
|
(i for i, todo in enumerate(sorted_todos) if todo.status == TodoStatus.PENDING),
|
|
22
|
-
-1
|
|
22
|
+
-1,
|
|
23
23
|
)
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
lines = []
|
|
26
26
|
for i, todo in enumerate(sorted_todos):
|
|
27
27
|
# Determine checkbox and formatting
|
|
@@ -40,61 +40,65 @@ def format_todos_display(todos):
|
|
|
40
40
|
content = f"**{todo.content}**" # Bold for next pending
|
|
41
41
|
else:
|
|
42
42
|
content = todo.content
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
lines.append(f"{prefix}{checkbox} {content}")
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
return "\n".join(lines)
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class TodoReadTool(BaseTool):
|
|
50
50
|
"""Tool for reading and displaying current todo items."""
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
name = "todo_read"
|
|
53
53
|
description = "View current todo items and their status."
|
|
54
54
|
readonly = True # Read-only tool, does not modify system state
|
|
55
55
|
needs_state = True # Tool needs agent state
|
|
56
56
|
inputs = {}
|
|
57
57
|
output_type = "string"
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
def _get_agent_id(self, state: AgentState) -> str:
|
|
60
60
|
"""Get agent ID from agent state."""
|
|
61
61
|
# Try to get from metadata
|
|
62
62
|
return state.agent.agent_id
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
def forward(self, state: AgentState) -> str:
|
|
65
65
|
"""Execute the todo read operation."""
|
|
66
66
|
try:
|
|
67
67
|
# Get agent ID from agent state
|
|
68
68
|
agent_id = self._get_agent_id(state)
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
# Get current todos
|
|
71
71
|
todos = get_todos(agent_id)
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
if not todos:
|
|
74
74
|
return "No todos currently"
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
# Generate statistics
|
|
77
77
|
stats = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
"total": len(todos),
|
|
79
|
+
"pending": len([t for t in todos if t.status == TodoStatus.PENDING]),
|
|
80
|
+
"in_progress": len(
|
|
81
|
+
[t for t in todos if t.status == TodoStatus.IN_PROGRESS]
|
|
82
|
+
),
|
|
83
|
+
"completed": len(
|
|
84
|
+
[t for t in todos if t.status == TodoStatus.COMPLETED]
|
|
85
|
+
),
|
|
82
86
|
}
|
|
83
|
-
|
|
87
|
+
|
|
84
88
|
# Update agent metadata with current todo stats
|
|
85
|
-
state.metadata[
|
|
86
|
-
|
|
89
|
+
state.metadata["current_todo_stats"] = stats
|
|
90
|
+
|
|
87
91
|
# Reset iteration counter since todo tool was used
|
|
88
92
|
state.metadata["iteration_without_todos"] = 0
|
|
89
|
-
|
|
93
|
+
|
|
90
94
|
# Format display
|
|
91
95
|
display = format_todos_display(todos)
|
|
92
|
-
|
|
96
|
+
|
|
93
97
|
# Generate summary
|
|
94
98
|
summary = f"Found {stats['total']} todo(s): {stats['pending']} pending, {stats['in_progress']} in progress, {stats['completed']} completed"
|
|
95
|
-
|
|
99
|
+
|
|
96
100
|
result = f"{summary}\n\n{display}"
|
|
97
101
|
return result
|
|
98
|
-
|
|
102
|
+
|
|
99
103
|
except Exception as e:
|
|
100
|
-
return f"Error reading todos: {str(e)}"
|
|
104
|
+
return f"Error reading todos: {str(e)}"
|
|
@@ -7,17 +7,24 @@ from minion.tools import BaseTool
|
|
|
7
7
|
from minion.types import AgentState
|
|
8
8
|
|
|
9
9
|
from ..utils.todo_storage import (
|
|
10
|
-
TodoItem,
|
|
11
|
-
|
|
10
|
+
TodoItem,
|
|
11
|
+
TodoStatus,
|
|
12
|
+
TodoPriority,
|
|
13
|
+
get_todos,
|
|
14
|
+
set_todos,
|
|
12
15
|
)
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
18
|
class ValidationResult:
|
|
19
19
|
"""Result of todo validation."""
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
result: bool,
|
|
24
|
+
error_code: int = 0,
|
|
25
|
+
message: str = "",
|
|
26
|
+
meta: Dict[str, Any] = None,
|
|
27
|
+
):
|
|
21
28
|
self.result = result
|
|
22
29
|
self.error_code = error_code
|
|
23
30
|
self.message = message
|
|
@@ -35,19 +42,21 @@ def validate_todos(todos: List[TodoItem]) -> ValidationResult:
|
|
|
35
42
|
result=False,
|
|
36
43
|
error_code=1,
|
|
37
44
|
message="Duplicate todo IDs found",
|
|
38
|
-
meta={"duplicate_ids": list(set(duplicate_ids))}
|
|
45
|
+
meta={"duplicate_ids": list(set(duplicate_ids))},
|
|
39
46
|
)
|
|
40
|
-
|
|
47
|
+
|
|
41
48
|
# Check for multiple in_progress tasks
|
|
42
|
-
in_progress_tasks = [
|
|
49
|
+
in_progress_tasks = [
|
|
50
|
+
todo for todo in todos if todo.status == TodoStatus.IN_PROGRESS
|
|
51
|
+
]
|
|
43
52
|
if len(in_progress_tasks) > 1:
|
|
44
53
|
return ValidationResult(
|
|
45
54
|
result=False,
|
|
46
55
|
error_code=2,
|
|
47
56
|
message="Only one task can be in_progress at a time",
|
|
48
|
-
meta={"in_progress_task_ids": [t.id for t in in_progress_tasks]}
|
|
57
|
+
meta={"in_progress_task_ids": [t.id for t in in_progress_tasks]},
|
|
49
58
|
)
|
|
50
|
-
|
|
59
|
+
|
|
51
60
|
# Validate each todo
|
|
52
61
|
for todo in todos:
|
|
53
62
|
if not todo.content.strip():
|
|
@@ -55,26 +64,26 @@ def validate_todos(todos: List[TodoItem]) -> ValidationResult:
|
|
|
55
64
|
result=False,
|
|
56
65
|
error_code=3,
|
|
57
66
|
message=f'Todo with ID "{todo.id}" has empty content',
|
|
58
|
-
meta={"todo_id": todo.id}
|
|
67
|
+
meta={"todo_id": todo.id},
|
|
59
68
|
)
|
|
60
|
-
|
|
69
|
+
|
|
61
70
|
return ValidationResult(result=True)
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
def generate_todo_summary(todos: List[TodoItem]) -> str:
|
|
65
74
|
"""Generate a summary of todos."""
|
|
66
75
|
stats = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
"total": len(todos),
|
|
77
|
+
"pending": len([t for t in todos if t.status == TodoStatus.PENDING]),
|
|
78
|
+
"in_progress": len([t for t in todos if t.status == TodoStatus.IN_PROGRESS]),
|
|
79
|
+
"completed": len([t for t in todos if t.status == TodoStatus.COMPLETED]),
|
|
71
80
|
}
|
|
72
|
-
|
|
81
|
+
|
|
73
82
|
summary = f"Updated {stats['total']} todo(s)"
|
|
74
|
-
if stats[
|
|
83
|
+
if stats["total"] > 0:
|
|
75
84
|
summary += f" ({stats['pending']} pending, {stats['in_progress']} in progress, {stats['completed']} completed)"
|
|
76
85
|
summary += ". Continue tracking your progress with the todo list."
|
|
77
|
-
|
|
86
|
+
|
|
78
87
|
return summary
|
|
79
88
|
|
|
80
89
|
|
|
@@ -82,17 +91,17 @@ def format_todos_display(todos: List[TodoItem]) -> str:
|
|
|
82
91
|
"""Format todos for display."""
|
|
83
92
|
if not todos:
|
|
84
93
|
return "No todos currently"
|
|
85
|
-
|
|
94
|
+
|
|
86
95
|
# Sort: [completed, in_progress, pending]
|
|
87
96
|
order = [TodoStatus.COMPLETED, TodoStatus.IN_PROGRESS, TodoStatus.PENDING]
|
|
88
97
|
sorted_todos = sorted(todos, key=lambda t: (order.index(t.status), t.content))
|
|
89
|
-
|
|
98
|
+
|
|
90
99
|
# Find the next pending task
|
|
91
100
|
next_pending_index = next(
|
|
92
101
|
(i for i, todo in enumerate(sorted_todos) if todo.status == TodoStatus.PENDING),
|
|
93
|
-
-1
|
|
102
|
+
-1,
|
|
94
103
|
)
|
|
95
|
-
|
|
104
|
+
|
|
96
105
|
lines = []
|
|
97
106
|
for i, todo in enumerate(sorted_todos):
|
|
98
107
|
# Determine checkbox and formatting
|
|
@@ -111,27 +120,27 @@ def format_todos_display(todos: List[TodoItem]) -> str:
|
|
|
111
120
|
content = f"**{todo.content}**" # Bold for next pending
|
|
112
121
|
else:
|
|
113
122
|
content = todo.content
|
|
114
|
-
|
|
123
|
+
|
|
115
124
|
lines.append(f"{prefix}{checkbox} {content}")
|
|
116
|
-
|
|
125
|
+
|
|
117
126
|
return "\n".join(lines)
|
|
118
127
|
|
|
119
128
|
|
|
120
129
|
class TodoWriteTool(BaseTool):
|
|
121
130
|
"""Tool for writing and managing todo items."""
|
|
122
|
-
|
|
131
|
+
|
|
123
132
|
name = "todo_write"
|
|
124
133
|
description = "Creates and manages todo items for task tracking and progress management in the current session."
|
|
125
134
|
readonly = False # Writing tool, modifies system state
|
|
126
135
|
needs_state = True # Tool needs agent state
|
|
127
136
|
inputs = {
|
|
128
137
|
"todos_json": {
|
|
129
|
-
"type": "string",
|
|
130
|
-
"description": "JSON string containing array of todo items with id, content, status, and priority fields"
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "JSON string containing array of todo items with id, content, status, and priority fields",
|
|
131
140
|
},
|
|
132
141
|
}
|
|
133
142
|
output_type = "string"
|
|
134
|
-
|
|
143
|
+
|
|
135
144
|
def _get_agent_id(self, state: AgentState) -> str:
|
|
136
145
|
"""Get agent ID from agent state."""
|
|
137
146
|
# Try to get from metadata
|
|
@@ -147,88 +156,96 @@ class TodoWriteTool(BaseTool):
|
|
|
147
156
|
# # Use a hash of the task as agent ID
|
|
148
157
|
# import hashlib
|
|
149
158
|
# return hashlib.md5(state.task.encode()).hexdigest()[:8]
|
|
150
|
-
|
|
159
|
+
|
|
151
160
|
# Default fallback
|
|
152
161
|
return state.agent.agent_id
|
|
153
|
-
|
|
162
|
+
|
|
154
163
|
def forward(self, todos_json: str, *, state: AgentState) -> str:
|
|
155
164
|
"""Execute the todo write operation."""
|
|
156
165
|
try:
|
|
157
166
|
# Get agent ID from agent state
|
|
158
167
|
agent_id = self._get_agent_id(state)
|
|
159
|
-
|
|
168
|
+
|
|
160
169
|
# Parse JSON input
|
|
161
170
|
try:
|
|
162
171
|
todos_data = json.loads(todos_json)
|
|
163
172
|
except json.JSONDecodeError as e:
|
|
164
173
|
return f"Error: Invalid JSON format - {str(e)}"
|
|
165
|
-
|
|
174
|
+
|
|
166
175
|
if not isinstance(todos_data, list):
|
|
167
176
|
return "Error: todos_json must be an array of todo items"
|
|
168
|
-
|
|
177
|
+
|
|
169
178
|
# Convert to TodoItem objects and validate
|
|
170
179
|
todo_items = []
|
|
171
180
|
for i, todo_data in enumerate(todos_data):
|
|
172
181
|
if not isinstance(todo_data, dict):
|
|
173
182
|
return f"Error: Todo item {i} must be an object"
|
|
174
|
-
|
|
183
|
+
|
|
175
184
|
# Validate required fields
|
|
176
|
-
required_fields = [
|
|
185
|
+
required_fields = ["id", "content", "status", "priority"]
|
|
177
186
|
for field in required_fields:
|
|
178
187
|
if field not in todo_data:
|
|
179
188
|
return f"Error: Todo item {i} missing required field '{field}'"
|
|
180
|
-
|
|
189
|
+
|
|
181
190
|
# Validate status
|
|
182
|
-
if todo_data[
|
|
191
|
+
if todo_data["status"] not in ["pending", "in_progress", "completed"]:
|
|
183
192
|
return f"Error: Invalid status '{todo_data['status']}' in todo item {i}"
|
|
184
|
-
|
|
193
|
+
|
|
185
194
|
# Validate priority
|
|
186
|
-
if todo_data[
|
|
195
|
+
if todo_data["priority"] not in ["high", "medium", "low"]:
|
|
187
196
|
return f"Error: Invalid priority '{todo_data['priority']}' in todo item {i}"
|
|
188
|
-
|
|
197
|
+
|
|
189
198
|
# Validate content
|
|
190
|
-
if not todo_data[
|
|
199
|
+
if not todo_data["content"].strip():
|
|
191
200
|
return f"Error: Todo item {i} has empty content"
|
|
192
|
-
|
|
201
|
+
|
|
193
202
|
todo_item = TodoItem(
|
|
194
|
-
id=todo_data[
|
|
195
|
-
content=todo_data[
|
|
196
|
-
status=TodoStatus(todo_data[
|
|
197
|
-
priority=TodoPriority(todo_data[
|
|
203
|
+
id=todo_data["id"],
|
|
204
|
+
content=todo_data["content"],
|
|
205
|
+
status=TodoStatus(todo_data["status"]),
|
|
206
|
+
priority=TodoPriority(todo_data["priority"]),
|
|
198
207
|
)
|
|
199
208
|
todo_items.append(todo_item)
|
|
200
|
-
|
|
209
|
+
|
|
201
210
|
# Validate todos
|
|
202
211
|
validation = validate_todos(todo_items)
|
|
203
212
|
if not validation.result:
|
|
204
213
|
return f"Validation Error: {validation.message}"
|
|
205
|
-
|
|
214
|
+
|
|
206
215
|
# Get previous todos for comparison
|
|
207
216
|
previous_todos = get_todos(agent_id)
|
|
208
|
-
|
|
217
|
+
|
|
209
218
|
# Update todos in storage
|
|
210
219
|
set_todos(todo_items, agent_id)
|
|
211
|
-
|
|
220
|
+
|
|
212
221
|
# Update agent metadata with todo info
|
|
213
|
-
state.metadata[
|
|
214
|
-
state.metadata[
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
state.metadata["todo_count"] = len(todo_items)
|
|
223
|
+
state.metadata["last_todo_update"] = json.dumps(
|
|
224
|
+
{
|
|
225
|
+
"total": len(todo_items),
|
|
226
|
+
"pending": len(
|
|
227
|
+
[t for t in todo_items if t.status == TodoStatus.PENDING]
|
|
228
|
+
),
|
|
229
|
+
"in_progress": len(
|
|
230
|
+
[t for t in todo_items if t.status == TodoStatus.IN_PROGRESS]
|
|
231
|
+
),
|
|
232
|
+
"completed": len(
|
|
233
|
+
[t for t in todo_items if t.status == TodoStatus.COMPLETED]
|
|
234
|
+
),
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
|
|
221
238
|
# Reset iteration counter since todo tool was used
|
|
222
239
|
state.metadata["iteration_without_todos"] = 0
|
|
223
|
-
|
|
240
|
+
|
|
224
241
|
# Generate summary
|
|
225
242
|
summary = generate_todo_summary(todo_items)
|
|
226
|
-
|
|
243
|
+
|
|
227
244
|
# Format display
|
|
228
245
|
display = format_todos_display(todo_items)
|
|
229
|
-
|
|
246
|
+
|
|
230
247
|
result = f"{summary}\n\n{display}"
|
|
231
248
|
return result
|
|
232
|
-
|
|
249
|
+
|
|
233
250
|
except Exception as e:
|
|
234
|
-
return f"Error updating todos: {str(e)}"
|
|
251
|
+
return f"Error updating todos: {str(e)}"
|