feat(chat): auto-abort agent on exact "stop" keyword during active run

When user sends exact cancel keywords (stop, cancel, abort, thôi, dừng,
hủy, 取消, 停, nevermind) while agent is busy, auto-trigger abort instead
of injecting as follow-up message. Exact match only - "stop now" still
injects normally to avoid false positives.
This commit is contained in:
viettranx
2026-04-16 20:32:37 +07:00
parent 085b89bf72
commit 01014954bd
3 changed files with 90 additions and 0 deletions
+17
View File
@@ -40,6 +40,23 @@ var cancelKeywords = []string{
"nevermind", "never mind",
}
// exactCancelKeywords is a set of keywords that trigger immediate abort when
// sent as the entire message (exact match, case-insensitive, trimmed).
// Used by chat.send to auto-abort when user sends "stop" during an active run.
var exactCancelKeywords = map[string]bool{
"stop": true, "cancel": true, "abort": true,
"thôi": true, "dừng": true, "hủy": true,
"取消": true, "停": true,
"nevermind": true, "never mind": true,
}
// IsExactCancelKeyword returns true if the message is an exact cancel keyword
// (case-insensitive, whitespace-trimmed). Used by chat.send to auto-abort
// running agent loops when user explicitly sends a stop command.
func IsExactCancelKeyword(msg string) bool {
return exactCancelKeywords[strings.ToLower(strings.TrimSpace(msg))]
}
// quickClassify attempts keyword-based classification for ultra-short messages
// before calling the LLM. Only messages ≤ 15 runes are fast-pathed; longer
// messages always go to LLM for proper context understanding.
+56
View File
@@ -227,3 +227,59 @@ func TestFormatStatusReply_WithStatus(t *testing.T) {
t.Error("expected non-empty reply with status")
}
}
// ─── IsExactCancelKeyword ─────────────────────────────────────────────────
func TestIsExactCancelKeyword_ExactMatches(t *testing.T) {
cases := []string{"stop", "cancel", "abort", "thôi", "dừng", "hủy", "取消", "停", "nevermind", "never mind"}
for _, kw := range cases {
t.Run(kw, func(t *testing.T) {
if !IsExactCancelKeyword(kw) {
t.Errorf("%q should be recognized as cancel keyword", kw)
}
})
}
}
func TestIsExactCancelKeyword_CaseInsensitive(t *testing.T) {
cases := []string{"STOP", "Stop", "CANCEL", "Cancel", "ABORT", "Abort"}
for _, kw := range cases {
t.Run(kw, func(t *testing.T) {
if !IsExactCancelKeyword(kw) {
t.Errorf("%q should be recognized (case-insensitive)", kw)
}
})
}
}
func TestIsExactCancelKeyword_WithWhitespace(t *testing.T) {
cases := []string{" stop ", "\tstop\n", " cancel "}
for _, kw := range cases {
t.Run(kw, func(t *testing.T) {
if !IsExactCancelKeyword(kw) {
t.Errorf("%q should be recognized after trimming", kw)
}
})
}
}
func TestIsExactCancelKeyword_NonMatches(t *testing.T) {
cases := []string{
"stop now", // not exact
"please stop", // not exact
"nonstop", // embedded
"stop it", // not exact
"cancel the order", // not exact
"don't stop", // not exact
"", // empty
"hello", // unrelated
"làm đơn giản thôi", // contains "thôi" but not exact
}
for _, msg := range cases {
t.Run(msg, func(t *testing.T) {
if IsExactCancelKeyword(msg) {
t.Errorf("%q should NOT be recognized as cancel keyword", msg)
}
})
}
}
+17
View File
@@ -195,6 +195,23 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re
// Mid-run injection: if session already has an active run, inject the message
// into the running loop instead of starting a new concurrent run.
if m.agents.IsSessionBusy(sessionKey) {
// Exact cancel keyword detection: auto-abort when user sends "stop", "cancel", etc.
if agent.IsExactCancelKeyword(params.Message) {
results := m.agents.AbortRunsForSession(sessionKey)
aborted := false
for _, r := range results {
if r.Stopped || r.Forced {
aborted = true
break
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"cancelled": true,
"aborted": aborted,
}))
return
}
injected := m.agents.InjectMessage(sessionKey, agent.InjectedMessage{
Content: params.Message,
UserID: userID,