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.
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.2.dist-info/METADATA +476 -0
  98. minion_code-0.1.2.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.2.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.2.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.2.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
- 'total': len(todos),
79
- 'pending': len([t for t in todos if t.status == TodoStatus.PENDING]),
80
- 'in_progress': len([t for t in todos if t.status == TodoStatus.IN_PROGRESS]),
81
- 'completed': len([t for t in todos if t.status == TodoStatus.COMPLETED])
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['current_todo_stats'] = stats
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, TodoStatus, TodoPriority,
11
- get_todos, set_todos
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
- def __init__(self, result: bool, error_code: int = 0, message: str = "", meta: Dict[str, Any] = None):
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 = [todo for todo in todos if todo.status == TodoStatus.IN_PROGRESS]
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
- 'total': len(todos),
68
- 'pending': len([t for t in todos if t.status == TodoStatus.PENDING]),
69
- 'in_progress': len([t for t in todos if t.status == TodoStatus.IN_PROGRESS]),
70
- 'completed': len([t for t in todos if t.status == TodoStatus.COMPLETED])
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['total'] > 0:
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 = ['id', 'content', 'status', 'priority']
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['status'] not in ['pending', 'in_progress', 'completed']:
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['priority'] not in ['high', 'medium', 'low']:
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['content'].strip():
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['id'],
195
- content=todo_data['content'],
196
- status=TodoStatus(todo_data['status']),
197
- priority=TodoPriority(todo_data['priority'])
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['todo_count'] = len(todo_items)
214
- state.metadata['last_todo_update'] = json.dumps({
215
- 'total': len(todo_items),
216
- 'pending': len([t for t in todo_items if t.status == TodoStatus.PENDING]),
217
- 'in_progress': len([t for t in todo_items if t.status == TodoStatus.IN_PROGRESS]),
218
- 'completed': len([t for t in todo_items if t.status == TodoStatus.COMPLETED])
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)}"