/** * @author jackice * @date 2026-08-10 * * 纯逻辑模块:不依赖 pi 扩展运行时 API(仅 type-only 导入),便于单元测试。 * 行为对齐 omp 的 handoff 实现(packages/agent/src/compaction + session-handoff.ts)。 */ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { SessionEntry } from "@earendil-works/pi-coding-agent"; const FOCUS_BLOCK_START = "{{#if additionalFocus}}"; const FOCUS_BLOCK_END = "{{/if}}"; const FOCUS_PLACEHOLDER = "{{additionalFocus}}"; /** * 渲染 handoff 模板:无 additionalFocus 时移除整个条件块, * 有则替换块内占位符。 */ export function renderHandoffPrompt(template: string, additionalFocus?: string): string { const start = template.indexOf(FOCUS_BLOCK_START); if (start < 0) return template; const end = template.indexOf(FOCUS_BLOCK_END, start); if (end < 0) return template; const block = template.slice(start, end + FOCUS_BLOCK_END.length); if (!additionalFocus) return template.replace(block, ""); const rendered = block .replace(FOCUS_BLOCK_START, "") .replace(FOCUS_BLOCK_END, "") .replace(FOCUS_PLACEHOLDER, additionalFocus); return template.replace(block, rendered); } /** * 把 handoff 文档包装成注入新会话的上下文消息。 * 与 omp 的 createHandoffContext 保持一致。 */ export function wrapHandoffContext(document: string): string { return `\n${document}\n\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`; } /** * 生成 handoff 文件名(与 omp 一致:ISO 时间戳去掉冒号与点)。 */ export function createHandoffFileName(date = new Date()): string { const fileTimestamp = date.toISOString().replace(/[:.]/g, "-"); return `handoff-${fileTimestamp}.md`; } /** * 解析会话的 artifacts 目录(与 omp 一致:会话文件去掉 .jsonl 后缀的同名目录)。 * 内存会话(无 sessionFile)返回 null。 */ export function resolveArtifactsDir(sessionFile: string | undefined): string | null { if (!sessionFile) return null; if (!sessionFile.endsWith(".jsonl")) return null; return sessionFile.slice(0, -".jsonl".length); } // ── 自动留存决策(压缩前自动存盘) ───────────────────────────────────── /** * 判断自动留存是否启用:CLI flag 优先,其次配置文件,默认关闭。 */ export function isAutoSaveEnabled(flagValue: boolean | undefined, configValue: boolean | undefined): boolean { return flagValue ?? configValue ?? false; } /** * overflow 恢复会重试被中断的回合,跳过以免干扰主流程。 */ export function shouldSkipAutoSave(willRetry: boolean | undefined): boolean { return willRetry === true; } /** * 把 handoff 文档保存到会话 artifacts 目录,返回文件路径;无持久化会话时返回 null。 * 与 omp 存盘位置一致:.jsonl → 同名 / 目录。 */ export function saveHandoffDocumentTo(sessionFile: string | undefined, document: string): string | null { const artifactsDir = resolveArtifactsDir(sessionFile); if (!artifactsDir) return null; mkdirSync(artifactsDir, { recursive: true }); const filePath = join(artifactsDir, createHandoffFileName()); writeFileSync(filePath, `${document}\n`); return filePath; } // ── 会话序列化(compaction 感知,来自 pi 官方 handoff 示例) ──────────── function entryToMessage(entry: SessionEntry): AgentMessage | undefined { if (entry.type === "message") { return entry.message; } if (entry.type === "compaction") { return { role: "compactionSummary", summary: entry.summary, tokensBefore: entry.tokensBefore, timestamp: new Date(entry.timestamp).getTime(), }; } return undefined; } /** * 从分支提取交接用消息:无压缩时返回全部消息; * 有压缩时保留压缩摘要 + firstKeptEntryId 起保留的消息。 */ export function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] { let compactionIndex = -1; for (let i = branch.length - 1; i >= 0; i--) { if (branch[i].type === "compaction") { compactionIndex = i; break; } } if (compactionIndex < 0) { return branch.map(entryToMessage).filter((message) => message !== undefined); } const compaction = branch[compactionIndex]; const firstKeptIndex = compaction.type === "compaction" ? branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId) : -1; const compactedBranch = [ compaction, ...(firstKeptIndex >= 0 ? branch.slice(firstKeptIndex, compactionIndex) : []), ...branch.slice(compactionIndex + 1), ]; return compactedBranch.map(entryToMessage).filter((message) => message !== undefined); }