mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
fix(memory): 时间线按声明渲染工具回执 + 能力判定改为配置层
时间线此前只写「→ 调用工具: a, b」,工具取回的正文下一轮彻底消失: 模型记得自己查过,却不记得查到了什么(刚说的话转头就忘)。 ToolDef 上的 Cleaner / NoMemory 声明一直存在,缺的是消费端。 - formatMergedTimeline 渲染 e.ToolResults:Cleaner 优先,NoMemory 工具跳过, 单条 160 字、每事件最多 3 条(+N more),预算估算同步计入 - 模态能力改由配置层声明(ProviderManager.SetCapabilities), 不再靠 provider.(ModalProvider) 断言穿过 adapter 与安全装饰器 - a2a 通道 RecallPolicy none→auto(NoMemory 仍 true:可读不写)
This commit is contained in:
@ -1087,6 +1087,68 @@ type ProviderManager struct {
|
||||
order []string
|
||||
default_ string
|
||||
status map[string]*providerStatus
|
||||
|
||||
// caps 是**配置层声明**的模态能力(源名 → vision/audio)。
|
||||
//
|
||||
// ★ 为什么不靠 `provider.(ModalProvider)` 断言(2026-10-27 用户指正):
|
||||
// 是否支持视觉是**配置事实**(core.llm.sources.<name>.vision=true),
|
||||
// 而 adapter 只负责格式转换。用类型断言去"猜"能力,等于让能力声明
|
||||
// 穿过 adapter 与安全装饰器两层——任何一层漏实现方法,能力就静默变成
|
||||
// false(这就是反复出现的"配了视觉却说没有视觉")。
|
||||
// 断言只保留为**兜底**(配置未声明时的旧路径)。
|
||||
caps map[string]ProviderCaps
|
||||
}
|
||||
|
||||
// ProviderCaps 是一个 provider 的模态能力(来自配置)。
|
||||
type ProviderCaps struct {
|
||||
Vision bool
|
||||
Audio bool
|
||||
}
|
||||
|
||||
// SetCapabilities 由装配层(llm 模块)把配置里的能力声明登记进来。
|
||||
func (m *ProviderManager) SetCapabilities(name string, caps ProviderCaps) {
|
||||
if m == nil || name == "" {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
if m.caps == nil {
|
||||
m.caps = map[string]ProviderCaps{}
|
||||
}
|
||||
m.caps[name] = caps
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Capabilities 返回某 provider 的配置层能力(ok=false 表示配置未声明)。
|
||||
func (m *ProviderManager) Capabilities(name string) (ProviderCaps, bool) {
|
||||
if m == nil {
|
||||
return ProviderCaps{}, false
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
c, ok := m.caps[name]
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// SupportsVision / SupportsAudio 是**面向调用方**的能力查询:
|
||||
// 先看配置声明,配置没说才退回类型断言(兼容外部/测试 provider)。
|
||||
func (m *ProviderManager) SupportsVision(name string) bool {
|
||||
if c, ok := m.Capabilities(name); ok {
|
||||
return c.Vision
|
||||
}
|
||||
if p := m.Get(name); p != nil {
|
||||
return ProviderSupportsVision(p)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *ProviderManager) SupportsAudio(name string) bool {
|
||||
if c, ok := m.Capabilities(name); ok {
|
||||
return c.Audio
|
||||
}
|
||||
if p := m.Get(name); p != nil {
|
||||
return ProviderSupportsAudio(p)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
62
internal/agent/api/provider_caps_test.go
Normal file
62
internal/agent/api/provider_caps_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package api
|
||||
|
||||
// 模态能力必须由**配置层**声明,而不是靠 adapter 的类型断言去猜。
|
||||
//
|
||||
// 现场(反复出现的"配了视觉却说没有视觉"):内核用
|
||||
// `provider.(ModalProvider)` 断言判定能力,而该断言要穿过 adapter 与
|
||||
// 安全装饰器两层 —— 任一层漏实现方法,能力就静默变成 false。
|
||||
//
|
||||
// 用户指正(2026-10-27):是否支持视觉是**配置事实**
|
||||
//(core.llm.sources.<name>.vision=true),adapter 只负责格式转换。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// plainProvider 故意**不实现** ModalProvider —— 模拟"装饰器漏方法"的情形。
|
||||
type plainProvider struct{ name string }
|
||||
|
||||
func (p *plainProvider) Name() string { return p.name }
|
||||
func (p *plainProvider) MaxContextTokens() int { return 8192 }
|
||||
func (p *plainProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *plainProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||||
ch := make(chan StreamChunk)
|
||||
close(ch)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func TestProviderManagerCapsDeclaredByConfig(t *testing.T) {
|
||||
m := NewProviderManager()
|
||||
p := &plainProvider{name: "proxy"}
|
||||
m.Register("proxy", p)
|
||||
|
||||
// 断言兜底:plainProvider 不是 ModalProvider ⇒ false
|
||||
if ProviderSupportsVision(p) {
|
||||
t.Fatal("前提不成立:plainProvider 不该被断言成支持视觉")
|
||||
}
|
||||
// 配置声明后 ⇒ true(这就是"配置层说了算")
|
||||
m.SetCapabilities("proxy", ProviderCaps{Vision: true})
|
||||
if !m.SupportsVision("proxy") {
|
||||
t.Fatal("配置声明 vision=true 后应判定为支持视觉,不能因为 provider 没实现 ModalProvider 就丢掉能力")
|
||||
}
|
||||
if m.SupportsAudio("proxy") {
|
||||
t.Fatal("未声明 audio 时不应判定支持音频")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderManagerCapsFallBackToAssertion(t *testing.T) {
|
||||
m := NewProviderManager()
|
||||
p := &plainProvider{name: "legacy"}
|
||||
m.Register("legacy", p)
|
||||
// 没声明(ok=false)⇒ 退回旧路径的断言结果
|
||||
if _, ok := m.Capabilities("legacy"); ok {
|
||||
t.Fatal("未声明时 Capabilities 应返回 ok=false")
|
||||
}
|
||||
if m.SupportsVision("legacy") {
|
||||
t.Fatal("未声明且非 ModalProvider 时应为 false")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user