From a0245c7b02e3828fab780c6ff5bdea221a291fd6 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Mon, 4 May 2026 08:41:29 +0200 Subject: [PATCH] feat(agent): stop command --- pkg/agent/agent_stop.go | 103 +++++++++++++++++++++++++++++++++++++++ pkg/commands/cmd_stop.go | 52 ++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 pkg/agent/agent_stop.go create mode 100644 pkg/commands/cmd_stop.go diff --git a/pkg/agent/agent_stop.go b/pkg/agent/agent_stop.go new file mode 100644 index 000000000..2f93c5684 --- /dev/null +++ b/pkg/agent/agent_stop.go @@ -0,0 +1,103 @@ +package agent + +import ( + "context" + "fmt" + "strings" + + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/commands" +) + +func (al *AgentLoop) tryHandleStopCommand( + ctx context.Context, + msg bus.InboundMessage, + sessionKey string, +) bool { + cmdName, ok := commands.CommandName(msg.Content) + if !ok || cmdName != "stop" { + return false + } + + result, err := al.stopActiveTurnForSession(sessionKey) + reply := commands.FormatStopReply(result) + if err != nil { + reply = "Failed to stop task: " + err.Error() + } + + if al.channelManager != nil { + al.channelManager.InvokeTypingStop(msg.Channel, msg.ChatID) + } + al.resetMessageToolRound(sessionKey) + al.PublishResponseIfNeeded(ctx, msg.Channel, msg.ChatID, sessionKey, reply) + return true +} + +func (al *AgentLoop) stopActiveTurnForSession(sessionKey string) (commands.StopResult, error) { + sessionKey = strings.TrimSpace(sessionKey) + if sessionKey == "" { + return commands.StopResult{}, fmt.Errorf("session key is required") + } + + result := commands.StopResult{} + cleared := al.clearSteeringMessagesForScope(sessionKey) + al.clearPendingSkills(sessionKey) + + ts := al.getActiveTurnState(sessionKey) + if ts == nil { + result.Stopped = cleared > 0 + return result, nil + } + + snap := ts.snapshot() + result.TaskName = snap.UserMessage + + if strings.HasPrefix(snap.TurnID, pendingTurnPrefix) { + al.markPendingStop(sessionKey) + result.Stopped = true + return result, nil + } + + if err := al.HardAbort(sessionKey); err != nil { + if al.getActiveTurnState(sessionKey) == nil { + result.Stopped = cleared > 0 + return result, nil + } + return commands.StopResult{}, err + } + + result.Stopped = true + return result, nil +} + +func (al *AgentLoop) markPendingStop(sessionKey string) { + sessionKey = strings.TrimSpace(sessionKey) + if sessionKey == "" { + return + } + al.pendingStops.Store(sessionKey, struct{}{}) +} + +func (al *AgentLoop) takePendingStop(sessionKey string) bool { + sessionKey = strings.TrimSpace(sessionKey) + if sessionKey == "" { + return false + } + _, ok := al.pendingStops.LoadAndDelete(sessionKey) + return ok +} + +func (al *AgentLoop) resetMessageToolRound(sessionKey string) { + if strings.TrimSpace(sessionKey) == "" { + return + } + if registry := al.GetRegistry(); registry != nil { + if agent := registry.GetDefaultAgent(); agent != nil { + if tool, ok := agent.Tools.Get("message"); ok { + if resetter, ok := tool.(interface{ ResetSentInRound(sessionKey string) }); ok { + resetter.ResetSentInRound(sessionKey) + } + } + } + } +} diff --git a/pkg/commands/cmd_stop.go b/pkg/commands/cmd_stop.go new file mode 100644 index 000000000..147688bdc --- /dev/null +++ b/pkg/commands/cmd_stop.go @@ -0,0 +1,52 @@ +package commands + +import ( + "context" + "fmt" + "strings" +) + +func stopCommand() Definition { + return Definition{ + Name: "stop", + Description: "Stop the current task", + Usage: "/stop", + Handler: func(_ context.Context, req Request, rt *Runtime) error { + if rt == nil || rt.StopActiveTurn == nil { + return req.Reply(unavailableMsg) + } + + result, err := rt.StopActiveTurn() + if err != nil { + return req.Reply("Failed to stop task: " + err.Error()) + } + + return req.Reply(FormatStopReply(result)) + }, + } +} + +// FormatStopReply renders a user-facing reply for a stop request. +func FormatStopReply(result StopResult) string { + if !result.Stopped { + return "No active task to stop." + } + + taskName := compactStopTaskName(result.TaskName) + if taskName == "" { + return "Task stopped. Current task was canceled." + } + + return fmt.Sprintf("Task stopped. %q was canceled.", taskName) +} + +func compactStopTaskName(taskName string) string { + taskName = strings.Join(strings.Fields(strings.TrimSpace(taskName)), " ") + if taskName == "" { + return "" + } + if len(taskName) > 80 { + return taskName[:77] + "..." + } + return taskName +}