tiny-agent-os 0.0.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 (64) hide show
  1. tiny_agent_os-0.0.1.dist-info/METADATA +377 -0
  2. tiny_agent_os-0.0.1.dist-info/RECORD +64 -0
  3. tiny_agent_os-0.0.1.dist-info/WHEEL +5 -0
  4. tiny_agent_os-0.0.1.dist-info/entry_points.txt +2 -0
  5. tiny_agent_os-0.0.1.dist-info/licenses/LICENSE +53 -0
  6. tiny_agent_os-0.0.1.dist-info/top_level.txt +1 -0
  7. tinyagent/__init__.py +75 -0
  8. tinyagent/_version.py +21 -0
  9. tinyagent/agent.py +957 -0
  10. tinyagent/chat/__init__.py +12 -0
  11. tinyagent/chat/chat_mode.py +291 -0
  12. tinyagent/cli/__init__.py +16 -0
  13. tinyagent/cli/colors.py +104 -0
  14. tinyagent/cli/main.py +664 -0
  15. tinyagent/cli/spinner.py +94 -0
  16. tinyagent/cli.py +47 -0
  17. tinyagent/config/__init__.py +14 -0
  18. tinyagent/config/config.py +258 -0
  19. tinyagent/decorators.py +187 -0
  20. tinyagent/exceptions.py +85 -0
  21. tinyagent/factory/__init__.py +18 -0
  22. tinyagent/factory/agent_factory.py +439 -0
  23. tinyagent/factory/dynamic_agent_factory.py +561 -0
  24. tinyagent/factory/orchestrator.py +1514 -0
  25. tinyagent/factory/tiny_chain.py +552 -0
  26. tinyagent/logging.py +97 -0
  27. tinyagent/mcp/__init__.py +14 -0
  28. tinyagent/mcp/manager.py +321 -0
  29. tinyagent/prompts/README.md +133 -0
  30. tinyagent/prompts/default.md +14 -0
  31. tinyagent/prompts/prompt_manager.py +206 -0
  32. tinyagent/prompts/system/agent.md +50 -0
  33. tinyagent/prompts/system/retry.md +55 -0
  34. tinyagent/prompts/system/strict_json.md +54 -0
  35. tinyagent/prompts/system.md +10 -0
  36. tinyagent/prompts/tools/calculator.md +13 -0
  37. tinyagent/prompts/tools/weather.md +7 -0
  38. tinyagent/prompts/workflows/riv_reflect.md +62 -0
  39. tinyagent/prompts/workflows/riv_verify.md +47 -0
  40. tinyagent/prompts/workflows/triage.md +129 -0
  41. tinyagent/tool.py +185 -0
  42. tinyagent/tools/README.md +391 -0
  43. tinyagent/tools/__init__.py +39 -0
  44. tinyagent/tools/aider.py +122 -0
  45. tinyagent/tools/anon_coder.py +296 -0
  46. tinyagent/tools/boilerplate_tool.py +147 -0
  47. tinyagent/tools/brave_search.py +104 -0
  48. tinyagent/tools/codeagent_tool.py +217 -0
  49. tinyagent/tools/content_processor.py +285 -0
  50. tinyagent/tools/custom_text_browser.py +965 -0
  51. tinyagent/tools/duckduckgo_search.py +153 -0
  52. tinyagent/tools/external.py +303 -0
  53. tinyagent/tools/file_manipulator.py +274 -0
  54. tinyagent/tools/final_extractor_tool.py +249 -0
  55. tinyagent/tools/llm_serializer.py +124 -0
  56. tinyagent/tools/markdown_gen.py +300 -0
  57. tinyagent/tools/ripgrep.py +136 -0
  58. tinyagent/utils/__init__.py +13 -0
  59. tinyagent/utils/json_parser.py +231 -0
  60. tinyagent/utils/logging_utils.py +78 -0
  61. tinyagent/utils/openrouter_request.py +123 -0
  62. tinyagent/utils/serialization.py +185 -0
  63. tinyagent/utils/structured_outputs.py +131 -0
  64. tinyagent/utils/type_converter.py +134 -0
@@ -0,0 +1,1514 @@
1
+ """
2
+ Orchestrator module for coordinating multiple agents.
3
+
4
+ This module provides an orchestrator that manages and coordinates multiple agents
5
+ to accomplish complex tasks, handling task delegation, agent coordination, and
6
+ result integration.
7
+ """
8
+ #this file is over 1000 lines, it is only this long for debugging purposes
9
+ import json
10
+ import pprint
11
+ import time
12
+ import re
13
+ import os
14
+ import threading
15
+ from typing import Dict, List, Any, Optional, Union, TypeVar, Type, cast
16
+ from dataclasses import dataclass, field
17
+
18
+ from ..logging import get_logger
19
+ from tinyagent.prompts.prompt_manager import PromptManager
20
+ from ..agent import Agent, get_llm
21
+ from ..config import load_config, get_config_value
22
+ from ..exceptions import OrchestratorError, AgentNotFoundError
23
+ from .dynamic_agent_factory import DynamicAgentFactory
24
+
25
+ # Set up logger with more detailed formatting
26
+ logger = get_logger(__name__)
27
+
28
+ from tinyagent.utils.logging_utils import (
29
+ log_task_event,
30
+ log_decision,
31
+ log_section_header,
32
+ log_step,
33
+ )
34
+
35
+
36
+ @dataclass
37
+ class TaskStatus:
38
+ """
39
+ Status information for a task being orchestrated.
40
+
41
+ Attributes:
42
+ task_id: Unique identifier for the task
43
+ description: Text description of the task
44
+ status: Current status (pending, in_progress, completed, failed, needs_permission)
45
+ assigned_agent: ID of the agent assigned to the task (optional)
46
+ created_at: Timestamp when the task was created
47
+ started_at: Timestamp when the task was started (optional)
48
+ completed_at: Timestamp when the task was completed (optional)
49
+ result: The result of the task (optional)
50
+ error: Error message if the task failed (optional)
51
+ """
52
+ task_id: str
53
+ description: str
54
+ status: str = "pending" # pending, in_progress, completed, failed, needs_permission
55
+ assigned_agent: Optional[str] = None
56
+ created_at: float = field(default_factory=time.time)
57
+ started_at: Optional[float] = None
58
+ completed_at: Optional[float] = None
59
+ result: Any = None
60
+ error: Optional[str] = None
61
+
62
+
63
+ # Type variable for the singleton pattern
64
+ T = TypeVar('T')
65
+
66
+
67
+ class Orchestrator:
68
+ """
69
+ Orchestrates multiple agents to accomplish complex tasks.
70
+
71
+ Manages task delegation, agent coordination, and result integration across
72
+ multiple specialized agents.
73
+
74
+ Attributes:
75
+ factory: DynamicAgentFactory instance for creating agents
76
+ tasks: Dictionary of tasks being managed
77
+ agents: Dictionary of registered agents
78
+ _lock: Threading lock for concurrency control
79
+ """
80
+
81
+ _instance = None # Singleton instance
82
+
83
+ @classmethod
84
+ def get_instance(cls: Type[T], config: Optional[Dict[str, Any]] = None) -> T:
85
+ """
86
+ Get or create the singleton orchestrator instance.
87
+
88
+ Args:
89
+ config: Optional configuration dictionary
90
+
91
+ Returns:
92
+ The singleton Orchestrator instance
93
+ """
94
+ if cls._instance is None:
95
+ cls._instance = cls(config)
96
+ return cls._instance
97
+
98
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
99
+ """
100
+ Initialize the orchestrator with configuration.
101
+
102
+ Args:
103
+ config: Optional configuration dictionary
104
+ """
105
+ self.factory = DynamicAgentFactory.get_instance(config)
106
+ self.tasks: Dict[str, TaskStatus] = {}
107
+ self.agents: Dict[str, Agent] = {}
108
+ self.next_task_id = 1
109
+ self.next_agent_id = 1
110
+ self.lock = threading.Lock()
111
+ self.prompt_manager = PromptManager()
112
+
113
+ # Load configuration
114
+ if config is None:
115
+ self.config = load_config()
116
+ else:
117
+ self.config = config
118
+
119
+ # Initialize the triage agent
120
+ self._create_triage_agent()
121
+ logger.info("Orchestrator initialized")
122
+
123
+ def _create_triage_agent(self) -> None:
124
+ """
125
+ Create and register the triage agent.
126
+
127
+ The triage agent is responsible for analyzing user queries and delegating
128
+ to specialized agents.
129
+ """
130
+ # Get max_retries from config if available, otherwise use default (3)
131
+ max_retries = get_config_value(self.config, 'retries.max_attempts', 3)
132
+
133
+ # Get preferred model from config if available
134
+ model = get_config_value(self.config, 'model.triage', None)
135
+
136
+ # Make sure all tools are registered with the factory first
137
+ try:
138
+ from ..tools import (
139
+ anon_coder_tool,
140
+ llm_serializer_tool,
141
+ brave_web_search_tool,
142
+ ripgrep_tool,
143
+ aider_tool,
144
+ load_external_tools
145
+ )
146
+
147
+ # Register all tools explicitly
148
+ self.factory.register_tool(anon_coder_tool)
149
+ self.factory.register_tool(llm_serializer_tool)
150
+ self.factory.register_tool(brave_web_search_tool)
151
+ self.factory.register_tool(ripgrep_tool)
152
+ self.factory.register_tool(aider_tool)
153
+
154
+ # Add external tools
155
+ external_tools = load_external_tools()
156
+ for tool in external_tools:
157
+ self.factory.register_tool(tool)
158
+
159
+ logger.info(f"Registered {len(self.factory.list_tools())} tools with factory")
160
+ except Exception as e:
161
+ logger.error(f"Error registering tools: {str(e)}")
162
+
163
+ # Create a specialized agent with all tools for triage using the factory
164
+ triage_agent = self.factory.create_agent(
165
+ tools=list(self.factory.list_tools().values()),
166
+ model=model
167
+ )
168
+
169
+ # Manually set max_retries
170
+ triage_agent.max_retries = max_retries
171
+
172
+ # Register the triage agent
173
+ triage_agent.name = "triage_agent"
174
+ triage_agent.description = "Analyzes queries and delegates to specialized agents"
175
+ self.agents["triage"] = triage_agent
176
+
177
+ logger.info("Triage agent created")
178
+
179
+ def _generate_task_id(self) -> str:
180
+ """
181
+ Generate a unique task ID.
182
+
183
+ Returns:
184
+ A unique task ID string
185
+ """
186
+ with self.lock:
187
+ task_id = f"task_{self.next_task_id}"
188
+ self.next_task_id += 1
189
+ return task_id
190
+
191
+ def _generate_agent_id(self) -> str:
192
+ """
193
+ Generate a unique agent ID.
194
+
195
+ Returns:
196
+ A unique agent ID string
197
+ """
198
+ with self.lock:
199
+ agent_id = f"agent_{self.next_agent_id}"
200
+ self.next_agent_id += 1
201
+ return agent_id
202
+
203
+ def submit_task(self, description: str, need_permission: bool = True, execution_mode: str = "standard", max_iterations: int = 5) -> str:
204
+ """
205
+ Submit a new task to the orchestrator.
206
+
207
+ Args:
208
+ description: Natural language description of the task
209
+ need_permission: Whether to ask for permission to create new tools
210
+ execution_mode: Execution mode to use ("standard", "riv")
211
+ max_iterations: Maximum number of iterations for iterative execution modes
212
+
213
+ Returns:
214
+ Task ID for tracking
215
+ """
216
+ task_id = self._generate_task_id()
217
+ self.tasks[task_id] = TaskStatus(
218
+ task_id=task_id,
219
+ description=description
220
+ )
221
+
222
+ log_section_header("New Task Submission")
223
+ log_step(
224
+ step_number=1,
225
+ title="Task Initialization",
226
+ details={
227
+ "task_id": task_id,
228
+ "description": description[:100] + "..." if len(description) > 100 else description,
229
+ "need_permission": need_permission,
230
+ "execution_mode": execution_mode,
231
+ "max_iterations": max_iterations if execution_mode != "standard" else None,
232
+ "timestamp": time.time()
233
+ },
234
+ reasoning="Initializing new task with provided description"
235
+ )
236
+
237
+ # Process the task based on the execution approach
238
+ if execution_mode == "riv":
239
+ # Use RIV (Reflect-Improve-Verify) execution approach
240
+ try:
241
+ threading.Thread(
242
+ target=self.execute_riv_task,
243
+ args=(task_id, max_iterations)
244
+ ).start()
245
+
246
+ log_step(
247
+ step_number=2,
248
+ title="Execution Approach",
249
+ details={
250
+ "approach": "RIV (Reflect-Improve-Verify)",
251
+ "max_iterations": max_iterations
252
+ },
253
+ reasoning="Starting RIV execution in background thread"
254
+ )
255
+ except Exception as e:
256
+ logger.error(f"Error starting RIV execution: {str(e)}")
257
+ # Fall back to standard execution
258
+ self._process_task(task_id, need_permission)
259
+ else:
260
+ # Use standard one-pass execution approach
261
+ self._process_task(task_id, need_permission)
262
+
263
+ return task_id
264
+
265
+ def _process_task(self, task_id: str, need_permission: bool) -> None:
266
+ """
267
+ Process a task by triaging and delegating to appropriate agents.
268
+
269
+ Args:
270
+ task_id: ID of the task to process
271
+ need_permission: Whether to require permission for new tools
272
+ """
273
+ task = self.tasks[task_id]
274
+ task.status = "in_progress"
275
+ task.started_at = time.time()
276
+
277
+ log_section_header("Task Processing")
278
+ log_step(
279
+ step_number=2,
280
+ title="Task Status Update",
281
+ details={
282
+ "task_id": task_id,
283
+ "status": task.status,
284
+ "started_at": task.started_at
285
+ },
286
+ reasoning="Starting task processing with initial status set to in_progress"
287
+ )
288
+
289
+ try:
290
+ # First, use the triage agent to analyze the task
291
+ try:
292
+ log_section_header("Task Analysis")
293
+ log_step(
294
+ step_number=3,
295
+ title="Triage Process",
296
+ details={
297
+ "task_id": task_id,
298
+ "timestamp": time.time()
299
+ },
300
+ reasoning="Starting task analysis to determine best handling approach"
301
+ )
302
+
303
+ triage_result = self._triage_task(task)
304
+
305
+ log_step(
306
+ step_number=4,
307
+ title="Triage Results",
308
+ details={
309
+ "assessment": triage_result.get("assessment", "unknown"),
310
+ "requires_new_agent": triage_result.get("requires_new_agent", False),
311
+ "duration": time.time() - task.started_at
312
+ },
313
+ reasoning=f"Task analysis completed with assessment: {triage_result.get('assessment', 'unknown')}"
314
+ )
315
+
316
+ except Exception as triage_error:
317
+ log_section_header("Triage Error")
318
+ log_step(
319
+ step_number=5,
320
+ title="Error Handling",
321
+ details={
322
+ "error": str(triage_error),
323
+ "error_type": type(triage_error).__name__,
324
+ "duration": time.time() - task.started_at
325
+ },
326
+ reasoning=f"Triage process failed due to: {str(triage_error)}"
327
+ )
328
+
329
+ # Capture detailed error information about retry attempts
330
+ if hasattr(triage_error, 'history') and triage_error.history:
331
+ attempts_info = []
332
+ for i, entry in enumerate(triage_error.history, 1):
333
+ if isinstance(entry, dict):
334
+ attempts_info.append(f"Attempt {i}: {entry.get('error', 'Unknown error')}")
335
+
336
+ if attempts_info:
337
+ task.error = "\n".join(attempts_info)
338
+ else:
339
+ task.error = f"Triage failed after multiple attempts: {str(triage_error)}"
340
+ else:
341
+ task.error = f"Triage error: {str(triage_error)}"
342
+
343
+ # Use a default triage result
344
+ triage_result = {
345
+ "assessment": "direct",
346
+ "requires_new_agent": False,
347
+ "reasoning": f"Triage failed: {str(triage_error)}, falling back to direct handling"
348
+ }
349
+
350
+ log_step(
351
+ step_number=6,
352
+ title="STEP 1: Initial Triage Analysis",
353
+ details={
354
+ "assessment": triage_result.get("assessment"),
355
+ "requires_new_agent": triage_result.get("requires_new_agent", False),
356
+ "has_tool": "tool" in triage_result,
357
+ "has_arguments": "arguments" in triage_result,
358
+ "has_tool_sequence": "tool_sequence" in triage_result
359
+ },
360
+ reasoning="Analyzing initial triage results"
361
+ )
362
+
363
+ # Check if triage_result contains a multi-step tool sequence
364
+ if "tool_sequence" in triage_result:
365
+ steps = triage_result["tool_sequence"]
366
+ # Make sure it's a list of steps
367
+ if not isinstance(steps, list):
368
+ raise ValueError("tool_sequence must be a list of tool-call steps.")
369
+
370
+ log_section_header("Multi-Step Tool Sequence Execution")
371
+
372
+ aggregated_results = []
373
+ for i, step_info in enumerate(steps, start=1):
374
+ tool_name = step_info.get("tool")
375
+ arguments = step_info.get("arguments", {})
376
+
377
+ if not tool_name:
378
+ raise ValueError(f"Missing 'tool' field in step {i} of tool_sequence.")
379
+
380
+ log_step(
381
+ step_number=i + 6,
382
+ title=f"Tool Sequence Step {i}",
383
+ details={
384
+ "tool_name": tool_name,
385
+ "arguments": arguments
386
+ },
387
+ reasoning=f"Executing step {i} in the multi-tool plan."
388
+ )
389
+
390
+ try:
391
+ # Execute the tool via the factory
392
+ result = self.factory.execute_tool(tool_name, **arguments)
393
+ aggregated_results.append({
394
+ "step": i,
395
+ "tool": tool_name,
396
+ "arguments": arguments,
397
+ "result": result
398
+ })
399
+
400
+ log_step(
401
+ step_number=i + 6,
402
+ title=f"Tool Execution Result for Step {i}",
403
+ details={
404
+ "tool": tool_name,
405
+ "result": result
406
+ },
407
+ reasoning=f"Step {i} completed successfully"
408
+ )
409
+ except Exception as e:
410
+ error_msg = f"Error executing tool {tool_name} in sequence step {i}: {str(e)}"
411
+ log_step(
412
+ step_number=i + 6,
413
+ title=f"Tool Execution Error for Step {i}",
414
+ details={
415
+ "tool": tool_name,
416
+ "error": str(e),
417
+ "error_type": type(e).__name__
418
+ },
419
+ reasoning=f"Step {i} failed: {str(e)}"
420
+ )
421
+ # Add the error to the results and continue with the next step
422
+ aggregated_results.append({
423
+ "step": i,
424
+ "tool": tool_name,
425
+ "arguments": arguments,
426
+ "error": str(e)
427
+ })
428
+
429
+ task.result = {
430
+ "type": "tool_sequence",
431
+ "steps": aggregated_results,
432
+ "reasoning": triage_result.get("reasoning", "Multi-step tool execution")
433
+ }
434
+ task.status = "completed"
435
+ task.completed_at = time.time()
436
+
437
+ log_section_header("Tool Sequence Completion")
438
+ log_step(
439
+ step_number=7 + len(steps),
440
+ title="Sequence Completion",
441
+ details={
442
+ "total_steps": len(steps),
443
+ "successful_steps": sum(1 for r in aggregated_results if "error" not in r),
444
+ "failed_steps": sum(1 for r in aggregated_results if "error" in r),
445
+ "duration": time.time() - task.started_at
446
+ },
447
+ reasoning="Tool sequence execution completed"
448
+ )
449
+
450
+ return
451
+
452
+ # Check if triage_result is actually a direct tool call
453
+ if "tool" in triage_result and "arguments" in triage_result:
454
+ log_section_header("Direct Tool Execution")
455
+ log_step(
456
+ step_number=8,
457
+ title="Tool Selection",
458
+ details={
459
+ "tool": triage_result["tool"],
460
+ "arguments": triage_result["arguments"]
461
+ },
462
+ reasoning=f"Task can be handled directly with tool: {triage_result['tool']}"
463
+ )
464
+
465
+ try:
466
+ result = self.factory.execute_tool(triage_result["tool"], **triage_result["arguments"])
467
+ task.result = result
468
+ task.status = "completed"
469
+
470
+ log_step(
471
+ step_number=9,
472
+ title="Tool Execution Results",
473
+ details={
474
+ "tool": triage_result["tool"],
475
+ "status": task.status,
476
+ "result": result,
477
+ "duration": time.time() - task.started_at
478
+ },
479
+ reasoning=f"Tool execution completed successfully with result: {result}"
480
+ )
481
+
482
+ task.completed_at = time.time()
483
+ return
484
+ except Exception as e:
485
+ log_section_header("Tool Execution Error")
486
+ log_step(
487
+ step_number=10,
488
+ title="Error Handling",
489
+ details={
490
+ "tool": triage_result["tool"],
491
+ "error": str(e),
492
+ "error_type": type(e).__name__
493
+ },
494
+ reasoning=f"Tool execution failed due to: {str(e)}"
495
+ )
496
+ logger.error(f"Error executing tool {triage_result['tool']} directly: {str(e)}")
497
+
498
+ if triage_result.get("requires_new_agent", False):
499
+ log_section_header("New Agent Creation")
500
+ log_step(
501
+ step_number=11,
502
+ title="Agent Creation Decision",
503
+ details={"need_permission": need_permission},
504
+ reasoning="Task complexity requires creation of a new specialized agent"
505
+ )
506
+
507
+ if need_permission:
508
+ agent_result = self.factory.create_agent_from_requirement(
509
+ requirement=task.description,
510
+ ask_permission=True
511
+ )
512
+
513
+ if agent_result.get("requires_permission", False):
514
+ log_step(
515
+ step_number=12,
516
+ title="Permission Required",
517
+ details={
518
+ "new_tools_needed": agent_result.get("analysis", {}).get("new_tools_needed", []),
519
+ "message": "This task requires creating new tools. Please run again with permission."
520
+ },
521
+ reasoning="Task requires new tools that need user permission to create"
522
+ )
523
+
524
+ task.result = {
525
+ "requires_permission": True,
526
+ "new_tools_needed": agent_result.get("analysis", {}).get("new_tools_needed", []),
527
+ "message": "This task requires creating new tools. Please run again with permission."
528
+ }
529
+ task.status = "needs_permission"
530
+ return
531
+
532
+ log_step(
533
+ step_number=13,
534
+ title="Creating New Agent",
535
+ details={"requirement": task.description},
536
+ reasoning="Creating new specialized agent to handle task requirements"
537
+ )
538
+
539
+ agent_result = self.factory.create_agent_from_requirement(
540
+ requirement=task.description,
541
+ ask_permission=False
542
+ )
543
+
544
+ if agent_result["success"]:
545
+ agent_id = self._generate_agent_id()
546
+ self.agents[agent_id] = agent_result["agent"]
547
+
548
+ log_step(
549
+ step_number=14,
550
+ title="New Agent Created",
551
+ details={
552
+ "agent_id": agent_id,
553
+ "agent_type": type(agent_result["agent"]).__name__
554
+ },
555
+ reasoning=f"Successfully created new agent of type: {type(agent_result['agent']).__name__}"
556
+ )
557
+
558
+ result = self._execute_with_agent(agent_id, task)
559
+ task.result = result
560
+ task.status = "completed"
561
+
562
+ log_step(
563
+ step_number=15,
564
+ title="New Agent Task Completion",
565
+ details={
566
+ "agent_id": agent_id,
567
+ "status": task.status,
568
+ "duration": time.time() - task.started_at
569
+ },
570
+ reasoning=f"New agent successfully completed task with status: {task.status}"
571
+ )
572
+ else:
573
+ task.error = f"Failed to create specialized agent: {agent_result.get('error', 'Unknown error')}"
574
+ task.status = "failed"
575
+
576
+ log_section_header("Agent Creation Error")
577
+ log_step(
578
+ step_number=16,
579
+ title="Error Handling",
580
+ details={
581
+ "error": task.error,
582
+ "duration": time.time() - task.started_at
583
+ },
584
+ reasoning=f"Failed to create new agent due to: {task.error}"
585
+ )
586
+ else:
587
+ agent_id = triage_result.get("agent_id", "triage")
588
+
589
+ log_section_header("Existing Agent Usage")
590
+ log_step(
591
+ step_number=17,
592
+ title="Agent Selection",
593
+ details={"agent_id": agent_id},
594
+ reasoning=f"Using existing agent {agent_id} as determined by triage analysis"
595
+ )
596
+
597
+ result = self._execute_with_agent(agent_id, task)
598
+ task.result = result
599
+ task.status = "completed"
600
+
601
+ log_step(
602
+ step_number=18,
603
+ title="Task Completion",
604
+ details={
605
+ "agent_id": agent_id,
606
+ "status": task.status,
607
+ "duration": time.time() - task.started_at
608
+ },
609
+ reasoning=f"Existing agent {agent_id} completed task with status: {task.status}"
610
+ )
611
+
612
+ except Exception as e:
613
+ task.error = str(e)
614
+ task.status = "failed"
615
+
616
+ log_section_header("Task Failure")
617
+ log_step(
618
+ step_number=19,
619
+ title="Error Handling",
620
+ details={
621
+ "error": str(e),
622
+ "error_type": type(e).__name__,
623
+ "duration": time.time() - task.started_at
624
+ },
625
+ reasoning=f"Task failed due to unexpected error: {str(e)}"
626
+ )
627
+
628
+ finally:
629
+ task.completed_at = time.time()
630
+
631
+ log_section_header("Task Summary")
632
+ log_step(
633
+ step_number=20,
634
+ title="Final Status",
635
+ details={
636
+ "final_status": task.status,
637
+ "total_duration": time.time() - task.started_at,
638
+ "has_error": bool(task.error)
639
+ },
640
+ reasoning=f"Task processing completed with final status: {task.status}"
641
+ )
642
+
643
+ def _triage_task(self, task: TaskStatus) -> Dict[str, Any]:
644
+ """
645
+ Use the triage agent to analyze a task and determine how to handle it.
646
+
647
+ Args:
648
+ task: Task to analyze
649
+
650
+ Returns:
651
+ Dict with triage results
652
+
653
+ Raises:
654
+ OrchestratorError: If triage fails and no fallback can be used
655
+ """
656
+ # First, check if we can handle with existing tools
657
+ existing_tools_check = self.factory.can_handle_with_existing_tools(task.description)
658
+
659
+ if existing_tools_check.get("success", False):
660
+ analysis = existing_tools_check["analysis"]
661
+ if analysis.get("can_handle", False):
662
+ # We can handle with existing tools
663
+ logger.info(f"Task '{task.task_id}' can be handled with existing tools")
664
+ return {
665
+ "assessment": "direct",
666
+ "requires_new_agent": False,
667
+ "required_tools": analysis.get("required_tools", []),
668
+ "reasoning": analysis.get("reasoning", "Can be handled with existing tools")
669
+ }
670
+
671
+ # If we get here, either the check failed or we need more analysis
672
+ triage_agent = self.agents["triage"]
673
+
674
+ # Get list of dynamic agents
675
+ dynamic_agents = self.factory.list_dynamic_agents()
676
+ dynamic_agent_ids = list(dynamic_agents.keys())
677
+
678
+ # Create a prompt for the triage agent, including information from our tools check
679
+ reasoning_from_check = ""
680
+ if existing_tools_check.get("success", False):
681
+ analysis = existing_tools_check["analysis"]
682
+ missing = analysis.get("missing_capabilities", [])
683
+ if missing:
684
+ reasoning_from_check = f"Missing capabilities: {', '.join(missing)}"
685
+
686
+ # Add special handling for capability queries like "what can you do"
687
+ if "what can you do" in task.description.lower() or "capabilities" in task.description.lower() or "help me" in task.description.lower():
688
+ return {
689
+ "assessment": "direct",
690
+ "requires_new_agent": False,
691
+ "reasoning": "Capability query detected. Responding with system capabilities information."
692
+ }
693
+
694
+ try:
695
+ triage_template = self.prompt_manager.load_template("workflows/triage.md")
696
+ triage_prompt = self.prompt_manager.process_template(
697
+ triage_template,
698
+ {
699
+ "query": task.description,
700
+ "tools": ', '.join(self.factory.list_tools().keys()),
701
+ "agents": ', '.join(list(self.agents.keys()) + dynamic_agent_ids),
702
+ "reasoning": reasoning_from_check,
703
+ },
704
+ )
705
+ logger.debug("Using PromptManager to generate triage prompt")
706
+ except Exception as e:
707
+ logger.error(f"Failed to load or process triage prompt template: {str(e)}")
708
+ raise RuntimeError(
709
+ "Critical: triage prompt template missing or invalid. "
710
+ "Please ensure 'prompts/workflows/triage.md' exists and is correct."
711
+ ) from e
712
+
713
+ # Get triage analysis - let the Agent.run method handle retries internally
714
+ try:
715
+ # Wrap the entire execution in an exception handler to catch tool execution errors
716
+ try:
717
+ result = triage_agent.run(triage_prompt)
718
+ except Exception as tool_error:
719
+ # If a tool execution fails, still return a valid assessment
720
+ logger.error(f"Tool execution failed during triage: {str(tool_error)}")
721
+ return {
722
+ "assessment": "direct",
723
+ "requires_new_agent": False,
724
+ "reasoning": f"Tool execution error: {str(tool_error)}"
725
+ }
726
+
727
+ # The Agent.run method should have already tried to parse and retry
728
+ # up to max_retries times, and returned a fallback if all failed.
729
+ # Here we just need to do a final check and parsing.
730
+
731
+ # First check if we already got a dictionary (already parsed)
732
+ if isinstance(result, dict):
733
+ # Special case: If we got a chat tool with empty arguments, convert to assessment format
734
+ if "tool" in result and result.get("tool") == "chat":
735
+ logger.info(f"Triage returned chat tool format for task {task.task_id}, converting to assessment")
736
+ # If arguments.message exists, use that for reasoning
737
+ reasoning = "Chat tool returned by triage agent, converting to direct assessment"
738
+ if "arguments" in result and isinstance(result["arguments"], dict):
739
+ if "message" in result["arguments"] and result["arguments"]["message"]:
740
+ reasoning = result["arguments"]["message"]
741
+
742
+ return {
743
+ "assessment": "direct",
744
+ "requires_new_agent": False,
745
+ "reasoning": reasoning
746
+ }
747
+
748
+ # Otherwise try to parse the JSON result
749
+ if isinstance(result, str):
750
+ # Strategy 1: Try to find JSON object using regex
751
+ json_match = re.search(r'({[\s\S]*})', result)
752
+ if json_match:
753
+ try:
754
+ parsed_result = json.loads(json_match.group(1))
755
+ print("\n[Parsed JSON from regex extraction]:")
756
+ pprint.pprint(parsed_result)
757
+ return parsed_result
758
+ except json.JSONDecodeError:
759
+ pass
760
+
761
+ # Strategy 2: Try parsing entire content as JSON
762
+ try:
763
+ parsed_result = json.loads(result)
764
+ print("\n[Parsed JSON from direct parse]:")
765
+ pprint.pprint(parsed_result)
766
+ return parsed_result
767
+ except json.JSONDecodeError:
768
+ # Check if this looks like a chat response rather than a JSON response
769
+ if isinstance(result, str) and len(result.strip()) > 0 and "{" not in result and "}" not in result:
770
+ logger.warning(f"Triage returned chat response instead of JSON format: {result[:100]}...")
771
+ # Extract any assessment-like keywords to make a best guess
772
+ assessment = "direct" # Default
773
+ if re.search(r'\b(new agent|specialized|create agent)\b', result, re.IGNORECASE):
774
+ assessment = "create_new"
775
+
776
+ default_result = {
777
+ "assessment": assessment,
778
+ "requires_new_agent": assessment == "create_new",
779
+ "reasoning": f"Inferred from chat response: {result[:100]}...",
780
+ "original_response": result[:500] # Store original for debugging
781
+ }
782
+ else:
783
+ # Standard JSON parsing failure
784
+ logger.error(f"Failed to parse triage result for task {task.task_id}: {result[:100]}...")
785
+ default_result = {
786
+ "assessment": "direct",
787
+ "requires_new_agent": False,
788
+ "reasoning": "Could not parse triage result, falling back to direct handling"
789
+ }
790
+ return default_result
791
+ else:
792
+ # If not a string, it's probably already a structured result
793
+ return result
794
+
795
+ except Exception as e:
796
+ # Fallback on error
797
+ logger.error(f"Error in triage for task {task.task_id}: {str(e)}")
798
+ fallback_result = {
799
+ "assessment": "direct",
800
+ "requires_new_agent": False,
801
+ "reasoning": f"Triage error: {str(e)}, falling back to direct handling"
802
+ }
803
+ return fallback_result
804
+
805
+ def _execute_with_agent(self, agent_id: str, task: TaskStatus) -> Any:
806
+ """
807
+ Execute a task using the specified agent.
808
+
809
+ Args:
810
+ agent_id: ID of the agent to use
811
+ task: Task to execute
812
+
813
+ Returns:
814
+ Result from the agent
815
+
816
+ Raises:
817
+ AgentNotFoundError: If the agent is not found
818
+ """
819
+ # First check if it's a dynamic agent
820
+ agent = self.factory.get_dynamic_agent(agent_id)
821
+
822
+ # If not found in dynamic agents, check regular agents
823
+ if agent is None:
824
+ agent = self.agents.get(agent_id)
825
+
826
+ if agent is None:
827
+ error_msg = f"Agent with ID '{agent_id}' not found"
828
+ logger.error(error_msg)
829
+ raise AgentNotFoundError(error_msg)
830
+
831
+ task.assigned_agent = agent_id
832
+ logger.info(f"Executing task {task.task_id} with agent {agent_id}")
833
+
834
+ # Execute task
835
+ result = agent.run(task.description)
836
+ return result
837
+
838
+ def get_task_status(self, task_id: str) -> Optional[TaskStatus]:
839
+ """
840
+ Get the current status of a task.
841
+
842
+ Args:
843
+ task_id: ID of the task to check
844
+
845
+ Returns:
846
+ TaskStatus or None if not found
847
+ """
848
+ return self.tasks.get(task_id)
849
+
850
+ def grant_permission(self, task_id: str) -> None:
851
+ """
852
+ Grant permission for a task that needs it.
853
+
854
+ Args:
855
+ task_id: ID of the task that needs permission
856
+
857
+ Raises:
858
+ ValueError: If the task is not found or does not need permission
859
+ """
860
+ task = self.tasks.get(task_id)
861
+ if not task:
862
+ error_msg = f"Task {task_id} not found"
863
+ logger.error(error_msg)
864
+ raise ValueError(error_msg)
865
+
866
+ if task.status != "needs_permission":
867
+ error_msg = f"Task {task_id} does not need permission (status: {task.status})"
868
+ logger.error(error_msg)
869
+ raise ValueError(error_msg)
870
+
871
+ # Reprocess the task with permission granted
872
+ task.status = "pending"
873
+ logger.info(f"Permission granted for task {task_id}")
874
+ self._process_task(task_id, need_permission=False)
875
+
876
+ def list_agents(self) -> Dict[str, Dict[str, Any]]:
877
+ """
878
+ List all registered agents.
879
+
880
+ Returns:
881
+ Dictionary of agent IDs to metadata
882
+ """
883
+ # Combine built-in agents and dynamic agents
884
+ all_agents = {}
885
+
886
+ # Add built-in agents
887
+ for agent_id, agent in self.agents.items():
888
+ all_agents[agent_id] = {
889
+ "name": getattr(agent, "name", agent_id),
890
+ "description": getattr(agent, "description", "No description"),
891
+ "dynamic": False
892
+ }
893
+
894
+ # Add dynamic agents
895
+ dynamic_agents = self.factory.list_dynamic_agents()
896
+ for agent_id, agent_info in dynamic_agents.items():
897
+ all_agents[agent_id] = {
898
+ **agent_info,
899
+ "dynamic": True
900
+ }
901
+
902
+ return all_agents
903
+
904
+ def list_tools(self) -> Dict[str, Any]:
905
+ """
906
+ Return a dictionary of all registered tools from the factory.
907
+
908
+ Returns:
909
+ Dictionary of tool names to tool definitions/objects
910
+ """
911
+ return self.factory.list_tools()
912
+
913
+ def execute_riv_task(self, task_id: str, max_iterations: int = 5) -> Dict[str, Any]:
914
+ """
915
+ Execute a task using the RIV (Reflect, Improve, Verify) pattern.
916
+
917
+ This adaptive approach uses an iterative loop that:
918
+ 1. Reflects on the current state
919
+ 2. Improves with targeted actions
920
+ 3. Verifies if the task is complete
921
+
922
+ Args:
923
+ task_id: ID of the task to execute
924
+ max_iterations: Maximum number of iterations to perform
925
+
926
+ Returns:
927
+ A dictionary containing the final result and execution history
928
+ """
929
+ task = self.tasks.get(task_id)
930
+ if not task:
931
+ raise ValueError(f"Task {task_id} not found")
932
+
933
+ task.status = "in_progress"
934
+ task.started_at = time.time()
935
+
936
+ # Initialize execution memory
937
+ memory = {
938
+ "iterations": [],
939
+ "current_result": None,
940
+ "original_task": task.description
941
+ }
942
+
943
+ iteration = 0
944
+ is_complete = False
945
+
946
+ log_section_header("Starting RIV Task Execution")
947
+ log_step(
948
+ step_number=1,
949
+ title="Task Initialization",
950
+ details={
951
+ "task_id": task_id,
952
+ "description": task.description[:100] + "..." if len(task.description) > 100 else task.description,
953
+ "max_iterations": max_iterations
954
+ },
955
+ reasoning="Beginning RIV execution loop (Reflect-Improve-Verify)"
956
+ )
957
+
958
+ # Main RIV execution loop
959
+ while iteration < max_iterations and not is_complete:
960
+ iteration += 1
961
+ iteration_memory = {"iteration": iteration, "timestamp": time.time()}
962
+
963
+ log_section_header(f"RIV Iteration {iteration}")
964
+
965
+ try:
966
+ # ------- REFLECT phase -------
967
+ # Analyze current state and identify what's needed
968
+ log_step(
969
+ step_number=iteration*3-2,
970
+ title=f"REFLECT (Iteration {iteration})",
971
+ details={"current_state": "analyzing"},
972
+ reasoning="Analyzing current state and planning next steps"
973
+ )
974
+
975
+ reflection_prompt = self._build_reflection_prompt(task, memory)
976
+ triage_agent = self.agents["triage"]
977
+ reflection_result = triage_agent.run(reflection_prompt)
978
+
979
+ # Parse the reflection result
980
+ if isinstance(reflection_result, dict):
981
+ reflection = reflection_result
982
+ else:
983
+ # Try to parse as JSON
984
+ try:
985
+ json_match = re.search(r'({[\s\S]*})', reflection_result)
986
+ if json_match:
987
+ reflection = json.loads(json_match.group(1))
988
+ else:
989
+ reflection = json.loads(reflection_result)
990
+ except (json.JSONDecodeError, TypeError):
991
+ logger.error(f"Failed to parse reflection result: {reflection_result[:200]}...")
992
+ reflection = {
993
+ "status": "needs_improvement",
994
+ "action_plan": {"type": "retry", "reason": "Failed to parse previous result"}
995
+ }
996
+
997
+ is_complete = reflection.get("status") == "complete"
998
+ iteration_memory["reflection"] = reflection
999
+
1000
+ log_step(
1001
+ step_number=iteration*3-1,
1002
+ title=f"Reflection Analysis (Iteration {iteration})",
1003
+ details={
1004
+ "status": reflection.get("status", "unknown"),
1005
+ "reasoning": reflection.get("reasoning", "No reasoning provided"),
1006
+ "is_complete": is_complete
1007
+ },
1008
+ reasoning="Reflecting on current progress and determining next actions"
1009
+ )
1010
+
1011
+ # If reflection says we're done, break the loop
1012
+ if is_complete:
1013
+ task.status = "completed"
1014
+ task.result = memory["current_result"]
1015
+ iteration_memory["status"] = "complete"
1016
+ memory["iterations"].append(iteration_memory)
1017
+ break
1018
+
1019
+ # ------- IMPROVE phase -------
1020
+ # Execute the planned action to improve the result
1021
+ action_plan = reflection.get("action_plan", {})
1022
+ action_type = action_plan.get("type", "unknown")
1023
+
1024
+ log_step(
1025
+ step_number=iteration*3,
1026
+ title=f"IMPROVE (Iteration {iteration})",
1027
+ details={
1028
+ "action_type": action_type,
1029
+ "details": action_plan
1030
+ },
1031
+ reasoning=f"Executing improvement action: {action_type}"
1032
+ )
1033
+
1034
+ action_result = None
1035
+ action_error = None
1036
+
1037
+ try:
1038
+ if action_type == "use_tool":
1039
+ # Execute a single tool
1040
+ tool_name = action_plan.get("tool")
1041
+ arguments = action_plan.get("arguments", {})
1042
+
1043
+ if not tool_name:
1044
+ raise ValueError("Missing tool name in action plan")
1045
+
1046
+ action_result = self.factory.execute_tool(tool_name, **arguments)
1047
+
1048
+ elif action_type == "use_tool_sequence":
1049
+ # Execute a sequence of tools
1050
+ steps = action_plan.get("tool_sequence", [])
1051
+ if not steps or not isinstance(steps, list):
1052
+ raise ValueError("Invalid or missing tool sequence")
1053
+
1054
+ sequence_results = []
1055
+ for i, step_info in enumerate(steps, start=1):
1056
+ tool_name = step_info.get("tool")
1057
+ arguments = step_info.get("arguments", {})
1058
+
1059
+ if not tool_name:
1060
+ raise ValueError(f"Missing tool name in sequence step {i}")
1061
+
1062
+ try:
1063
+ result = self.factory.execute_tool(tool_name, **arguments)
1064
+ sequence_results.append({
1065
+ "step": i,
1066
+ "tool": tool_name,
1067
+ "arguments": arguments,
1068
+ "result": result
1069
+ })
1070
+ except Exception as e:
1071
+ sequence_results.append({
1072
+ "step": i,
1073
+ "tool": tool_name,
1074
+ "arguments": arguments,
1075
+ "error": str(e)
1076
+ })
1077
+
1078
+ action_result = sequence_results
1079
+
1080
+ elif action_type == "use_agent":
1081
+ # Use a specific agent
1082
+ agent_id = action_plan.get("agent_id")
1083
+ if not agent_id:
1084
+ raise ValueError("Missing agent_id in action plan")
1085
+
1086
+ prompt = action_plan.get("prompt", task.description)
1087
+ action_result = self._execute_with_agent(agent_id, prompt)
1088
+
1089
+ elif action_type == "create_agent":
1090
+ # Create a new specialized agent
1091
+ requirement = action_plan.get("requirement", task.description)
1092
+ agent_result = self.factory.create_agent_from_requirement(
1093
+ requirement=requirement,
1094
+ ask_permission=False
1095
+ )
1096
+
1097
+ if agent_result["success"]:
1098
+ agent_id = self._generate_agent_id()
1099
+ self.agents[agent_id] = agent_result["agent"]
1100
+ prompt = action_plan.get("prompt", task.description)
1101
+ action_result = self._execute_with_agent(agent_id, prompt)
1102
+ else:
1103
+ raise ValueError(f"Failed to create agent: {agent_result.get('error')}")
1104
+
1105
+ elif action_type == "refine_result":
1106
+ # Refine the current result
1107
+ refinement_prompt = action_plan.get("prompt")
1108
+ if not refinement_prompt:
1109
+ raise ValueError("Missing refinement prompt in action plan")
1110
+
1111
+ action_result = triage_agent.run(refinement_prompt)
1112
+
1113
+ elif action_type == "retry":
1114
+ # Simply retry with the triage agent
1115
+ action_result = triage_agent.run(task.description)
1116
+
1117
+ else:
1118
+ raise ValueError(f"Unknown action type: {action_type}")
1119
+
1120
+ except Exception as e:
1121
+ action_error = str(e)
1122
+ logger.error(f"Error in IMPROVE phase (iteration {iteration}): {action_error}")
1123
+
1124
+ # Store the action result
1125
+ iteration_memory["action"] = {
1126
+ "type": action_type,
1127
+ "details": action_plan,
1128
+ "result": action_result,
1129
+ "error": action_error
1130
+ }
1131
+
1132
+ # Update the current result
1133
+ if action_error is None:
1134
+ memory["current_result"] = action_result
1135
+
1136
+ # ------- VERIFY phase -------
1137
+ # Check if the result is satisfactory
1138
+ log_step(
1139
+ step_number=iteration*3+1,
1140
+ title=f"VERIFY (Iteration {iteration})",
1141
+ details={
1142
+ "has_result": action_result is not None,
1143
+ "has_error": action_error is not None
1144
+ },
1145
+ reasoning="Verifying result of improvement action"
1146
+ )
1147
+
1148
+ verify_prompt = self._build_verification_prompt(task, memory, action_result, action_error)
1149
+ verification_result = triage_agent.run(verify_prompt)
1150
+
1151
+ # Parse the verification result
1152
+ if isinstance(verification_result, dict):
1153
+ verification = verification_result
1154
+ else:
1155
+ try:
1156
+ json_match = re.search(r'({[\s\S]*})', verification_result)
1157
+ if json_match:
1158
+ verification = json.loads(json_match.group(1))
1159
+ else:
1160
+ verification = json.loads(verification_result)
1161
+ except (json.JSONDecodeError, TypeError):
1162
+ logger.error(f"Failed to parse verification result: {verification_result[:200]}...")
1163
+ verification = {
1164
+ "is_complete": False,
1165
+ "quality": "unknown",
1166
+ "reasoning": "Failed to parse verification result"
1167
+ }
1168
+
1169
+ is_complete = verification.get("is_complete", False)
1170
+ iteration_memory["verification"] = verification
1171
+
1172
+ log_step(
1173
+ step_number=iteration*3+2,
1174
+ title=f"Verification Results (Iteration {iteration})",
1175
+ details={
1176
+ "is_complete": is_complete,
1177
+ "quality": verification.get("quality", "unknown"),
1178
+ "reasoning": verification.get("reasoning", "No reasoning provided")
1179
+ },
1180
+ reasoning="Determining if task is complete based on verification"
1181
+ )
1182
+
1183
+ # Add the iteration to memory
1184
+ memory["iterations"].append(iteration_memory)
1185
+
1186
+ # If verification says we're done, break the loop
1187
+ if is_complete:
1188
+ task.status = "completed"
1189
+ task.result = memory["current_result"]
1190
+ break
1191
+
1192
+ except Exception as e:
1193
+ # Handle any uncaught exceptions in the iteration
1194
+ logger.error(f"Error in RIV cycle {iteration}: {str(e)}")
1195
+ iteration_memory["error"] = str(e)
1196
+ memory["iterations"].append(iteration_memory)
1197
+
1198
+ if iteration == max_iterations:
1199
+ task.error = f"Failed after {max_iterations} iterations: {str(e)}"
1200
+ task.status = "failed"
1201
+
1202
+ # If we've hit max iterations without completing
1203
+ if iteration >= max_iterations and not is_complete:
1204
+ task.status = "failed" if task.status == "in_progress" else task.status
1205
+ task.error = f"Reached maximum iterations ({max_iterations}) without completing task"
1206
+
1207
+ log_section_header("Max Iterations Reached")
1208
+ log_step(
1209
+ step_number=iteration*3+3,
1210
+ title="Iteration Limit Reached",
1211
+ details={
1212
+ "max_iterations": max_iterations,
1213
+ "final_status": task.status
1214
+ },
1215
+ reasoning="Task execution stopped due to reaching maximum allowed iterations"
1216
+ )
1217
+
1218
+ # Update task with final results
1219
+ task.completed_at = time.time()
1220
+ final_result = {
1221
+ "execution_memory": memory,
1222
+ "iterations_completed": iteration,
1223
+ "task_completed": is_complete,
1224
+ "final_result": memory["current_result"]
1225
+ }
1226
+ task.result = final_result
1227
+
1228
+ log_section_header("RIV Execution Complete")
1229
+ log_step(
1230
+ step_number=iteration*3+4,
1231
+ title="Final Status",
1232
+ details={
1233
+ "task_id": task_id,
1234
+ "status": task.status,
1235
+ "iterations": iteration,
1236
+ "duration": task.completed_at - task.started_at
1237
+ },
1238
+ reasoning="RIV process completed with final status"
1239
+ )
1240
+
1241
+ return final_result
1242
+
1243
+ def _build_reflection_prompt(self, task: TaskStatus, memory: Dict[str, Any]) -> str:
1244
+ """
1245
+ Build a prompt for the REFLECT phase to analyze current state and plan next steps.
1246
+
1247
+ Args:
1248
+ task: The task being executed
1249
+ memory: The execution memory so far
1250
+
1251
+ Returns:
1252
+ A prompt string for the LLM to analyze
1253
+ """
1254
+ available_tools = ', '.join(self.factory.list_tools().keys())
1255
+ available_agents = ', '.join(self.agents.keys())
1256
+
1257
+ # Create a concise summary of previous iterations
1258
+ iterations_summary = ""
1259
+ if memory["iterations"]:
1260
+ for i, iteration in enumerate(memory["iterations"], start=1):
1261
+ iterations_summary += f"\nITERATION {i} SUMMARY:\n"
1262
+
1263
+ # Add reflection
1264
+ reflection = iteration.get("reflection", {})
1265
+ iterations_summary += f"- Status: {reflection.get('status', 'unknown')}\n"
1266
+ iterations_summary += f"- Reasoning: {reflection.get('reasoning', 'N/A')[:200]}...\n"
1267
+
1268
+ # Add action
1269
+ action = iteration.get("action", {})
1270
+ action_type = action.get("type", "unknown")
1271
+ iterations_summary += f"- Action: {action_type}\n"
1272
+
1273
+ if action_type == "use_tool":
1274
+ tool_name = action.get("details", {}).get("tool", "unknown")
1275
+ iterations_summary += f" - Tool used: {tool_name}\n"
1276
+
1277
+ # Add results summary
1278
+ result = action.get("result")
1279
+ if result is not None:
1280
+ if isinstance(result, str) and len(result) > 100:
1281
+ iterations_summary += f" - Result: {result[:100]}...(truncated)\n"
1282
+ elif isinstance(result, list) and len(result) > 0:
1283
+ iterations_summary += f" - Result: List with {len(result)} items\n"
1284
+ else:
1285
+ iterations_summary += f" - Result: Result obtained\n"
1286
+
1287
+ # Add verification
1288
+ verification = iteration.get("verification", {})
1289
+ iterations_summary += f"- Verification: {verification.get('quality', 'unknown')}\n"
1290
+ iterations_summary += f"- Complete: {verification.get('is_complete', False)}\n"
1291
+ if "missing_outputs" in verification:
1292
+ missing = verification.get("missing_outputs", [])
1293
+ if missing:
1294
+ iterations_summary += f"- Missing outputs: {', '.join(missing)}\n"
1295
+
1296
+ # Check for existing files in the current directory that might be related to the task
1297
+ cwd = os.getcwd()
1298
+ existing_files = []
1299
+ try:
1300
+ import glob
1301
+ # Look for files that might be related to the task
1302
+ for file_path in glob.glob(os.path.join(cwd, "*.md")) + glob.glob(os.path.join(cwd, "*.txt")):
1303
+ if os.path.isfile(file_path):
1304
+ rel_path = os.path.relpath(file_path, cwd)
1305
+ existing_files.append(rel_path)
1306
+ except Exception as e:
1307
+ logger.error(f"Error checking for existing files: {str(e)}")
1308
+
1309
+ files_info = ""
1310
+ if existing_files:
1311
+ files_info = "Files already in workspace:\n"
1312
+ for file_path in existing_files:
1313
+ files_info += f"- {file_path}\n"
1314
+
1315
+ current_result = memory["current_result"]
1316
+ result_summary = ""
1317
+ if current_result is not None:
1318
+ # Format and truncate the result for readability
1319
+ if isinstance(current_result, str):
1320
+ result_summary = current_result[:500] + "..." if len(current_result) > 500 else current_result
1321
+ else:
1322
+ try:
1323
+ result_summary = json.dumps(current_result, indent=2, default=str)
1324
+ if len(result_summary) > 500:
1325
+ result_summary = result_summary[:500] + "...(truncated)"
1326
+ except:
1327
+ result_summary = str(current_result)[:500] + "..." if len(str(current_result)) > 500 else str(current_result)
1328
+
1329
+ # Add tool details to make better recommendations
1330
+ tool_details = ""
1331
+ # Get information about all available tools
1332
+ useful_tools = list(self.factory.list_tools().keys())
1333
+ for tool_name in useful_tools:
1334
+ try:
1335
+ tool_info = self._get_tool_info(tool_name)
1336
+ if tool_info:
1337
+ tool_details += f"\n{tool_name}:\n"
1338
+ tool_details += f" Description: {tool_info.get('description', 'No description')}\n"
1339
+ params = tool_info.get("parameters", {})
1340
+ if params:
1341
+ tool_details += f" Parameters: {json.dumps(params, indent=2)[:200]}...\n"
1342
+ except:
1343
+ pass # Skip if tool not found
1344
+
1345
+ # First iteration vs subsequent iterations
1346
+ if not memory["iterations"]:
1347
+ # First iteration - no history yet
1348
+ try:
1349
+ template = self.prompt_manager.load_template("workflows/riv_reflect.md")
1350
+ prompt = self.prompt_manager.process_template(
1351
+ template,
1352
+ {
1353
+ "task_description": task.description,
1354
+ "available_tools": available_tools,
1355
+ "available_agents": available_agents,
1356
+ "iterations_summary": iterations_summary,
1357
+ "result_summary": result_summary,
1358
+ "files_info": files_info,
1359
+ "tool_details": tool_details,
1360
+ },
1361
+ )
1362
+ return prompt
1363
+ except Exception as e:
1364
+ logger.error(f"Failed to load or process RIV reflect prompt template: {str(e)}")
1365
+ raise RuntimeError(
1366
+ "Critical: RIV reflect prompt template missing or invalid. "
1367
+ "Please ensure 'prompts/workflows/riv_reflect.md' exists and is correct."
1368
+ ) from e
1369
+
1370
+ def _get_tool_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
1371
+ """
1372
+ Get information about a specific tool.
1373
+
1374
+ Args:
1375
+ tool_name: Name of the tool to get information about
1376
+
1377
+ Returns:
1378
+ Dictionary with tool information or None if the tool is not found
1379
+ """
1380
+ tools = self.factory.list_tools()
1381
+ tool = tools.get(tool_name)
1382
+ if not tool:
1383
+ return None
1384
+
1385
+ return {
1386
+ "name": tool_name,
1387
+ "description": getattr(tool, "description", "No description available"),
1388
+ "parameters": getattr(tool, "parameters", {})
1389
+ }
1390
+
1391
+ def _build_verification_prompt(self, task: TaskStatus, memory: Dict[str, Any],
1392
+ action_result: Any, action_error: Optional[str]) -> str:
1393
+ """
1394
+ Build a prompt for the VERIFY phase to check if the task is complete.
1395
+
1396
+ Args:
1397
+ task: The task being executed
1398
+ memory: The execution memory so far
1399
+ action_result: The result of the most recent action
1400
+ action_error: Any error that occurred during the action
1401
+
1402
+ Returns:
1403
+ A prompt string for the LLM to verify
1404
+ """
1405
+ # Format action result for readability
1406
+ if action_result is not None:
1407
+ if isinstance(action_result, str):
1408
+ result_text = action_result[:1000] + "..." if len(action_result) > 1000 else action_result
1409
+ else:
1410
+ try:
1411
+ result_text = json.dumps(action_result, indent=2, default=str)
1412
+ if len(result_text) > 1000:
1413
+ result_text = result_text[:1000] + "...(truncated)"
1414
+ except:
1415
+ result_text = str(action_result)[:1000] + "..." if len(str(action_result)) > 1000 else str(action_result)
1416
+ else:
1417
+ result_text = "No result produced."
1418
+
1419
+ # Add error information if present
1420
+ error_text = f"ERROR: {action_error}" if action_error else "No errors."
1421
+
1422
+ # Create a summary of all previous iterations and results
1423
+ iteration_count = len(memory.get("iterations", []))
1424
+ history_summary = ""
1425
+ if iteration_count > 0:
1426
+ history_summary = f"This is iteration {iteration_count}. Previous steps include:\n"
1427
+ for i, iter_data in enumerate(memory.get("iterations", []), 1):
1428
+ action = iter_data.get("action", {})
1429
+ action_type = action.get("type", "unknown")
1430
+
1431
+ # Add specific details based on action type
1432
+ if action_type == "use_tool":
1433
+ tool_name = action.get("details", {}).get("tool", "unknown")
1434
+ history_summary += f"- Iteration {i}: Used tool '{tool_name}'\n"
1435
+ else:
1436
+ history_summary += f"- Iteration {i}: Performed {action_type}\n"
1437
+
1438
+ # Check for all files created or modified in the current workspace
1439
+ cwd = os.getcwd()
1440
+ files_created = []
1441
+ try:
1442
+ import glob
1443
+ for md_file in glob.glob(os.path.join(cwd, "*.md")):
1444
+ if os.path.isfile(md_file):
1445
+ rel_path = os.path.relpath(md_file, cwd)
1446
+ files_created.append(rel_path)
1447
+ except Exception as e:
1448
+ logger.error(f"Error checking for created files: {str(e)}")
1449
+
1450
+ files_info = ""
1451
+ if files_created:
1452
+ files_info = "Files found in workspace:\n"
1453
+ for file_path in files_created:
1454
+ files_info += f"- {file_path}\n"
1455
+ try:
1456
+ full_path = os.path.join(cwd, file_path)
1457
+ if os.path.getsize(full_path) < 5000:
1458
+ with open(full_path, 'r', encoding='utf-8') as f:
1459
+ content = f.read()
1460
+ files_info += f"Content of {file_path}:\n```\n{content}\n```\n"
1461
+ except Exception as e:
1462
+ files_info += f"(Error reading file: {str(e)})\n"
1463
+ else:
1464
+ files_info = "No files have been created or modified in the workspace."
1465
+
1466
+ # Format action result
1467
+ if action_result is not None:
1468
+ try:
1469
+ if isinstance(action_result, str):
1470
+ result_text = action_result[:1000] + "..." if len(action_result) > 1000 else action_result
1471
+ else:
1472
+ result_text = json.dumps(action_result, indent=2, default=str)
1473
+ if len(result_text) > 1000:
1474
+ result_text = result_text[:1000] + "...(truncated)"
1475
+ except:
1476
+ result_text = str(action_result)[:1000] + "..." if len(str(action_result)) > 1000 else str(action_result)
1477
+ else:
1478
+ result_text = "No result produced."
1479
+
1480
+ error_text = f"ERROR: {action_error}" if action_error else "No errors."
1481
+
1482
+ # Build history summary
1483
+ iteration_count = len(memory.get("iterations", []))
1484
+ history_summary = ""
1485
+ if iteration_count > 0:
1486
+ history_summary = f"This is iteration {iteration_count}. Previous steps include:\n"
1487
+ for i, iter_data in enumerate(memory.get("iterations", []), 1):
1488
+ action = iter_data.get("action", {})
1489
+ action_type = action.get("type", "unknown")
1490
+ if action_type == "use_tool":
1491
+ tool_name = action.get("details", {}).get("tool", "unknown")
1492
+ history_summary += f"- Iteration {i}: Used tool '{tool_name}'\n"
1493
+ else:
1494
+ history_summary += f"- Iteration {i}: Performed {action_type}\n"
1495
+
1496
+ try:
1497
+ template = self.prompt_manager.load_template("workflows/riv_verify.md")
1498
+ prompt = self.prompt_manager.process_template(
1499
+ template,
1500
+ {
1501
+ "task_description": task.description,
1502
+ "history_summary": history_summary,
1503
+ "result_text": result_text,
1504
+ "files_info": files_info,
1505
+ "error_text": error_text,
1506
+ },
1507
+ )
1508
+ return prompt
1509
+ except Exception as e:
1510
+ logger.error(f"Failed to load or process RIV verify prompt template: {str(e)}")
1511
+ raise RuntimeError(
1512
+ "Critical: RIV verify prompt template missing or invalid. "
1513
+ "Please ensure 'prompts/workflows/riv_verify.md' exists and is correct."
1514
+ ) from e