/**
* @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);
}