From ca32311b9a367cce7b8a13c459c258c2d4ca6d15 Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Tue, 10 Feb 2026 15:08:56 -0800 Subject: [PATCH] feat: allow users to specify via query to stip messages [LET-7392] (#9411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: allow users to specify via query to stip messages * chore: regenerate API SDK and OpenAPI spec [LET-7392] 🤖 Generated with [Letta Code](https://letta.com) Co-Authored-By: Ari Webb Co-Authored-By: Letta --------- Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com> Co-authored-by: Ari Webb Co-authored-by: Letta --- fern/openapi.json | 18 +++++ letta/server/rest_api/routers/v1/agents.py | 17 ++++- letta/services/agent_serialization_manager.py | 75 ++++++++++++------- 3 files changed, 82 insertions(+), 28 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index d588e9c3..2647f06e 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -4694,6 +4694,18 @@ "title": "Conversation Id" }, "description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history." + }, + { + "name": "scrub_messages", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, excludes all messages from the export. Useful for sharing agent configs without conversation history.", + "default": false, + "title": "Scrub Messages" + }, + "description": "If True, excludes all messages from the export. Useful for sharing agent configs without conversation history." } ], "requestBody": { @@ -32743,6 +32755,12 @@ ], "title": "Conversation Id", "description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history." + }, + "scrub_messages": { + "type": "boolean", + "title": "Scrub Messages", + "description": "If True, excludes all messages from the export. Useful for sharing agent configs without conversation history.", + "default": false } }, "type": "object", diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index 425df400..5436954e 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -297,6 +297,10 @@ async def export_agent( None, description="Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history.", ), + scrub_messages: bool = Query( + False, + description="If True, excludes all messages from the export. Useful for sharing agent configs without conversation history.", + ), # do not remove, used to autogeneration of spec # TODO: Think of a better way to export AgentFileSchema spec: AgentFileSchema | None = None, @@ -308,7 +312,12 @@ async def export_agent( if use_legacy_format: raise HTTPException(status_code=400, detail="Legacy format is not supported") actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) - agent_file_schema = await server.agent_serialization_manager.export(agent_ids=[agent_id], actor=actor, conversation_id=conversation_id) + agent_file_schema = await server.agent_serialization_manager.export( + agent_ids=[agent_id], + actor=actor, + conversation_id=conversation_id, + scrub_messages=scrub_messages, + ) return agent_file_schema.model_dump() @@ -323,6 +332,10 @@ class ExportAgentRequest(BaseModel): None, description="Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history.", ) + scrub_messages: bool = Field( + default=False, + description="If True, excludes all messages from the export. Useful for sharing agent configs without conversation history.", + ) @router.post("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent_with_skills") @@ -343,12 +356,14 @@ async def export_agent_with_skills( # Use defaults if no request body provided skills = request.skills if request else [] conversation_id = request.conversation_id if request else None + scrub_messages = request.scrub_messages if request else False agent_file_schema = await server.agent_serialization_manager.export( agent_ids=[agent_id], actor=actor, conversation_id=conversation_id, skills=skills, + scrub_messages=scrub_messages, ) return agent_file_schema.model_dump() diff --git a/letta/services/agent_serialization_manager.py b/letta/services/agent_serialization_manager.py index 900c293b..718be37f 100644 --- a/letta/services/agent_serialization_manager.py +++ b/letta/services/agent_serialization_manager.py @@ -189,7 +189,13 @@ class AgentSerializationManager: return sources, files - async def _convert_agent_state_to_schema(self, agent_state: AgentState, actor: User, files_agents_cache: dict = None) -> AgentSchema: + async def _convert_agent_state_to_schema( + self, + agent_state: AgentState, + actor: User, + files_agents_cache: dict = None, + scrub_messages: bool = False, + ) -> AgentSchema: """Convert AgentState to AgentSchema with ID remapping""" agent_file_id = self._map_db_to_file_id(agent_state.id, AgentSchema.__id_prefix__) @@ -210,21 +216,27 @@ class AgentSerializationManager: ) agent_schema.id = agent_file_id - # Ensure all in-context messages are present before ID remapping. - # AgentSchema.from_agent_state fetches a limited slice (~50) and may exclude messages still - # referenced by in_context_message_ids. Fetch any missing in-context messages by ID so remapping succeeds. - existing_msg_ids = {m.id for m in (agent_schema.messages or [])} - in_context_ids = agent_schema.in_context_message_ids or [] - missing_in_context_ids = [mid for mid in in_context_ids if mid not in existing_msg_ids] - if missing_in_context_ids: - missing_msgs = await self.message_manager.get_messages_by_ids_async(message_ids=missing_in_context_ids, actor=actor) - fetched_ids = {m.id for m in missing_msgs} - not_found = [mid for mid in missing_in_context_ids if mid not in fetched_ids] - if not_found: - # Surface a clear mapping error; handled upstream by the route/export wrapper. - raise AgentExportIdMappingError(db_id=not_found[0], entity_type=MessageSchema.__id_prefix__) - for msg in missing_msgs: - agent_schema.messages.append(MessageSchema.from_message(msg)) + # Handle message scrubbing + if not scrub_messages: + # Ensure all in-context messages are present before ID remapping. + # AgentSchema.from_agent_state fetches a limited slice (~50) and may exclude messages still + # referenced by in_context_message_ids. Fetch any missing in-context messages by ID so remapping succeeds. + existing_msg_ids = {m.id for m in (agent_schema.messages or [])} + in_context_ids = agent_schema.in_context_message_ids or [] + missing_in_context_ids = [mid for mid in in_context_ids if mid not in existing_msg_ids] + if missing_in_context_ids: + missing_msgs = await self.message_manager.get_messages_by_ids_async(message_ids=missing_in_context_ids, actor=actor) + fetched_ids = {m.id for m in missing_msgs} + not_found = [mid for mid in missing_in_context_ids if mid not in fetched_ids] + if not_found: + # Surface a clear mapping error; handled upstream by the route/export wrapper. + raise AgentExportIdMappingError(db_id=not_found[0], entity_type=MessageSchema.__id_prefix__) + for msg in missing_msgs: + agent_schema.messages.append(MessageSchema.from_message(msg)) + else: + # Scrub all messages from export + agent_schema.messages = [] + agent_schema.in_context_message_ids = [] # wipe the values of tool_exec_environment_variables (they contain secrets) agent_secrets = agent_schema.secrets or agent_schema.tool_exec_environment_variables @@ -232,17 +244,18 @@ class AgentSerializationManager: agent_schema.tool_exec_environment_variables = {key: "" for key in agent_secrets} agent_schema.secrets = {key: "" for key in agent_secrets} - if agent_schema.messages: - for message in agent_schema.messages: - message_file_id = self._map_db_to_file_id(message.id, MessageSchema.__id_prefix__) - message.id = message_file_id - message.agent_id = agent_file_id + if not scrub_messages: + if agent_schema.messages: + for message in agent_schema.messages: + message_file_id = self._map_db_to_file_id(message.id, MessageSchema.__id_prefix__) + message.id = message_file_id + message.agent_id = agent_file_id - if agent_schema.in_context_message_ids: - agent_schema.in_context_message_ids = [ - self._map_db_to_file_id(message_id, MessageSchema.__id_prefix__, allow_new=False) - for message_id in agent_schema.in_context_message_ids - ] + if agent_schema.in_context_message_ids: + agent_schema.in_context_message_ids = [ + self._map_db_to_file_id(message_id, MessageSchema.__id_prefix__, allow_new=False) + for message_id in agent_schema.in_context_message_ids + ] if agent_schema.tool_ids: agent_schema.tool_ids = [self._map_db_to_file_id(tool_id, ToolSchema.__id_prefix__) for tool_id in agent_schema.tool_ids] @@ -366,6 +379,7 @@ class AgentSerializationManager: actor: User, conversation_id: Optional[str] = None, skills: Optional[List[SkillSchema]] = None, + scrub_messages: bool = False, ) -> AgentFileSchema: """ Export agents and their related entities to AgentFileSchema format. @@ -376,6 +390,8 @@ class AgentSerializationManager: in-context message_ids instead of the agent's global message_ids. skills: Optional list of skills to include in the export. Skills are resolved client-side and passed as SkillSchema objects. + scrub_messages: If True, excludes all messages from the export. Useful for + sharing agent configs without conversation history. Returns: AgentFileSchema with all related entities @@ -443,7 +459,12 @@ class AgentSerializationManager: # Convert to schemas with ID remapping (reusing cached file-agent data) agent_schemas = [ - await self._convert_agent_state_to_schema(agent_state, actor=actor, files_agents_cache=files_agents_cache) + await self._convert_agent_state_to_schema( + agent_state, + actor=actor, + files_agents_cache=files_agents_cache, + scrub_messages=scrub_messages, + ) for agent_state in agent_states ] tool_schemas = [self._convert_tool_to_schema(tool) for tool in tool_set]