minion-code 0.1.0__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 (59) hide show
  1. examples/advance_tui.py +508 -0
  2. examples/agent_with_todos.py +165 -0
  3. examples/file_freshness_example.py +97 -0
  4. examples/file_watching_example.py +110 -0
  5. examples/interruptible_tui.py +5 -0
  6. examples/message_response_children_demo.py +226 -0
  7. examples/rich_example.py +4 -0
  8. examples/simple_file_watching.py +57 -0
  9. examples/simple_tui.py +267 -0
  10. examples/simple_usage.py +69 -0
  11. minion_code/__init__.py +16 -0
  12. minion_code/agents/__init__.py +11 -0
  13. minion_code/agents/code_agent.py +320 -0
  14. minion_code/cli.py +502 -0
  15. minion_code/commands/__init__.py +90 -0
  16. minion_code/commands/clear_command.py +70 -0
  17. minion_code/commands/help_command.py +90 -0
  18. minion_code/commands/history_command.py +104 -0
  19. minion_code/commands/quit_command.py +32 -0
  20. minion_code/commands/status_command.py +115 -0
  21. minion_code/commands/tools_command.py +86 -0
  22. minion_code/commands/version_command.py +104 -0
  23. minion_code/components/Message.py +304 -0
  24. minion_code/components/MessageResponse.py +188 -0
  25. minion_code/components/PromptInput.py +534 -0
  26. minion_code/components/__init__.py +29 -0
  27. minion_code/screens/REPL.py +925 -0
  28. minion_code/screens/__init__.py +4 -0
  29. minion_code/services/__init__.py +50 -0
  30. minion_code/services/event_system.py +108 -0
  31. minion_code/services/file_freshness_service.py +582 -0
  32. minion_code/tools/__init__.py +69 -0
  33. minion_code/tools/bash_tool.py +58 -0
  34. minion_code/tools/file_edit_tool.py +238 -0
  35. minion_code/tools/file_read_tool.py +73 -0
  36. minion_code/tools/file_write_tool.py +36 -0
  37. minion_code/tools/glob_tool.py +58 -0
  38. minion_code/tools/grep_tool.py +105 -0
  39. minion_code/tools/ls_tool.py +65 -0
  40. minion_code/tools/multi_edit_tool.py +271 -0
  41. minion_code/tools/python_interpreter_tool.py +105 -0
  42. minion_code/tools/todo_read_tool.py +100 -0
  43. minion_code/tools/todo_write_tool.py +234 -0
  44. minion_code/tools/user_input_tool.py +53 -0
  45. minion_code/types.py +88 -0
  46. minion_code/utils/__init__.py +44 -0
  47. minion_code/utils/mcp_loader.py +211 -0
  48. minion_code/utils/todo_file_utils.py +110 -0
  49. minion_code/utils/todo_storage.py +149 -0
  50. minion_code-0.1.0.dist-info/METADATA +350 -0
  51. minion_code-0.1.0.dist-info/RECORD +59 -0
  52. minion_code-0.1.0.dist-info/WHEEL +5 -0
  53. minion_code-0.1.0.dist-info/entry_points.txt +4 -0
  54. minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
  55. minion_code-0.1.0.dist-info/top_level.txt +3 -0
  56. tests/__init__.py +1 -0
  57. tests/test_basic.py +20 -0
  58. tests/test_readonly_tools.py +102 -0
  59. tests/test_tools.py +83 -0
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Textual UI for MinionCodeAgent
5
+
6
+ A modern terminal user interface using the Textual library for the MinionCodeAgent.
7
+ Features include:
8
+ - Real-time chat interface
9
+ - Tool management
10
+ - Conversation history
11
+ - Task interruption support
12
+ - Rich text formatting
13
+ """
14
+
15
+ import asyncio
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Optional, List
19
+
20
+ # Add project root to path
21
+ sys.path.insert(0, str(Path(__file__).parent.parent))
22
+
23
+ from textual.app import App, ComposeResult
24
+ from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
25
+ from textual.widgets import (
26
+ Header, Footer, Input, Button, Static, DataTable,
27
+ TabbedContent, TabPane, Log, ProgressBar, Label
28
+ )
29
+ from textual.reactive import reactive
30
+ from textual.message import Message
31
+ from textual.binding import Binding
32
+ from textual import events
33
+ from textual.screen import ModalScreen
34
+ from rich.text import Text
35
+ from rich.markdown import Markdown
36
+ from rich.console import Console
37
+ from rich.panel import Panel
38
+
39
+ from minion_code import MinionCodeAgent
40
+ from minion_code.utils.mcp_loader import MCPToolsLoader
41
+
42
+
43
+ class ToolsModal(ModalScreen):
44
+ """Modal screen to display available tools."""
45
+
46
+ def __init__(self, tools: List):
47
+ super().__init__()
48
+ self.tools = tools
49
+
50
+ def compose(self) -> ComposeResult:
51
+ with Container(id="tools-modal"):
52
+ yield Static("🛠️ Available Tools", classes="modal-title")
53
+
54
+ table = DataTable()
55
+ table.add_columns("Name", "Description", "Type")
56
+
57
+ for tool in self.tools:
58
+ tool_type = "Read-only" if getattr(tool, 'readonly', False) else "Read-write"
59
+ description = tool.description[:60] + "..." if len(tool.description) > 60 else tool.description
60
+ table.add_row(tool.name, description, tool_type)
61
+
62
+ yield table
63
+ yield Button("Close", id="close-tools", variant="primary")
64
+
65
+ def on_button_pressed(self, event: Button.Pressed) -> None:
66
+ if event.button.id == "close-tools":
67
+ self.dismiss()
68
+
69
+
70
+ class HistoryModal(ModalScreen):
71
+ """Modal screen to display conversation history."""
72
+
73
+ def __init__(self, history: List):
74
+ super().__init__()
75
+ self.history = history
76
+
77
+ def compose(self) -> ComposeResult:
78
+ with Container(id="history-modal"):
79
+ yield Static("📝 Conversation History", classes="modal-title")
80
+
81
+ with ScrollableContainer():
82
+ if not self.history:
83
+ yield Static("No conversation history yet.", classes="empty-history")
84
+ else:
85
+ for i, entry in enumerate(self.history[-10:], 1): # Show last 10
86
+ yield Static(f"👤 You (Message {len(self.history)-10+i}):", classes="user-label")
87
+ yield Static(entry['user_message'][:200] + "..." if len(entry['user_message']) > 200 else entry['user_message'], classes="user-message")
88
+ yield Static("🤖 Agent:", classes="agent-label")
89
+ yield Static(entry['agent_response'][:200] + "..." if len(entry['agent_response']) > 200 else entry['agent_response'], classes="agent-message")
90
+ yield Static("", classes="message-separator")
91
+
92
+ yield Button("Close", id="close-history", variant="primary")
93
+
94
+ def on_button_pressed(self, event: Button.Pressed) -> None:
95
+ if event.button.id == "close-history":
96
+ self.dismiss()
97
+
98
+
99
+ class MinionCodeTUI(App):
100
+ """Textual UI for MinionCodeAgent."""
101
+
102
+ CSS = """
103
+ #main-container {
104
+ layout: vertical;
105
+ height: 100%;
106
+ }
107
+
108
+ #chat-container {
109
+ layout: vertical;
110
+ height: 1fr;
111
+ border: solid $primary;
112
+ margin: 1;
113
+ }
114
+
115
+ #chat-log {
116
+ height: 1fr;
117
+ scrollbar-gutter: stable;
118
+ border: solid $accent;
119
+ margin: 1;
120
+ }
121
+
122
+ #input-container {
123
+ layout: horizontal;
124
+ height: 3;
125
+ margin: 1;
126
+ }
127
+
128
+ #user-input {
129
+ width: 1fr;
130
+ margin-right: 1;
131
+ }
132
+
133
+ #send-button {
134
+ width: 10;
135
+ }
136
+
137
+ #status-bar {
138
+ height: 3;
139
+ background: $surface;
140
+ border: solid $primary;
141
+ margin: 1;
142
+ }
143
+
144
+ #tools-modal, #history-modal {
145
+ align: center middle;
146
+ width: 80%;
147
+ height: 80%;
148
+ background: $surface;
149
+ border: solid $primary;
150
+ }
151
+
152
+ .modal-title {
153
+ text-align: center;
154
+ text-style: bold;
155
+ background: $primary;
156
+ color: $text;
157
+ height: 3;
158
+ content-align: center middle;
159
+ }
160
+
161
+ .user-message {
162
+ background: $primary 20%;
163
+ margin: 1;
164
+ padding: 1;
165
+ }
166
+
167
+ .agent-message {
168
+ background: $success 20%;
169
+ margin: 1;
170
+ padding: 1;
171
+ }
172
+
173
+ .user-label {
174
+ text-style: bold;
175
+ color: $primary;
176
+ margin-top: 1;
177
+ }
178
+
179
+ .agent-label {
180
+ text-style: bold;
181
+ color: $success;
182
+ margin-top: 1;
183
+ }
184
+
185
+ .message-separator {
186
+ height: 1;
187
+ }
188
+
189
+ .empty-history {
190
+ text-align: center;
191
+ text-style: italic;
192
+ margin: 2;
193
+ }
194
+
195
+ .status-text {
196
+ text-align: center;
197
+ content-align: center middle;
198
+ }
199
+
200
+ .error-message {
201
+ color: $error;
202
+ text-style: bold;
203
+ }
204
+
205
+ .success-message {
206
+ color: $success;
207
+ text-style: bold;
208
+ }
209
+
210
+ .processing-message {
211
+ color: $warning;
212
+ text-style: bold;
213
+ }
214
+ """
215
+
216
+ BINDINGS = [
217
+ Binding("ctrl+q", "quit", "Quit"),
218
+ Binding("ctrl+t", "show_tools", "Tools"),
219
+ Binding("ctrl+h", "show_history", "History"),
220
+ Binding("ctrl+c", "interrupt", "Interrupt"),
221
+ Binding("ctrl+l", "clear_chat", "Clear Chat"),
222
+ ]
223
+
224
+ agent_status = reactive("Initializing...")
225
+ processing = reactive(False)
226
+
227
+ def __init__(self, mcp_config: Optional[Path] = None, verbose: bool = False):
228
+ super().__init__()
229
+ self.agent = None
230
+ self.mcp_config = mcp_config
231
+ self.verbose = verbose
232
+ self.mcp_tools = []
233
+ self.mcp_loader = None
234
+ self.current_task = None
235
+ self.interrupt_requested = False
236
+
237
+ def compose(self) -> ComposeResult:
238
+ """Create the UI layout."""
239
+ yield Header()
240
+
241
+ with Container(id="main-container"):
242
+ with Container(id="chat-container"):
243
+ yield Log(id="chat-log", auto_scroll=True)
244
+
245
+ with Horizontal(id="input-container"):
246
+ yield Input(placeholder="Type your message here...", id="user-input")
247
+ yield Button("Send", id="send-button", variant="primary")
248
+
249
+ yield Static(self.agent_status, id="status-bar", classes="status-text")
250
+
251
+ yield Footer()
252
+
253
+ async def on_mount(self) -> None:
254
+ """Initialize the application."""
255
+ self.title = "🤖 MinionCodeAgent TUI"
256
+ self.sub_title = "AI-powered code assistant"
257
+
258
+ # Start agent setup
259
+ await self.setup_agent()
260
+
261
+ # Focus on input
262
+ self.query_one("#user-input").focus()
263
+
264
+ async def setup_agent(self) -> None:
265
+ """Setup the MinionCodeAgent."""
266
+ try:
267
+ self.agent_status = "🔧 Setting up MinionCodeAgent..."
268
+
269
+ # Load MCP tools if config provided
270
+ if self.mcp_config:
271
+ self.agent_status = "🔌 Loading MCP tools..."
272
+ try:
273
+ self.mcp_loader = MCPToolsLoader(self.mcp_config)
274
+ self.mcp_loader.load_config()
275
+ self.mcp_tools = await self.mcp_loader.load_all_tools()
276
+
277
+ if self.mcp_tools:
278
+ self.log_message(f"✅ Loaded {len(self.mcp_tools)} MCP tools", "success")
279
+ else:
280
+ self.log_message("⚠️ No MCP tools loaded", "warning")
281
+
282
+ except Exception as e:
283
+ self.log_message(f"❌ Failed to load MCP tools: {e}", "error")
284
+
285
+ # Create agent
286
+ self.agent_status = "🤖 Creating agent..."
287
+ self.agent = await MinionCodeAgent.create(
288
+ name="TUI Code Assistant",
289
+ llm="sonnet",
290
+ additional_tools=self.mcp_tools if self.mcp_tools else None
291
+ )
292
+
293
+ # Update status
294
+ total_tools = len(self.agent.tools)
295
+ mcp_count = len(self.mcp_tools)
296
+ builtin_count = total_tools - mcp_count
297
+
298
+ self.agent_status = f"✅ Ready! {total_tools} tools available"
299
+
300
+ # Log welcome message
301
+ welcome_msg = f"🚀 MinionCodeAgent TUI Ready!\n"
302
+ welcome_msg += f"🛠️ Total tools: {total_tools}"
303
+ if mcp_count > 0:
304
+ welcome_msg += f" (Built-in: {builtin_count}, MCP: {mcp_count})"
305
+ welcome_msg += f"\n💡 Type your message and press Enter or click Send"
306
+ welcome_msg += f"\n⚠️ Use Ctrl+C to interrupt tasks, Ctrl+Q to quit"
307
+
308
+ self.log_message(welcome_msg, "success")
309
+
310
+ except Exception as e:
311
+ self.agent_status = f"❌ Setup failed: {e}"
312
+ self.log_message(f"❌ Failed to setup agent: {e}", "error")
313
+
314
+ def log_message(self, message: str, msg_type: str = "info") -> None:
315
+ """Add a message to the chat log."""
316
+ chat_log = self.query_one("#chat-log", Log)
317
+
318
+ # Format messages with simple text prefixes since Log only supports plain text
319
+ if msg_type == "error":
320
+ chat_log.write(f"❌ {message}")
321
+ elif msg_type == "success":
322
+ chat_log.write(f"✅ {message}")
323
+ elif msg_type == "warning":
324
+ chat_log.write(f"⚠️ {message}")
325
+ elif msg_type == "user":
326
+ chat_log.write(f"� You: {message}")
327
+ elif msg_type == "agent":
328
+ # Handle markdown in agent responses
329
+ if "```" in message:
330
+ chat_log.write("🤖 Agent:")
331
+ chat_log.write(message)
332
+ else:
333
+ chat_log.write(f"🤖 Agent: {message}")
334
+ else:
335
+ chat_log.write(message)
336
+
337
+ async def process_user_input(self, message: str) -> None:
338
+ """Process user input with the agent."""
339
+ if not self.agent:
340
+ self.log_message("❌ Agent not ready yet", "error")
341
+ return
342
+
343
+ if not message.strip():
344
+ return
345
+
346
+ # Log user message
347
+ self.log_message(message, "user")
348
+
349
+ # Check for commands
350
+ if message.startswith('/'):
351
+ await self.process_command(message)
352
+ return
353
+
354
+ # Process with agent
355
+ try:
356
+ self.processing = True
357
+ self.agent_status = "🤖 Processing... (Ctrl+C to interrupt)"
358
+ self.interrupt_requested = False
359
+
360
+ # Create processing task
361
+ async def processing_task():
362
+ response = await self.agent.run_async(message)
363
+ return response
364
+
365
+ self.current_task = asyncio.create_task(processing_task())
366
+
367
+ # Monitor for interruption
368
+ while not self.current_task.done():
369
+ if self.interrupt_requested:
370
+ self.current_task.cancel()
371
+ try:
372
+ await self.current_task
373
+ except asyncio.CancelledError:
374
+ pass
375
+ self.log_message("⚠️ Task interrupted by user", "warning")
376
+ return
377
+
378
+ await asyncio.sleep(0.1)
379
+
380
+ # Get result
381
+ response = await self.current_task
382
+ self.log_message(response.answer, "agent")
383
+
384
+ except asyncio.CancelledError:
385
+ self.log_message("⚠️ Task was cancelled", "warning")
386
+ except Exception as e:
387
+ self.log_message(f"❌ Error: {e}", "error")
388
+ finally:
389
+ self.processing = False
390
+ self.current_task = None
391
+ total_tools = len(self.agent.tools) if self.agent else 0
392
+ self.agent_status = f"✅ Ready! {total_tools} tools available"
393
+
394
+ async def process_command(self, command: str) -> None:
395
+ """Process a command."""
396
+ command = command[1:].lower().strip() # Remove leading /
397
+
398
+ if command == "help":
399
+ help_msg = """📚 Available Commands:
400
+ /help - Show this help
401
+ /tools - Show available tools
402
+ /history - Show conversation history
403
+ /clear - Clear chat log
404
+ /quit - Exit application
405
+
406
+ 🔧 Keyboard Shortcuts:
407
+ Ctrl+T - Show tools
408
+ Ctrl+H - Show history
409
+ Ctrl+L - Clear chat
410
+ Ctrl+C - Interrupt current task
411
+ Ctrl+Q - Quit application"""
412
+ self.log_message(help_msg, "info")
413
+
414
+ elif command == "tools":
415
+ await self.action_show_tools()
416
+
417
+ elif command == "history":
418
+ await self.action_show_history()
419
+
420
+ elif command == "clear":
421
+ await self.action_clear_chat()
422
+
423
+ elif command in ["quit", "exit", "q"]:
424
+ await self.action_quit()
425
+
426
+ else:
427
+ self.log_message(f"❌ Unknown command: /{command}. Use /help for available commands.", "error")
428
+
429
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
430
+ """Handle input submission."""
431
+ if event.input.id == "user-input":
432
+ message = event.value
433
+ event.input.clear()
434
+ asyncio.create_task(self.process_user_input(message))
435
+
436
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
437
+ """Handle button presses."""
438
+ if event.button.id == "send-button":
439
+ user_input = self.query_one("#user-input", Input)
440
+ message = user_input.value
441
+ user_input.clear()
442
+ asyncio.create_task(self.process_user_input(message))
443
+
444
+ async def action_show_tools(self) -> None:
445
+ """Show available tools."""
446
+ if not self.agent:
447
+ self.log_message("❌ Agent not ready yet", "error")
448
+ return
449
+
450
+ await self.push_screen(ToolsModal(self.agent.tools))
451
+
452
+ async def action_show_history(self) -> None:
453
+ """Show conversation history."""
454
+ if not self.agent:
455
+ self.log_message("❌ Agent not ready yet", "error")
456
+ return
457
+
458
+ history = self.agent.get_conversation_history()
459
+ await self.push_screen(HistoryModal(history))
460
+
461
+ async def action_clear_chat(self) -> None:
462
+ """Clear the chat log."""
463
+ chat_log = self.query_one("#chat-log", Log)
464
+ chat_log.clear()
465
+ self.log_message("🧹 Chat cleared", "info")
466
+
467
+ async def action_interrupt(self) -> None:
468
+ """Interrupt current task."""
469
+ if self.current_task and not self.current_task.done():
470
+ self.interrupt_requested = True
471
+ self.log_message("⚠️ Interruption requested...", "warning")
472
+ else:
473
+ self.log_message("ℹ️ No task to interrupt", "info")
474
+
475
+ async def action_quit(self) -> None:
476
+ """Quit the application."""
477
+ if self.mcp_loader:
478
+ try:
479
+ await self.mcp_loader.close()
480
+ except Exception:
481
+ pass
482
+
483
+ self.exit()
484
+
485
+
486
+ def main():
487
+ """Main entry point."""
488
+ import argparse
489
+
490
+ parser = argparse.ArgumentParser(description="MinionCodeAgent Textual UI")
491
+ parser.add_argument("--config", "-c", type=str, help="Path to MCP configuration file")
492
+ parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output")
493
+
494
+ args = parser.parse_args()
495
+
496
+ mcp_config = None
497
+ if args.config:
498
+ mcp_config = Path(args.config)
499
+ if not mcp_config.exists():
500
+ print(f"❌ MCP config file does not exist: {args.config}")
501
+ sys.exit(1)
502
+
503
+ app = MinionCodeTUI(mcp_config=mcp_config, verbose=args.verbose)
504
+ app.run()
505
+
506
+
507
+ if __name__ == "__main__":
508
+ main()
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """Example of using Todo tools with MinionCodeAgent."""
3
+
4
+ import sys
5
+ import os
6
+ import asyncio
7
+ import uuid
8
+ import json
9
+
10
+ # Add the parent directory to the path so we can import minion_code
11
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
12
+
13
+ from minion_code.agents.code_agent import MinionCodeAgent
14
+
15
+
16
+ async def demonstrate_todo_workflow():
17
+ """Demonstrate using Todo tools with MinionCodeAgent."""
18
+ print("=== MinionCodeAgent Todo Workflow Demo ===\n")
19
+
20
+ # Create agent
21
+ print("1. Creating MinionCodeAgent...")
22
+ agent = await MinionCodeAgent.create(
23
+ name="Todo Demo Agent",
24
+ llm="gpt-4o-mini"
25
+ )
26
+ print("✓ Agent created successfully\n")
27
+
28
+ # Show available tools
29
+ print("2. Available tools:")
30
+ tools_info = agent.get_tools_info()
31
+ todo_tools = [tool for tool in tools_info if 'todo' in tool['name'].lower()]
32
+ for tool in todo_tools:
33
+ readonly_icon = "🔒" if tool['readonly'] else "✏️"
34
+ print(f" {readonly_icon} {tool['name']}: {tool['description']}")
35
+ print()
36
+
37
+ # Define project tasks
38
+ tasks = [
39
+ "Analyze project requirements",
40
+ "Design system architecture",
41
+ "Implement core functionality",
42
+ "Write unit tests",
43
+ "Create documentation",
44
+ "Deploy to production"
45
+ ]
46
+
47
+ # Create initial todos using agent
48
+ print("3. Creating initial todos...")
49
+ todos_data = []
50
+ for i, task in enumerate(tasks):
51
+ todos_data.append({
52
+ "id": str(uuid.uuid4()),
53
+ "content": task,
54
+ "status": "pending" if i > 0 else "in_progress", # First task in progress
55
+ "priority": "high" if i < 2 else "medium"
56
+ })
57
+
58
+ todos_json = json.dumps(todos_data)
59
+
60
+ # Use agent to create todos
61
+ create_message = f"""Please use the todo_write tool to create these todos:
62
+ {todos_json}"""
63
+
64
+ response = await agent.run_async(create_message)
65
+ print(f"✓ Agent response: {response.answer if hasattr(response, 'answer') else str(response)}\n")
66
+
67
+ # Read current todos
68
+ print("4. Reading current todos...")
69
+ read_message = """Please use the todo_read tool to show current todos."""
70
+
71
+ response = await agent.run_async(read_message)
72
+ print(f"✓ Current todos:\n{response.answer if hasattr(response, 'answer') else str(response)}\n")
73
+
74
+ # Simulate completing a few tasks
75
+ print("5. Completing tasks...")
76
+
77
+ # Complete first task and start next
78
+ for i in range(3): # Complete 3 tasks
79
+ print(f" Step {i+1}: Completing current task and starting next...")
80
+
81
+ # Get current todos from storage to update them
82
+ from minion_code.utils.todo_storage import get_todos, TodoStatus
83
+ # Use the same agent ID logic as the tool
84
+ import hashlib
85
+ agent_id = "demo_agent" # We'll use a consistent ID for this demo
86
+ current_todos = get_todos(agent_id)
87
+
88
+ if not current_todos:
89
+ print(" No todos found")
90
+ break
91
+
92
+ # Update todos: complete in_progress, start next pending
93
+ updated_todos = []
94
+ found_in_progress = False
95
+ started_next = False
96
+
97
+ for todo in current_todos:
98
+ if todo.status == TodoStatus.IN_PROGRESS:
99
+ # Mark as completed
100
+ todo_data = {
101
+ "id": todo.id,
102
+ "content": todo.content,
103
+ "status": "completed",
104
+ "priority": todo.priority.value
105
+ }
106
+ found_in_progress = True
107
+ elif todo.status == TodoStatus.PENDING and not started_next:
108
+ # Start next pending task
109
+ todo_data = {
110
+ "id": todo.id,
111
+ "content": todo.content,
112
+ "status": "in_progress",
113
+ "priority": todo.priority.value
114
+ }
115
+ started_next = True
116
+ else:
117
+ # Keep as is
118
+ todo_data = {
119
+ "id": todo.id,
120
+ "content": todo.content,
121
+ "status": todo.status.value,
122
+ "priority": todo.priority.value
123
+ }
124
+
125
+ updated_todos.append(todo_data)
126
+
127
+ if not found_in_progress:
128
+ print(" No in_progress task found")
129
+ break
130
+
131
+ # Update todos using agent
132
+ updated_json = json.dumps(updated_todos)
133
+ update_message = f"""Please use the todo_write tool to update todos:
134
+ {updated_json}"""
135
+
136
+ response = await agent.run_async(update_message)
137
+ print(f" ✓ Updated: {response.answer if hasattr(response, 'answer') else str(response)}")
138
+
139
+ print()
140
+
141
+ # Final status
142
+ print("6. Final todo status...")
143
+ final_message = """Please use the todo_read tool to show final todos."""
144
+
145
+ response = await agent.run_async(final_message)
146
+ print(f"✓ Final todos:\n{response.answer if hasattr(response, 'answer') else str(response)}\n")
147
+
148
+ # Show conversation history
149
+ print("7. Conversation summary:")
150
+ history = agent.get_conversation_history()
151
+ print(f" Total interactions: {len(history)}")
152
+ for i, interaction in enumerate(history, 1):
153
+ print(f" {i}. User: {interaction['user_message'][:50]}...")
154
+ print(f" Agent: {str(interaction['agent_response'])[:50]}...")
155
+
156
+ print("\n=== Demo Complete ===")
157
+
158
+
159
+ def main():
160
+ """Run the async demo."""
161
+ asyncio.run(demonstrate_todo_workflow())
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()