只展示代码,不修改文件。以下是方案 A(刷新钩子)的完整写法。

1. internal/config/sections.go — 注册回调

// onSectionApplied 是配置段成功落盘并写入内存配置后的回调(可为 nil)。
// 由调用方(通常是 container 层)在启动时注入,用于把配置变更同步到
// 那些在启动时就已生成快照的运行时组件(例如 agent 的模型提供商表)。
//
// 放在 config 包内而不是直接 import agent 包,是为了避免 config ← agent 的循环依赖。
var onSectionApplied func(key string)

// SetSectionAppliedHook 注册「配置段已生效」回调;传 nil 表示取消。
// 必须在服务开始处理请求之前调用。
func SetSectionAppliedHook(fn func(key string)) {
	onSectionApplied = fn
}

// notifySectionApplied 通知回调;回调内的 panic 不应影响保存结果。
func notifySectionApplied(key string) {
	if onSectionApplied == nil {
		return
	}
	defer func() {
		if r := recover(); r != nil {
			logger.Errorf(context.Background(), "[Config] section applied hook panic for %q: %v", key, r)
		}
	}()
	onSectionApplied(key)
}

在 Section.Apply 调用处(internal/handler/config.go:126)触发:

	if err := section.Apply(h.cfg, value); err != nil {
		c.Error(errors.NewInternalServerError("failed to apply config section").WithDetails(err.Error()))
		return
	}
	// 配置已落盘并写入内存,通知运行时组件刷新快照(未注册回调时为空操作)。
	config.NotifySectionApplied(section.Key)

(用导出名 NotifySectionApplied,因为 handler 在别的包。也可以把触发点收进 config 包的一个 ApplySection(cfg, key, value) 包装函数里,保持 handler 无感知。)

2. internal/agent/client.go — 支持替换默认 Options

// SetProviders 更新默认 Options 中的模型提供商配置表快照(热更新,无需重启)。
func (c *Client) SetProviders(providers map[string]ModelProviderConfig) {
	c.mu.Lock()
	c.defaultOptions.Providers = providers
	c.mu.Unlock()
}

// SetOptions 整体替换默认 Options 中由配置派生的部分(目前即 Providers)。
// 供 container 层在配置变更后调用;其余进程级服务能力(Tools / FileReader 等)不受影响。
func (c *Client) SetProvidersFrom(fn func() map[string]ModelProviderConfig) {
	c.SetProviders(fn())
}

3. internal/container/container.go — 启动时注入钩子

// registerConfigHooks 把「配置段已生效」回调注入 config 包。
//
// config 包不认识 agent.Client,这里通过闭包把两者连起来:
// agent 段保存后,client 立刻重建 providers 快照,下一次请求即使用新配置。
func registerConfigHooks(cfg *config.Config, client *agent.Client) {
	config.SetSectionAppliedHook(func(key string) {
		switch key {
		case "agent":
			client.SetProviders(agentProviders(cfg))
		}
	})
}

在 DI 容器里、*agent.Client 构造完成之后调用(container.go 的 Provide 区块附近):

	must(container.Invoke(func(cfg *config.Config, client *agent.Client) {
		registerConfigHooks(cfg, client)
	}))

对比方案 B

方案 A(钩子推送)方案 B(当前已实现,拉取)
生效时机Apply 之后立即刷新快照下一次请求时现取
config 包依赖需要新增导出函数 SetSectionAppliedHook无改动
agent 包改动需要 SetProviders,options() 不变Options.ProviderSource + options() 改动
遗漏风险以后新增其它"启动快照型"组件,要记得在钩子里加分支只要组件拿到的是 func() 就自动最新
并发写 defaultOptions 需持写锁;正在执行的请求仍用旧快照无写操作,只读当前 map

你已经实现了 B,A 属于"推送式"替代方案,两者也可以共存(ProviderSource 优先时会覆盖 SetProviders 的结果,见 client.go:options() 的写法)。