// Package context - bundle_overlay.go 提供 BundleOverlay 类型. // // BundleOverlay 让 SDK 用户以"最小改动"适配新模型或新场景, // 而不必从头实现完整的 PromptBundle 接口(那需要了解全部 14 个 section 的语义). // // 典型用法: // // // 接入 Qwen 模型:只需覆盖行为准则和工具说明两个 section,其余继承 DefaultBundle // qwenBundle := context.NewBundleOverlay(context.NewDefaultBundle()). // OverrideStatic("intro", "你是 Flyto,一个智能助手,帮助用户完成软件工程任务."). // OverrideStatic("using_tools", "使用工具时优先选择专用工具而非 Bash..."). // OverrideDynamic("env_info", func(ctx context.Context) string { // return "# 运行环境\n..." // 用中文写环境描述 // }) // // eng.RegisterPromptBundle( // context.BundleKey{ModelFamily: "qwen", Scenario: "programming"}, // qwenBundle, // ) // // 设计决策: // // 1. 一层 Overlay,不支持嵌套.嵌套 Overlay(Overlay 套 Overlay)会导致调用链难以 // 追踪,且实际需求从未出现.如果真的需要深度定制,直接实现 PromptBundle 接口. // // 2. 以 section Name 为键.Name 是 section 在 Bundle 中的语义标识符, // 用它覆盖而非用下标,让覆盖代码可读("我在覆盖 'intro' 段落"而非"第 0 个 section"). // // 3. 不修改 SectionRegistry 的行为.被覆盖的 section 走自己的 Compute 函数, // base bundle 中同名 section 的缓存条目在第一次 Overlay.Compute 时被新值覆盖. // 这是 SectionRegistry 的 double-check 写入语义的自然行为,无需额外处理. package context import "context" // BundleOverlay 在 base PromptBundle 基础上覆盖指定 section,其余继承. // // 升华改进(ELEVATED): 早期实现 没有任何机制让外部覆盖提示词 section-- // 早期方案是一个大函数,所有内容硬编码,外部无法插入. // 我们设计了 BundleOverlay,让 SDK 用户只需声明"我要覆盖哪个段落", // 引擎自动把被覆盖的段落替换为新内容,其余段落继承 base Bundle 不变. // 对于 CLI/SDK 用户:DefaultBundle 继续工作,零影响. // 对于第三方模型接入:只需覆盖 1-3 个 section,即可适配新模型的指令风格. // 替代方案:<要求外部完整实现 PromptBundle 接口> // - 否决原因:需要了解全部 14 个 section 的语义,门槛过高,且 base 内容无法复用. type BundleOverlay struct { base PromptBundle overrides map[string]*Section // key = section.Name } // NewBundleOverlay 创建一个基于 base 的 Overlay. // base 不得为 nil(使用 NewDefaultBundle() 作为最常见的基础). func NewBundleOverlay(base PromptBundle) *BundleOverlay { return &BundleOverlay{ base: base, overrides: make(map[string]*Section), } } // OverrideStatic 覆盖指定名称的 section,用静态文字替换. // // 精妙之处(CLEVER): 返回 *BundleOverlay 自身,支持链式调用-- // 这是 Builder 模式的惯用法,让覆盖代码紧凑可读: // // overlay.OverrideStatic("intro", "...").OverrideStatic("using_tools", "...") // // 替代方案:<每次覆盖都返回新的 BundleOverlay 副本(不可变风格)> // - 否决原因:每次分配新对象,在 section 较多时有不必要的 GC 压力; // BundleOverlay 在构建阶段(引擎启动时)完成,不是并发修改的热路径. func (o *BundleOverlay) OverrideStatic(name, text string) *BundleOverlay { o.overrides[name] = StaticSection(name, text) return o } // OverrideDynamic 覆盖指定名称的 section,用动态计算函数替换. // compute 函数从 context 读取运行时注入值(CwdFromCtx,ModelIDFromCtx 等). // 返回空字符串表示跳过此 section(不注入到提示词). func (o *BundleOverlay) OverrideDynamic(name string, compute ComputeFn) *BundleOverlay { o.overrides[name] = DynamicSection(name, compute) return o } // OverrideVolatile 覆盖指定名称的 section,用每轮重算的函数替换. // reason 说明为什么此 section 必须每轮重算(用于代码审查和文档). func (o *BundleOverlay) OverrideVolatile(name string, compute ComputeFn, reason string) *BundleOverlay { o.overrides[name] = VolatileSection(name, compute, reason) return o } // OverrideSection 直接覆盖整个 Section 对象(最大灵活度). // 适合需要精细控制 CacheBreak / NoCacheReason 等字段的场景. func (o *BundleOverlay) OverrideSection(s *Section) *BundleOverlay { o.overrides[s.Name] = s return o } // StaticSections 返回静态 section 列表,被覆盖的 section 用 override 替换. // // 精妙之处(CLEVER): applyOverrides 统一处理静态和动态 section 的覆盖逻辑-- // 不区分 section 原来是静态还是动态,只按 Name 匹配. // 这意味着可以用动态 override 替换静态 base section(反之亦然), // 给 SDK 用户提供最大灵活度. // 如果用 index 匹配(如 sections[0] = override),就无法跨静态/动态替换. func (o *BundleOverlay) StaticSections() []*Section { return o.applyOverrides(o.base.StaticSections()) } // DynamicSections 返回动态 section 列表,被覆盖的 section 用 override 替换. func (o *BundleOverlay) DynamicSections() []*Section { return o.applyOverrides(o.base.DynamicSections()) } // applyOverrides 将 overrides 应用到 sections 列表. // 遍历 sections,发现 Name 有对应 override 时替换,否则保留原 section. // 注意:override 中存在但 base 中不存在的 section 名称会被静默忽略-- // 这是有意设计:避免用户拼写错误时静默引入幽灵 section. // 若需要追加 base 中不存在的 section,应直接实现完整的 PromptBundle 接口. func (o *BundleOverlay) applyOverrides(sections []*Section) []*Section { if len(o.overrides) == 0 { // 无覆盖时直接返回 base 的 slice,零分配 return sections } result := make([]*Section, len(sections)) for i, s := range sections { if ov, ok := o.overrides[s.Name]; ok { result[i] = ov } else { result[i] = s } } return result } // --------------------------------------------------------------------------- // BundleOverlayFromFunc - 函数式覆盖工厂(简洁用法) // --------------------------------------------------------------------------- // NewBundleFromFunc 用工厂函数构建完整的 PromptBundle. // // 升华改进(ELEVATED): 为需要完全自定义 Bundle 但又不想定义新类型的场景提供捷径. // BundleOverlay 覆盖"有基础,改几处"的场景; // BundleFromFunc 覆盖"从头定义一套 sections"但想用函数式风格的场景. // // 用法: // // bundle := context.NewBundleFromFunc( // func() []*context.Section { return []*context.Section{...} }, // func() []*context.Section { return []*context.Section{...} }, // ) // // 替代方案:<强制实现 PromptBundle 接口> // - 否决原因:定义新类型有认知开销;工厂函数更内联,适合测试和快速原型. type bundleFromFunc struct { staticFn func() []*Section dynamicFn func() []*Section } // NewBundleFromFunc 从静态/动态函数构建 PromptBundle. func NewBundleFromFunc(staticFn, dynamicFn func() []*Section) PromptBundle { return &bundleFromFunc{staticFn: staticFn, dynamicFn: dynamicFn} } func (b *bundleFromFunc) StaticSections() []*Section { if b.staticFn == nil { return nil } return b.staticFn() } func (b *bundleFromFunc) DynamicSections() []*Section { if b.dynamicFn == nil { return nil } return b.dynamicFn() } // --------------------------------------------------------------------------- // ComputeFnFromContext - 快捷 context 感知计算函数 // --------------------------------------------------------------------------- // PromptLanguageFromCtx 从 context 读取语言偏好(可选,P2 功能预留). // // 历史包袱(LEGACY): 当前未被任何 section 使用--语言本地化是 P2 特性, // 此函数是为将来的本地化 Bundle 提前埋下的 context key 接入点. // 一旦 P2 实现,删除此注释,添加使用示例. func PromptLanguageFromCtx(ctx context.Context) string { v, _ := ctx.Value(promptLangCtxKey{}).(string) return v } // WithPromptLanguage 将语言偏好注入 context(P2 功能预留). func WithPromptLanguage(ctx context.Context, lang string) context.Context { return context.WithValue(ctx, promptLangCtxKey{}, lang) } // promptLangCtxKey 是语言偏好的 context key 类型. // 使用 unexported struct 类型保证不同包间不冲突. type promptLangCtxKey struct{}