feat: Add memory_apply_patch for non-Anthropic models (#149)

This commit is contained in:
Kevin Lin
2025-12-02 17:21:37 -08:00
committed by GitHub
parent a33160e029
commit baf3faf969
3 changed files with 150 additions and 3 deletions

View File

@@ -89,14 +89,44 @@ export async function createAgent(
getServerToolName(name),
);
const baseMemoryTool = modelHandle.startsWith("anthropic/")
? "memory"
: "memory_apply_patch";
const defaultBaseTools = baseTools ?? [
"memory",
baseMemoryTool,
"web_search",
"conversation_search",
"fetch_webpage",
];
const toolNames = [...serverToolNames, ...defaultBaseTools];
let toolNames = [...serverToolNames, ...defaultBaseTools];
// Fallback: if server doesn't have memory_apply_patch, use legacy memory tool
if (toolNames.includes("memory_apply_patch")) {
try {
const resp = await client.tools.list({ name: "memory_apply_patch" });
const hasMemoryApplyPatch =
Array.isArray(resp.items) && resp.items.length > 0;
if (!hasMemoryApplyPatch) {
console.warn(
"memory_apply_patch tool not found on server; falling back to 'memory' tool",
);
toolNames = toolNames.map((n) =>
n === "memory_apply_patch" ? "memory" : n,
);
}
} catch (err) {
// If the capability check fails for any reason, conservatively fall back to 'memory'
console.warn(
`Unable to verify memory_apply_patch availability (falling back to 'memory'): ${
err instanceof Error ? err.message : String(err)
}`,
);
toolNames = toolNames.map((n) =>
n === "memory_apply_patch" ? "memory" : n,
);
}
}
// Load memory blocks from .mdx files
const defaultMemoryBlocks =
@@ -312,6 +342,8 @@ export async function createAgent(
enable_sleeptime: enableSleeptime,
});
// Note: Preflight check above falls back to 'memory' when 'memory_apply_patch' is unavailable.
// Apply updateArgs if provided (e.g., reasoningEffort, verbosity, etc.)
// Skip if updateArgs only contains context_window (already set in create)
if (updateArgs && Object.keys(updateArgs).length > 0) {

View File

@@ -3,4 +3,4 @@ The `loaded_skills` block has at least one skill loaded. You should:
1. Check if loaded skills are relevant for the current task.
2. For any skills that are irrelevant, unload them using the `memory` tool.
If the block will be empty after unloading, add a "[CURRENTLY EMPTY]" tag.
</system-reminder>
</system-reminder>

View File

@@ -149,6 +149,65 @@ export async function forceToolsetSwitch(
// Remove old Letta tools and add new ones
await unlinkToolsFromAgent(agentId);
await linkToolsToAgent(agentId);
// Ensure base memory tool uses memory_apply_patch instead of legacy memory
try {
const client = await getClient();
const agent = await client.agents.retrieve(agentId, {
include: ["agent.tools"],
});
const currentTools = agent.tools || [];
const mapByName = new Map(currentTools.map((t) => [t.name, t.id]));
// Determine which memory tool we want based on toolset
const desiredMemoryTool =
toolsetName === "default" ? "memory" : "memory_apply_patch";
const otherMemoryTool =
desiredMemoryTool === "memory" ? "memory_apply_patch" : "memory";
// Ensure desired memory tool is attached
let desiredId = mapByName.get(desiredMemoryTool);
if (!desiredId) {
const resp = await client.tools.list({ name: desiredMemoryTool });
desiredId = resp.items[0]?.id;
}
if (!desiredId) {
console.warn(
`Could not find tool id for ${desiredMemoryTool}. Keeping existing memory tool if present.`,
);
}
const otherId = mapByName.get(otherMemoryTool);
// Build new tool_ids: add desired memory tool, remove the other if present
const currentIds = currentTools
.map((t) => t.id)
.filter((id): id is string => typeof id === "string");
const newIds = new Set(currentIds);
// Only swap if we have a valid desired tool id; otherwise keep existing
if (desiredId) {
if (otherId) newIds.delete(otherId);
newIds.add(desiredId);
}
// Update tool_rules: rewrite any rules targeting the other tool to the desired tool
const updatedRules = (agent.tool_rules || []).map((r) =>
r.tool_name === otherMemoryTool
? { ...r, tool_name: desiredMemoryTool }
: r,
);
await client.agents.update(agentId, {
tool_ids: Array.from(newIds),
tool_rules: updatedRules,
});
} catch (err) {
console.warn(
`Warning: Failed to enforce memory_apply_patch base tool: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
/**
@@ -193,6 +252,62 @@ export async function switchToolsetForModel(
await unlinkToolsFromAgent(agentId);
await linkToolsToAgent(agentId);
// Ensure base memory tool uses memory_apply_patch instead of legacy memory
try {
const agentWithTools = await client.agents.retrieve(agentId, {
include: ["agent.tools"],
});
const currentTools = agentWithTools.tools || [];
const mapByName = new Map(currentTools.map((t) => [t.name, t.id]));
// Determine which memory tool we want based on provider
const desiredMemoryTool = isOpenAIModel(resolvedModel)
? "memory_apply_patch"
: (await import("./manager")).isGeminiModel(resolvedModel)
? "memory_apply_patch"
: "memory";
const otherMemoryTool =
desiredMemoryTool === "memory" ? "memory_apply_patch" : "memory";
// Ensure desired memory tool attached
let desiredId = mapByName.get(desiredMemoryTool);
if (!desiredId) {
const resp = await client.tools.list({ name: desiredMemoryTool });
desiredId = resp.items[0]?.id;
}
if (!desiredId) {
console.warn(
`Could not find tool id for ${desiredMemoryTool}. Keeping existing memory tool if present.`,
);
}
const otherId = mapByName.get(otherMemoryTool);
const currentIds = currentTools
.map((t) => t.id)
.filter((id): id is string => typeof id === "string");
const newIds = new Set(currentIds);
if (desiredId) {
if (otherId) newIds.delete(otherId);
newIds.add(desiredId);
}
const updatedRules = (agentWithTools.tool_rules || []).map((r) =>
r.tool_name === otherMemoryTool
? { ...r, tool_name: desiredMemoryTool }
: r,
);
await client.agents.update(agentId, {
tool_ids: Array.from(newIds),
tool_rules: updatedRules,
});
} catch (err) {
console.warn(
`Warning: Failed to enforce memory_apply_patch base tool: ${err instanceof Error ? err.message : String(err)}`,
);
}
const { isGeminiModel } = await import("./manager");
const toolsetName = isOpenAIModel(resolvedModel)
? "codex"