// Package context - bundle.go 定义系统提示词的分段装配基础设施. // // 核心概念: // // 1. Section - 系统提示词的一个命名片段,携带缓存语义(静态/会话级/每轮重算). // 2. SectionRegistry - Engine 实例绑定的 Section 计算缓存,/clear 或 /compact 后 Reset. // 3. PromptBundle - 一组 Section 的命名集合,按 (ModelFamily, Scenario) 调优. // 4. BundleRegistry - 持有所有已注册 Bundle,精确匹配 + 默认回退. // 5. SystemPromptBlock - 带缓存语义的 API 块,与 internal/api 包解耦. // // 设计决策: // // - M+N 而非 M×N:ModelFamily 和 Scenario 两个维度独立组合,避免组合爆炸. // - 实例绑定而非全局:SectionRegistry 和 BundleRegistry 绑定到 Engine 实例, // 支持 SaaS 多租户下每个工作区独立的提示词配置. // - 静态/动态分离:静态 sections 全局可缓存,动态 sections 会话级缓存, // volatile sections 每轮重算--直接对应 Anthropic prompt caching 的三种语义. package context import ( "context" "strings" "sync" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // --------------------------------------------------------------------------- // Section - 系统提示词的命名片段 // --------------------------------------------------------------------------- // ComputeFn 是 Section 的动态计算函数. // 返回空字符串表示该 Section 在本次调用中应被跳过(不注入提示词). // ctx 携带运行时注入值,通过 With* 系列帮助函数写入,在 Compute 内读出. type ComputeFn func(ctx context.Context) string // Section 是系统提示词的一个命名片段,可单独缓存和管理. // // 升华改进(ELEVATED): 早期实现用 JS 对象 {name, compute, cacheBreak} // + 全局 state 做缓存(Map,进程级全局变量). // 我们将缓存状态移入 SectionRegistry 实例,绑定到 Engine-- // 同一进程中多个 Engine(多用户,多工作区)各有独立缓存,不会互相污染. // CLI 单用户场景行为等价;SaaS 多租户场景获得隔离性. // 替代方案:<沿用全局 sync.Map> - 否决原因:多 Engine 实例会互相污染缓存, // Reset() 时要么清掉别人的缓存要么无法精确清除. type Section struct { // Name Section 的唯一标识符,用于 SectionRegistry 的缓存 key. // 同一 Bundle 内应唯一;不同 Bundle 之间允许重名(各自独立缓存). Name string // Static marks whether this Section's content is globally invariant // (role definitions, behavior rules) and thus a candidate for a long-lived // cache scope. // // true = globally invariant — suitable for "global" cache scope once // the Anthropic beta stabilizes (currently assembled as // "ephemeral" in BuildPromptBlocks, see LEGACY note there). // false = may vary per session/turn (env_info, FLYTO.md, volatile // sections) — assembled as "ephemeral" or uncached. // // Field is read at three points, making it a load-bearing descriptor, // not a redundant self-description: // // (a) Runtime invariant check: BuildPromptBlocks asserts that every // section returned by StaticSections() has Static=true, and every // non-Volatile section returned by DynamicSections() has // Static=false. Violations fire a `section_contract_violation` // observer event so SDK consumers who hand-build &Section{...} // and place it in the wrong bucket see the bug at wire time, not // via a cache-behavior regression weeks later. // (b) Future global cache-scope branching: once the Anthropic beta // header stabilizes, BuildPromptBlocks' static-block CacheScope // will switch from "ephemeral" to "global" gated on Static=true. // Keeping the field explicit (rather than inferred from the // interface method) makes that upgrade a one-line flip. // (c) SectionRegistry cache-benefit: Static=true sections still go // through the registry cache to avoid per-call string allocation // even though their content would be cheap to recompute. // // Static 标记此 Section 是否为全局不变内容 (角色定义/行为规则), 从而 // 是长寿命 cache scope 的候选. // // true = 全局不变 -- 适合 Anthropic beta 稳定后的 "global" cache scope // (当前 BuildPromptBlocks 统一组装为 "ephemeral", 见 LEGACY 注释). // false = 可能逐会话/每轮变化 (env_info, FLYTO.md, volatile section) // -- 组装为 "ephemeral" 或不缓存. // // 字段在三处被读, 是承载性描述, 不是冗余 self-description: // // (a) 运行时 invariant check: BuildPromptBlocks 断言 StaticSections() // 返回的 section 必须 Static=true, DynamicSections() 返回的非 // Volatile section 必须 Static=false. 违反会发 // `section_contract_violation` observer 事件, SDK 消费者手搓 // &Section{...} 放错 bucket 时立即在 wire time 发现 bug, 而不是 // 数周后通过 cache 行为回归暴露. // (b) 未来 global cache-scope 分支: Anthropic beta header 稳定后, // BuildPromptBlocks 的 static-block CacheScope 会从 "ephemeral" // 切到 "global", gate 在 Static=true. 显式字段 (而非 interface // method 推断) 让此升级只需改一行. // (c) SectionRegistry cache 收益: Static=true section 仍走 registry // 缓存以避免每次调用的字符串分配, 即便重算开销低. Static bool // Text 静态文字内容. // Static=true 且内容已知时,直接填此字段,Compute 留 nil. // Compute 不为 nil 时,Text 字段被忽略. Text string // Compute 动态计算函数(可选). // nil 时直接使用 Text 字段. // 非 nil 时:SectionRegistry.Compute 调用此函数,结果缓存(除非 CacheBreak=true). Compute ComputeFn // CacheBreak 为 true 时,此 Section 每轮强制重算,完全绕过缓存. // // 历史包袱(LEGACY): 名字和语义沿用早期方案 DANGEROUS_uncachedSystemPromptSection 的设计-- // 用于 MCP server 连接状态等在会话中途可能变化的内容. // 早期方案用前缀 DANGEROUS 标记此类 section,以提示开发者"这会打碎 prompt cache". // 我们保留相同语义,用 CacheBreak bool + NoCacheReason string 替代命名约定, // 更容易静态分析和 lint. // 理想状态:用 event-driven invalidation(section 订阅特定事件)替代, // 但需要更多基础设施,暂时保持简单. CacheBreak bool // NoCacheReason explains why this Section must be recomputed every turn. // Only meaningful when CacheBreak=true. Consumed by three audiences: // 1. Documentation and code review (grep for VolatileSection call sites). // 2. Runtime diagnostics: SectionRegistry.Compute emits a // `section_cache_break` observer event carrying this string whenever // a CacheBreak section is computed, so operators tracing a // prompt-cache miss can see *which* section invalidated and why. // 3. Future event-driven invalidation: a registry that subscribes to // a reason taxonomy can selectively invalidate rather than per-turn recompute. // // NoCacheReason 说明为什么此 Section 必须每轮重算. // 仅在 CacheBreak=true 时有意义, 三个消费者: // 1. 文档与代码审查 (grep VolatileSection 调用点). // 2. 运行时诊断: SectionRegistry.Compute 每次计算 CacheBreak section // 时, 经 observer 发 `section_cache_break` 事件携带此字符串, // 运维追踪 prompt-cache miss 可看到 *哪个* section 打碎 cache 以及原因. // 3. 未来事件驱动失效: 订阅 reason 分类的 registry 可精确失效 // 而不必每轮重算. NoCacheReason string } // resolve 计算 Section 的实际文字内容. // 优先调用 Compute 函数;Compute 为 nil 时直接返回 Text 字段. func (s *Section) resolve(ctx context.Context) string { if s.Compute != nil { return s.Compute(ctx) } return s.Text } // StaticSection 是创建静态 Section 的便捷函数. // 用于不需要运行时计算的 Section(角色定义,行为准则等常量文本). func StaticSection(name, text string) *Section { return &Section{ Name: name, Static: true, Text: text, } } // DynamicSection 是创建动态(会话级缓存)Section 的便捷函数. func DynamicSection(name string, compute ComputeFn) *Section { return &Section{ Name: name, Static: false, Compute: compute, } } // VolatileSection 是创建每轮重算 Section 的便捷函数(对应早期方案 DANGEROUS_uncachedSection). func VolatileSection(name string, compute ComputeFn, reason string) *Section { return &Section{ Name: name, Static: false, Compute: compute, CacheBreak: true, NoCacheReason: reason, } } // --------------------------------------------------------------------------- // SectionRegistry - 会话级 Section 计算缓存 // --------------------------------------------------------------------------- // SectionRegistry 是会话级的 Section 计算缓存,绑定到 Engine 实例. // // 生命周期: // - New(): 随 Engine 创建 // - Compute(): 首次计算后缓存,后续命中直接返回 // - Reset(): 在 /clear 或 /compact 后清空,让动态 section 下一轮重新计算 // - Invalidate(name): 精确使某个 section 失效(如 MCP 连接状态变化时) // // 线程安全:读多写少场景用 RWMutex,Compute 路径用 RLock 快速读,仅 miss 时升级 Lock. type SectionRegistry struct { mu sync.RWMutex cache map[string]string // observer receives `section_cache_break` diagnostic events whenever a // CacheBreak Section is computed. Optional; nil means events are dropped. // // observer 在每次计算 CacheBreak Section 时收到 `section_cache_break` // 诊断事件. 可选, nil 时事件丢弃. observer flyto.EventObserver } // NewSectionRegistry 创建空的 Section 缓存. func NewSectionRegistry() *SectionRegistry { return &SectionRegistry{cache: make(map[string]string)} } // NewSectionRegistryWithObserver creates an empty Section cache that emits // `section_cache_break` diagnostic events to obs whenever a CacheBreak // Section is computed. obs may be nil (equivalent to NewSectionRegistry). // // NewSectionRegistryWithObserver 创建空的 Section 缓存, 并在每次计算 // CacheBreak Section 时经 obs 发 `section_cache_break` 诊断事件. // obs 可为 nil (等价于 NewSectionRegistry). func NewSectionRegistryWithObserver(obs flyto.EventObserver) *SectionRegistry { return &SectionRegistry{ cache: make(map[string]string), observer: obs, } } // Compute 返回 Section 的文字内容,优先使用缓存. // // 精妙之处(CLEVER): CacheBreak=true 的 Section 完全跳过缓存-- // 不读也不写缓存,保证每轮拿到实时值. // 普通 Section(静态或动态)首次调用后缓存结果,后续直接返回, // 避免重复执行 IO 操作(git status,FLYTO.md 读取等). func (r *SectionRegistry) Compute(ctx context.Context, s *Section) string { if s.CacheBreak { // CacheBreak: bypass cache AND emit a diagnostic event so operators // tracing prompt-cache misses can see which section invalidated and why. // Gated on observer != nil so the zero-value registry stays zero-cost. // // CacheBreak: 跳过缓存, 同时发诊断事件让运维追踪 prompt-cache miss // 时能看到哪个 section 打碎 cache + 原因. // 仅在 observer != nil 时发, 零值 registry 零开销. if r.observer != nil { r.observer.Event("section_cache_break", map[string]any{ "name": s.Name, "reason": s.NoCacheReason, }) } return s.resolve(ctx) } // 先用读锁快速检查缓存命中 r.mu.RLock() if v, ok := r.cache[s.Name]; ok { r.mu.RUnlock() return v } r.mu.RUnlock() // 缓存 miss:计算并存入 v := s.resolve(ctx) r.mu.Lock() // 精妙之处(CLEVER): double-check - 防止并发 goroutine 重复写入(最终一致即可) if _, exists := r.cache[s.Name]; !exists { r.cache[s.Name] = v } r.mu.Unlock() return v } // Reset 清空所有缓存条目. // 在 /clear 或 /compact 后调用,让动态 Section 在下一轮重新计算. func (r *SectionRegistry) Reset() { r.mu.Lock() // 精妙之处(CLEVER): 重新分配 map 而非 range delete-- // Go map 的 range delete 只是标记 slot,底层内存不会立即回收. // 分配新 map 让 GC 直接回收旧 map 的所有内存. r.cache = make(map[string]string) r.mu.Unlock() } // Invalidate 使特定 Section 的缓存条目失效. // 用于已知某个 Section 内容变化时的精确失效(如 MCP server 连接/断开). func (r *SectionRegistry) Invalidate(name string) { r.mu.Lock() delete(r.cache, name) r.mu.Unlock() } // emitEvent forwards to the observer when one is attached. Used by // BuildPromptBlocks to report Section.Static contract violations through // the same channel as cache-break diagnostics, so SDK consumers see both // classes of Section misuse via one observer. // // emitEvent 在配置了 observer 时转发事件. BuildPromptBlocks 用它报告 // Section.Static 契约违规, 与 cache-break 诊断走同一通道, SDK 消费者 // 经一个 observer 看到两类 Section 误用. func (r *SectionRegistry) emitEvent(name string, data map[string]any) { if r.observer == nil { return } r.observer.Event(name, data) } // --------------------------------------------------------------------------- // PromptBundle - 提示词 Section 集合接口 // --------------------------------------------------------------------------- // PromptBundle 是一组系统提示词 Section 的命名集合, // 按 (ModelFamily, Scenario) 组合调优. // // 升华改进(ELEVATED): 早期实现 只有一套提示词(对 claude+编程场景深度优化). // 我们设计 Bundle 机制支持多模型/多行业-- // 引擎内置默认 Bundle(claude+programming,内容一字不改), // 平台层或 SDK 用户通过 BundleRegistry.Register 注册行业 Bundle,互不干扰. // 不同模型和行业的踩坑改动彼此隔离,不会影响已验证的默认 Bundle. // 替代方案:<单一全局提示词,按模型/场景 if-else 分支> // - 否决原因:所有场景耦合在一起,行业 bundle 的实验改动会影响编程场景稳定性. // // Shape: pull (consumer-registered). Consumers register industry-specific // PromptBundle implementations via engine.RegisterPromptBundle(key, bundle); // the engine pulls StaticSections() / DynamicSections() during prompt // assembly at every turn. // // 形态: 调取 (消费者注册). 消费者经 engine.RegisterPromptBundle(key, bundle) // 注册行业专用 PromptBundle; 引擎在每轮拼 prompt 时 pull StaticSections() / // DynamicSections(). type PromptBundle interface { // StaticSections 返回静态部分(边界前,全局可缓存). // 所有返回的 Section 的 Static 字段应为 true. // 返回顺序即组装顺序. StaticSections() []*Section // DynamicSections 返回动态部分(边界后,会话级缓存). // 包括 volatile(CacheBreak=true)和普通动态 section. // 返回顺序即组装顺序. DynamicSections() []*Section } // --------------------------------------------------------------------------- // BundleKey + BundleRegistry - Bundle 索引和查找 // --------------------------------------------------------------------------- // BundleKey 按 (模型族, 场景) 唯一标识一个 PromptBundle. // // 精妙之处(CLEVER): M+N 而非 M×N-- // ModelFamily 描述模型侧的调优(工具调用格式,指令遵循风格); // Scenario 描述任务侧的调优(编程/仓储/金融). // 二者独立:3 个 ModelFamily × 4 个 Scenario 在理论上有 12 个 Bundle, // 但每个维度的调优只需维护 M+N=7 个变体(通过组合 base + overlay 实现), // 不是每种组合都要完整实现. // BundleRegistry.Resolve 先精确匹配,再回退到默认 Bundle. type BundleKey struct { ModelFamily string // "claude", "gpt", "gemini", "local", ... Scenario string // "programming", "warehouse", "medical", ... } // DefaultBundleKey 是系统内置的默认 Bundle key(claude + programming). var DefaultBundleKey = BundleKey{ModelFamily: "claude", Scenario: "programming"} // String formats the BundleKey as "/" for use in logs, // error messages, and observer event payloads. Without this method, code // that prints a BundleKey (via fmt.Sprintf %v or similar) emits the raw // struct literal "{claude programming}", which is unfriendly for operators // grepping through production logs. Empty fields render as the literal // "" so zero-value keys are still distinguishable. // // Design note (CLEVER): this also makes BundleKey.ModelFamily / // BundleKey.Scenario load-bearing at the type level — without a method // reading them, they are only used for struct hashing as map keys (which // the dead-field scanner treats as unread, a known struct-as-map-key // false positive). A real String() method gives operators human output // AND serves as the syntactic read site that makes the field's purpose // statically inspectable. // // String 将 BundleKey 格式化为 "/", 用于日志/错误 // 消息/observer 事件 payload. 无此方法时, 打印 BundleKey (fmt.Sprintf // %v 等) 产出原生 struct 字面 "{claude programming}", 运维 grep 生产 // 日志不友好. 空字段渲染为字面 "" 让零值 key 仍可区分. // // 设计说明 (CLEVER): 此方法也让 BundleKey.ModelFamily / // BundleKey.Scenario 在类型层面承载 -- 没有读它们的方法时, 它们仅被 // struct-as-map-key 哈希使用 (dead-field scanner 视其为未读, struct- // as-map-key 已知假阳性). 真的 String() 方法给运维人类可读输出 + // 作为让字段用途静态可验的 syntactic read site. func (k BundleKey) String() string { family := k.ModelFamily if family == "" { family = "" } scenario := k.Scenario if scenario == "" { scenario = "" } return family + "/" + scenario } // BundleRegistry 持有所有已注册的 PromptBundle,按 BundleKey 索引. // // 升华改进(ELEVATED): 与 SkillRegistry / AgentRegistry 设计一致--实例绑定到 Engine, // SaaS 多租户场景下每个工作区有独立的 Bundle 集合,互不干扰. // 替代方案:<全局注册表 sync.Map> // - 否决原因:多租户下不同租户可能需要不同 Bundle,全局变量无法隔离. type BundleRegistry struct { mu sync.RWMutex bundles map[BundleKey]PromptBundle defKey BundleKey } // NewBundleRegistry 创建 Bundle 注册表,默认 key 为 claude+programming. func NewBundleRegistry() *BundleRegistry { return &BundleRegistry{ bundles: make(map[BundleKey]PromptBundle), defKey: DefaultBundleKey, } } // Register 注册或覆盖一个 Bundle. // 消费层(SDK 用户,平台层)通过此方法添加行业特化 Bundle. // key 相同时覆盖已有 Bundle(支持热更新). func (r *BundleRegistry) Register(key BundleKey, bundle PromptBundle) { r.mu.Lock() defer r.mu.Unlock() r.bundles[key] = bundle } // SetDefault 设置未精确匹配时使用的默认 Bundle key. // 通常不需要调用(默认是 claude+programming), // 但允许专注非编程场景的部署覆盖默认值. func (r *BundleRegistry) SetDefault(key BundleKey) { r.mu.Lock() defer r.mu.Unlock() r.defKey = key } // Resolve 按 BundleKey 查找 Bundle,找不到则回退到默认 Bundle. // // 精妙之处(CLEVER): 两级回退策略,保证引擎在任何配置下都不返回 nil-- // 1. 精确匹配 (ModelFamily, Scenario) // 2. 回退到注册表的 defaultKey(通常是 claude+programming) // // 不会 panic 或返回 nil,引擎可以安全地用 Resolve 结果直接调用. // 如果注册表为空(未注册任何 Bundle),返回 nil(调用方必须自行检查). func (r *BundleRegistry) Resolve(key BundleKey) PromptBundle { r.mu.RLock() defer r.mu.RUnlock() if b, ok := r.bundles[key]; ok { return b } if b, ok := r.bundles[r.defKey]; ok { return b } return nil } // --------------------------------------------------------------------------- // SystemPromptBlock - 带缓存语义的 API 块 // --------------------------------------------------------------------------- // SystemPromptBlock 是一个带缓存语义的系统提示词 API 块. // // 升华改进(ELEVATED): 早期实现 buildSystemPromptBlocks 直接生成 Anthropic API 结构体 // (anthropic.TextBlockParam with cache_control). // 我们用中间层 SystemPromptBlock 解耦--context 包生成 Block(纯逻辑), // engine 层负责将 Block 转换为 api.SystemContentBlock(API 序列化). // 好处:context 包不需要 import internal/api,避免循环依赖; // 也更容易测试(不需要对 API 结构体做断言). // 替代方案: // - 否决原因:context 包会循环依赖 internal/api 包. type SystemPromptBlock struct { // Text 是此块的文字内容. Text string // CacheScope 控制此块的缓存行为: // "global" - 全局可缓存(跨会话/跨用户),对应 Anthropic beta cache_scope='global' // "ephemeral" - 会话级缓存,对应 cache_control: {type: "ephemeral"} // "" - 不缓存(每次请求都发送,无 cache_control) CacheScope string } // --------------------------------------------------------------------------- // BuildPromptBlocks - 将 Bundle 转换为 API 块 // --------------------------------------------------------------------------- // BuildPromptBlocks 将 Bundle 的所有 Section 转换为带缓存语义的 API 块. // // 组装策略(enableCaching=true 时): // 1. 静态 sections → 合并为一个 "ephemeral" 块(将来升级为 "global",当 beta 可用) // 2. 非 volatile 动态 sections → 合并为一个 "ephemeral" 块(会话级命中) // 3. volatile(CacheBreak=true)动态 sections → 每个单独的 "" 块(不缓存) // // 组装策略(enableCaching=false 时): // - 所有内容合并为单个无缓存块(向后兼容,与旧 buildAPIRequest 行为一致) // // 精妙之处(CLEVER): 静态和动态各自合并为单块而非逐 section 分块-- // Anthropic API 的 cache_control breakpoint 限额为 4 个. // 把同类 sections 合并可以节约 breakpoint 预算,留给工具列表等其他可缓存位置. // 替代方案:<每个 Section 独立成块> - 否决原因:轻易超过 4 个 breakpoint 限额. func BuildPromptBlocks(ctx context.Context, bundle PromptBundle, registry *SectionRegistry, enableCaching bool) []SystemPromptBlock { if bundle == nil { return nil } var staticParts, dynamicParts []string var volatileBlocks []SystemPromptBlock // 处理静态 sections for _, s := range bundle.StaticSections() { // Invariant: every section returned by StaticSections() must have // Static=true. A Section constructed with `&Section{...}` that // forgets to set Static and is placed in the static bucket lands // here. Fire a diagnostic event (non-fatal) so the bug surfaces // now rather than as a mysterious cache miss later when // BuildPromptBlocks' future global-scope branching kicks in. // // Invariant: StaticSections() 返回的 section 必须 Static=true. // 手搓 `&Section{...}` 忘 set Static 再塞进 static bucket 会走到 // 这里. 发诊断事件 (不 fatal), bug 现在暴露, 而不是未来 // BuildPromptBlocks 的 global-scope 分支启用时再变成神秘 // cache miss. if !s.Static { registry.emitEvent("section_contract_violation", map[string]any{ "name": s.Name, "bucket": "StaticSections", "expected_static": true, "actual_static": false, }) } if text := registry.Compute(ctx, s); text != "" { staticParts = append(staticParts, text) } } // 处理动态 sections for _, s := range bundle.DynamicSections() { // Invariant: DynamicSections() returns either CacheBreak=true // (Volatile) or non-Volatile dynamic sections; neither should be // Static=true. A Static=true section misplaced in the dynamic // bucket would miss its natural "global" cache tier upgrade. // // Invariant: DynamicSections() 返回 CacheBreak=true (Volatile) // 或普通动态 section; 两者都不应 Static=true. Static=true section // 被误放进动态 bucket 会错过将来升级到 "global" cache tier. if s.Static { registry.emitEvent("section_contract_violation", map[string]any{ "name": s.Name, "bucket": "DynamicSections", "expected_static": false, "actual_static": true, }) } text := registry.Compute(ctx, s) if text == "" { continue } if s.CacheBreak { // volatile section:单独放一个不缓存块 volatileBlocks = append(volatileBlocks, SystemPromptBlock{ Text: text, CacheScope: "", }) } else { dynamicParts = append(dynamicParts, text) } } if !enableCaching { // 合并所有内容为单个无缓存块(向后兼容模式) var all []string all = append(all, staticParts...) all = append(all, dynamicParts...) for _, vb := range volatileBlocks { all = append(all, vb.Text) } if len(all) == 0 { return nil } return []SystemPromptBlock{{Text: strings.Join(all, "\n\n")}} } var blocks []SystemPromptBlock if len(staticParts) > 0 { blocks = append(blocks, SystemPromptBlock{ Text: strings.Join(staticParts, "\n\n"), // 历史包袱(LEGACY): 暂时用 "ephemeral" 而非 "global"-- // "global" 需要 anthropic-beta: prompt-caching-2024-07-31 且需要 shouldUseGlobalCacheScope() 检查. // 早期实现 做了 beta feature check (shouldUseGlobalCacheScope()),我们保守处理, // 待 global scope 稳定上线后升级此处. CacheScope: "ephemeral", }) } if len(dynamicParts) > 0 { blocks = append(blocks, SystemPromptBlock{ Text: strings.Join(dynamicParts, "\n\n"), CacheScope: "ephemeral", }) } blocks = append(blocks, volatileBlocks...) return blocks } // BlocksToString 将 SystemPromptBlock 列表合并为单个字符串. // 用于不支持多块缓存的场景(降级路径,调试,测试断言等). func BlocksToString(blocks []SystemPromptBlock) string { parts := make([]string, 0, len(blocks)) for _, b := range blocks { if b.Text != "" { parts = append(parts, b.Text) } } return strings.Join(parts, "\n\n") } // --------------------------------------------------------------------------- // Context value helpers - 运行时状态注入 // --------------------------------------------------------------------------- // 以下 context key 类型和 With*/From* 帮助函数用于将运行时状态 // 注入 context.Context,供动态 Section 的 Compute 函数读取. // // 精妙之处(CLEVER): 用 unexported struct 类型作为 context key-- // 不同包的 cwdCtxKey{} 等价,但不同 key 类型互不冲突. // 标准 Go context 惯用法,零运行时开销. type ( cwdCtxKey struct{} modelIDCtxKey struct{} toolDescsCtxKey struct{} evolveFragCtxKey struct{} appendPromptKey struct{} mcpServersCtxKey struct{} ) // MCPServerStatus is a snapshot of one MCP server's connection state at prompt // build time. The mcp_servers volatile section renders this into a short // bulleted line so the model knows which MCP-backed tools are live this turn. // // MCPServerStatus 是一次 prompt 构建时某个 MCP server 的连接状态快照. // mcp_servers volatile section 将其渲染为简短列表, 让模型知道本轮哪些 // MCP-backed 工具可用. type MCPServerStatus struct { Name string // Server 配置名 (e.g. "filesystem", "git"). Connected bool // True 表示本轮可调用其工具; false 表示断连或未连上. } // WithCwd 将工作目录注入 context,供动态 Section 读取. func WithCwd(ctx context.Context, cwd string) context.Context { return context.WithValue(ctx, cwdCtxKey{}, cwd) } // CwdFromCtx 从 context 读取工作目录. func CwdFromCtx(ctx context.Context) string { v, _ := ctx.Value(cwdCtxKey{}).(string) return v } // WithModelID 将模型 ID 注入 context,供 env_info section 构建模型描述. func WithModelID(ctx context.Context, modelID string) context.Context { return context.WithValue(ctx, modelIDCtxKey{}, modelID) } // ModelIDFromCtx 从 context 读取模型 ID. func ModelIDFromCtx(ctx context.Context) string { v, _ := ctx.Value(modelIDCtxKey{}).(string) return v } // WithToolDescriptions 将工具描述列表注入 context. func WithToolDescriptions(ctx context.Context, descs []ToolDescription) context.Context { return context.WithValue(ctx, toolDescsCtxKey{}, descs) } // ToolDescriptionsFromCtx 从 context 读取工具描述列表. func ToolDescriptionsFromCtx(ctx context.Context) []ToolDescription { v, _ := ctx.Value(toolDescsCtxKey{}).([]ToolDescription) return v } // WithEvolveFragment 将进化能力提示片段注入 context. func WithEvolveFragment(ctx context.Context, frag string) context.Context { return context.WithValue(ctx, evolveFragCtxKey{}, frag) } // EvolveFragmentFromCtx 从 context 读取进化能力提示片段. func EvolveFragmentFromCtx(ctx context.Context) string { v, _ := ctx.Value(evolveFragCtxKey{}).(string) return v } // WithAppendPrompt 将用户追加提示注入 context. func WithAppendPrompt(ctx context.Context, prompt string) context.Context { return context.WithValue(ctx, appendPromptKey{}, prompt) } // AppendPromptFromCtx 从 context 读取用户追加提示. func AppendPromptFromCtx(ctx context.Context) string { v, _ := ctx.Value(appendPromptKey{}).(string) return v } // WithMCPServerStatuses injects a snapshot of MCP server connection state // into ctx, consumed by the mcp_servers volatile section. // // WithMCPServerStatuses 将 MCP server 连接状态快照注入 ctx, // 由 mcp_servers volatile section 消费. func WithMCPServerStatuses(ctx context.Context, statuses []MCPServerStatus) context.Context { return context.WithValue(ctx, mcpServersCtxKey{}, statuses) } // MCPServerStatusesFromCtx reads the MCP server status snapshot from ctx. // Returns nil if not injected (volatile section then yields empty string // and the block drops out). // // MCPServerStatusesFromCtx 从 ctx 读取 MCP server 状态快照. 未注入时返回 // nil (volatile section 返回空串, 对应 block 被过滤). func MCPServerStatusesFromCtx(ctx context.Context) []MCPServerStatus { v, _ := ctx.Value(mcpServersCtxKey{}).([]MCPServerStatus) return v }