mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 20:34:30 +00:00
137 lines
5.0 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|