feat: new agent id query param for default convo (#9756)

* feat: new agent id query param for default convo

* update stainless
This commit is contained in:
cthomas
2026-03-03 16:29:09 -08:00
committed by Caren Thomas
parent a5bac26556
commit aeeec41859
5 changed files with 366 additions and 106 deletions

View File

@@ -8629,7 +8629,7 @@
"schema": { "schema": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/CompactionRequest" "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__agents__CompactionRequest"
}, },
{ {
"type": "null" "type": "null"
@@ -8855,18 +8855,14 @@
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 41,
"maxLength": 42, "maxLength": 41,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The ID of the conv in the format 'conv-<uuid4>'",
"examples": [ "examples": ["conv-123e4567-e89b-42d3-8456-426614174000"],
"default",
"conv-123e4567-e89b-42d3-8456-426614174000",
"agent-123e4567-e89b-42d3-8456-426614174000"
],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The ID of the conv in the format 'conv-<uuid4>'"
} }
], ],
"responses": { "responses": {
@@ -8904,18 +8900,14 @@
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 41,
"maxLength": 42, "maxLength": 41,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The ID of the conv in the format 'conv-<uuid4>'",
"examples": [ "examples": ["conv-123e4567-e89b-42d3-8456-426614174000"],
"default",
"conv-123e4567-e89b-42d3-8456-426614174000",
"agent-123e4567-e89b-42d3-8456-426614174000"
],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The ID of the conv in the format 'conv-<uuid4>'"
} }
], ],
"requestBody": { "requestBody": {
@@ -8963,18 +8955,14 @@
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 41,
"maxLength": 42, "maxLength": 41,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The ID of the conv in the format 'conv-<uuid4>'",
"examples": [ "examples": ["conv-123e4567-e89b-42d3-8456-426614174000"],
"default",
"conv-123e4567-e89b-42d3-8456-426614174000",
"agent-123e4567-e89b-42d3-8456-426614174000"
],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The ID of the conv in the format 'conv-<uuid4>'"
} }
], ],
"responses": { "responses": {
@@ -9003,7 +8991,7 @@
"get": { "get": {
"tags": ["conversations"], "tags": ["conversations"],
"summary": "List Conversation Messages", "summary": "List Conversation Messages",
"description": "List all messages in a conversation.\n\nReturns LettaMessage objects (UserMessage, AssistantMessage, etc.) for all\nmessages in the conversation, with support for cursor-based pagination.\n\nIf conversation_id is an agent ID (starts with \"agent-\"), returns messages\nfrom the agent's default conversation (no conversation isolation).", "description": "List all messages in a conversation.\n\nReturns LettaMessage objects (UserMessage, AssistantMessage, etc.) for all\nmessages in the conversation, with support for cursor-based pagination.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id parameter\nto list messages from the agent's default conversation.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.",
"operationId": "list_conversation_messages", "operationId": "list_conversation_messages",
"parameters": [ "parameters": [
{ {
@@ -9015,7 +9003,7 @@
"minLength": 1, "minLength": 1,
"maxLength": 42, "maxLength": 42,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated).",
"examples": [ "examples": [
"default", "default",
"conv-123e4567-e89b-42d3-8456-426614174000", "conv-123e4567-e89b-42d3-8456-426614174000",
@@ -9023,7 +9011,25 @@
], ],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated)."
},
{
"name": "agent_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Agent ID for agent-direct mode with 'default' conversation",
"title": "Agent Id"
},
"description": "Agent ID for agent-direct mode with 'default' conversation"
}, },
{ {
"name": "before", "name": "before",
@@ -9173,7 +9179,7 @@
"post": { "post": {
"tags": ["conversations"], "tags": ["conversations"],
"summary": "Send Conversation Message", "summary": "Send Conversation Message",
"description": "Send a message to a conversation and get a response.\n\nThis endpoint sends a message to an existing conversation.\nBy default (streaming=true), returns a streaming response (Server-Sent Events).\nSet streaming=false to get a complete JSON response.\n\nIf conversation_id is an agent ID (starts with \"agent-\"), routes to agent-direct\nmode with locking but without conversation-specific features.", "description": "Send a message to a conversation and get a response.\n\nThis endpoint sends a message to an existing conversation.\nBy default (streaming=true), returns a streaming response (Server-Sent Events).\nSet streaming=false to get a complete JSON response.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id in request body\nto send messages to the agent's default conversation with locking.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.",
"operationId": "send_conversation_message", "operationId": "send_conversation_message",
"parameters": [ "parameters": [
{ {
@@ -9185,7 +9191,7 @@
"minLength": 1, "minLength": 1,
"maxLength": 42, "maxLength": 42,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated).",
"examples": [ "examples": [
"default", "default",
"conv-123e4567-e89b-42d3-8456-426614174000", "conv-123e4567-e89b-42d3-8456-426614174000",
@@ -9193,7 +9199,7 @@
], ],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated)."
} }
], ],
"requestBody": { "requestBody": {
@@ -9238,7 +9244,7 @@
"post": { "post": {
"tags": ["conversations"], "tags": ["conversations"],
"summary": "Retrieve Conversation Stream", "summary": "Retrieve Conversation Stream",
"description": "Resume the stream for the most recent active run in a conversation.\n\nThis endpoint allows you to reconnect to an active background stream\nfor a conversation, enabling recovery from network interruptions.\n\nIf conversation_id is an agent ID (starts with \"agent-\"), retrieves the\nstream for the agent's most recent active run.", "description": "Resume the stream for the most recent active run in a conversation.\n\nThis endpoint allows you to reconnect to an active background stream\nfor a conversation, enabling recovery from network interruptions.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id in request body\nto retrieve the stream for the agent's most recent active run.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.",
"operationId": "retrieve_conversation_stream", "operationId": "retrieve_conversation_stream",
"parameters": [ "parameters": [
{ {
@@ -9250,7 +9256,7 @@
"minLength": 1, "minLength": 1,
"maxLength": 42, "maxLength": 42,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated).",
"examples": [ "examples": [
"default", "default",
"conv-123e4567-e89b-42d3-8456-426614174000", "conv-123e4567-e89b-42d3-8456-426614174000",
@@ -9258,7 +9264,7 @@
], ],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated)."
} }
], ],
"requestBody": { "requestBody": {
@@ -9342,7 +9348,7 @@
"post": { "post": {
"tags": ["conversations"], "tags": ["conversations"],
"summary": "Cancel Conversation", "summary": "Cancel Conversation",
"description": "Cancel runs associated with a conversation.\n\nNote: To cancel active runs, Redis is required.\n\nIf conversation_id is an agent ID (starts with \"agent-\"), cancels runs\nfor the agent's default conversation.", "description": "Cancel runs associated with a conversation.\n\nNote: To cancel active runs, Redis is required.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id query parameter\nto cancel runs for the agent's default conversation.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.",
"operationId": "cancel_conversation", "operationId": "cancel_conversation",
"parameters": [ "parameters": [
{ {
@@ -9354,7 +9360,7 @@
"minLength": 1, "minLength": 1,
"maxLength": 42, "maxLength": 42,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated).",
"examples": [ "examples": [
"default", "default",
"conv-123e4567-e89b-42d3-8456-426614174000", "conv-123e4567-e89b-42d3-8456-426614174000",
@@ -9362,7 +9368,25 @@
], ],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated)."
},
{
"name": "agent_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Agent ID for agent-direct mode with 'default' conversation",
"title": "Agent Id"
},
"description": "Agent ID for agent-direct mode with 'default' conversation"
} }
], ],
"responses": { "responses": {
@@ -9395,7 +9419,7 @@
"post": { "post": {
"tags": ["conversations"], "tags": ["conversations"],
"summary": "Compact Conversation", "summary": "Compact Conversation",
"description": "Compact (summarize) a conversation's message history.\n\nThis endpoint summarizes the in-context messages for a specific conversation,\nreducing the message count while preserving important context.\n\nIf conversation_id is an agent ID (starts with \"agent-\"), compacts the\nagent's default conversation messages.", "description": "Compact (summarize) a conversation's message history.\n\nThis endpoint summarizes the in-context messages for a specific conversation,\nreducing the message count while preserving important context.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id in request body\nto compact the agent's default conversation messages.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.",
"operationId": "compact_conversation", "operationId": "compact_conversation",
"parameters": [ "parameters": [
{ {
@@ -9407,7 +9431,7 @@
"minLength": 1, "minLength": 1,
"maxLength": 42, "maxLength": 42,
"pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$",
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'.", "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated).",
"examples": [ "examples": [
"default", "default",
"conv-123e4567-e89b-42d3-8456-426614174000", "conv-123e4567-e89b-42d3-8456-426614174000",
@@ -9415,7 +9439,7 @@
], ],
"title": "Conversation Id" "title": "Conversation Id"
}, },
"description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), an agent ID ('agent-<uuid4>') for agent-direct messaging, or 'default'." "description": "The conversation identifier. Can be a conversation ID ('conv-<uuid4>'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-<uuid4>') for backwards compatibility (deprecated)."
} }
], ],
"requestBody": { "requestBody": {
@@ -9424,7 +9448,7 @@
"schema": { "schema": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/CompactionRequest" "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__conversations__CompactionRequest"
}, },
{ {
"type": "null" "type": "null"
@@ -31460,23 +31484,6 @@
"required": ["code"], "required": ["code"],
"title": "CodeInput" "title": "CodeInput"
}, },
"CompactionRequest": {
"properties": {
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings-Input"
},
{
"type": "null"
}
],
"description": "Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used."
}
},
"type": "object",
"title": "CompactionRequest"
},
"CompactionResponse": { "CompactionResponse": {
"properties": { "properties": {
"summary": { "summary": {
@@ -32611,6 +32618,18 @@
"description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.", "description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.",
"default": false "default": false
}, },
"agent_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Id",
"description": "Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path."
},
"streaming": { "streaming": {
"type": "boolean", "type": "boolean",
"title": "Streaming", "title": "Streaming",
@@ -43448,6 +43467,18 @@
}, },
"RetrieveStreamRequest": { "RetrieveStreamRequest": {
"properties": { "properties": {
"agent_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Id",
"description": "Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path."
},
"starting_after": { "starting_after": {
"type": "integer", "type": "integer",
"title": "Starting After", "title": "Starting After",
@@ -51563,6 +51594,52 @@
], ],
"title": "ToolSchema" "title": "ToolSchema"
}, },
"letta__server__rest_api__routers__v1__agents__CompactionRequest": {
"properties": {
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings-Input"
},
{
"type": "null"
}
],
"description": "Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used."
}
},
"type": "object",
"title": "CompactionRequest"
},
"letta__server__rest_api__routers__v1__conversations__CompactionRequest": {
"properties": {
"agent_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Id",
"description": "Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path."
},
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings-Input"
},
{
"type": "null"
}
],
"description": "Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used."
}
},
"type": "object",
"title": "CompactionRequest"
},
"letta__server__rest_api__routers__v1__tools__ToolExecuteRequest": { "letta__server__rest_api__routers__v1__tools__ToolExecuteRequest": {
"properties": { "properties": {
"args": { "args": {

View File

@@ -88,8 +88,7 @@ class LettaRequest(BaseModel):
) )
top_logprobs: Optional[int] = Field( top_logprobs: Optional[int] = Field(
default=None, default=None,
description="Number of most likely tokens to return at each position (0-20). " description="Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True.",
"Requires return_logprobs=True.",
) )
return_token_ids: bool = Field( return_token_ids: bool = Field(
default=False, default=False,
@@ -155,6 +154,10 @@ class LettaStreamingRequest(LettaRequest):
class ConversationMessageRequest(LettaRequest): class ConversationMessageRequest(LettaRequest):
"""Request for sending messages to a conversation. Streams by default.""" """Request for sending messages to a conversation. Streams by default."""
agent_id: Optional[str] = Field(
default=None,
description="Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path.",
)
streaming: bool = Field( streaming: bool = Field(
default=True, default=True,
description="If True (default), returns a streaming response (Server-Sent Events). If False, returns a complete JSON response.", description="If True (default), returns a streaming response (Server-Sent Events). If False, returns a complete JSON response.",
@@ -194,6 +197,10 @@ class CreateBatch(BaseModel):
class RetrieveStreamRequest(BaseModel): class RetrieveStreamRequest(BaseModel):
agent_id: Optional[str] = Field(
default=None,
description="Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path.",
)
starting_after: int = Field( starting_after: int = Field(
0, description="Sequence id to use as a cursor for pagination. Response will start streaming after this chunk sequence id" 0, description="Sequence id to use as a cursor for pagination. Response will start streaming after this chunk sequence id"
) )

View File

@@ -34,7 +34,7 @@ from letta.services.run_manager import RunManager
from letta.services.streaming_service import StreamingService from letta.services.streaming_service import StreamingService
from letta.services.summarizer.summarizer_config import CompactionSettings from letta.services.summarizer.summarizer_config import CompactionSettings
from letta.settings import settings from letta.settings import settings
from letta.validators import ConversationId from letta.validators import ConversationId, ConversationIdOrDefault
router = APIRouter(prefix="/conversations", tags=["conversations"]) router = APIRouter(prefix="/conversations", tags=["conversations"])
@@ -150,7 +150,8 @@ ConversationMessagesResponse = Annotated[
operation_id="list_conversation_messages", operation_id="list_conversation_messages",
) )
async def list_conversation_messages( async def list_conversation_messages(
conversation_id: ConversationId, conversation_id: ConversationIdOrDefault,
agent_id: Optional[str] = Query(None, description="Agent ID for agent-direct mode with 'default' conversation"),
server: SyncServer = Depends(get_letta_server), server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers), headers: HeaderParams = Depends(get_headers),
before: Optional[str] = Query( before: Optional[str] = Query(
@@ -175,15 +176,24 @@ async def list_conversation_messages(
Returns LettaMessage objects (UserMessage, AssistantMessage, etc.) for all Returns LettaMessage objects (UserMessage, AssistantMessage, etc.) for all
messages in the conversation, with support for cursor-based pagination. messages in the conversation, with support for cursor-based pagination.
If conversation_id is an agent ID (starts with "agent-"), returns messages **Agent-direct mode**: Pass conversation_id="default" with agent_id parameter
from the agent's default conversation (no conversation isolation). to list messages from the agent's default conversation.
**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.
""" """
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
# Agent-direct mode: list agent's default conversation messages # Agent-direct mode: conversation_id="default" + agent_id param (preferred)
if conversation_id.startswith("agent-"): # OR conversation_id="agent-*" (backwards compat, deprecated)
resolved_agent_id = None
if conversation_id == "default" and agent_id:
resolved_agent_id = agent_id
elif conversation_id.startswith("agent-"):
resolved_agent_id = conversation_id
if resolved_agent_id:
return await server.get_agent_recall_async( return await server.get_agent_recall_async(
agent_id=conversation_id, agent_id=resolved_agent_id,
after=after, after=after,
before=before, before=before,
limit=limit, limit=limit,
@@ -324,7 +334,7 @@ async def _send_agent_direct_message(
}, },
) )
async def send_conversation_message( async def send_conversation_message(
conversation_id: ConversationId, conversation_id: ConversationIdOrDefault,
request: ConversationMessageRequest = Body(...), request: ConversationMessageRequest = Body(...),
server: SyncServer = Depends(get_letta_server), server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers), headers: HeaderParams = Depends(get_headers),
@@ -336,22 +346,28 @@ async def send_conversation_message(
By default (streaming=true), returns a streaming response (Server-Sent Events). By default (streaming=true), returns a streaming response (Server-Sent Events).
Set streaming=false to get a complete JSON response. Set streaming=false to get a complete JSON response.
If conversation_id is an agent ID (starts with "agent-"), routes to agent-direct **Agent-direct mode**: Pass conversation_id="default" with agent_id in request body
mode with locking but without conversation-specific features. to send messages to the agent's default conversation with locking.
**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.
""" """
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
if not request.messages or len(request.messages) == 0: if not request.messages or len(request.messages) == 0:
raise HTTPException(status_code=422, detail="Messages must not be empty") raise HTTPException(status_code=422, detail="Messages must not be empty")
# Detect agent-direct mode: conversation_id is actually an agent ID # Agent-direct mode: conversation_id="default" + agent_id in body (preferred)
is_agent_direct = conversation_id.startswith("agent-") # OR conversation_id="agent-*" (backwards compat, deprecated)
resolved_agent_id = None
if conversation_id == "default" and request.agent_id:
resolved_agent_id = request.agent_id
elif conversation_id.startswith("agent-"):
resolved_agent_id = conversation_id
if is_agent_direct: if resolved_agent_id:
# Agent-direct mode: use agent ID, enable locking, skip conversation features # Agent-direct mode: use agent ID, enable locking, skip conversation features
agent_id = conversation_id
return await _send_agent_direct_message( return await _send_agent_direct_message(
agent_id=agent_id, agent_id=resolved_agent_id,
request=request, request=request,
server=server, server=server,
actor=actor, actor=actor,
@@ -488,7 +504,7 @@ async def send_conversation_message(
}, },
) )
async def retrieve_conversation_stream( async def retrieve_conversation_stream(
conversation_id: ConversationId, conversation_id: ConversationIdOrDefault,
request: RetrieveStreamRequest = Body(None), request: RetrieveStreamRequest = Body(None),
headers: HeaderParams = Depends(get_headers), headers: HeaderParams = Depends(get_headers),
server: SyncServer = Depends(get_letta_server), server: SyncServer = Depends(get_letta_server),
@@ -499,18 +515,28 @@ async def retrieve_conversation_stream(
This endpoint allows you to reconnect to an active background stream This endpoint allows you to reconnect to an active background stream
for a conversation, enabling recovery from network interruptions. for a conversation, enabling recovery from network interruptions.
If conversation_id is an agent ID (starts with "agent-"), retrieves the **Agent-direct mode**: Pass conversation_id="default" with agent_id in request body
stream for the agent's most recent active run. to retrieve the stream for the agent's most recent active run.
**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.
""" """
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
runs_manager = RunManager() runs_manager = RunManager()
# Agent-direct mode: conversation_id="default" + agent_id in body (preferred)
# OR conversation_id="agent-*" (backwards compat, deprecated)
resolved_agent_id = None
if conversation_id == "default" and request and request.agent_id:
resolved_agent_id = request.agent_id
elif conversation_id.startswith("agent-"):
resolved_agent_id = conversation_id
# Find the most recent active run # Find the most recent active run
if conversation_id.startswith("agent-"): if resolved_agent_id:
# Agent-direct mode: find runs by agent_id # Agent-direct mode: find runs by agent_id
active_runs = await runs_manager.list_runs( active_runs = await runs_manager.list_runs(
actor=actor, actor=actor,
agent_id=conversation_id, agent_id=resolved_agent_id,
statuses=[RunStatus.created, RunStatus.running], statuses=[RunStatus.created, RunStatus.running],
limit=1, limit=1,
ascending=False, ascending=False,
@@ -578,7 +604,8 @@ async def retrieve_conversation_stream(
@router.post("/{conversation_id}/cancel", operation_id="cancel_conversation") @router.post("/{conversation_id}/cancel", operation_id="cancel_conversation")
async def cancel_conversation( async def cancel_conversation(
conversation_id: ConversationId, conversation_id: ConversationIdOrDefault,
agent_id: Optional[str] = Query(None, description="Agent ID for agent-direct mode with 'default' conversation"),
server: SyncServer = Depends(get_letta_server), server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers), headers: HeaderParams = Depends(get_headers),
) -> dict: ) -> dict:
@@ -587,8 +614,10 @@ async def cancel_conversation(
Note: To cancel active runs, Redis is required. Note: To cancel active runs, Redis is required.
If conversation_id is an agent ID (starts with "agent-"), cancels runs **Agent-direct mode**: Pass conversation_id="default" with agent_id query parameter
for the agent's default conversation. to cancel runs for the agent's default conversation.
**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.
""" """
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
logger.info( logger.info(
@@ -601,13 +630,20 @@ async def cancel_conversation(
if not settings.track_agent_run: if not settings.track_agent_run:
raise HTTPException(status_code=400, detail="Agent run tracking is disabled") raise HTTPException(status_code=400, detail="Agent run tracking is disabled")
# Agent-direct mode: use agent_id directly, skip conversation lookup # Agent-direct mode: conversation_id="default" + agent_id param (preferred)
if conversation_id.startswith("agent-"): # OR conversation_id="agent-*" (backwards compat, deprecated)
agent_id = conversation_id resolved_agent_id = None
if conversation_id == "default" and agent_id:
resolved_agent_id = agent_id
elif conversation_id.startswith("agent-"):
resolved_agent_id = conversation_id
if resolved_agent_id:
# Agent-direct mode: use agent_id directly, skip conversation lookup
# Find active runs for this agent (default conversation has conversation_id=None) # Find active runs for this agent (default conversation has conversation_id=None)
runs = await server.run_manager.list_runs( runs = await server.run_manager.list_runs(
actor=actor, actor=actor,
agent_id=agent_id, agent_id=resolved_agent_id,
statuses=[RunStatus.created, RunStatus.running], statuses=[RunStatus.created, RunStatus.running],
ascending=False, ascending=False,
limit=100, limit=100,
@@ -657,6 +693,10 @@ async def cancel_conversation(
class CompactionRequest(BaseModel): class CompactionRequest(BaseModel):
agent_id: Optional[str] = Field(
default=None,
description="Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path.",
)
compaction_settings: Optional[CompactionSettings] = Field( compaction_settings: Optional[CompactionSettings] = Field(
default=None, default=None,
description="Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used.", description="Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used.",
@@ -671,7 +711,7 @@ class CompactionResponse(BaseModel):
@router.post("/{conversation_id}/compact", response_model=CompactionResponse, operation_id="compact_conversation") @router.post("/{conversation_id}/compact", response_model=CompactionResponse, operation_id="compact_conversation")
async def compact_conversation( async def compact_conversation(
conversation_id: ConversationId, conversation_id: ConversationIdOrDefault,
request: Optional[CompactionRequest] = Body(default=None), request: Optional[CompactionRequest] = Body(default=None),
server: SyncServer = Depends(get_letta_server), server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers), headers: HeaderParams = Depends(get_headers),
@@ -682,15 +722,24 @@ async def compact_conversation(
This endpoint summarizes the in-context messages for a specific conversation, This endpoint summarizes the in-context messages for a specific conversation,
reducing the message count while preserving important context. reducing the message count while preserving important context.
If conversation_id is an agent ID (starts with "agent-"), compacts the **Agent-direct mode**: Pass conversation_id="default" with agent_id in request body
agent's default conversation messages. to compact the agent's default conversation messages.
**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.
""" """
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
# Agent-direct mode: compact agent's default conversation # Agent-direct mode: conversation_id="default" + agent_id in body (preferred)
if conversation_id.startswith("agent-"): # OR conversation_id="agent-*" (backwards compat, deprecated)
agent_id = conversation_id resolved_agent_id = None
agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor, include_relationships=["multi_agent_group"]) if conversation_id == "default" and request and request.agent_id:
resolved_agent_id = request.agent_id
elif conversation_id.startswith("agent-"):
resolved_agent_id = conversation_id
if resolved_agent_id:
# Agent-direct mode: compact agent's default conversation
agent = await server.agent_manager.get_agent_by_id_async(resolved_agent_id, actor, include_relationships=["multi_agent_group"])
in_context_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor) in_context_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor)
agent_loop = LettaAgentV3(agent_state=agent, actor=actor) agent_loop = LettaAgentV3(agent_state=agent, actor=actor)
else: else:

View File

@@ -45,7 +45,7 @@ PATH_VALIDATORS = {primitive_type.value: _create_path_validator_factory(primitiv
def _create_conversation_id_or_default_path_validator_factory(): def _create_conversation_id_or_default_path_validator_factory():
"""Conversation IDs accept the usual primitive format, 'default', or an agent ID.""" """Conversation IDs with support for 'default' and agent IDs (backwards compatibility)."""
conversation_primitive = PrimitiveType.CONVERSATION.value conversation_primitive = PrimitiveType.CONVERSATION.value
agent_primitive = PrimitiveType.AGENT.value agent_primitive = PrimitiveType.AGENT.value
@@ -59,7 +59,8 @@ def _create_conversation_id_or_default_path_validator_factory():
return Path( return Path(
description=( description=(
f"The conversation identifier. Can be a conversation ID ('{conversation_primitive}-<uuid4>'), " f"The conversation identifier. Can be a conversation ID ('{conversation_primitive}-<uuid4>'), "
f"an agent ID ('{agent_primitive}-<uuid4>') for agent-direct messaging, or 'default'." f"'default' for agent-direct mode (with agent_id parameter), "
f"or an agent ID ('{agent_primitive}-<uuid4>') for backwards compatibility (deprecated)."
), ),
pattern=conversation_or_agent_or_default_pattern, pattern=conversation_or_agent_or_default_pattern,
examples=[ examples=[
@@ -74,10 +75,6 @@ def _create_conversation_id_or_default_path_validator_factory():
return factory return factory
# Override conversation ID path validation to also allow 'default' and agent IDs.
PATH_VALIDATORS[PrimitiveType.CONVERSATION.value] = _create_conversation_id_or_default_path_validator_factory()
# Type aliases for common ID types # Type aliases for common ID types
# These can be used directly in route handler signatures for cleaner code # These can be used directly in route handler signatures for cleaner code
AgentId = Annotated[str, PATH_VALIDATORS[PrimitiveType.AGENT.value]()] AgentId = Annotated[str, PATH_VALIDATORS[PrimitiveType.AGENT.value]()]
@@ -98,6 +95,10 @@ StepId = Annotated[str, PATH_VALIDATORS[PrimitiveType.STEP.value]()]
IdentityId = Annotated[str, PATH_VALIDATORS[PrimitiveType.IDENTITY.value]()] IdentityId = Annotated[str, PATH_VALIDATORS[PrimitiveType.IDENTITY.value]()]
ConversationId = Annotated[str, PATH_VALIDATORS[PrimitiveType.CONVERSATION.value]()] ConversationId = Annotated[str, PATH_VALIDATORS[PrimitiveType.CONVERSATION.value]()]
# Conversation ID with support for 'default' and agent IDs (for agent-direct mode endpoints)
# Backwards compatible - agent-* will be deprecated in favor of conversation_id='default' + agent_id param
ConversationIdOrDefault = Annotated[str, _create_conversation_id_or_default_path_validator_factory()()]
# Infrastructure types # Infrastructure types
McpServerId = Annotated[str, PATH_VALIDATORS[PrimitiveType.MCP_SERVER.value]()] McpServerId = Annotated[str, PATH_VALIDATORS[PrimitiveType.MCP_SERVER.value]()]
McpOAuthId = Annotated[str, PATH_VALIDATORS[PrimitiveType.MCP_OAUTH.value]()] McpOAuthId = Annotated[str, PATH_VALIDATORS[PrimitiveType.MCP_OAUTH.value]()]

View File

@@ -725,6 +725,132 @@ class TestConversationsSDK:
if "No active runs" not in str(e): if "No active runs" not in str(e):
raise raise
def test_backwards_compatibility_old_pattern(self, client: Letta, agent, server_url: str):
"""Test that the old pattern (agent_id as conversation_id) still works for backwards compatibility."""
# OLD PATTERN: conversation_id=agent.id (should still work)
# Use raw HTTP requests since SDK might not be up to date
# Test 1: Send message using old pattern
response = requests.post(
f"{server_url}/v1/conversations/{agent.id}/messages",
json={
"messages": [{"role": "user", "content": "Testing old pattern still works"}],
"streaming": False,
},
)
assert response.status_code == 200, f"Old pattern should work for sending messages: {response.text}"
data = response.json()
assert "messages" in data, "Response should contain messages"
assert len(data["messages"]) > 0, "Should receive response messages"
# Test 2: List messages using old pattern
response = requests.get(f"{server_url}/v1/conversations/{agent.id}/messages")
assert response.status_code == 200, f"Old pattern should work for listing messages: {response.text}"
data = response.json()
# Response is a list of messages directly
assert isinstance(data, list), "Response should be a list of messages"
assert len(data) >= 3, "Should have at least system + user + assistant messages"
# Verify our message is there
user_messages = [m for m in data if m.get("message_type") == "user_message"]
assert any("Testing old pattern still works" in str(m.get("content", "")) for m in user_messages), "Should find our test message"
def test_new_pattern_send_message(self, client: Letta, agent, server_url: str):
"""Test sending messages using the new pattern: conversation_id='default' + agent_id in body."""
# NEW PATTERN: conversation_id='default' + agent_id in request body
response = requests.post(
f"{server_url}/v1/conversations/default/messages",
json={
"agent_id": agent.id,
"messages": [{"role": "user", "content": "Testing new pattern send message"}],
"streaming": False,
},
)
assert response.status_code == 200, f"New pattern should work for sending messages: {response.text}"
data = response.json()
assert "messages" in data, "Response should contain messages"
assert len(data["messages"]) > 0, "Should receive response messages"
# Verify we got an assistant message
assistant_messages = [m for m in data["messages"] if m.get("message_type") == "assistant_message"]
assert len(assistant_messages) > 0, "Should receive at least one assistant message"
def test_new_pattern_list_messages(self, client: Letta, agent, server_url: str):
"""Test listing messages using the new pattern: conversation_id='default' + agent_id query param."""
# First send a message to populate the conversation
requests.post(
f"{server_url}/v1/conversations/{agent.id}/messages",
json={
"messages": [{"role": "user", "content": "Setup message for list test"}],
"streaming": False,
},
)
# NEW PATTERN: conversation_id='default' + agent_id as query param
response = requests.get(
f"{server_url}/v1/conversations/default/messages",
params={"agent_id": agent.id},
)
assert response.status_code == 200, f"New pattern should work for listing messages: {response.text}"
data = response.json()
# Response is a list of messages directly
assert isinstance(data, list), "Response should be a list of messages"
assert len(data) >= 3, "Should have at least system + user + assistant messages"
def test_new_pattern_cancel(self, client: Letta, agent, server_url: str):
"""Test canceling runs using the new pattern: conversation_id='default' + agent_id query param."""
from letta.settings import settings
if not settings.track_agent_run:
pytest.skip("Run tracking disabled - skipping cancel test")
# NEW PATTERN: conversation_id='default' + agent_id as query param
response = requests.post(
f"{server_url}/v1/conversations/default/cancel",
params={"agent_id": agent.id},
)
# Returns 200 with results if runs exist, or 409 if no active runs
assert response.status_code in [200, 409], f"New pattern should work for cancel: {response.text}"
if response.status_code == 200:
data = response.json()
assert isinstance(data, dict), "Cancel should return a dict"
def test_new_pattern_compact(self, client: Letta, agent, server_url: str):
"""Test compacting conversation using the new pattern: conversation_id='default' + agent_id in body."""
# Send many messages to have enough for compaction
for i in range(10):
requests.post(
f"{server_url}/v1/conversations/{agent.id}/messages",
json={
"messages": [{"role": "user", "content": f"Message {i} for compaction test"}],
"streaming": False,
},
)
# NEW PATTERN: conversation_id='default' + agent_id in request body
response = requests.post(
f"{server_url}/v1/conversations/default/compact",
json={"agent_id": agent.id},
)
# May return 200 (success) or 400 (not enough messages to compact)
assert response.status_code in [200, 400], f"New pattern should accept agent_id parameter: {response.text}"
if response.status_code == 200:
data = response.json()
assert "summary" in data, "Response should contain summary"
assert "num_messages_before" in data, "Response should contain num_messages_before"
assert "num_messages_after" in data, "Response should contain num_messages_after"
def test_new_pattern_stream_retrieve(self, client: Letta, agent, server_url: str):
"""Test retrieving stream using the new pattern: conversation_id='default' + agent_id in body."""
# NEW PATTERN: conversation_id='default' + agent_id in request body
# Note: This will likely return 400 if no active run exists, which is expected
response = requests.post(
f"{server_url}/v1/conversations/default/stream",
json={"agent_id": agent.id},
)
# Either 200 (if run exists) or 400 (no active run) are both acceptable
assert response.status_code in [200, 400], f"Stream retrieve should accept new pattern: {response.text}"
class TestConversationDelete: class TestConversationDelete:
"""Tests for the conversation delete endpoint.""" """Tests for the conversation delete endpoint."""