feat: only post Slack 'Working on it!' on first thread mention (#1250)

* feat: only post Slack 'Working on it!' on first thread mention

* feat: randomize Slack trace reply phrase

Pick from a small list of friendly phrases instead of always saying
'Working on it!' so the bot feels less robotic. Explicit messages (e.g.
'Taking a look...' from PR review path) are unaffected.

* adjust phrases

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
This commit is contained in:
open-swe[bot] 2026-05-07 12:37:46 -07:00 • committed by GitHub
parent bb836b58bc
commit 5a01aff1c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 128 additions and 9 deletions

View file

@ -150,7 +150,7 @@ Do not use this tool for GitHub API calls. Use `GH_TOKEN=dummy gh` in the sandbo
Posts a comment to a Linear ticket given a `ticket_id`. Call this after opening/updating the pull request to notify stakeholders and include the PR link. You can tag Linear users with `@username` (their Linear display name).
#### `slack_thread_reply`
Posts a message to the active Slack thread. Use this for clarifying questions, status updates, and final summaries when the task was triggered from Slack.
Posts a message to the active Slack thread. Use this for clarifying questions, mid-run progress updates, and final summaries when the task was triggered from Slack. You can call it multiple times during a run — if you're about to do something long-running (cloning a large repo, big refactors, running heavy test suites), post a short status update first so the user knows what's happening. Always end the run with a final reply that summarizes what you did or answers the question. Do not post a status reply before quick, single-tool answers — only when the user would otherwise be left waiting.
Format messages using Slack's mrkdwn format, NOT standard Markdown.
Key differences: *bold*, _italic_, ~strikethrough~, <url|link text>,
bullet lists with "• ", ```code blocks```, > blockquotes.

View file

@ -9,6 +9,12 @@ from ..utils.slack import convert_mentions_to_slack_format, post_slack_thread_re
def slack_thread_reply(message: str) -> dict[str, Any]:
"""Post a message to the current Slack thread.
Use this for clarifying questions, mid-run progress updates, and the final
summary. You can call this multiple times during a run — if you're about to
do long-running work (cloning, large refactors, big test runs) consider
posting a brief status update first so the user knows what's happening.
Always end the run with a final reply summarizing what you did.
Format messages using Slack's mrkdwn format, NOT standard Markdown.
Key differences: *bold*, _italic_, ~strikethrough~, <url|link text>,
bullet lists with "• ", ```code blocks```, > blockquotes.

View file

@ -7,6 +7,7 @@ import hashlib
import hmac
import logging
import os
import random
import re
import time
from dataclasses import dataclass
@ -596,10 +597,25 @@ async def resolve_slack_links_in_context(
return resolved_links_section, image_urls
TRACE_REPLY_PHRASES: tuple[str, ...] = (
"Working on it!",
"On it!",
"Diving in!",
"Powering up!",
"Heads down.",
"Cracking knuckles...",
"Spinning up...",
"Looking...",
"Time to cook. 🧑‍🍳",
)
async def post_slack_trace_reply(
channel_id: str, thread_ts: str, thread_id: str, message: str = "Working on it!"
channel_id: str, thread_ts: str, thread_id: str, message: str | None = None
) -> None:
"""Post a trace URL reply in a Slack thread."""
if message is None:
message = random.choice(TRACE_REPLY_PHRASES)
trace_url = get_langsmith_trace_url(thread_id)
if trace_url:
await post_slack_thread_reply(channel_id, thread_ts, f"{message} <{trace_url}|View trace>")

View file

@ -863,6 +863,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
}
langgraph_client = get_client(url=LANGGRAPH_URL)
is_first_mention = not await _thread_exists(thread_id)
await _upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client)
thread_active = await is_thread_active(thread_id)
@ -895,7 +896,13 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
_run_id_for_logging(run),
thread_id,
)
await post_slack_trace_reply(channel_id, thread_ts, thread_id)
if is_first_mention:
await post_slack_trace_reply(channel_id, thread_ts, thread_id)
else:
logger.info(
"Skipping Slack trace reply for thread %s — agent will reply when run completes",
thread_id,
)
async def process_slack_pr_review_request(

View file

@ -3,12 +3,15 @@ import asyncio
import pytest
from agent import webapp
from agent.utils import slack as slack_utils
from agent.utils.slack import (
TRACE_REPLY_PHRASES,
convert_mentions_to_slack_format,
format_slack_messages_for_prompt,
looks_like_slack_pr_review_command,
parse_github_pr_url,
parse_slack_review_command,
post_slack_trace_reply,
replace_bot_mention_with_username,
select_slack_context_messages,
strip_bot_mention,
@ -218,6 +221,39 @@ def test_format_slack_messages_for_prompt_replaces_bot_id_mention_in_text() -> N
assert formatted == "@alice(U123): @open-swe status update?"
def test_post_slack_trace_reply_picks_random_phrase_when_no_message(
monkeypatch: pytest.MonkeyPatch,
) -> None:
posted: list[str] = []
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, text: str) -> None:
posted.append(text)
monkeypatch.setattr(slack_utils, "post_slack_thread_reply", fake_post_slack_thread_reply)
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: None)
asyncio.run(post_slack_trace_reply("C123", "1.0", "thread-id"))
assert len(posted) == 1
assert posted[0] in TRACE_REPLY_PHRASES
def test_post_slack_trace_reply_uses_explicit_message_when_provided(
monkeypatch: pytest.MonkeyPatch,
) -> None:
posted: list[str] = []
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, text: str) -> None:
posted.append(text)
monkeypatch.setattr(slack_utils, "post_slack_thread_reply", fake_post_slack_thread_reply)
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: None)
asyncio.run(post_slack_trace_reply("C123", "1.0", "thread-id", message="Taking a look..."))
assert posted == ["Taking a look..."]
def test_select_slack_context_messages_detects_username_mention() -> None:
selected, mode = select_slack_context_messages(
[
@ -416,11 +452,9 @@ def test_get_slack_repo_config_repo_name_only_space_syntax(
assert repo == {"owner": "langchain-ai", "name": "open-swe"}
def test_process_slack_mention_creates_thread_followup_run(
monkeypatch: pytest.MonkeyPatch,
def _setup_slack_mention_fakes(
monkeypatch: pytest.MonkeyPatch, captured: dict[str, object]
) -> None:
captured: dict[str, object] = {}
async def fake_add_slack_reaction(channel_id: str, message_ts: str, emoji: str) -> bool:
captured["reaction"] = {
"channel_id": channel_id,
@ -465,7 +499,7 @@ def test_process_slack_mention_creates_thread_followup_run(
return False
async def fake_post_slack_trace_reply(
channel_id: str, thread_ts: str, thread_id: str, message: str = "Working on it!"
channel_id: str, thread_ts: str, thread_id: str, message: str | None = None
) -> None:
captured["trace_reply"] = {
"channel_id": channel_id,
@ -503,6 +537,19 @@ def test_process_slack_mention_creates_thread_followup_run(
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClientForProcess())
def test_process_slack_mention_creates_thread_first_run_with_trace_reply(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
_setup_slack_mention_fakes(monkeypatch, captured)
async def fake_thread_exists(thread_id: str) -> bool:
captured["thread_exists_check"] = thread_id
return False
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
thread_ts = "1700000000.000100"
event_ts = "1700000000.000200"
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
@ -521,6 +568,7 @@ def test_process_slack_mention_creates_thread_followup_run(
)
)
assert captured["thread_exists_check"] == expected_thread_id
assert captured["fetch_thread"] == {"channel_id": "C123", "thread_ts": thread_ts}
assert captured["active_thread_id"] == expected_thread_id
assert captured["metadata_update"] == {
@ -531,7 +579,7 @@ def test_process_slack_mention_creates_thread_followup_run(
"channel_id": "C123",
"thread_ts": thread_ts,
"thread_id": expected_thread_id,
"message": "Working on it!",
"message": None,
}
run_create = captured["run_create"]
@ -548,6 +596,44 @@ def test_process_slack_mention_creates_thread_followup_run(
assert "## Latest Mention Request\ncontinue on the branch" in prompt_block["text"]
def test_process_slack_mention_skips_trace_reply_on_followup_mention(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Subsequent mentions in a Slack thread should not post 'Working on it!'."""
captured: dict[str, object] = {}
_setup_slack_mention_fakes(monkeypatch, captured)
async def fake_thread_exists(thread_id: str) -> bool:
captured["thread_exists_check"] = thread_id
return True
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
thread_ts = "1700000000.000100"
event_ts = "1700000000.000300"
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
asyncio.run(
webapp.process_slack_mention(
{
"channel_id": "C123",
"thread_ts": thread_ts,
"event_ts": event_ts,
"user_id": "U123",
"text": "<@UBOT> follow up question",
"bot_user_id": "UBOT",
},
{"owner": "langchain-ai", "name": "open-swe"},
)
)
assert captured["thread_exists_check"] == expected_thread_id
assert "trace_reply" not in captured
run_create = captured["run_create"]
assert isinstance(run_create, dict)
assert run_create["thread_id"] == expected_thread_id
def test_process_slack_mention_queues_active_thread_message(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@ -604,6 +690,9 @@ def test_process_slack_mention_queues_active_thread_message(
async def fake_post_slack_trace_reply(*args, **kwargs) -> None:
raise AssertionError("trace reply should not be posted for queued mid-run Slack messages")
async def fake_thread_exists(thread_id: str) -> bool:
return True
class _FakeRunsClient:
async def create(self, *args, **kwargs) -> None:
raise AssertionError("run should not be created for active Slack threads")
@ -628,6 +717,7 @@ def test_process_slack_mention_queues_active_thread_message(
monkeypatch.setattr(webapp, "is_thread_active", fake_is_thread_active)
monkeypatch.setattr(webapp, "queue_message_for_thread", fake_queue_message_for_thread)
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClientForProcess())
thread_ts = "1700000000.000100"