mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
原实现对所有工具结果(含 read)在下一次请求即替换为约 4K 的首尾存根, 不看上下文压力,导致模型看不到大文件中段;后台逐条语义摘要又反复改写 历史、打碎 prompt 缓存。参照 Anthropic 上下文编辑、Manus 与 JetBrains 观察遮蔽研究重构为: - L2:上下文达到阈值时在 turn_end 批量提交 Pi 原生 context_edit,把最近 keepRecentTurns 轮之外的旧 toolResult 换成确定性占位符;trigger/target 回差与 minClearTokens 保证少触发、一次清够;原始条目保留,可经 context_tree_query 取回 - L3:空闲 agent_settled 且超过绝对阈值时调用一次 ctx.compact - 阈值按绝对 token 计并受窗口比例约束,默认针对 1M 窗口(DeepSeek V4.1 Flash):遮蔽 200K→100K、整段摘要 400K - 删除逐条存根、语义 worker、soft/hard/target 比例与自行取消 Pi 压缩的门控; Pi 原生阈值压缩保留为最终安全网 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { estimateTokens } from "@earendil-works/pi-coding-agent";
|
|
import type { ProjectionSnapshot, UsageBaseline } from "./types.js";
|
|
|
|
export function timestampOf(value: unknown): number {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string") {
|
|
const parsed = Date.parse(value);
|
|
if (Number.isFinite(parsed)) return parsed;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function contentText(content: unknown): string {
|
|
if (typeof content === "string") return content;
|
|
if (!Array.isArray(content)) return "";
|
|
return content
|
|
.flatMap((part) => part && typeof part === "object" && (part as any).type === "text" && typeof (part as any).text === "string"
|
|
? [(part as any).text]
|
|
: [])
|
|
.join("\n");
|
|
}
|
|
|
|
function hash(value: string): string {
|
|
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
}
|
|
|
|
function messageKey(message: any): string {
|
|
const role = String(message?.role ?? "unknown");
|
|
const timestamp = timestampOf(message?.timestamp);
|
|
if (role === "toolResult") return `${role}:${message.toolCallId ?? ""}:${timestamp}`;
|
|
if (role === "assistant") {
|
|
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
const ids = blocks.flatMap((block: any) => block?.type === "toolCall" ? [block.id] : []).join(",");
|
|
return `${role}:${timestamp}:${ids}:${blocks.length}`;
|
|
}
|
|
return `${role}:${timestamp}:${typeof message?.content === "string" ? message.content.length : (message?.content?.length ?? 0)}`;
|
|
}
|
|
|
|
/**
|
|
* Incremental context projection: tracks the branch's message list, estimates
|
|
* tokens, and maintains the usage-backed effective-token baseline. Projection
|
|
* is identity — pi-compaction never rewrites messages on this path; context
|
|
* pressure reduction happens through append-only context_edit entries.
|
|
*/
|
|
export class SegmentIndex {
|
|
private generation = 0;
|
|
private usageBaseline?: UsageBaseline;
|
|
private sourceMessages: any[] = [];
|
|
private sourceKeys: string[] = [];
|
|
private projectedMessages: any[] = [];
|
|
private rawEstimates: number[] = [];
|
|
private effectiveEstimates: number[] = [];
|
|
private cached?: ProjectionSnapshot;
|
|
|
|
readonly sessionId: string;
|
|
|
|
constructor(sessionId: string) {
|
|
this.sessionId = sessionId;
|
|
}
|
|
|
|
hydrate(): void {
|
|
this.reset();
|
|
this.generation++;
|
|
}
|
|
|
|
reset(): void {
|
|
this.generation = 0;
|
|
this.usageBaseline = undefined;
|
|
this.sourceMessages = [];
|
|
this.sourceKeys = [];
|
|
this.projectedMessages = [];
|
|
this.rawEstimates = [];
|
|
this.effectiveEstimates = [];
|
|
this.cached = undefined;
|
|
}
|
|
|
|
recordUsage(tokens: number, messageEstimate: number, environmentHash: string): void {
|
|
if (!Number.isFinite(tokens) || tokens <= 0) return;
|
|
this.usageBaseline = { tokens, messageEstimate, environmentHash };
|
|
this.cached = undefined;
|
|
}
|
|
|
|
project(messages: readonly any[], params: {
|
|
environmentHash: string;
|
|
environmentTokens: number;
|
|
contextWindow: number;
|
|
maskedResults: number;
|
|
reclaimedTokens: number;
|
|
}): ProjectionSnapshot {
|
|
const sameTail = messages.length === this.sourceMessages.length
|
|
&& messages.length > 0
|
|
&& messageKey(messages[messages.length - 1]) === this.sourceKeys[this.sourceKeys.length - 1];
|
|
if (this.cached && sameTail && this.cached.environmentHash === params.environmentHash) {
|
|
if (this.cached.maskedResults === params.maskedResults
|
|
&& this.cached.reclaimedTokens === params.reclaimedTokens) {
|
|
return this.cached;
|
|
}
|
|
// Masking stats changed but the message list did not: publish a new
|
|
// immutable snapshot object instead of re-projecting or mutating.
|
|
this.cached = {
|
|
...this.cached,
|
|
maskedResults: params.maskedResults,
|
|
reclaimedTokens: params.reclaimedTokens,
|
|
rawTokens: this.cached.effectiveTokens + params.reclaimedTokens,
|
|
};
|
|
return this.cached;
|
|
}
|
|
|
|
const appendOnly = this.sourceMessages.length > 0
|
|
&& messages.length >= this.sourceMessages.length
|
|
&& messageKey(messages[this.sourceMessages.length - 1]) === this.sourceKeys[this.sourceKeys.length - 1]
|
|
&& this.cached?.environmentHash === params.environmentHash;
|
|
|
|
let start = 0;
|
|
if (!appendOnly) {
|
|
this.sourceMessages = [];
|
|
this.sourceKeys = [];
|
|
this.projectedMessages = [];
|
|
this.rawEstimates = [];
|
|
this.effectiveEstimates = [];
|
|
} else {
|
|
start = this.sourceMessages.length;
|
|
}
|
|
|
|
for (let index = start; index < messages.length; index++) {
|
|
const message = messages[index];
|
|
this.sourceMessages.push(message);
|
|
this.sourceKeys.push(messageKey(message));
|
|
this.projectedMessages.push(message);
|
|
this.rawEstimates.push(estimateTokens(message as any));
|
|
this.effectiveEstimates.push(estimateTokens(message as any));
|
|
}
|
|
|
|
const effectiveMessageTokens = this.effectiveEstimates.reduce((sum, value) => sum + value, 0);
|
|
const baseline = this.usageBaseline;
|
|
const usageBacked = baseline?.environmentHash === params.environmentHash;
|
|
const effectiveTokens = usageBacked
|
|
? Math.max(0, baseline.tokens + effectiveMessageTokens - baseline.messageEstimate)
|
|
: effectiveMessageTokens + params.environmentTokens;
|
|
const confidence = usageBacked ? "usage-backed" as const : "estimated" as const;
|
|
const lowerBound = Math.max(0, Math.floor(effectiveTokens * (usageBacked ? 0.98 : 0.9)));
|
|
const upperBound = Math.ceil(effectiveTokens * (usageBacked ? 1.05 : 1.2));
|
|
const fingerprint = hash(`${this.sessionId}:${this.generation}:${params.environmentHash}:${this.sourceKeys.join("|")}`);
|
|
|
|
this.cached = {
|
|
sessionId: this.sessionId,
|
|
generation: this.generation,
|
|
fingerprint,
|
|
environmentHash: params.environmentHash,
|
|
messages: this.projectedMessages,
|
|
messageEstimate: effectiveMessageTokens,
|
|
rawTokens: effectiveTokens + params.reclaimedTokens,
|
|
effectiveTokens,
|
|
reclaimedTokens: params.reclaimedTokens,
|
|
contextWindow: params.contextWindow,
|
|
confidence,
|
|
lowerBound,
|
|
upperBound,
|
|
segmentCount: this.sourceMessages.length,
|
|
maskedResults: params.maskedResults,
|
|
};
|
|
return this.cached;
|
|
}
|
|
|
|
getSnapshot(): ProjectionSnapshot | undefined {
|
|
return this.cached;
|
|
}
|
|
}
|