diff --git a/strix/agents/base_agent.py b/strix/agents/base_agent.py index c759f9a9..78898dba 100644 --- a/strix/agents/base_agent.py +++ b/strix/agents/base_agent.py @@ -334,6 +334,13 @@ class BaseAgent(metaclass=AgentMeta): sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true" if not sandbox_mode and self.state.sandbox_id is None: from strix.runtime import get_runtime + from strix.telemetry.tracer import get_global_tracer + + tracer = get_global_tracer() + if tracer: + tracer.update_agent_system_message( + self.state.agent_id, "Setting up sandbox environment..." + ) try: runtime = get_runtime() @@ -368,6 +375,9 @@ class BaseAgent(metaclass=AgentMeta): async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool | None: final_response = None + if tracer: + tracer.update_agent_system_message(self.state.agent_id, "Thinking...") + async for response in self.llm.generate(self.state.get_conversation_history()): final_response = response if tracer and response.content: @@ -409,8 +419,19 @@ class BaseAgent(metaclass=AgentMeta): ) if actions: + if tracer: + tool_names = [a.get("toolName") or a.get("tool_name") or "tool" for a in actions] + display_names = tool_names[:2] + overflow = len(tool_names) - 2 + suffix = f" +{overflow} more" if overflow > 0 else "" + tracer.update_agent_system_message( + self.state.agent_id, f"Executing {', '.join(display_names)}{suffix}..." + ) return await self._execute_actions(actions, tracer) + if tracer: + tracer.update_agent_system_message(self.state.agent_id, "Processing response...") + return None async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool: diff --git a/strix/interface/tool_components/thinking_renderer.py b/strix/interface/tool_components/thinking_renderer.py index 598bdf33..bbcc64cc 100644 --- a/strix/interface/tool_components/thinking_renderer.py +++ b/strix/interface/tool_components/thinking_renderer.py @@ -23,7 +23,8 @@ class ThinkRenderer(BaseToolRenderer): text.append("\n ") if thought: - text.append(thought, style="italic dim") + indented_thought = "\n ".join(thought.split("\n")) + text.append(indented_thought, style="italic dim") else: text.append("Thinking...", style="italic dim") diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 0cfd7541..9a4577fa 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -1243,14 +1243,19 @@ class StrixTUIApp(App): # type: ignore[misc] return (Text(" "), keymap, False) if status == "running": + sys_msg = agent_data.get("system_message", "") if self._agent_has_real_activity(agent_id): animated_text = Text() animated_text.append_text(self._get_sweep_animation(self._sweep_colors)) + if sys_msg: + animated_text.append(sys_msg, style="dim italic") + animated_text.append(" ", style="dim") animated_text.append("esc", style="white") animated_text.append(" ", style="dim") animated_text.append("stop", style="dim") return (animated_text, keymap_styled([("ctrl-q", "quit")]), True) - animated_text = self._get_animated_verb_text(agent_id, "Initializing") + msg = sys_msg or "Initializing..." + animated_text = self._get_animated_verb_text(agent_id, msg) return (animated_text, keymap_styled([("ctrl-q", "quit")]), True) return (None, Text(), False) @@ -1683,21 +1688,46 @@ class StrixTUIApp(App): # type: ignore[misc] content = msg_data.get("content", "") metadata = msg_data.get("metadata", {}) - if not content: - return None - if role == "user": + if not content: + return None return UserMessageRenderer.render_simple(content) + renderables = [] + + if "thinking_blocks" in metadata and metadata["thinking_blocks"]: + from strix.interface.tool_components.thinking_renderer import ThinkRenderer + + for block in metadata["thinking_blocks"]: + thought = block.get("thinking", "") + if thought: + renderables.append( + ThinkRenderer.render({"args": {"thought": thought}}).renderable + ) + + if not content and not renderables: + return None + if metadata.get("interrupted"): streaming_result = self._render_streaming_content(content) interrupted_text = Text() interrupted_text.append("\n") interrupted_text.append("⚠ ", style="yellow") interrupted_text.append("Interrupted by user", style="yellow dim") - return self._merge_renderables([streaming_result, interrupted_text]) + return self._merge_renderables([*renderables, streaming_result, interrupted_text]) - return AgentMessageRenderer.render_simple(content) + if content: + msg_renderable = AgentMessageRenderer.render_simple(content) + renderables.append(msg_renderable) + + if not renderables: + return None + + if len(renderables) == 1: + r = renderables[0] + return self._sanitize_text(r) if isinstance(r, Text) else r + + return self._merge_renderables(renderables) def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> Any: tool_name = tool_data.get("tool_name", "Unknown Tool") diff --git a/strix/llm/llm.py b/strix/llm/llm.py index 4f624956..bbf3a63b 100644 --- a/strix/llm/llm.py +++ b/strix/llm/llm.py @@ -156,12 +156,21 @@ class LLM: async def generate( self, conversation_history: list[dict[str, Any]] ) -> AsyncIterator[LLMResponse]: + from strix.telemetry.tracer import get_global_tracer + + tracer = get_global_tracer() + if tracer and self.agent_id: + tracer.update_agent_system_message(self.agent_id, "Compressing memory...") + messages = self._prepare_messages(conversation_history) max_retries = int(Config.get("strix_llm_max_retries") or "5") for attempt in range(max_retries + 1): try: - async for response in self._stream(messages): + if tracer and self.agent_id: + tracer.update_agent_system_message(self.agent_id, "Waiting for LLM provider...") + + async for response in self._stream(messages, tracer): yield response return # noqa: TRY300 except Exception as e: # noqa: BLE001 @@ -170,15 +179,23 @@ class LLM: wait = min(90, 2 * (2**attempt)) await asyncio.sleep(wait) - async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]: + async def _stream( + self, messages: list[dict[str, Any]], tracer: Any = None + ) -> AsyncIterator[LLMResponse]: accumulated = "" chunks: list[Any] = [] done_streaming = 0 + first_chunk_received = False self._total_stats.requests += 1 response = await acompletion(**self._build_completion_args(messages), stream=True) async for chunk in response: + if not first_chunk_received: + first_chunk_received = True + if tracer and self.agent_id: + tracer.update_agent_system_message(self.agent_id, "Generating response...") + chunks.append(chunk) if done_streaming: done_streaming += 1 diff --git a/strix/telemetry/tracer.py b/strix/telemetry/tracer.py index 3f3ca6c6..686a2f09 100644 --- a/strix/telemetry/tracer.py +++ b/strix/telemetry/tracer.py @@ -439,6 +439,7 @@ class Tracer: "name": name, "task": task, "status": "running", + "system_message": "", "parent_id": parent_id, "created_at": datetime.now(UTC).isoformat(), "updated_at": datetime.now(UTC).isoformat(), @@ -587,6 +588,11 @@ class Tracer: source="strix.agents", ) + def update_agent_system_message(self, agent_id: str, message: str) -> None: + if agent_id in self.agents: + self.agents[agent_id]["system_message"] = message + self.agents[agent_id]["updated_at"] = datetime.now(UTC).isoformat() + def set_scan_config(self, config: dict[str, Any]) -> None: self.scan_config = config self.run_metadata.update(