feat: add ID format validation to agent and user schemas (#9151)

* feat: add ID format validation to agent and user schemas

Reuse existing validator types (ToolId, SourceId, BlockId, MessageId,
IdentityId, UserId) from letta.validators to enforce ID format validation
at the schema level. This ensures malformed IDs are rejected with a 422
validation error instead of causing 500 database errors.

Changes:
- CreateAgent: validate tool_ids, source_ids, folder_ids, block_ids, identity_ids
- UpdateAgent: validate tool_ids, source_ids, folder_ids, block_ids, message_ids, identity_ids
- UserUpdate: validate id

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* chore: regenerate API spec and SDK

* fix: override ID validation in AgentSchema for agent file portability

AgentSchema extends CreateAgent but needs to allow arbitrary short IDs
(e.g., tool-0, block-0) for portable agent files. Override the validated
ID fields to use plain List[str] instead of the validated types.

Also fix test_agent.af to use proper UUID-format IDs.

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* chore: regenerate API spec and SDK

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* fix: revert test_agent.af - short IDs are valid for agent files

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* fix openapi schema

---------

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2026-02-03 11:40:31 -08:00
committed by Caren Thomas
parent 025eeaa363
commit a206f7f345
5 changed files with 123 additions and 32 deletions

View File

@@ -30546,7 +30546,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 41,
"minLength": 41,
"pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the tool in the format 'tool-<uuid4>'",
"examples": ["tool-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -30561,7 +30566,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 43,
"minLength": 43,
"pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the source in the format 'source-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -30577,7 +30587,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 43,
"minLength": 43,
"pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the source in the format 'source-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -30592,7 +30607,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 42,
"minLength": 42,
"pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the block in the format 'block-<uuid4>'",
"examples": ["block-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -31093,7 +31113,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 45,
"minLength": 45,
"pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the identity in the format 'identity-<uuid4>'",
"examples": ["identity-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -34958,7 +34983,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 41,
"minLength": 41,
"pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the tool in the format 'tool-<uuid4>'",
"examples": ["tool-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -34973,7 +35003,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 43,
"minLength": 43,
"pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the source in the format 'source-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -34989,7 +35024,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 43,
"minLength": 43,
"pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the source in the format 'source-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -35004,7 +35044,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 42,
"minLength": 42,
"pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the block in the format 'block-<uuid4>'",
"examples": ["block-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -35489,7 +35534,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 45,
"minLength": 45,
"pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the identity in the format 'identity-<uuid4>'",
"examples": ["identity-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -44947,7 +44997,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 41,
"minLength": 41,
"pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the tool in the format 'tool-<uuid4>'",
"examples": ["tool-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -44962,7 +45017,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 43,
"minLength": 43,
"pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the source in the format 'source-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -44978,7 +45038,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 43,
"minLength": 43,
"pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the source in the format 'source-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -44993,7 +45058,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 42,
"minLength": 42,
"pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the block in the format 'block-<uuid4>'",
"examples": ["block-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -45092,7 +45162,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 44,
"minLength": 44,
"pattern": "^message-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the message in the format 'message-<uuid4>'",
"examples": ["message-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -45198,7 +45273,12 @@
"anyOf": [
{
"items": {
"type": "string"
"type": "string",
"maxLength": 45,
"minLength": 45,
"pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the identity in the format 'identity-<uuid4>'",
"examples": ["identity-123e4567-e89b-42d3-8456-426614174000"]
},
"type": "array"
},
@@ -46051,8 +46131,12 @@
"properties": {
"id": {
"type": "string",
"maxLength": 41,
"minLength": 41,
"pattern": "^user-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"title": "Id",
"description": "The id of the user to update."
"description": "The id of the user to update.",
"examples": ["user-123e4567-e89b-42d3-8456-426614174000"]
},
"name": {
"anyOf": [
@@ -46468,8 +46552,7 @@
}
],
"title": "Source Ids",
"description": "Deprecated: Use `folder_ids` field instead. The ids of the sources used by the agent.",
"deprecated": true
"description": "The ids of the sources used by the agent."
},
"folder_ids": {
"anyOf": [

View File

@@ -32,6 +32,7 @@ from letta.schemas.tool import Tool
from letta.schemas.tool_rule import ToolRule
from letta.services.summarizer.summarizer_config import CompactionSettings
from letta.utils import calculate_file_defaults_based_on_context_window, create_random_username
from letta.validators import BlockId, IdentityId, MessageId, SourceId, ToolId
# TODO: Remove this upon next OSS release, there's a duplicate AgentType in enums
@@ -215,12 +216,12 @@ class CreateAgent(BaseModel, validate_assignment=True): #
)
# TODO: This is a legacy field and should be removed ASAP to force `tool_ids` usage
tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
tool_ids: Optional[List[str]] = Field(None, description="The ids of the tools used by the agent.")
source_ids: Optional[List[str]] = Field(
tool_ids: Optional[List[ToolId]] = Field(None, description="The ids of the tools used by the agent.")
source_ids: Optional[List[SourceId]] = Field(
None, description="Deprecated: Use `folder_ids` field instead. The ids of the sources used by the agent.", deprecated=True
)
folder_ids: Optional[List[str]] = Field(None, description="The ids of the folders used by the agent.")
block_ids: Optional[List[str]] = Field(None, description="The ids of the blocks used by the agent.")
folder_ids: Optional[List[SourceId]] = Field(None, description="The ids of the folders used by the agent.")
block_ids: Optional[List[BlockId]] = Field(None, description="The ids of the blocks used by the agent.")
tool_rules: Optional[List[ToolRule]] = Field(None, description="The tool rules governing the agent.")
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
system: Optional[str] = Field(None, description="The system prompt used by the agent.")
@@ -311,7 +312,7 @@ class CreateAgent(BaseModel, validate_assignment=True): #
base_template_id: Optional[str] = Field(
None, description="Deprecated: No longer used. The base template id of the agent.", deprecated=True
)
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
identity_ids: Optional[List[IdentityId]] = Field(None, description="The ids of the identities associated with this agent.")
message_buffer_autoclear: bool = Field(
False,
description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
@@ -444,16 +445,16 @@ class InternalTemplateAgentCreate(CreateAgent):
class UpdateAgent(BaseModel):
name: Optional[str] = Field(None, description="The name of the agent.")
tool_ids: Optional[List[str]] = Field(None, description="The ids of the tools used by the agent.")
source_ids: Optional[List[str]] = Field(
tool_ids: Optional[List[ToolId]] = Field(None, description="The ids of the tools used by the agent.")
source_ids: Optional[List[SourceId]] = Field(
None, description="Deprecated: Use `folder_ids` field instead. The ids of the sources used by the agent.", deprecated=True
)
folder_ids: Optional[List[str]] = Field(None, description="The ids of the folders used by the agent.")
block_ids: Optional[List[str]] = Field(None, description="The ids of the blocks used by the agent.")
folder_ids: Optional[List[SourceId]] = Field(None, description="The ids of the folders used by the agent.")
block_ids: Optional[List[BlockId]] = Field(None, description="The ids of the blocks used by the agent.")
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
system: Optional[str] = Field(None, description="The system prompt used by the agent.")
tool_rules: Optional[List[ToolRule]] = Field(None, description="The tool rules governing the agent.")
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
message_ids: Optional[List[MessageId]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
description: Optional[str] = Field(None, description="The description of the agent.")
metadata: Optional[Dict] = Field(None, description="The metadata of the agent.")
tool_exec_environment_variables: Optional[Dict[str, str]] = Field(None, description="Deprecated: use `secrets` field instead")
@@ -461,7 +462,7 @@ class UpdateAgent(BaseModel):
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
identity_ids: Optional[List[IdentityId]] = Field(None, description="The ids of the identities associated with this agent.")
message_buffer_autoclear: Optional[bool] = Field(
None,
description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",

View File

@@ -135,6 +135,12 @@ class AgentSchema(CreateAgent):
files_agents: List[FileAgentSchema] = Field(default_factory=list, description="List of file-agent relationships for this agent")
group_ids: List[str] = Field(default_factory=list, description="List of groups that the agent manages")
tool_ids: Optional[List[str]] = Field(None, description="The ids of the tools used by the agent.")
source_ids: Optional[List[str]] = Field(None, description="The ids of the sources used by the agent.")
folder_ids: Optional[List[str]] = Field(None, description="The ids of the folders used by the agent.")
block_ids: Optional[List[str]] = Field(None, description="The ids of the blocks used by the agent.")
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
@classmethod
async def from_agent_state(
cls, agent_state: AgentState, message_manager: MessageManager, files_agents: List[FileAgent], actor: User

View File

@@ -6,6 +6,7 @@ from pydantic import Field
from letta.constants import DEFAULT_ORG_ID
from letta.schemas.enums import PrimitiveType
from letta.schemas.letta_base import LettaBase
from letta.validators import UserId
class UserBase(LettaBase):
@@ -29,6 +30,6 @@ class UserCreate(UserBase):
class UserUpdate(UserBase):
id: str = Field(..., description="The id of the user to update.")
id: UserId = Field(..., description="The id of the user to update.")
name: Optional[str] = Field(None, description="The new name of the user.")
organization_id: Optional[str] = Field(None, description="The new organization id of the user.")

View File

@@ -691,7 +691,7 @@ async def test_update_agent(server: SyncServer, comprehensive_test_agent_fixture
system="train system",
llm_config=LLMConfig.default_config("gpt-4o-mini"),
embedding_config=EmbeddingConfig.default_config(model_name="letta"),
message_ids=["10", "20"],
message_ids=[f"message-{uuid.uuid4()}", f"message-{uuid.uuid4()}"],
metadata={"train_key": "train_value"},
tool_exec_environment_variables={"test_env_var_key_a": "a", "new_tool_exec_key": "n"},
message_buffer_autoclear=False,