fix(code): warn on /trace when thread has no messages (#4162)

`/trace` now warns when the current thread has no messages yet, since
the LangSmith view is empty until the first message is sent.

---

When a user runs `/trace`, dcode opens the LangSmith thread view. That
view stays empty until the first message is sent in the thread, which is
confusing — the user clicks through expecting to see something. This
appends a short note after the URL when the current thread has no
messages yet, warning that the trace will be empty until they send their
first message.

The empty-thread check reuses the existing `_has_conversation_messages`
helper, so it covers both deferred (busy) and immediate output paths and
degrades safely (no warning shown on lookup errors).

Made by [Open
SWE](https://openswe.vercel.app/agents/3a51b629-ea5a-3486-ef49-8ebc226646be)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-06-23 20:21:26 -04:00
committed by GitHub
parent 456ce5c2f5
commit c338fc914f
2 changed files with 146 additions and 13 deletions
+21 -13
View File
@@ -6777,7 +6777,8 @@ class DeepAgentsApp(App):
state. When the app is busy, chat output (user echo + clickable link)
is deferred until the current task finishes. Error conditions (no
session, URL failure, tracing not configured) render immediately
regardless of busy state.
regardless of busy state. When the thread has no messages yet, a note
is appended warning that the trace stays empty until the first message.
Args:
command: The raw command text (displayed as user message).
@@ -6900,6 +6901,21 @@ class DeepAgentsApp(App):
asyncio.get_running_loop().run_in_executor(None, _open_browser)
# Warn when the thread has no human turn yet — the LangSmith view stays
# empty until the first message is sent. `_has_conversation_messages`
# returns True on errors so transient state failures suppress this warning
# rather than showing a false empty-thread note.
parts: list[str | Content | tuple[str, str | TStyle]] = [
f"Opening tracing project {project_name!r}:\n",
(url, TStyle(dim=True, italic=True, link=url)),
]
if not await self._has_conversation_messages():
parts.append(
"\n\nYou haven't sent a message in this thread yet, so the "
"trace will be empty until you send your first message.",
)
msg = Content.assemble(*parts)
# Defer chat output while a turn is in progress — rendering the user
# echo + link immediately would splice it into the middle of the
# streaming assistant response
@@ -6914,10 +6930,6 @@ class DeepAgentsApp(App):
with suppress(Exception):
await queued_widget.remove()
await self._mount_message(UserMessage(command))
msg = Content.assemble(
f"Opening tracing project {project_name!r}:\n",
(url, TStyle(dim=True, italic=True, link=url)),
)
await self._mount_message(AppMessage(msg))
# Append directly — no dedup; each /trace invocation gets its own output.
@@ -6927,10 +6939,6 @@ class DeepAgentsApp(App):
return
await self._mount_message(UserMessage(command))
msg = Content.assemble(
f"Opening tracing project {project_name!r}:\n",
(url, TStyle(dim=True, italic=True, link=url)),
)
await self._mount_message(AppMessage(msg))
async def _handle_command(self, command: str) -> None:
@@ -7461,9 +7469,9 @@ class DeepAgentsApp(App):
Returns:
`True` if the conversation contains a `HumanMessage`, `False`
otherwise. On transient errors (network, corrupt state) returns
`True` so that `/remember` is not blocked with a misleading
"nothing to remember" message.
otherwise. On transient errors (network, corrupt state) returns
`True` so callers do not block or warn based on an unreliable
empty-thread check.
"""
if not self._agent or not self._lc_thread_id:
return False
@@ -7486,7 +7494,7 @@ class DeepAgentsApp(App):
)
except Exception:
logger.warning(
"Failed to check conversation messages; allowing /remember to proceed",
"Failed to check conversation messages",
exc_info=True,
)
return True
+125
View File
@@ -3352,6 +3352,95 @@ class TestTraceCommand:
rendered = "\n".join(str(w._content) for w in app_msgs)
assert f"Opening tracing project 'proj':\n{expected_url}" in rendered
async def test_trace_warns_when_no_messages_sent(self) -> None:
"""Should append a note when the thread has no messages yet."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
app._session_state = TextualSessionState(thread_id="test-thread-123")
with (
patch(
"deepagents_code.config.get_langsmith_project_name",
return_value="proj",
),
patch(
"deepagents_code.config.fetch_langsmith_project_url_or_raise",
return_value="https://smith.langchain.com",
),
patch("deepagents_code.app.webbrowser.open"),
patch.object(
app, "_has_conversation_messages", AsyncMock(return_value=False)
),
):
await app._handle_trace_command("/trace")
await pilot.pause()
app_msgs = app.query(AppMessage)
rendered = "\n".join(str(w._content) for w in app_msgs)
assert "until you send your first message" in rendered
async def test_trace_no_warning_when_messages_exist(self) -> None:
"""Should not append the empty-thread note once messages exist."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
app._session_state = TextualSessionState(thread_id="test-thread-123")
with (
patch(
"deepagents_code.config.get_langsmith_project_name",
return_value="proj",
),
patch(
"deepagents_code.config.fetch_langsmith_project_url_or_raise",
return_value="https://smith.langchain.com",
),
patch("deepagents_code.app.webbrowser.open"),
patch.object(
app, "_has_conversation_messages", AsyncMock(return_value=True)
),
):
await app._handle_trace_command("/trace")
await pilot.pause()
app_msgs = app.query(AppMessage)
rendered = "\n".join(str(w._content) for w in app_msgs)
assert "until you send your first message" not in rendered
async def test_trace_no_warning_when_message_lookup_fails(self) -> None:
"""Should fail open when the empty-thread check cannot read state."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
app._session_state = TextualSessionState(thread_id="test-thread-123")
app._agent = MagicMock()
app._lc_thread_id = "test-thread-123"
with (
patch(
"deepagents_code.config.get_langsmith_project_name",
return_value="proj",
),
patch(
"deepagents_code.config.fetch_langsmith_project_url_or_raise",
return_value="https://smith.langchain.com",
),
patch("deepagents_code.app.webbrowser.open"),
patch.object(
app,
"_get_thread_state_values",
AsyncMock(side_effect=RuntimeError("connection lost")),
),
):
await app._handle_trace_command("/trace")
await pilot.pause()
app_msgs = app.query(AppMessage)
rendered = "\n".join(str(w._content) for w in app_msgs)
assert "https://smith.langchain.com/t/test-thread-123" in rendered
assert "until you send your first message" not in rendered
async def test_trace_shows_error_when_not_configured(self) -> None:
"""Should show configuration hint when LangSmith is not set up."""
app = DeepAgentsApp()
@@ -3603,6 +3692,42 @@ class TestTraceCommand:
"https://smith.langchain.com/t/test-thread-123"
) in rendered
async def test_trace_deferred_output_includes_empty_thread_warning(self) -> None:
"""Should keep the empty-thread warning when busy output is drained."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
app._session_state = TextualSessionState(thread_id="test-thread-123")
app._agent_running = True
with (
patch(
"deepagents_code.config.get_langsmith_project_name",
return_value="proj",
),
patch(
"deepagents_code.config.fetch_langsmith_project_url_or_raise",
return_value="https://smith.langchain.com",
),
patch("deepagents_code.app.webbrowser.open"),
patch.object(
app, "_has_conversation_messages", AsyncMock(return_value=False)
),
):
await app._handle_trace_command("/trace")
await pilot.pause()
assert len(app._deferred_actions) == 1
action = app._deferred_actions[0]
assert action.kind == "chat_output"
await action.execute()
await pilot.pause()
app_msgs = app.query(AppMessage)
rendered = "\n".join(str(w._content) for w in app_msgs)
assert "until you send your first message" in rendered
async def test_trace_shows_error_when_url_build_raises(self) -> None:
"""Should show error message when the URL fetch raises."""
app = DeepAgentsApp()