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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user