mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-10 06:20:43 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user