diff --git a/navi/core/subagent_runner.py b/navi/core/subagent_runner.py index 1c6c202..23dc34a 100644 --- a/navi/core/subagent_runner.py +++ b/navi/core/subagent_runner.py @@ -207,7 +207,8 @@ for iteration in range(max_iterations): if stop_event and stop_event.is_set(): - return accumulated_text, False + report = self._build_progress_report(context, "user_stop", iteration) + return report + "\n\n" + (accumulated_text or ""), False elapsed = time.monotonic() - _start_time if elapsed >= timeout_seconds: @@ -220,7 +221,8 @@ token_count=_turn_tokens, tool_call_count=_sub_tool_count ) ) - return accumulated_text or "[Sub-agent timed out]", False + report = self._build_progress_report(context, "timeout", iteration) + return report + "\n\n" + (accumulated_text or "[Sub-agent timed out]"), False log.debug("agent.subagent.iteration", iteration=iteration) @@ -293,7 +295,8 @@ tool_call_count=_sub_tool_count, ) ) - return f"[{thinking_stalled_reason}]", False + report = self._build_progress_report(context, "thinking_stall", iteration + 1) + return report + "\n\n" + f"[{thinking_stalled_reason}]", False if not turn_tool_calls: log.info( @@ -308,7 +311,13 @@ tool_call_count=_sub_tool_count, ) ) - return accumulated_text, True + report = self._build_progress_report(context, "completed", iteration + 1) + result = accumulated_text or "" + if result: + result = report + "\n\n" + result + else: + result = report + return result, True if accumulated_thinking and sink is not None: log.debug( @@ -341,7 +350,8 @@ # skip remaining tools in this batch. if stop_event and stop_event.is_set(): log.info("agent.subagent.tool_batch_stopped", tool=tc.name) - return "", False + report = self._build_progress_report(context, "user_stop", iteration + 1) + return report + "\n\n", False _sub_tool_count += 1 if sink is not None: @@ -424,11 +434,9 @@ token_count=_turn_tokens, tool_call_count=_sub_tool_count ) ) - return ( - accumulated_text - or "[Sub-agent reached iteration limit without a final answer]", - False, - ) + report = self._build_progress_report(context, "max_iterations", max_iterations) + result = accumulated_text or "[Sub-agent reached iteration limit without a final answer]" + return report + "\n\n" + result, False finally: _sid_var.set(_prev_sid) _model_var.set(_prev_model) @@ -436,5 +444,64 @@ _role_var.set(_prev_role) _uinfo_var.set(_prev_uinfo) + @staticmethod + def _build_progress_report( + context: list[Message], + reason: str, + iterations_used: int, + ) -> str: + """Serialize the sub-agent's execution history into a progress report. + + The report lists every tool call the sub-agent made, the arguments, + and a short result snippet so the parent agent knows what was + accomplished before the sub-agent stopped. + """ + lines = [ + f"[Sub-agent stopped: {reason}]", + f"Iterations used: {iterations_used}", + "", + ] + + turn_number = 0 + msg_idx = 0 + while msg_idx < len(context): + msg = context[msg_idx] + if msg.role == "assistant" and msg.tool_calls: + turn_number += 1 + turn_lines = [f"Turn {turn_number}:", ""] + if msg.content: + text = msg.content.strip().replace("\n", " ") + turn_lines.append(f" Assistant text: {text[:200]}{'...' if len(text) > 200 else ''}") + for tc in msg.tool_calls: + # Find matching tool result + result_msg = None + for m in context[msg_idx + 1 :]: + if m.role == "tool" and m.tool_call_id == tc.id: + result_msg = m + break + if result_msg: + status = "error" if result_msg.content.startswith("Error:") else "success" + snippet = result_msg.content.strip().replace("\n", " ")[:120] + turn_lines.append(f" - {tc.name} → {status}: {snippet}{'...' if len(result_msg.content) > 120 else ''}") + else: + args = str(tc.arguments).replace("\n", " ")[:80] + turn_lines.append(f" - {tc.name}({args}) → (no result yet)") + lines.extend(turn_lines) + lines.append("") + msg_idx += 1 + len(msg.tool_calls) + elif msg.role == "assistant" and msg.content: + turn_number += 1 + text = msg.content.strip().replace("\n", " ") + lines.append(f"Turn {turn_number}: Assistant response: {text[:200]}{'...' if len(text) > 200 else ''}") + lines.append("") + msg_idx += 1 + else: + msg_idx += 1 + + if turn_number == 0: + lines.append("No tool calls recorded.") + + return "\n".join(lines) + def _get_backend(self, backend_key: str) -> LLMBackend: return self._backends.get(backend_key) diff --git a/tests/unit/core/test_agent.py b/tests/unit/core/test_agent.py index e48fa6c..022e44d 100644 --- a/tests/unit/core/test_agent.py +++ b/tests/unit/core/test_agent.py @@ -224,7 +224,8 @@ agent._backends.register("ollama", backend) result, ok = await agent.run_ephemeral("task", profile_id="test") - assert result == "subagent result" + assert "subagent result" in result + assert "[Sub-agent stopped: completed]" in result assert ok is True @pytest.mark.asyncio