import type { ContextStrategy, ContextConfig, ReuseInputSample } from './types'; import type { createOpencodeClient } from '@opencode-ai/sdk'; type SdkClient = ReturnType; export declare class ContextEngine { /** contextId → TaskContext */ private registry; /** * contextId → 最后一次被读写的时间戳(ms)。 * registry 的唯二读取者是 `formatContextDetails()`(index.ts 注入详情)与 * `consumeMarkedContext()`(消息标记替换);两者都会刷新本时间戳。 * 定时清理据此判断条目是否已「过期未被合法消费」—— * 时间戳只在读写时更新,因此不会误删仍在被后续读取的条目。 */ private registryTouchedAt; /** alias → 前置子代理结果 */ private dependencyCache; /** * In-flight promise 去重 map:key = `${parentSessionId}::${windowSize}`。 * 同批次并发调用共享同一次 API 调用;promise settle 后自动从 Map 删除, * 确保跨批次串行调用重新调 API(无脏读)。 */ private inflight; private client; private config; constructor(client: SdkClient, config?: Partial); /** * 同步注册上下文占位并返回 contextId。 * 标记可立即注入到 output.args.description。 */ registerContext(args: { description: string; }): string; /** * 异步填充已注册的上下文(不阻塞工具启动)。 */ fillContextAsync(contextId: string, parentSessionId: string, args: { strategy: ContextStrategy; }): Promise; /** * summary 策略:读取相关文件正文并按 token 预算截断。 * 预算按文件数均摊,总注入不超过 summarizeMaxTokens;读取失败的文件保留 summary 回退。 * * 读取模式(config.summaryReadMode,缺省 'bounded'): * - 'bounded'(默认):按预算推导字节上限(见 prompt-budget.ts,默认 2000 tokens → 16KB), * open/read/close 部分读取 + 4 字节 lookahead 判定「是否超出读取上限」+ UTF-8 边界修正, * 再 truncateByTokens。ASCII/CJK 常规场景注入结果与旧全量读+截断**等价**; * 当文件超出读取上限且预算未触发 token 截断时(如纯 emoji 恰停在预算边界、或预算触发 1MiB 硬 cap), * 附加诚实读取截断标记「… [正文已按读取上限截断]」,不再宣称逐字节等价; * 与旧 truncateByTokens 的差异仅源于修复其在 4 字节非 CJK 字符边界切出孤立 surrogate 的缺陷。 * - 'full':保留旧行为——全量 readFileSync 后再按预算截断(保守选项,关闭激进裁剪)。 */ private attachTruncatedBodies; /** * 按字节上限有界读取 UTF-8 文本(默认 summary 模式使用)。 * 读取 min(size, byteLimit + SUMMARY_READ_LOOKAHEAD_BYTES) 字节: * - 实际读到的字节数 > byteLimit → 文件超出读取上限(lookahead 判定,比 fstat size 更诚实, * 覆盖读取期间文件被修改等边缘); * - 返回文本始终先 utf8TrimIncomplete 修正字符边界(避免 U+FFFD 与末尾未配对 surrogate)。 * fd 在正常与异常路径都会关闭(try/finally);open 失败由调用方 catch 兜底。 */ private readBoundedUtf8; /** * Phase B: 消费标记文本。 * 从消息文本中提取 contextId → 查 registry → 替换标记为格式化上下文。 * 返回替换后的完整文本,或 null(无标记或未找到上下文)。 */ consumeMarkedContext(messageText: string): string | null; /** * 格式化上下文详情(文件、决策、错误、依赖),不含主标题。 * 用于在 tool.execute.before 中直接追加到子代理 prompt。 */ formatContextDetails(contextId: string): string; /** * Phase C: 捕获子代理结果。 * 读取子 session 的最近 limit 条消息(有界)→ 提取关键信息 → 存入 dependencyCache。 * 同时返回质量判定输入(output / decisions / tokens)与复用预算采样(reuseInput)。 * dependencyPropagation 为 false 时跳过 dependencyCache 写入,不影响质量数据提取。 * * 有界/时序安全(复用预算核心): * - limit 20 防无界读;SDK 不保证顺序 → 仅当「可确认最新」时才有最近 assistant: * · 存在缺失 time.created 的 assistant(无法排序)→ 不可确认; * · 最大 time.created 并列多条 → 无法判定哪个更新 → 不可确认; * · 全部缺失 / 无 assistant → 不可确认。 * 不可确认 → reuseInput/tokens 均未知(绝不按数组顺序判安全,绝不捞旧带 tokens 消息回填); * - reuseInput 仅从可确认最新消息计算可信归一化输入(input+cache.read+cache.write, * 不叠加 output/reasoning);三字段均须为非负有限数,求和溢出 → 未知(绝不虚报 0); * - sampledAt 只取消息真实 time.created(可确认最新时必为数值), * 绝不回退当前时间伪装老用量新鲜; * - keyOutput 优先取可确认最新 assistant 的 text,无法确认/无 text 时 * 按 time.created 降序回退更早 assistant(摘要保持有界内最佳努力,不被未知采样破坏); * - 新鲜度边界(opts.notBefore = 本轮调用开始时间):采样消息创建时间早于调用边界 → * 该消息不属于当前调用(复用会话旧历史)→ reuseInput 标未知,绝不回填旧 low/high 到本轮。 * 仅在能确认最新时才有 sampledAt;样本时间不伪造。 */ captureResult(childSessionId: string, alias: string, agent: string, opts?: { notBefore?: number; parentSessionId?: string; }): Promise<{ output: string; decisions: number; tokens?: { input: number; output: number; }; reuseInput?: ReuseInputSample; } | null>; /** * 生成上下文标记文本,追加到 task description 末尾。 */ formatMarker(contextId: string): string; /** * 暴露合并后的 strategy 供 index.ts 使用 */ getStrategy(agentType: string): ContextStrategy; /** * 清理过期的依赖缓存(超过 10 分钟的条目)。 */ cleanupStaleDependencies(maxAgeMs?: number): void; /** 刷新 registry 条目的「最后读写时间」(内部使用) */ private touch; /** * 显式释放一个已消费完的上下文(供调用方在注入完成后主动回收)。 * 未注册的 contextId 为无害 no-op。 */ releaseContext(contextId: string): void; /** * 清理长期未被读写的 registry 条目(默认 20 分钟)。 * * 背景:`registry` 的唯一删除点原本在 `consumeMarkedContext()` 内,而该方法全仓库零调用 * (真实注入路径是 `formatContextDetails()`),于是每次委派都新增一条 TaskContext 且永不回收。 * * 为何用「最后读写时间」而非 `createdAt`:TaskContext 类型上没有任何时间字段 * (见 `context/types.ts`),故另开 `registryTouchedAt` 侧表记录;且清理**不能早于消费**—— * 时间戳在 `fillContextAsync` 结束时刷新、并在每次 `formatContextDetails()` / `consumeMarkedContext()` * 读取时刷新,因此仍会被读取的条目不会因登记时间久远而被误删。 * 阈值取 20 分钟(两倍于依赖缓存 TTL):一次委派从登记到注入详情是秒级, * 20 分钟已远超任何合法读写窗口,仅回收真正的泄漏。 * * @returns 被回收的条目数 */ cleanupStaleContexts(maxAgeMs?: number): number; /** 当前 registry 条目数(诊断/测试用,只读) */ get registrySize(): number; } export {}; //# sourceMappingURL=engine.d.ts.map