diff --git a/trpc_agent_sdk/dsl/graph/_node_action/_agent.py b/trpc_agent_sdk/dsl/graph/_node_action/_agent.py index adf1e7a7..85d064dc 100644 --- a/trpc_agent_sdk/dsl/graph/_node_action/_agent.py +++ b/trpc_agent_sdk/dsl/graph/_node_action/_agent.py @@ -181,11 +181,17 @@ async def execute(self, state: State) -> dict[str, Any]: if isinstance(candidate, str) and candidate: last_response = candidate - if (not self._is_graph_event(event)) and ( - not event.partial) and event.content and event.content.parts: - text_parts = [part.text for part in event.content.parts if part.text] - if text_parts: - last_response = text_parts[-1] + if (not self._is_graph_event(event)) and event.is_final_response(): + # Only clean final answers may become last_response. Skip + # thought parts (part.thought) so a thinking model's + # reasoning monologue is never surfaced as the answer. + # is_final_response() already excludes partial, function + # call/response, and code-execution events, so plan and + # transfer_to_agent rounds are naturally ignored. + if event.content and event.content.parts: + visible_text = [part.text for part in event.content.parts if part.text and not part.thought] + if visible_text: + last_response = "".join(visible_text) if not event.visible: if event.actions and event.actions.transfer_to_agent: @@ -246,7 +252,7 @@ async def execute(self, state: State) -> dict[str, Any]: node_response: Any = last_response structured_output: Any = None - if isinstance(self.agent, LlmAgent) and self.agent.output_schema is not None: + if isinstance(self.agent, LlmAgent) and self.agent.output_schema is not None and last_response: node_response = json.loads(last_response) structured_output = node_response diff --git a/trpc_agent_sdk/dsl/graph/_node_action/_llm.py b/trpc_agent_sdk/dsl/graph/_node_action/_llm.py index 631e4d2a..4c0bcb16 100644 --- a/trpc_agent_sdk/dsl/graph/_node_action/_llm.py +++ b/trpc_agent_sdk/dsl/graph/_node_action/_llm.py @@ -251,7 +251,9 @@ async def _run_model_round( continue response_parts = list(llm_response.content.parts) - text_parts = [part.text for part in response_parts if part.text] + # Exclude thought parts so a thinking model's reasoning does not + # leak into response_text / STATE_KEY_LAST_RESPONSE. + text_parts = [part.text for part in response_parts if part.text and not part.thought] response_text = "".join(text_parts) logger.debug(f"[{self.name}] LLM response received ({len(response_text)} chars)")