From c4fb7a20014e58c4c1bd7a6229abc266ef7fa3c1 Mon Sep 17 00:00:00 2001 From: Ethan1918 <75773519+Ethan1918@users.noreply.github.com> Date: Sat, 4 Jul 2026 04:14:03 +0800 Subject: [PATCH] fix(agent): clear routed agent session --- pkg/agent/agent_command.go | 36 ++++++++++++- pkg/agent/agent_test.go | 99 +++++++++++++++++++++++++++++++++++ pkg/agent/context_seahorse.go | 6 ++- 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/pkg/agent/agent_command.go b/pkg/agent/agent_command.go index 277d1bda..23729750 100644 --- a/pkg/agent/agent_command.go +++ b/pkg/agent/agent_command.go @@ -333,7 +333,7 @@ func (al *AgentLoop) buildCommandsRuntime( if opts == nil { return fmt.Errorf("process options not available") } - return al.contextManager.Clear(ctx, opts.SessionKey) + return al.clearAgentSessionContext(ctx, agent, opts.SessionKey) } rt.AskSideQuestion = func(ctx context.Context, question string) (string, error) { @@ -363,6 +363,40 @@ func (al *AgentLoop) buildCommandsRuntime( return rt } +type contextStoreClearer interface { + ClearContextStore(ctx context.Context, sessionKey string) error +} + +func (al *AgentLoop) clearAgentSessionContext( + ctx context.Context, + agent *AgentInstance, + sessionKey string, +) error { + if agent == nil || agent.Sessions == nil { + return fmt.Errorf("sessions not initialized") + } + if al != nil && al.registry != nil && agent == al.registry.GetDefaultAgent() { + if al.contextManager != nil { + return al.contextManager.Clear(ctx, sessionKey) + } + return clearAgentSessionStore(agent, sessionKey) + } + if al != nil && al.contextManager != nil { + if clearer, ok := al.contextManager.(contextStoreClearer); ok { + if err := clearer.ClearContextStore(ctx, sessionKey); err != nil { + return err + } + } + } + return clearAgentSessionStore(agent, sessionKey) +} + +func clearAgentSessionStore(agent *AgentInstance, sessionKey string) error { + agent.Sessions.SetHistory(sessionKey, []providers.Message{}) + agent.Sessions.SetSummary(sessionKey, "") + return agent.Sessions.Save(sessionKey) +} + func summarizeMCPToolParameters(schema any) []commands.MCPToolParameterInfo { schemaMap := normalizeMCPSchema(schema) properties, ok := schemaMap["properties"].(map[string]any) diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index 4d6ba293..fb505d1e 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -3365,6 +3365,105 @@ func TestProcessMessage_CommandOutcomes(t *testing.T) { } } +func TestProcessMessage_ClearCommandClearsRoutedAgentSession(t *testing.T) { + workspace := t.TempDir() + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: filepath.Join(workspace, "default"), + ModelName: "test-model", + MaxTokens: 4096, + MaxToolIterations: 10, + }, + List: []config.AgentConfig{ + { + ID: "main", + Default: true, + Workspace: filepath.Join(workspace, "main"), + }, + { + ID: "support", + Workspace: filepath.Join(workspace, "support"), + }, + }, + Dispatch: &config.DispatchConfig{ + Rules: []config.DispatchRule{ + { + Name: "support-dingtalk", + Agent: "support", + When: config.DispatchSelector{ + Channel: "dingtalk", + }, + }, + }, + }, + }, + Session: config.SessionConfig{ + Dimensions: []string{"chat"}, + }, + } + + al := NewAgentLoop(cfg, bus.NewMessageBus(), &countingMockProvider{response: "LLM reply"}) + mainAgent, ok := al.registry.GetAgent("main") + if !ok { + t.Fatal("expected main agent") + } + supportAgent, ok := al.registry.GetAgent("support") + if !ok { + t.Fatal("expected support agent") + } + + msg := testInboundMessage(bus.InboundMessage{ + Context: bus.InboundContext{ + Channel: "dingtalk", + ChatID: "chat1", + ChatType: "direct", + SenderID: "user1", + }, + Content: "/clear", + }) + route, routedAgent, err := al.resolveMessageRoute(msg) + if err != nil { + t.Fatalf("resolveMessageRoute() error = %v", err) + } + if routedAgent != supportAgent { + t.Fatalf("routed agent = %s, want support", routedAgent.ID) + } + sessionKey := al.allocateRouteSession(route, msg).SessionKey + + mainHistory := []providers.Message{{Role: "user", Content: "main history"}} + supportHistory := []providers.Message{{Role: "user", Content: "support history"}} + mainAgent.Sessions.SetHistory(sessionKey, mainHistory) + mainAgent.Sessions.SetSummary(sessionKey, "main summary") + supportAgent.Sessions.SetHistory(sessionKey, supportHistory) + supportAgent.Sessions.SetSummary(sessionKey, "support summary") + + response, err := al.processMessage(context.Background(), msg) + if err != nil { + t.Fatalf("processMessage() error = %v", err) + } + if response != "Chat history cleared!" { + t.Fatalf("response = %q, want clear confirmation", response) + } + + if got := supportAgent.Sessions.GetHistory(sessionKey); len(got) != 0 { + t.Fatalf("support history len = %d, want 0", len(got)) + } + if got := supportAgent.Sessions.GetSummary(sessionKey); got != "" { + t.Fatalf("support summary = %q, want empty", got) + } + if got := mainAgent.Sessions.GetHistory(sessionKey); len(got) != len(mainHistory) { + t.Fatalf("main history len = %d, want %d", len(got), len(mainHistory)) + } else if got[0].Role != mainHistory[0].Role { + t.Fatalf("main history[0].Role = %q, want %q", got[0].Role, mainHistory[0].Role) + } else if got[0].Content != mainHistory[0].Content { + t.Fatalf("main history[0].Content = %q, want %q", got[0].Content, mainHistory[0].Content) + } + if got := mainAgent.Sessions.GetSummary(sessionKey); got != "main summary" { + t.Fatalf("main summary = %q, want %q", got, "main summary") + } +} + func TestProcessMessage_MCPCommandsHandledWithoutLLMCall(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") if err != nil { diff --git a/pkg/agent/context_seahorse.go b/pkg/agent/context_seahorse.go index 6bd32c21..c2ad3ea8 100644 --- a/pkg/agent/context_seahorse.go +++ b/pkg/agent/context_seahorse.go @@ -157,7 +157,7 @@ func (m *seahorseContextManager) Ingest(ctx context.Context, req *IngestRequest) // Clear removes all stored context for a session (seahorse DB + JSONL). func (m *seahorseContextManager) Clear(ctx context.Context, sessionKey string) error { - if err := m.engine.ClearSession(ctx, sessionKey); err != nil { + if err := m.ClearContextStore(ctx, sessionKey); err != nil { return err } if m.sessions != nil { @@ -168,6 +168,10 @@ func (m *seahorseContextManager) Clear(ctx context.Context, sessionKey string) e return nil } +func (m *seahorseContextManager) ClearContextStore(ctx context.Context, sessionKey string) error { + return m.engine.ClearSession(ctx, sessionKey) +} + // bootstrapSession reconciles JSONL session history into seahorse SQLite. func (m *seahorseContextManager) bootstrapSession(ctx context.Context, sessionKey string) { if m.sessions == nil {