diff --git a/run_agent.py b/run_agent.py index 5b0c40e4d..4d37a365b 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1023,6 +1023,10 @@ class KawaiiSpinner: self.thread = threading.Thread(target=self._animate, daemon=True) self.thread.start() + def update_text(self, new_message: str): + """Update the spinner message text while it's running.""" + self.message = new_message + def stop(self, final_message: str = None): """Stop the spinner and optionally print a final message.""" self.running = False @@ -2978,18 +2982,37 @@ class AIAgent: # Delegate task -- spawn child agent(s) with isolated context elif function_name == "delegate_task": from tools.delegate_tool import delegate_task as _delegate_task - function_result = _delegate_task( - goal=function_args.get("goal"), - context=function_args.get("context"), - toolsets=function_args.get("toolsets"), - tasks=function_args.get("tasks"), - model=function_args.get("model"), - max_iterations=function_args.get("max_iterations"), - parent_agent=self, - ) - tool_duration = time.time() - tool_start_time + tasks_arg = function_args.get("tasks") + if tasks_arg and isinstance(tasks_arg, list): + spinner_label = f"🔀 delegating {len(tasks_arg)} tasks" + else: + goal_preview = (function_args.get("goal") or "")[:30] + spinner_label = f"🔀 {goal_preview}" if goal_preview else "🔀 delegating" + spinner = None if self.quiet_mode: - print(f" {self._get_cute_tool_message('delegate_task', function_args, tool_duration)}") + face = random.choice(KawaiiSpinner.KAWAII_WAITING) + spinner = KawaiiSpinner(f"{face} {spinner_label}", spinner_type='dots') + spinner.start() + # Store spinner on self so delegate_tool can update its text + self._delegate_spinner = spinner + try: + function_result = _delegate_task( + goal=function_args.get("goal"), + context=function_args.get("context"), + toolsets=function_args.get("toolsets"), + tasks=tasks_arg, + model=function_args.get("model"), + max_iterations=function_args.get("max_iterations"), + parent_agent=self, + ) + finally: + self._delegate_spinner = None + tool_duration = time.time() - tool_start_time + cute_msg = self._get_cute_tool_message('delegate_task', function_args, tool_duration) + if spinner: + spinner.stop(cute_msg) + elif self.quiet_mode: + print(f" {cute_msg}") # Execute other tools - with animated kawaii spinner in quiet mode # The face is "alive" while the tool works, then vanishes # and is replaced by the clean result line. diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index ffd7f17d4..e56ad5da1 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -231,7 +231,11 @@ def delegate_task( overall_start = time.monotonic() results = [] - if len(task_list) == 1: + n_tasks = len(task_list) + # Track goal labels for progress display (truncated for readability) + task_labels = [t["goal"][:40] for t in task_list] + + if n_tasks == 1: # Single task -- run directly (no thread pool overhead) t = task_list[0] result = _run_single_child( @@ -245,7 +249,10 @@ def delegate_task( ) results.append(result) else: - # Batch -- run in parallel + # Batch -- run in parallel with per-task progress lines + completed_count = 0 + spinner_ref = getattr(parent_agent, '_delegate_spinner', None) + with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_CHILDREN) as executor: futures = {} for i, t in enumerate(task_list): @@ -263,17 +270,35 @@ def delegate_task( for future in as_completed(futures): try: - results.append(future.result()) + entry = future.result() except Exception as exc: idx = futures[future] - results.append({ + entry = { "task_index": idx, "status": "error", "summary": None, "error": str(exc), "api_calls": 0, "duration_seconds": 0, - }) + } + results.append(entry) + completed_count += 1 + + # Print per-task completion line (visible in CLI via patch_stdout) + idx = entry["task_index"] + label = task_labels[idx] if idx < len(task_labels) else f"Task {idx}" + dur = entry.get("duration_seconds", 0) + status = entry.get("status", "?") + icon = "✓" if status == "completed" else "✗" + remaining = n_tasks - completed_count + print(f" {icon} [{idx+1}/{n_tasks}] {label} ({dur}s)") + + # Update spinner text to show remaining count + if spinner_ref and remaining > 0: + try: + spinner_ref.update_text(f"🔀 {remaining} task{'s' if remaining != 1 else ''} remaining") + except Exception: + pass # Sort by task_index so results match input order results.sort(key=lambda r: r["task_index"])