Files
TencentDB-Agent-Memory/src/offload/hooks/before-agent-start.ts
T
2026-05-28 14:32:52 +08:00

132 lines
5.3 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.debug?.(`[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.debug?.(`[context-offload] L1.5: New MMD created and activated: ${filename}`);
};
const reactivateMmd = async (contFile: string) => {
logger.debug?.(`[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.debug?.(`[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.debug?.("[context-offload] L1.5: No MMD needed (casual/short), clearing active MMD");
stateManager.setActiveMmd(null, null);
}
} else {
logger.debug?.(`[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);
}
}
}
}