mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-04 13:21:17 +00:00
* 🧠 feat: Memory Agent Capability with Inline Tools and Ephemeral Badge
Add `AgentCapabilities.memory`, which expands into the inline set_memory/delete_memory tool pair (mirroring the execute_code expansion via registerMemoryTools) when a run-level memoryAvailable gate holds: capability enabled, memory configured, MEMORIES.USE permission, and personalization not opted out. Surfaces the memory artifact as an attachment in the agents tool-end callback.
Adds the ephemeral path (TEphemeralAgent.memory, load/added agent tool injection), a fully-gated memory badge plus tools-dropdown entry, the agent-builder Memory toggle with form round-trip, and a mock e2e test asserting the badge reaches the request payload. Additive to and independent of the existing post-turn memory extraction agent.
* 🩹 fix: Address Codex review on memory capability (gating, validKeys, usage guard)
- Strip the memory capability from the served agents capabilities when memory is not configured/enabled, so the badge, tools dropdown, agent-builder toggle, and backend capability gate stay consistent instead of exposing an inert toggle on default installs (where MEMORIES.USE defaults true).
- Surface configured memory.validKeys in the inline tool definitions so the model is told the allowed keys up front, matching the runtime createMemoryTool schema.
- Append a strict explicit-request usage guard to the agent instructions when inline memory tools are registered, preserving the memory-agent's privacy behavior.
- Add AppService tests covering memory-capability stripping.
* ✅ test: Update AppService capability snapshots for memory strip
AppService now strips the memory capability from the served agents defaults when no memory block is configured; update the spec's expected capability lists to defaultAgentCapabilitiesWithoutMemory for the no-memory-config cases.
* 🛡️ fix: Address Codex re-review on memory capability (round 2)
- Strip the memory capability from the FINAL served agents config, not just defaults; loadEndpoints reparses any endpoints.agents block, so memory was still exposed in that common shape (packages/data-schemas/src/app/service.ts) + regression test.
- Re-check the full memory gate (config, opt-out, MEMORIES.USE) inside handleTools before constructing set_memory/delete_memory, so an unsolicited tool call from a model/custom endpoint can't bypass the runtime gates (api/app/clients/tools/util/handleTools.js).
- Restore the persisted memory toggle for model-spec conversations via applyModelSpecEphemeralAgent (client/src/utils/endpoints.ts).
- Clear LAST_MEMORY_TOGGLE_ on logout and clear-all-chats so a stale memory preference can't leak across users on a shared browser (client/src/utils/localStorage.ts).
* 🧠 fix: Address Codex re-review on memory capability (round 3)
- Serialize set_memory writes and advance a running token total inside createMemoryTool, so parallel batched calls in one event-driven turn can't each pass the limit check against a stale total and collectively exceed memory.tokenLimit (packages/api/src/agents/memory.ts) + tests.
- Inject the keyed memory context (withKeys) instead of withoutKeys when the running agent has the inline memory capability, so delete_memory has a visible key to target (api/server/controllers/agents/client.js).
* 🔐 fix: Address Codex re-review on memory capability (round 4)
- Detect inline memory by tool NAME (set_memory/delete_memory) across an initialized agent's tools + toolDefinitions, since the 'memory' marker is expanded at init and the prior string check never matched; inject the keyed memory context for any primary OR sub-agent that carries the inline memory tools (api/server/controllers/agents/client.js).
- Enforce memory WRITE permissions in the inline tool gate: set_memory requires CREATE+UPDATE and delete_memory requires UPDATE (matching the REST memory routes), so a USE-only role can't mutate/delete memories via agent tool calls (api/app/clients/tools/util/handleTools.js).
* 🔒 fix: Address Codex re-review on memory capability (round 5)
- Gate inline memory registration (memoryAvailable) on the memory WRITE permissions (USE+CREATE+UPDATE), so a read-only-memory role no longer has set_memory/delete_memory shown to the model only for the runtime loader to refuse them (api/server/services/Endpoints/agents/initialize.js).
- Enforce the per-agent memory opt-in at execution: handleTools now refuses to construct set_memory/delete_memory unless the agent actually declared them (toolDefinitions/tools), blocking hallucinated/undeclared memory tool calls from mutating memory.
- Fail closed when getFormattedMemories errors with a configured tokenLimit, instead of writing as if storage were empty and bypassing the cap (api/app/clients/tools/util/handleTools.js).
* 🩹 fix: Address Codex re-review on memory capability (round 6)
- Fix a P1 regression from the prior round: the execution-context agent keeps the raw 'memory' capability marker (not the expanded set_memory/delete_memory names), so the opt-in check now matches the marker. This restores memory writes/deletes AND avoids hijacking an MCP tool that merely shares the set_memory/delete_memory name (api/app/clients/tools/util/handleTools.js).
- Count repeated set_memory writes to the same key as replacements, not additions, against tokenLimit — set_memory upserts, so a same-key rewrite swaps its prior token contribution instead of double-counting (packages/api/src/agents/memory.ts) + test.
- Gate the memory badge, tools dropdown, and agent-builder toggle on the full memory write permissions (USE+CREATE+UPDATE) via a shared useHasMemoryAccess hook, so a read-only-memory role no longer sees an enabled Memory control the backend would refuse to wire up.
* 🧷 fix: Address Codex re-review on memory capability (round 7)
- Recognize inline memory across both execution-context agent shapes: initializeAgent now sets a LibreChat-only memoryToolsRegistered flag on the InitializedAgent, and the opt-in/detection checks accept that flag OR the raw 'memory' marker. Fixes memory failing for processAddedConvo agents (which store the initialized config, marker already expanded) while staying MCP-name-collision-safe (api/app/clients/tools/util/handleTools.js, packages/api/src/agents/initialize.ts, api/server/controllers/agents/client.js).
- Scope keyed memory context to memory-enabled agents only: useMemory now returns both keyed and unkeyed contexts, and buildMessages injects the keyed one (memory keys + token metadata) only to agents that can call delete_memory, while the primary/post-turn path keeps the unkeyed values — so a primary without memory tools no longer sees memory keys it doesn't need.
* 🔏 fix: Address Codex re-review on memory capability (round 8)
- Enforce memory size limits on inline writes: createMemoryTool now rejects keys over 1000 chars and values over memory.charLimit, matching the REST memory routes, so an inline-memory agent can't persist blobs the memory UI/API would reject (packages/api/src/agents/memory.ts, api/app/clients/tools/util/handleTools.js) + test.
- Recheck the agents 'memory' endpoint capability at execution time, so a stale/hallucinated set_memory/delete_memory call can't mutate memory after an admin removes the capability while the agent document still carries the marker (api/app/clients/tools/util/handleTools.js).
* ♻️ refactor: Move inline-memory backend logic into packages/api + share memory load
Workspace boundary: the inline-memory gating/detection logic that had crept into /api now lives in packages/api/src/agents/memory.ts (TS), with /api kept as thin wrappers.
- Add agentHasInlineMemoryTools, isMemoryToolAllowed, and buildInlineMemoryTool to packages/api; handleTools.js now calls buildInlineMemoryTool instead of constructing/gating the tools inline, and client.js imports agentHasInlineMemoryTools instead of redefining it.
- Optimize repeated memory loads: getRequestMemories memoizes getFormattedMemories per request (WeakMap keyed by req), so the run's memory-context load and every memory-enabled agent's set_memory token-usage load share a single DB fetch instead of one per agent.
* 🧠 fix: Invalidate request memory cache after inline writes
Inline set_memory/delete_memory now invalidate the request-scoped
getFormattedMemories cache on a successful write, so a later tool round
in the same response is seeded with the post-write usage total instead
of the stale pre-write one (multi-round writes no longer collectively
exceed tokenLimit, and a set after a delete is not over-counted). The
within-round sharing across multiple memory-enabled agents is preserved.
* 🧠 fix: Persist memory capability on saved agents; honor registration flag
- Add Tools.memory to the v1 systemTools allowlist so filterAuthorizedTools
no longer silently drops the memory marker when an agent with the Memory
capability is created/updated/duplicated through the builder (previously
the capability only worked for ephemeral chats, not persisted agents).
- agentHasInlineMemoryTools now honors an explicit memoryToolsRegistered
boolean before falling back to the raw `memory` marker, so an initialized
config whose registration was denied (memoryAvailable false) is not given
keyed memory context just because the marker survives in tools.
* 🧩 fix: Bring memory tool to parity with other ephemeral tools
- Add `memory` to the model-spec schema/type and honor `modelSpec.memory`
in both ephemeral paths (load.ts, added.ts) and the frontend spec
application, so admins can pre-enable Memory from a model spec exactly
like webSearch/fileSearch/executeCode.
- Add LAST_MEMORY_TOGGLE_ to the timestamped-storage cleanup list so stale
per-conversation memory toggles are purged on startup like the others.
- Hide the agent-builder Memory toggle for users who disabled memory in
personalization (memories === false), mirroring the chat badge's opt-out
gate, so the setting isn't shown as inert/misleading.
* ✅ test: Cover memory in applyModelSpecEphemeralAgent spec defaults
Update the exact-object assertions to include the new `memory` field and
add positive coverage that `modelSpec.memory` maps to the ephemeral
agent's `memory` flag. Fixes the shard 2/4 failure from 672a03b05.
226 lines
7.8 KiB
JavaScript
226 lines
7.8 KiB
JavaScript
const { logger } = require('@librechat/data-schemas');
|
|
const {
|
|
ADDED_AGENT_ID,
|
|
initializeAgent,
|
|
validateAgentModel,
|
|
resolveAgentScopedSkillIds,
|
|
resolveModelSpecSkillIds,
|
|
loadAddedAgent: loadAddedAgentFn,
|
|
} = require('@librechat/api');
|
|
const { isEphemeralAgentId } = require('librechat-data-provider');
|
|
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
|
|
const { getMCPServerTools } = require('~/server/services/Config');
|
|
const { canAuthorSkillFiles } = require('./skillDeps');
|
|
const db = require('~/models');
|
|
|
|
const loadAddedAgent = (params) =>
|
|
loadAddedAgentFn(params, { getAgent: db.getAgent, getMCPServerTools });
|
|
|
|
/**
|
|
* Process addedConvo for parallel agent execution.
|
|
* Creates a parallel agent config from an added conversation.
|
|
*
|
|
* When an added agent has no incoming edges, it becomes a start node
|
|
* and runs in parallel with the primary agent automatically.
|
|
*
|
|
* Edge cases handled:
|
|
* - Primary agent has edges (handoffs): Added agent runs in parallel with primary,
|
|
* but doesn't participate in the primary's handoff graph
|
|
* - Primary agent has agent_ids (legacy chain): Added agent runs in parallel with primary,
|
|
* but doesn't participate in the chain
|
|
* - Primary agent has both: Added agent is independent, runs parallel from start
|
|
*
|
|
* @param {Object} params
|
|
* @param {import('express').Request} params.req
|
|
* @param {import('express').Response} params.res
|
|
* @param {Object} params.endpointOption - The endpoint option containing addedConvo
|
|
* @param {Object} params.modelsConfig - The models configuration
|
|
* @param {Function} params.logViolation - Function to log violations
|
|
* @param {Function} params.loadTools - Function to load agent tools
|
|
* @param {Array} params.requestFiles - Request files
|
|
* @param {string} params.conversationId - The conversation ID
|
|
* @param {string} [params.parentMessageId] - The parent message ID for thread filtering
|
|
* @param {Set} params.allowedProviders - Set of allowed providers
|
|
* @param {Map} params.agentConfigs - Map of agent configs to add to
|
|
* @param {string} params.primaryAgentId - The primary agent ID
|
|
* @param {Object|undefined} params.userMCPAuthMap - User MCP auth map to merge into
|
|
* @param {Array} [params.accessibleSkillIds] - Full VIEW-accessible skill IDs for the user
|
|
* @param {Array} [params.editableSkillIds] - Full EDIT-accessible skill IDs for the user
|
|
* @param {boolean} [params.skillsCapabilityEnabled] - Whether endpoint Skills are enabled
|
|
* @param {boolean} [params.ephemeralSkillsToggle] - Per-request ephemeral Skills badge state
|
|
* @param {boolean} [params.skillCreateAllowed] - Whether the user can create Skills
|
|
* @param {Record<string, boolean>} [params.skillStates] - Per-user Skill active overrides
|
|
* @param {boolean} [params.defaultActiveOnShare] - Default active state for shared Skills
|
|
* @param {boolean} [params.codeEnvAvailable] - `execute_code` capability flag;
|
|
* forwarded verbatim to the added agent's `initializeAgent`. @see
|
|
* InitializeAgentParams.codeEnvAvailable for full semantics.
|
|
* @returns {Promise<{userMCPAuthMap: Object|undefined}>} The updated userMCPAuthMap
|
|
*/
|
|
const processAddedConvo = async ({
|
|
req,
|
|
res,
|
|
endpointOption,
|
|
modelsConfig,
|
|
logViolation,
|
|
loadTools,
|
|
requestFiles,
|
|
conversationId,
|
|
parentMessageId,
|
|
allowedProviders,
|
|
agentConfigs,
|
|
primaryAgentId,
|
|
primaryAgent,
|
|
userMCPAuthMap,
|
|
accessibleSkillIds = [],
|
|
editableSkillIds = [],
|
|
skillsCapabilityEnabled = false,
|
|
ephemeralSkillsToggle = false,
|
|
skillCreateAllowed = false,
|
|
skillStates,
|
|
defaultActiveOnShare,
|
|
codeEnvAvailable,
|
|
memoryAvailable,
|
|
}) => {
|
|
const addedConvo = endpointOption.addedConvo;
|
|
if (addedConvo == null) {
|
|
return { userMCPAuthMap };
|
|
}
|
|
|
|
logger.debug('[processAddedConvo] Processing added conversation', {
|
|
model: addedConvo.model,
|
|
agentId: addedConvo.agent_id,
|
|
endpoint: addedConvo.endpoint,
|
|
});
|
|
|
|
try {
|
|
const addedAgent = await loadAddedAgent({ req, conversation: addedConvo, primaryAgent });
|
|
if (!addedAgent) {
|
|
return { userMCPAuthMap };
|
|
}
|
|
|
|
const addedValidation = await validateAgentModel({
|
|
req,
|
|
res,
|
|
modelsConfig,
|
|
logViolation,
|
|
agent: addedAgent,
|
|
});
|
|
|
|
if (!addedValidation.isValid) {
|
|
logger.warn(
|
|
`[processAddedConvo] Added agent validation failed: ${addedValidation.error?.message}`,
|
|
);
|
|
return { userMCPAuthMap };
|
|
}
|
|
|
|
const selectedModelSpec =
|
|
addedConvo.spec && Array.isArray(req.config?.modelSpecs?.list)
|
|
? req.config.modelSpecs.list.find((modelSpec) => modelSpec.name === addedConvo.spec)
|
|
: null;
|
|
|
|
if (
|
|
addedAgent &&
|
|
isEphemeralAgentId(addedAgent.id) &&
|
|
selectedModelSpec &&
|
|
Object.hasOwn(selectedModelSpec, 'skills')
|
|
) {
|
|
if (selectedModelSpec.skills === true) {
|
|
addedAgent.skills_enabled = true;
|
|
delete addedAgent.skills;
|
|
} else if (selectedModelSpec.skills === false) {
|
|
addedAgent.skills_enabled = false;
|
|
addedAgent.skills = [];
|
|
} else if (Array.isArray(selectedModelSpec.skills)) {
|
|
const resolvedSkillIds = await resolveModelSpecSkillIds({
|
|
names: selectedModelSpec.skills,
|
|
accessibleSkillIds,
|
|
getSkillByName: db.getSkillByName,
|
|
});
|
|
addedAgent.skills_enabled = true;
|
|
addedAgent.skills = resolvedSkillIds.map((id) => id.toString());
|
|
}
|
|
}
|
|
|
|
const scopedSkillIds = resolveAgentScopedSkillIds({
|
|
agent: addedAgent,
|
|
accessibleSkillIds,
|
|
skillsCapabilityEnabled,
|
|
ephemeralSkillsToggle,
|
|
});
|
|
const scopedEditableSkillIds = resolveAgentScopedSkillIds({
|
|
agent: addedAgent,
|
|
accessibleSkillIds: editableSkillIds,
|
|
skillsCapabilityEnabled,
|
|
ephemeralSkillsToggle,
|
|
});
|
|
|
|
const addedConfig = await initializeAgent(
|
|
{
|
|
req,
|
|
res,
|
|
loadTools,
|
|
requestFiles,
|
|
conversationId,
|
|
parentMessageId,
|
|
agent: addedAgent,
|
|
endpointOption,
|
|
allowedProviders,
|
|
accessibleSkillIds: scopedSkillIds,
|
|
skillAuthoringAvailable: canAuthorSkillFiles({
|
|
agent: addedAgent,
|
|
scopedEditableSkillIds,
|
|
skillCreateAllowed,
|
|
skillsCapabilityEnabled,
|
|
ephemeralSkillsToggle,
|
|
}),
|
|
codeEnvAvailable,
|
|
memoryAvailable,
|
|
skillStates,
|
|
defaultActiveOnShare,
|
|
},
|
|
{
|
|
getFiles: db.getFiles,
|
|
getUserKey: db.getUserKey,
|
|
getMessages: db.getMessages,
|
|
getConvoFiles: db.getConvoFiles,
|
|
updateFilesUsage: db.updateFilesUsage,
|
|
getUserCodeFiles: db.getUserCodeFiles,
|
|
getUserKeyValues: db.getUserKeyValues,
|
|
getToolFilesByIds: db.getToolFilesByIds,
|
|
getCodeGeneratedFiles: db.getCodeGeneratedFiles,
|
|
filterFilesByAgentAccess,
|
|
listSkillsByAccess: db.listSkillsByAccess,
|
|
listAlwaysApplySkills: db.listAlwaysApplySkills,
|
|
getSkillByName: db.getSkillByName,
|
|
},
|
|
);
|
|
|
|
if (userMCPAuthMap != null) {
|
|
Object.assign(userMCPAuthMap, addedConfig.userMCPAuthMap ?? {});
|
|
} else {
|
|
userMCPAuthMap = addedConfig.userMCPAuthMap;
|
|
}
|
|
|
|
const addedAgentId = addedConfig.id || ADDED_AGENT_ID;
|
|
agentConfigs.set(addedAgentId, addedConfig);
|
|
|
|
// No edges needed - agent without incoming edges becomes a start node
|
|
// and runs in parallel with the primary agent automatically.
|
|
// This is independent of any edges/agent_ids the primary agent has.
|
|
|
|
logger.debug(
|
|
`[processAddedConvo] Added parallel agent: ${addedAgentId} (primary: ${primaryAgentId}, ` +
|
|
`primary has edges: ${!!endpointOption.edges}, primary has agent_ids: ${!!endpointOption.agent_ids})`,
|
|
);
|
|
|
|
return { userMCPAuthMap };
|
|
} catch (err) {
|
|
logger.error('[processAddedConvo] Error processing addedConvo for parallel agent', err);
|
|
return { userMCPAuthMap };
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
processAddedConvo,
|
|
ADDED_AGENT_ID,
|
|
};
|