mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 12:34:27 +00:00
feat: release v0.3.3 — Hermes adapter, context offload, core refactor
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user