package promptkit import ( "strings" ) // PromptBundle assembles a system prompt in the three-layer pattern: // static base + conditional sections + runtime reminders. // // PromptBundle 按三层模式装配 system prompt: 静态基座 + 条件块 + 运行时 reminder. // // All three methods are caller-defined. promptkit owns assembly + cache // boundary placement; content ownership stays with the implementor. // // 三个方法都由调用方实现. promptkit 只管装配 + cache 边界, 内容由实现者负责. // // See ADR-0005 § 2.3 for the design rationale (mirror cc, no new concepts). // // 设计依据见 ADR-0005 § 2.3 (mirror cc, 不发明新概念). type PromptBundle interface { // BaseSections returns the cache-stable always-on prefix. // Engine places cache_control marker after the last base section. // // 始终在的 cache 稳定前缀. engine 在末尾打 cache_control marker. BaseSections() []Section // ConditionalSections evaluates trigger predicates once at session // setup (NOT per-turn) and returns sections whose condition matched. // // 会话启动时判定一次 (不每轮跑), 返回触发段. ConditionalSections(ctx BuildContext) []Section // ReminderTriggers returns runtime reminder hooks. Engine fires them // on matching events and injects rendered content into messages array. // // 运行时 reminder hook. engine 在匹配事件触发, 注入 messages 数组. ReminderTriggers() []Trigger } // Section is a renderable prompt fragment. // // Section 是可渲染的提示词片段. type Section interface { // Name returns a stable identifier for debugging and cache-key derivation. // Implementations should return the same value across the process lifetime. // // 稳定标识符, 用于调试和 cache key 派生. 实现者应返回进程生命期不变的值. Name() string // Render produces the text content. Context carries cwd, model family, // language, etc. — engine-provided invariants, NOT domain content. // Empty string return is legal (signals "section produces nothing this time"). // // 产出文本. Context 带 cwd / model family / language 等 engine 提供的不变量, // 不带 domain 内容. 返回空串合法 (表示"本次此段不产出"). Render(ctx Context) (string, error) } // Trigger declares a runtime event handler that injects a system reminder. // // Trigger 是运行时事件处理器, 命中时注入 system reminder. type Trigger interface { // On returns the event type this trigger listens for. // // 监听的事件类型. On() TriggerEvent // Inject decides whether to fire on this specific event instance and // returns the reminder text if firing. Empty string = skip. // Returned text is wrapped by the engine in tags. // // 决定是否在此具体事件触发, 返回 reminder 文本; 空串 = 跳过. // 返回文本由 engine 包 标签. Inject(evt Event) string } // TriggerEvent enumerates engine-emitted event types Layer-3 triggers // can listen for. EventCustom is reserved for caller-defined extensions. // // TriggerEvent 列举 engine 发出的事件类型. EventCustom 给调用方扩展. type TriggerEvent int const ( // EventToolResult fires after any tool returns. // 任一工具返回后触发. EventToolResult TriggerEvent = iota // EventFileRead fires when a Read-style tool reads a file. // Read 类工具读完文件触发. EventFileRead // EventFileMTimeDrift fires when a previously-read file's mtime // has drifted since last cache snapshot. // 之前读过的文件 mtime 自上次缓存以来漂移时触发. EventFileMTimeDrift // EventPermissionWait fires while a permission request is pending. // 权限请求挂起时触发. EventPermissionWait // EventTokenBudgetLow fires when remaining token budget < 20% of cap. // 剩余 token budget < 上限 20% 时触发. EventTokenBudgetLow // EventTurnStart fires at the start of every turn (use sparingly — // per-turn injection inflates messages array fast). // 每轮开始触发 (慎用, 每轮注入让 messages 快速膨胀). EventTurnStart // EventCustom is reserved for caller-defined events. Use Event.Custom // field to discriminate sub-types. // 给调用方自定义, 用 Event.Custom 字段区分子类型. EventCustom ) // BuildContext carries session-level invariants supplied to // ConditionalSections at session setup. // // BuildContext 是 ConditionalSections 会话启动时收到的会话级不变量. // // Implementations should treat these as read-only. // // 实现者应视为只读. type BuildContext struct { // Cwd is the engine's working directory. // 引擎工作目录. Cwd string // ModelFamily is a coarse model classifier (e.g. "claude", "openai", // "qwen"). Used by sections that adjust phrasing per model family. // 模型粗分类 (e.g. "claude" / "openai" / "qwen"). 给按模型家族调整措辞的段用. ModelFamily string // Language is an optional language hint (e.g. "zh-CN", "en-US"). // Empty = caller didn't specify; section may default. // 可选语言提示. 空 = 调用方未指定, 段可默认. Language string // HasGitRepo: a git repository was detected at or above Cwd. // 在 Cwd 或祖先目录检测到 git 仓库. HasGitRepo bool // HasMCPServer: at least one MCP server is configured. // 至少配了一个 MCP server. HasMCPServer bool // HasFLYTOMD: a FLYTO.md file was found in any of the standard // search locations (project root, cwd, ~/.flyto/). // 在标准搜索位置 (项目根 / cwd / ~/.flyto/) 找到 FLYTO.md. HasFLYTOMD bool // Custom is for caller-defined predicates. Section implementations // type-assert keys they recognize. // 调用方自定义谓词. Section 实现按识别的 key 做类型断言. Custom map[string]any } // Context is supplied to Section.Render at render time. Currently aliased // to BuildContext; kept as a separate type so render-time vs setup-time // invariants can diverge later without breaking implementations. // // Context 是 Section.Render 时收到的不变量. 当前别名 BuildContext, 单独 // 命名让 render 时 vs 启动时不变量未来可独立演进而不破坏实现. type Context = BuildContext // Event carries trigger-time payload supplied to Trigger.Inject. // // Event 是 Trigger.Inject 时收到的 payload. type Event struct { // Type identifies which TriggerEvent fired. // 触发的事件类型. Type TriggerEvent // ToolName is set for EventToolResult / EventFileRead. // 给 EventToolResult / EventFileRead 用. ToolName string // FilePath is set for EventFileRead / EventFileMTimeDrift. // 给 EventFileRead / EventFileMTimeDrift 用. FilePath string // Payload carries event-specific data. Trigger implementations // type-assert keys they recognize. // 事件特定数据. Trigger 实现按识别的 key 做类型断言. Payload map[string]any // Custom is the discriminator for EventCustom sub-types. // EventCustom 子类型的区分字符串. Custom string } // Build assembles a final system prompt string from a PromptBundle. // // Build 装配 PromptBundle 成 system prompt 字符串. // // Returns: (assembled prompt, cache boundary byte offset, error). // cache boundary is the byte offset after Layer-1 (BaseSections) ends — // callers pass this to providers that support cache_control marker // placement (e.g. Anthropic API ephemeral cache). // // 返回: (装配后 prompt, cache 边界字节偏移量, error). cache 边界是 Layer-1 // (BaseSections) 结束后的字节偏移 — 调用方将其传给支持 cache_control marker // 的 provider (例如 Anthropic API ephemeral cache). // // Sections separated by "\n\n" (cc convention). If a Section.Render // returns empty string the section is skipped (no spacing emitted). // // 段间用 "\n\n" 分隔 (cc 惯例). Section.Render 返回空串则跳过该段 (不输出空白). // // CLEVER: cache boundary tracked by sb.Len() snapshot after last base // section. This requires Build to NOT trim trailing whitespace before // the snapshot — providers expect the marker at the byte boundary they // see in the wire payload, not the visual string boundary. // // CLEVER: cache 边界用 sb.Len() snapshot 在最后一个 base section 后捕获. // 这要求 Build 在 snapshot 前不修剪尾部空白 — provider 期望 marker 落在 // 它在 wire payload 中看到的字节边界, 不是视觉字符串边界. func Build(b PromptBundle, ctx BuildContext) (string, int, error) { if b == nil { return "", 0, ErrNilBundle } var sb strings.Builder // Layer 1: base sections (cache-stable prefix). // 第一层: 静态基座, cache 稳定前缀. for _, sec := range b.BaseSections() { text, err := sec.Render(ctx) if err != nil { return "", 0, &SectionRenderError{Layer: "base", Section: sec.Name(), Cause: err} } if text == "" { continue } sb.WriteString(text) sb.WriteString("\n\n") } cacheBoundary := sb.Len() // Layer 2: conditional sections (per-session-stable, after cache marker). // 第二层: 条件块 (会话级稳定, 在 cache marker 之后). for _, sec := range b.ConditionalSections(ctx) { text, err := sec.Render(ctx) if err != nil { return "", cacheBoundary, &SectionRenderError{Layer: "conditional", Section: sec.Name(), Cause: err} } if text == "" { continue } sb.WriteString(text) sb.WriteString("\n\n") } // Layer 3 (ReminderTriggers) is NOT assembled into system prompt; // engine consumes ReminderTriggers() separately and fires Inject() // at runtime, injecting results into the messages array. See // ADR-0005 § 2.3 for why reminders live outside system prompt // (cache stability). // // 第三层 (ReminderTriggers) 不装配进 system prompt, engine 单独消费 // 并在运行时触发 Inject() 注入 messages 数组. 为何 reminder 不进 // system prompt 见 ADR-0005 § 2.3 (cache 稳定性). return sb.String(), cacheBoundary, nil }