Skip to content

Commit 5ca63ba

Browse files
committed
Centralize model-change TUI notification in the main loop
Add a change-detection mechanism (lastEmittedModelID + emitModelInfo closure) in RunStream that automatically emits AgentInfo only when the effective model actually changes. This is checked before and after each LLM call, covering per-tool overrides, fallback, model picker, cooldowns, and any future model-switching feature — without each one having to remember to notify the TUI. - loop.go: replace 3 scattered manual AgentInfo emissions with emitModelInfo calls driven by the closure - model_picker.go: remove AgentInfo emission from the tool handler; rename setModelAndEmitInfo to setCurrentAgentModel (no longer emits) - agent_delegation.go: use getEffectiveModelID instead of getAgentModelID so agent-switch events reflect active fallback cooldowns Assisted-By: docker-agent
1 parent 8bf7424 commit 5ca63ba

3 files changed

Lines changed: 34 additions & 30 deletions

File tree

pkg/runtime/agent_delegation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,13 @@ func (r *LocalRuntime) handleTaskTransfer(ctx context.Context, sess *session.Ses
168168

169169
// Restore original agent info in sidebar
170170
if originalAgent, err := r.team.Agent(ca); err == nil {
171-
evts <- AgentInfo(originalAgent.Name(), getAgentModelID(originalAgent), originalAgent.Description(), originalAgent.WelcomeMessage())
171+
evts <- AgentInfo(originalAgent.Name(), r.getEffectiveModelID(originalAgent), originalAgent.Description(), originalAgent.WelcomeMessage())
172172
}
173173
}()
174174

175175
// Emit agent info for the new agent
176176
if newAgent, err := r.team.Agent(params.Agent); err == nil {
177-
evts <- AgentInfo(newAgent.Name(), getAgentModelID(newAgent), newAgent.Description(), newAgent.WelcomeMessage())
177+
evts <- AgentInfo(newAgent.Name(), r.getEffectiveModelID(newAgent), newAgent.Description(), newAgent.WelcomeMessage())
178178
}
179179

180180
slog.Debug("Creating new session with parent session", "parent_session_id", sess.ID, "tools_approved", sess.ToolsApproved, "thinking", sess.Thinking)

pkg/runtime/loop.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,21 @@ func (r *LocalRuntime) RunStream(ctx context.Context, sess *session.Session) <-c
9797

9898
a := r.resolveSessionAgent(sess)
9999

100+
// lastEmittedModelID tracks what the TUI currently displays.
101+
// emitModelInfo sends an AgentInfo only when the model actually changed,
102+
// so new features (routing, alloy, fallback, model picker, …) never need
103+
// to notify the TUI themselves — the loop handles it.
104+
lastEmittedModelID := r.getEffectiveModelID(a)
105+
emitModelInfo := func(a *agent.Agent, modelID string) {
106+
if modelID == lastEmittedModelID {
107+
return
108+
}
109+
lastEmittedModelID = modelID
110+
events <- AgentInfo(a.Name(), modelID, a.Description(), a.WelcomeMessage())
111+
}
112+
100113
// Emit agent information for sidebar display
101-
// Use getEffectiveModelID to account for active fallback cooldowns
102-
events <- AgentInfo(a.Name(), r.getEffectiveModelID(a), a.Description(), a.WelcomeMessage())
114+
events <- AgentInfo(a.Name(), lastEmittedModelID, a.Description(), a.WelcomeMessage())
103115

104116
// Emit team information
105117
events <- TeamInfo(r.agentDetailsFromTeam(), a.Name())
@@ -247,10 +259,9 @@ func (r *LocalRuntime) RunStream(ctx context.Context, sess *session.Session) <-c
247259

248260
modelID := model.ID()
249261

250-
// Notify sidebar when this turn uses a different model (per-tool override).
251-
if modelID != defaultModelID {
252-
events <- AgentInfo(a.Name(), modelID, a.Description(), a.WelcomeMessage())
253-
}
262+
// Notify sidebar when this turn uses a different model
263+
// (per-tool override, model picker, fallback cooldown, …).
264+
emitModelInfo(a, modelID)
254265

255266
slog.Debug("Using agent", "agent", a.Name(), "model", modelID)
256267
slog.Debug("Getting model definition", "model_id", modelID)
@@ -325,17 +336,15 @@ func (r *LocalRuntime) RunStream(ctx context.Context, sess *session.Session) <-c
325336
return
326337
}
327338

328-
// Update sidebar model info to reflect what was actually used this turn.
329-
// Fallback models are sticky (cooldown system persists them), so we only
330-
// emit once. Per-tool model overrides are temporary (one turn), so we
331-
// emit the override and then revert to the agent's default.
339+
// Update sidebar to reflect the model actually used this turn.
340+
// When no fallback kicked in, revert to the agent's default
341+
// (undoes any temporary per-tool override).
342+
actualModelID := defaultModelID
332343
if usedModel != nil && usedModel.ID() != model.ID() {
333344
slog.Info("Used fallback model", "agent", a.Name(), "primary", model.ID(), "used", usedModel.ID())
334-
events <- AgentInfo(a.Name(), usedModel.ID(), a.Description(), a.WelcomeMessage())
335-
} else if model.ID() != defaultModelID {
336-
// Per-tool override was active: revert sidebar to the agent's default model.
337-
events <- AgentInfo(a.Name(), defaultModelID, a.Description(), a.WelcomeMessage())
345+
actualModelID = usedModel.ID()
338346
}
347+
emitModelInfo(a, actualModelID)
339348
streamSpan.SetAttributes(
340349
attribute.Int("tool.calls", len(res.Calls)),
341350
attribute.Int("content.length", len(res.Content)),

pkg/runtime/model_picker.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (r *LocalRuntime) findModelPickerTool() *builtin.ModelPickerTool {
3030
}
3131

3232
// handleChangeModel handles the change_model tool call by switching the current agent's model.
33-
func (r *LocalRuntime) handleChangeModel(ctx context.Context, _ *session.Session, toolCall tools.ToolCall, events chan Event) (*tools.ToolCallResult, error) {
33+
func (r *LocalRuntime) handleChangeModel(ctx context.Context, _ *session.Session, toolCall tools.ToolCall, _ chan Event) (*tools.ToolCallResult, error) {
3434
var params builtin.ChangeModelArgs
3535
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &params); err != nil {
3636
return nil, fmt.Errorf("invalid arguments: %w", err)
@@ -53,29 +53,24 @@ func (r *LocalRuntime) handleChangeModel(ctx context.Context, _ *session.Session
5353
)), nil
5454
}
5555

56-
return r.setModelAndEmitInfo(ctx, params.Model, events)
56+
return r.setCurrentAgentModel(ctx, params.Model)
5757
}
5858

5959
// handleRevertModel handles the revert_model tool call by reverting the current agent to its default model.
60-
func (r *LocalRuntime) handleRevertModel(ctx context.Context, _ *session.Session, _ tools.ToolCall, events chan Event) (*tools.ToolCallResult, error) {
61-
return r.setModelAndEmitInfo(ctx, "", events)
60+
func (r *LocalRuntime) handleRevertModel(ctx context.Context, _ *session.Session, _ tools.ToolCall, _ chan Event) (*tools.ToolCallResult, error) {
61+
return r.setCurrentAgentModel(ctx, "")
6262
}
6363

64-
// setModelAndEmitInfo sets the model for the current agent and emits an updated
65-
// AgentInfo event so the UI reflects the change. An empty modelRef reverts to
66-
// the agent's default model.
67-
func (r *LocalRuntime) setModelAndEmitInfo(ctx context.Context, modelRef string, events chan Event) (*tools.ToolCallResult, error) {
64+
// setCurrentAgentModel sets the model for the current agent. An empty modelRef
65+
// reverts to the agent's default model. The main loop detects the resulting
66+
// model change and automatically notifies the TUI, so no AgentInfo event is
67+
// emitted here.
68+
func (r *LocalRuntime) setCurrentAgentModel(ctx context.Context, modelRef string) (*tools.ToolCallResult, error) {
6869
currentName := r.CurrentAgentName()
6970
if err := r.SetAgentModel(ctx, currentName, modelRef); err != nil {
7071
return tools.ResultError(fmt.Sprintf("failed to set model: %v", err)), nil
7172
}
7273

73-
if a, err := r.team.Agent(currentName); err == nil {
74-
events <- AgentInfo(a.Name(), r.getEffectiveModelID(a), a.Description(), a.WelcomeMessage())
75-
} else {
76-
slog.Warn("Failed to retrieve agent after model change; UI may not reflect the update", "agent", currentName, "error", err)
77-
}
78-
7974
if modelRef == "" {
8075
slog.Info("Model reverted via model_picker tool", "agent", currentName)
8176
return tools.ResultSuccess("Model reverted to the agent's default model"), nil

0 commit comments

Comments
 (0)