fix(mcp): publish agent capability with profile events

Include inference_capable in agent profile events published by the MCP
config handlers so capability-scoped pickers can classify profiles that
arrive before settings-agent hydration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
ayattara
2026-09-14 05:52:33 +00:00
parent f0b0ff91a4
commit c6eb8730af
2 changed files with 78 additions and 2 deletions
@@ -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",
@@ -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")
}
}