Files
2026-07-13 13:39:12 +08:00

166 lines
5.4 KiB
TypeScript

import { retrieveMemories } from "@/lib/memory/retrieval";
import { getMemorySettings, DEFAULT_MEMORY_SETTINGS, toMemoryRetrievalConfig } from "@/lib/memory/settings";
import { injectMemory, shouldInjectMemory } from "@/lib/memory/injection";
import { injectSkills } from "@/lib/skills/injection";
import { FORMATS } from "../../translator/formats.ts";
import { detectCachingContext } from "../../services/compression/cachingAware.ts";
export function getSkillsProviderForFormat(format: string): "openai" | "anthropic" | "google" | "other" {
switch (format) {
case FORMATS.CLAUDE:
return "anthropic";
case FORMATS.GEMINI:
return "google";
default:
return "openai";
}
}
export async function injectMemoryAndSkills({
body,
memoryOwnerId,
provider,
effectiveModel,
sourceFormat,
targetFormat,
backgroundReason,
log,
}: {
body: Record<string, unknown>;
memoryOwnerId: string | null;
provider: string;
effectiveModel: string;
sourceFormat: string;
targetFormat: string;
backgroundReason: string | null;
log: unknown;
}) {
const memorySettings = memoryOwnerId
? await getMemorySettings().catch(() => DEFAULT_MEMORY_SETTINGS)
: null;
if (
memoryOwnerId &&
memorySettings &&
shouldInjectMemory(body as Parameters<typeof shouldInjectMemory>[0], {
enabled: memorySettings.enabled && memorySettings.maxTokens > 0,
})
) {
try {
const lastUserQuery = ((): string => {
const NON_USER_TYPES = new Set([
"function_call",
"function_call_output",
"tool_call",
"tool_call_output",
"reasoning",
"computer_call",
"computer_call_output",
"web_search_call",
"file_search_call",
]);
function pickFrom(arr: unknown[]): string {
for (let i = arr.length - 1; i >= 0; i--) {
const item = arr[i] as Record<string, unknown> | undefined;
if (!item) continue;
if (item.role !== undefined && item.role !== "user") continue;
if (item.role === undefined && typeof item.type === "string") {
if (NON_USER_TYPES.has(item.type)) continue;
}
const content = item.content ?? item.text;
if (typeof content === "string" && content.trim().length > 0) {
return content;
}
if (Array.isArray(content)) {
const parts: string[] = [];
for (const p of content) {
if (typeof p === "string") {
parts.push(p);
} else if (p && typeof p === "object") {
const pp = p as Record<string, unknown>;
const ptype = typeof pp.type === "string" ? pp.type : "";
if (
ptype &&
ptype !== "text" &&
ptype !== "input_text" &&
ptype !== "output_text"
) {
continue;
}
const t = pp.text ?? pp.input_text;
if (typeof t === "string") parts.push(t);
}
}
if (parts.length > 0) return parts.join(" ").trim();
}
}
return "";
}
if (Array.isArray(body.messages)) {
const r = pickFrom(body.messages);
if (r) return r;
}
if (Array.isArray(body.input)) {
const r = pickFrom(body.input);
if (r) return r;
}
return "";
})();
const memories = await retrieveMemories(
memoryOwnerId,
toMemoryRetrievalConfig(memorySettings, { query: lastUserQuery })
);
if (memories.length > 0) {
// #3890: when the client uses prompt caching (cache_control breakpoints), inject
// memory cache-safely (before the last user message) so the per-query memory text
// does not poison the cacheable prefix and force a cache miss on every turn.
const cacheSafe = detectCachingContext(body, { provider, targetFormat }).hasCacheControl;
const injected = injectMemory(
body as Parameters<typeof injectMemory>[0],
memories,
provider,
{ cacheSafe }
);
body = injected as typeof body;
log?.debug?.("MEMORY", `Injected ${memories.length} memories for key=${memoryOwnerId}`);
}
} catch (memErr) {
log?.debug?.(
"MEMORY",
`Memory injection skipped: ${memErr instanceof Error ? memErr.message : String(memErr)}`
);
}
}
if (memoryOwnerId && memorySettings?.skillsEnabled) {
const existingTools = Array.isArray(body.tools) ? body.tools : [];
const mergedTools = injectSkills({
provider: getSkillsProviderForFormat(sourceFormat),
existingTools,
apiKeyId: memoryOwnerId,
model: typeof effectiveModel === "string" ? effectiveModel : undefined,
sourceFormat,
targetFormat,
backgroundReason,
messages: Array.isArray(body.messages)
? body.messages
: Array.isArray(body.input)
? body.input
: undefined,
});
if (mergedTools.length > existingTools.length) {
body = {
...body,
tools: mergedTools,
};
log?.debug?.("SKILLS", `Injected ${mergedTools.length - existingTools.length} skills`);
}
}
return { body, memorySettings };
}