feat: add /new and /status slash commands to agent loop

Fixes #572: /new saves current session and clears conversation history,
giving users a fresh start without restarting the process.

Fixes #573: /status shows model, agent ID, channel, message count, and
max iterations for debugging and visibility.
This commit is contained in:
Rahul Bansal 2026-02-21 10:28:11 +05:30
parent 56c441e760
commit 08bb45aeb6
2 changed files with 191 additions and 0 deletions

View file

@ -1071,6 +1071,8 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage)
case "/help":
return `Available commands:
/help Show this help message
/new Start a new conversation
/status Show current session info
/show model Show current model
/show channel Show current channel
/show agents Show registered agents
@ -1139,6 +1141,63 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage)
return fmt.Sprintf("Unknown list target: %s", args[0]), true
}
case "/new":
// Resolve the route to find the correct agent and session key
route := al.registry.ResolveRoute(routing.RouteInput{
Channel: msg.Channel,
AccountID: msg.Metadata["account_id"],
Peer: extractPeer(msg),
GuildID: msg.Metadata["guild_id"],
TeamID: msg.Metadata["team_id"],
})
agent, ok := al.registry.GetAgent(route.AgentID)
if !ok {
agent = al.registry.GetDefaultAgent()
}
if agent == nil {
return "No agent configured", true
}
// Save current session before clearing
sessionKey := route.SessionKey
agent.Sessions.Save(sessionKey)
// Clear the in-memory conversation history and summary
agent.Sessions.TruncateHistory(sessionKey, 0)
agent.Sessions.SetSummary(sessionKey, "")
agent.Sessions.Save(sessionKey)
return "Started a new conversation. Previous session saved.", true
case "/status":
// Resolve the route to find the correct agent and session key
route := al.registry.ResolveRoute(routing.RouteInput{
Channel: msg.Channel,
AccountID: msg.Metadata["account_id"],
Peer: extractPeer(msg),
GuildID: msg.Metadata["guild_id"],
TeamID: msg.Metadata["team_id"],
})
agent, ok := al.registry.GetAgent(route.AgentID)
if !ok {
agent = al.registry.GetDefaultAgent()
}
if agent == nil {
return "No agent configured", true
}
sessionKey := route.SessionKey
history := agent.Sessions.GetHistory(sessionKey)
return fmt.Sprintf(`Status:
Model: %s
Agent: %s
Channel: %s
Messages: %d in current session
Max iterations: %d`, agent.Model, agent.ID, msg.Channel, len(history), agent.MaxIterations), true
case "/switch":
if len(args) < 3 || args[1] != "to" {
return "Usage: /switch [model|channel] to <name>", true

View file

@ -789,3 +789,135 @@ func TestHandleCommand_NotACommand(t *testing.T) {
})
}
}
func TestHandleCommand_New(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
},
},
}
msgBus := bus.NewMessageBus()
provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider)
// Add some messages to the session so we can verify they get cleared
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent == nil {
t.Fatal("No default agent found")
}
sessionKey := "agent:main:main"
defaultAgent.Sessions.AddMessage(sessionKey, "user", "hello")
defaultAgent.Sessions.AddMessage(sessionKey, "assistant", "hi there")
history := defaultAgent.Sessions.GetHistory(sessionKey)
if len(history) != 2 {
t.Fatalf("Expected 2 messages before /new, got %d", len(history))
}
ctx := context.Background()
msg := bus.InboundMessage{
Channel: "test",
SenderID: "user1",
ChatID: "chat1",
Content: "/new",
}
response, handled := al.handleCommand(ctx, msg)
if !handled {
t.Fatal("Expected /new to be handled")
}
if !strings.Contains(response, "Started a new conversation") {
t.Errorf("Expected confirmation message, got: %s", response)
}
if !strings.Contains(response, "Previous session saved") {
t.Errorf("Expected 'Previous session saved' in response, got: %s", response)
}
// Verify history was cleared
history = defaultAgent.Sessions.GetHistory(sessionKey)
if len(history) != 0 {
t.Errorf("Expected 0 messages after /new, got %d", len(history))
}
// Verify summary was cleared
summary := defaultAgent.Sessions.GetSummary(sessionKey)
if summary != "" {
t.Errorf("Expected empty summary after /new, got: %s", summary)
}
}
func TestHandleCommand_Status(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "claude-3-opus",
MaxTokens: 4096,
MaxToolIterations: 15,
},
},
}
msgBus := bus.NewMessageBus()
provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider)
// Add some messages so the count is nonzero
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent == nil {
t.Fatal("No default agent found")
}
sessionKey := "agent:main:main"
defaultAgent.Sessions.AddMessage(sessionKey, "user", "hello")
defaultAgent.Sessions.AddMessage(sessionKey, "assistant", "hi")
defaultAgent.Sessions.AddMessage(sessionKey, "user", "how are you?")
ctx := context.Background()
msg := bus.InboundMessage{
Channel: "telegram",
SenderID: "user1",
ChatID: "chat1",
Content: "/status",
}
response, handled := al.handleCommand(ctx, msg)
if !handled {
t.Fatal("Expected /status to be handled")
}
// Verify key fields are present
expectedFields := []struct {
label string
value string
}{
{"Model", "claude-3-opus"},
{"Agent", "main"},
{"Channel", "telegram"},
{"Messages", "3 in current session"},
{"Max iterations", "15"},
}
for _, f := range expectedFields {
expected := fmt.Sprintf("%s: %s", f.label, f.value)
if !strings.Contains(response, expected) {
t.Errorf("Expected status to contain %q, got:\n%s", expected, response)
}
}
}