mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user