mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 12:34:27 +00:00
132 lines
5.2 KiB
TypeScript
132 lines
5.2 KiB
TypeScript
|
|
/**
|
||
|
|
* before_agent_start hook handler.
|
||
|
|
* Implements L1.5: Task completion judgment and active MMD management.
|
||
|
|
*
|
||
|
|
* Backend-only mode: local LLM judge has been removed.
|
||
|
|
* Only normalizeJudgment and handleTaskTransition are exported for use by index.ts.
|
||
|
|
*/
|
||
|
|
import { readMmd, writeMmd, deleteMmd, type StorageContext } from "../storage.js";
|
||
|
|
import type { OffloadStateManager } from "../state-manager.js";
|
||
|
|
import type { PluginLogger, TaskJudgment } from "../types.js";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Normalize a raw L1.5 judgment response (from backend)
|
||
|
|
* into a safe TaskJudgment with guaranteed boolean fields.
|
||
|
|
* Handles null/undefined values from backend fallback responses.
|
||
|
|
*/
|
||
|
|
export function normalizeJudgment(raw: Record<string, unknown>): TaskJudgment | null {
|
||
|
|
// All-null response from backend means "LLM unavailable" — treat as no judgment
|
||
|
|
if (raw.taskCompleted == null && raw.isContinuation == null && raw.isLongTask == null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
taskCompleted: Boolean(raw.taskCompleted),
|
||
|
|
isContinuation: Boolean(raw.isContinuation),
|
||
|
|
continuationMmdFile:
|
||
|
|
typeof raw.continuationMmdFile === "string" ? raw.continuationMmdFile : undefined,
|
||
|
|
newTaskLabel:
|
||
|
|
typeof raw.newTaskLabel === "string" ? raw.newTaskLabel : undefined,
|
||
|
|
isLongTask: Boolean(raw.isLongTask),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function handleTaskTransition(
|
||
|
|
stateManager: OffloadStateManager,
|
||
|
|
judgment: TaskJudgment,
|
||
|
|
logger: PluginLogger,
|
||
|
|
): Promise<void> {
|
||
|
|
const currentMmd = stateManager.getActiveMmdFile();
|
||
|
|
|
||
|
|
const ctx = stateManager.ctx;
|
||
|
|
|
||
|
|
const isEmptyShellMmd = async (filename: string | null): Promise<boolean> => {
|
||
|
|
if (!filename) return false;
|
||
|
|
try {
|
||
|
|
const content = await readMmd(ctx, filename);
|
||
|
|
if (!content) return false;
|
||
|
|
const trimmed = content.trim();
|
||
|
|
if (trimmed.includes("%%{")) return false;
|
||
|
|
const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
|
||
|
|
return lines.length <= 3;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const cleanupIfEmptyShell = async (oldFilename: string | null) => {
|
||
|
|
if (!oldFilename) return;
|
||
|
|
const isShell = await isEmptyShellMmd(oldFilename);
|
||
|
|
if (isShell) {
|
||
|
|
try {
|
||
|
|
await deleteMmd(ctx, oldFilename);
|
||
|
|
} catch {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const createNewMmd = async (label: string) => {
|
||
|
|
const num = await stateManager.nextMmdNumber();
|
||
|
|
const paddedNum = String(num).padStart(3, "0");
|
||
|
|
const filename = `${paddedNum}-${label}.mmd`;
|
||
|
|
logger.info(`[context-offload] L1.5: Creating new MMD: ${filename} (replacing ${currentMmd ?? "(none)"})`);
|
||
|
|
await cleanupIfEmptyShell(currentMmd);
|
||
|
|
stateManager.setActiveMmd(filename, label);
|
||
|
|
const initialMmd = `flowchart TD\n ${paddedNum}-N1["${label}"]\n`;
|
||
|
|
await writeMmd(ctx, filename, initialMmd);
|
||
|
|
logger.info(`[context-offload] L1.5: New MMD created and activated: ${filename}`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const reactivateMmd = async (contFile: string) => {
|
||
|
|
logger.info(`[context-offload] L1.5: Reactivating MMD: ${contFile} (current=${currentMmd ?? "(none)"})`);
|
||
|
|
if (currentMmd && currentMmd !== contFile) {
|
||
|
|
await cleanupIfEmptyShell(currentMmd);
|
||
|
|
}
|
||
|
|
const mmdId = contFile.replace(/^\d+-/, "").replace(/\.mmd$/, "");
|
||
|
|
stateManager.setActiveMmd(contFile, mmdId);
|
||
|
|
const existing = await readMmd(ctx, contFile);
|
||
|
|
if (existing === null) {
|
||
|
|
const prefixMatch = contFile.match(/^(\d+)-/);
|
||
|
|
const prefix = prefixMatch ? prefixMatch[1] : "000";
|
||
|
|
const initialMmd = `flowchart TD\n ${prefix}-N1["${mmdId}"]\n`;
|
||
|
|
await writeMmd(ctx, contFile, initialMmd);
|
||
|
|
logger.warn(`[context-offload] L1.5: Reactivated MMD file was missing, wrote initial template: ${contFile}`);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (judgment.taskCompleted) {
|
||
|
|
logger.info(`[context-offload] L1.5: Task COMPLETED — continuation=${judgment.isContinuation}, longTask=${judgment.isLongTask}, contFile=${judgment.continuationMmdFile ?? "N/A"}, newLabel=${judgment.newTaskLabel ?? "N/A"}`);
|
||
|
|
if (judgment.isContinuation && judgment.continuationMmdFile) {
|
||
|
|
await reactivateMmd(judgment.continuationMmdFile);
|
||
|
|
} else if (judgment.isLongTask && judgment.newTaskLabel) {
|
||
|
|
const currentLabel = currentMmd
|
||
|
|
? currentMmd.replace(/^\d+-/, "").replace(/\.mmd$/, "")
|
||
|
|
: null;
|
||
|
|
if (currentLabel !== judgment.newTaskLabel) {
|
||
|
|
await createNewMmd(judgment.newTaskLabel);
|
||
|
|
}
|
||
|
|
} else if (judgment.isContinuation && !judgment.continuationMmdFile) {
|
||
|
|
if (!currentMmd) {
|
||
|
|
stateManager.setActiveMmd(null, null);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
logger.info("[context-offload] L1.5: No MMD needed (casual/short), clearing active MMD");
|
||
|
|
stateManager.setActiveMmd(null, null);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
logger.info(`[context-offload] L1.5: Task NOT completed — continuation=${judgment.isContinuation}, longTask=${judgment.isLongTask}, current=${currentMmd ?? "(none)"}`);
|
||
|
|
if (judgment.isContinuation) {
|
||
|
|
if (!currentMmd && judgment.continuationMmdFile) {
|
||
|
|
await reactivateMmd(judgment.continuationMmdFile);
|
||
|
|
}
|
||
|
|
} else if (judgment.isLongTask && judgment.newTaskLabel) {
|
||
|
|
const currentLabel = currentMmd
|
||
|
|
? currentMmd.replace(/^\d+-/, "").replace(/\.mmd$/, "")
|
||
|
|
: null;
|
||
|
|
if (currentLabel !== judgment.newTaskLabel) {
|
||
|
|
await createNewMmd(judgment.newTaskLabel);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|