Files
TencentDB-Agent-Memory/src/core/persona/persona-trigger.ts
T
2026-06-11 14:56:11 +08:00

137 lines
5.0 KiB
TypeScript

/**
* PersonaTrigger: determines whether to trigger persona generation.
* Implements the 5 trigger conditions from the legacy system.
*/
import { CheckpointManager } from "../../utils/checkpoint.js";
import { stripSceneNavigation } from "../scene/scene-navigation.js";
import type { StorageAdapter } from "../storage/adapter.js";
import { StoragePaths } from "../storage/types.js";
import type { Logger } from "../types.js";
const TAG = "[memory-tdai] [trigger]";
type TriggerLogger = Logger;
export interface TriggerResult {
should: boolean;
reason: string;
}
export class PersonaTrigger {
private dataDir: string;
private interval: number;
private logger: TriggerLogger | undefined;
private storage: StorageAdapter | undefined;
constructor(opts: { dataDir: string; interval: number; logger?: TriggerLogger; storage?: StorageAdapter }) {
this.dataDir = opts.dataDir;
this.interval = opts.interval;
this.logger = opts.logger;
this.storage = opts.storage;
}
async shouldGenerate(): Promise<TriggerResult> {
const cpManager = new CheckpointManager(this.dataDir, undefined, this.storage);
const cp = await cpManager.read();
this.logger?.debug?.(`${TAG} Evaluating: total_processed=${cp.total_processed}, last_persona_at=${cp.last_persona_at}, memories_since=${cp.memories_since_last_persona}, scenes=${cp.scenes_processed}`);
// Priority 1: Agent explicitly requested persona update
if (cp.request_persona_update) {
const result: TriggerResult = {
should: true,
reason: `主动请求: ${cp.persona_update_reason || "Agent 请求更新"}`,
};
this.logger?.debug?.(`${TAG} Trigger P1 (explicit request): ${result.reason}`);
return result;
}
const hasSceneFiles = await this.hasSceneFiles();
const hasPersonaBody = await this.hasPersonaBody();
const hasGeneratedPersona = cp.last_persona_at > 0 || cp.last_persona_time !== "" || hasPersonaBody;
// Priority 2: Cold start — first extraction done, no persona yet, has scene files
if (
cp.scenes_processed > 0 &&
!hasGeneratedPersona &&
hasSceneFiles
) {
const result: TriggerResult = { should: true, reason: "首次冷启动:首次提取完成且有场景文件" };
this.logger?.debug?.(`${TAG} Trigger P2 (cold start): scenes_processed=${cp.scenes_processed}, total_processed=${cp.total_processed}`);
return result;
}
// Priority 2.5: Recovery — persona was generated before but persona.md body
// is now empty (corrupted/missing). Regenerate to restore.
if (
hasGeneratedPersona &&
hasSceneFiles &&
!hasPersonaBody
) {
const result: TriggerResult = { should: true, reason: "恢复:persona.md 正文丢失或为空,需要重新生成" };
this.logger?.debug?.(`${TAG} Trigger P2.5 (recovery): last_persona_time=${cp.last_persona_time || "(empty)"}, persona body missing`);
return result;
}
// Priority 3: First scene block extraction
if (cp.scenes_processed === 1 && cp.memories_since_last_persona > 0) {
const result: TriggerResult = { should: true, reason: "首次 Scene Block 提取完成" };
this.logger?.debug?.(`${TAG} Trigger P3 (first scene): scenes_processed=${cp.scenes_processed}`);
return result;
}
// Priority 4: Reached threshold
if (cp.memories_since_last_persona >= this.interval) {
const result: TriggerResult = {
should: true,
reason: `达到阈值: ${cp.memories_since_last_persona} >= ${this.interval}`,
};
this.logger?.debug?.(`${TAG} Trigger P4 (threshold): ${result.reason}`);
return result;
}
this.logger?.debug?.(`${TAG} No trigger conditions met`);
return { should: false, reason: "" };
}
private async hasSceneFiles(): Promise<boolean> {
try {
if (this.storage) {
const files = await this.storage.readdirNames(StoragePaths.sceneBlocksDir, ".md");
return files.length > 0;
}
const fs = await import("node:fs/promises");
const path = await import("node:path");
const blocksDir = path.default.join(this.dataDir, "scene_blocks");
const files = await fs.default.readdir(blocksDir);
return files.some((f) => f.endsWith(".md"));
} catch {
return false;
}
}
/**
* Check whether persona.md has a non-empty body (excluding scene navigation).
* Returns false if the file doesn't exist, is empty, or only contains
* scene navigation (no actual persona content).
*/
private async hasPersonaBody(): Promise<boolean> {
try {
let raw: string | null;
if (this.storage) {
raw = await this.storage.readFile(StoragePaths.persona);
} else {
const fs = await import("node:fs/promises");
const path = await import("node:path");
raw = await fs.default.readFile(path.default.join(this.dataDir, "persona.md"), "utf-8");
}
if (!raw) return false;
const body = stripSceneNavigation(raw).trim();
return body.length > 0;
} catch {
return false;
}
}
}