diff --git a/tests/telemetry/test_trace.py b/tests/telemetry/test_trace.py
index 27cf2a35..cce8dad0 100644
--- a/tests/telemetry/test_trace.py
+++ b/tests/telemetry/test_trace.py
@@ -50,12 +50,13 @@ def _mock_span():
return span
-def _make_part(text=None, function_call=None, function_response=None, inline_data=None):
+def _make_part(text=None, function_call=None, function_response=None, inline_data=None, thought=False):
part = MagicMock()
part.text = text
part.function_call = function_call
part.function_response = function_response
part.inline_data = inline_data
+ part.thought = thought
return part
@@ -229,6 +230,31 @@ def test_with_new_message(self, mock_get_span):
span.set_attribute.assert_any_call("trpc.python.agent.runner.input", "hello\nworld")
+ @patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span")
+ def test_with_thought_parts(self, mock_get_span):
+ span = _mock_span()
+ mock_get_span.return_value = span
+ ctx = _make_invocation_context()
+
+ parts = [
+ _make_part(text="thinking...", thought=True),
+ _make_part(text="final answer"),
+ ]
+ content = _make_content(parts=parts)
+
+ trace_runner(
+ app_name="app",
+ user_id="u",
+ session_id="s",
+ invocation_context=ctx,
+ new_message=content,
+ )
+
+ span.set_attribute.assert_any_call(
+ "trpc.python.agent.runner.input",
+ "thinking...\nfinal answer",
+ )
+
@patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span")
def test_with_none_text_parts(self, mock_get_span):
span = _mock_span()
@@ -544,6 +570,25 @@ def test_with_user_content(self, mock_get_span):
span.set_attribute.assert_any_call("trpc.python.agent.agent.input", "hello\n agent")
+ @patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span")
+ def test_with_thought_user_content(self, mock_get_span):
+ span = _mock_span()
+ mock_get_span.return_value = span
+
+ parts = [
+ _make_part(text="reasoning", thought=True),
+ _make_part(text="answer"),
+ ]
+ user_content = _make_content(parts=parts)
+ ctx = _make_invocation_context(user_content=user_content)
+
+ trace_agent(ctx)
+
+ span.set_attribute.assert_any_call(
+ "trpc.python.agent.agent.input",
+ "reasoning\nanswer",
+ )
+
@patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span")
def test_without_user_content(self, mock_get_span):
span = _mock_span()
diff --git a/trpc_agent_sdk/telemetry/_trace.py b/trpc_agent_sdk/telemetry/_trace.py
index 22ff2ad2..9c725906 100644
--- a/trpc_agent_sdk/telemetry/_trace.py
+++ b/trpc_agent_sdk/telemetry/_trace.py
@@ -62,6 +62,30 @@ def get_trpc_agent_span_name() -> str:
return _trpc_agent_span_name
+def _join_parts_with_thought_tag(parts) -> str:
+ """Join part texts, wrapping thought parts in tags.
+
+ Parts with ``thought=True`` (model reasoning/thinking) are wrapped in
+ ``...`` tags so downstream consumers (e.g.
+ zhiyan-llm exporter) can distinguish and optionally strip them.
+
+ Args:
+ parts: A sequence of Part objects with ``text`` and optional
+ ``thought`` attributes.
+
+ Returns:
+ The joined text string with thought parts tagged.
+ """
+ result = []
+ for part in parts:
+ text = part.text or ""
+ if getattr(part, "thought", False):
+ result.append(f"{text}")
+ else:
+ result.append(text)
+ return "\n".join(result)
+
+
def _safe_json_serialize(obj) -> str:
"""Convert any Python object to a JSON-serializable type or string.
@@ -116,11 +140,11 @@ def trace_runner(
span.set_attribute(f"{_trpc_agent_span_name}.runner.session_id", session_id)
input_str = ""
if new_message and new_message.parts:
- input_str = "\n".join([part.text or "" for part in new_message.parts])
+ input_str = _join_parts_with_thought_tag(new_message.parts)
span.set_attribute(f"{_trpc_agent_span_name}.runner.input", input_str)
output_str = ""
if last_event and last_event.content and last_event.content.parts:
- output_str = "\n".join([part.text or "" for part in last_event.content.parts])
+ output_str = _join_parts_with_thought_tag(last_event.content.parts)
span.set_attribute(f"{_trpc_agent_span_name}.runner.output", output_str)
# Set state attributes for begin and end
@@ -181,7 +205,7 @@ def trace_cancellation(
# Input (prefixed with [CANCELLED] marker)
input_str = ""
if new_message and new_message.parts:
- input_str += "\n".join([part.text or "" for part in new_message.parts])
+ input_str += _join_parts_with_thought_tag(new_message.parts)
span.set_attribute(f"{_trpc_agent_span_name}.runner.input", input_str)
# Output (prefixed with [CANCELLED] marker, includes partial text or last event)
@@ -189,7 +213,7 @@ def trace_cancellation(
if partial_text:
output_str += partial_text
elif last_event and last_event.content and last_event.content.parts:
- output_str += "\n".join([part.text or "" for part in last_event.content.parts])
+ output_str += _join_parts_with_thought_tag(last_event.content.parts)
span.set_attribute(f"{_trpc_agent_span_name}.runner.output", output_str)
# Cancellation-specific attributes
@@ -245,11 +269,14 @@ def trace_agent(
for content in override_messages:
if content and content.parts:
for part in content.parts:
- if part.text and not part.thought:
- text_parts.append(part.text)
+ if part.text:
+ if getattr(part, "thought", False):
+ text_parts.append(f"{part.text}")
+ else:
+ text_parts.append(part.text)
input_str = "\n".join(text_parts)
elif invocation_context.user_content and invocation_context.user_content.parts:
- input_str = "\n".join([part.text or "" for part in invocation_context.user_content.parts])
+ input_str = _join_parts_with_thought_tag(invocation_context.user_content.parts)
span.set_attribute(f"{_trpc_agent_span_name}.agent.input", input_str)
span.set_attribute(f"{_trpc_agent_span_name}.agent.output", agent_action)