diff --git a/apps/backend/internal/mcp/handlers/config_agent_handlers.go b/apps/backend/internal/mcp/handlers/config_agent_handlers.go index 54f460c609..294aeec41a 100644 --- a/apps/backend/internal/mcp/handlers/config_agent_handlers.go +++ b/apps/backend/internal/mcp/handlers/config_agent_handlers.go @@ -6,6 +6,7 @@ import ( "errors" agentsettingscontroller "github.com/kandev/kandev/internal/agent/settings/controller" + settingsdto "github.com/kandev/kandev/internal/agent/settings/dto" "github.com/kandev/kandev/internal/events" "github.com/kandev/kandev/internal/events/bus" ws "github.com/kandev/kandev/pkg/websocket" @@ -161,12 +162,23 @@ func (h *Handlers) handleUpdateAgentProfile(ctx context.Context, msg *ws.Message // publishAgentProfileEvent publishes an agent profile event to the event bus. // The payload wraps the profile in a "profile" key to match the format expected // by existing frontend WS handlers (same format as HTTP agent settings handlers). -func (h *Handlers) publishAgentProfileEvent(ctx context.Context, eventType string, profile interface{}) { +func (h *Handlers) publishAgentProfileEvent(ctx context.Context, eventType string, profile *settingsdto.AgentProfileDTO) { if h.eventBus == nil || profile == nil { return } + // Profile events can arrive before settings-agent hydration (the profile + // still carries its owning agent ID on delete), so include the capability + // needed by sessionless pickers mirroring the HTTP broadcaster. + inferenceCapable := false + agent, err := h.agentSettingsCtrl.GetAgent(ctx, profile.AgentID) + if err != nil { + h.logger.Warn("failed to load agent capability for profile event", zap.Error(err)) + } else if agent != nil { + inferenceCapable = agent.InferenceCapable + } data := map[string]interface{}{ - "profile": profile, + "profile": profile, + "inference_capable": inferenceCapable, } if err := h.eventBus.Publish(ctx, eventType, bus.NewEvent(eventType, "mcp-handlers", data)); err != nil { h.logger.Error("failed to publish agent profile event", diff --git a/apps/backend/internal/mcp/handlers/config_agent_handlers_test.go b/apps/backend/internal/mcp/handlers/config_agent_handlers_test.go index f3de6196d0..f6de9aba99 100644 --- a/apps/backend/internal/mcp/handlers/config_agent_handlers_test.go +++ b/apps/backend/internal/mcp/handlers/config_agent_handlers_test.go @@ -15,6 +15,8 @@ import ( settingsmodels "github.com/kandev/kandev/internal/agent/settings/models" settingsstore "github.com/kandev/kandev/internal/agent/settings/store" "github.com/kandev/kandev/internal/common/logger" + "github.com/kandev/kandev/internal/events" + "github.com/kandev/kandev/internal/events/bus" ws "github.com/kandev/kandev/pkg/websocket" ) @@ -195,3 +197,65 @@ func TestHandleDeleteAgentProfile_InUseStaysAValidationError(t *testing.T) { require.NoError(t, err) assertWSError(t, resp, ws.ErrorCodeValidation) } + +// A profile event published by the MCP config handlers must carry the owning +// agent's sessionless inference capability, matching the HTTP settings +// publisher, so capability-scoped pickers can classify profiles that arrive +// before settings-agent hydration. +func TestPublishAgentProfileEvent_IncludesInferenceCapability(t *testing.T) { + log, err := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"}) + require.NoError(t, err) + eventBus := bus.NewMemoryEventBus(log) + + db, err := sqlx.Open("sqlite3", ":memory:") + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + repo, cleanup, err := settingsstore.Provide(db, db, log) + require.NoError(t, err) + t.Cleanup(func() { _ = cleanup() }) + + agentRegistry := registry.NewRegistry(log) + agentRegistry.LoadDefaults() + ctrl := agentsettingscontroller.NewController(repo, nil, agentRegistry, nil, log) + h := &Handlers{logger: log, agentSettingsCtrl: ctrl, eventBus: eventBus} + + agent := &settingsmodels.Agent{Name: "opencode-acp"} + require.NoError(t, repo.CreateAgent(context.Background(), agent)) + + type profileEvent struct { + Profile settingsdto.AgentProfileDTO `json:"profile"` + InferenceCapable bool `json:"inference_capable"` + } + received := make(chan profileEvent, 1) + _, subErr := eventBus.Subscribe(events.AgentProfileCreated, func(ctx context.Context, event *bus.Event) error { + data, marshalErr := json.Marshal(event.Data) + if marshalErr != nil { + return marshalErr + } + var decoded profileEvent + if decodeErr := json.Unmarshal(data, &decoded); decodeErr != nil { + return decodeErr + } + received <- decoded + return nil + }) + require.NoError(t, subErr) + + msg := makeWSMessage(t, ws.ActionMCPCreateAgentProfile, map[string]interface{}{ + "agent_id": agent.ID, + "name": "Capability", + "model": "gpt-5", + }) + resp, err := h.handleCreateAgentProfile(context.Background(), msg) + require.NoError(t, err) + created := decodeAgentProfileResponse(t, resp) + + select { + case got := <-received: + require.Equal(t, created.ID, got.Profile.ID) + _, inference := agentRegistry.GetInferenceAgent(agent.Name) + require.Equal(t, inference, got.InferenceCapable) + default: + t.Fatalf("no agent_profile.created event was published") + } +}