From 530d33c2542d6a4f34fe98569a812e7f582dbe51 Mon Sep 17 00:00:00 2001 From: cthomas Date: Wed, 4 Feb 2026 20:10:19 -0800 Subject: [PATCH] feat: add skills support to agentfile (#9287) --- fern/openapi.json | 131 ++++++++++++++++++ letta/schemas/agent_file.py | 34 ++++- letta/server/rest_api/routers/v1/agents.py | 43 +++++- letta/services/agent_serialization_manager.py | 12 +- 4 files changed, 217 insertions(+), 3 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index 2135a1f2..ede210fe 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -4727,6 +4727,62 @@ } } } + }, + "post": { + "tags": ["agents"], + "summary": "Export Agent With Skills", + "description": "Export the serialized JSON representation of an agent with optional skills.\n\nThis POST endpoint allows including skills in the export by providing them in the request body.\nSkills are resolved client-side and passed as SkillSchema objects containing the skill files.", + "operationId": "export_agent_with_skills", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Agent Id" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExportAgentRequest" + }, + { + "type": "null" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/v1/agents/import": { @@ -25279,6 +25335,14 @@ "title": "Mcp Servers", "description": "List of MCP servers in this agent file" }, + "skills": { + "items": { + "$ref": "#/components/schemas/SkillSchema" + }, + "type": "array", + "title": "Skills", + "description": "List of skills in this agent file" + }, "metadata": { "additionalProperties": { "type": "string" @@ -32503,6 +32567,33 @@ "title": "EventMessage", "description": "A message for notifying the developer that an event that has occured (e.g. a compaction). Events are NOT part of the context window." }, + "ExportAgentRequest": { + "properties": { + "skills": { + "items": { + "$ref": "#/components/schemas/SkillSchema" + }, + "type": "array", + "title": "Skills", + "description": "Skills to include in the export. Each skill must have a name and files (including SKILL.md)." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history." + } + }, + "type": "object", + "title": "ExportAgentRequest", + "description": "Request body for POST /export endpoint." + }, "FeedbackType": { "type": "string", "enum": ["positive", "negative"], @@ -41771,6 +41862,46 @@ "required": ["query"], "title": "SearchAllMessagesRequest" }, + "SkillSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Skill name, also serves as unique identifier (e.g., 'slack', 'pdf')" + }, + "files": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Files", + "description": "Skill files as path -> content mapping. Must include 'SKILL.md' key if provided." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Source URL for skill resolution (e.g., 'letta:slack', 'anthropic:pdf', 'owner/repo/path')" + } + }, + "type": "object", + "required": ["name"], + "title": "SkillSchema", + "description": "Skill schema for agent files.\n\nSkills are folders of instructions, scripts, and resources that agents can load.\nEither files (with SKILL.md) or source_url must be provided:\n- files with SKILL.md: inline skill content\n- source_url: reference to resolve later (e.g., 'letta:slack')\n- both: inline content with provenance tracking" + }, "SleeptimeManager": { "properties": { "manager_type": { diff --git a/letta/schemas/agent_file.py b/letta/schemas/agent_file.py index e359c415..129b12f1 100644 --- a/letta/schemas/agent_file.py +++ b/letta/schemas/agent_file.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Annotated, Any, Dict, List, Literal, Optional, Union from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from letta.helpers.datetime_helpers import get_utc_time from letta.schemas.agent import AgentState, CreateAgent @@ -367,6 +367,37 @@ class ToolSchema(Tool): return cls(**tool.model_dump()) +class SkillSchema(BaseModel): + """Skill schema for agent files. + + Skills are folders of instructions, scripts, and resources that agents can load. + Either files (with SKILL.md) or source_url must be provided: + - files with SKILL.md: inline skill content + - source_url: reference to resolve later (e.g., 'letta:slack') + - both: inline content with provenance tracking + """ + + name: str = Field(..., description="Skill name, also serves as unique identifier (e.g., 'slack', 'pdf')") + files: Optional[Dict[str, str]] = Field( + default=None, + description="Skill files as path -> content mapping. Must include 'SKILL.md' key if provided.", + ) + source_url: Optional[str] = Field( + default=None, + description="Source URL for skill resolution (e.g., 'letta:slack', 'anthropic:pdf', 'owner/repo/path')", + ) + + @model_validator(mode="after") + def check_files_or_source_url(self) -> "SkillSchema": + """Ensure either files (with SKILL.md) or source_url is provided.""" + has_files = self.files and "SKILL.md" in self.files + has_source_url = self.source_url is not None + + if not has_files and not has_source_url: + raise ValueError("Either files (with 'SKILL.md') or source_url must be provided") + return self + + class MCPServerSchema(BaseModel): """MCP server schema for agent files with remapped ID.""" @@ -407,6 +438,7 @@ class AgentFileSchema(BaseModel): sources: List[SourceSchema] = Field(..., description="List of sources in this agent file") tools: List[ToolSchema] = Field(..., description="List of tools in this agent file") mcp_servers: List[MCPServerSchema] = Field(..., description="List of MCP servers in this agent file") + skills: List[SkillSchema] = Field(default_factory=list, description="List of skills in this agent file") metadata: Dict[str, str] = Field( default_factory=dict, description="Metadata for this agent file, including revision_id and other export information." ) diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index c817a398..4ee04ef4 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -34,7 +34,7 @@ from letta.orm.errors import NoResultFound from letta.otel.context import get_ctx_attributes from letta.otel.metric_registry import MetricRegistry from letta.schemas.agent import AgentRelationships, AgentState, CreateAgent, UpdateAgent -from letta.schemas.agent_file import AgentFileSchema +from letta.schemas.agent_file import AgentFileSchema, SkillSchema from letta.schemas.block import BaseBlock, Block, BlockResponse, BlockUpdate from letta.schemas.enums import AgentType, MessageRole, RunStatus from letta.schemas.file import AgentFileAttachment, FileMetadataBase, PaginatedAgentFiles @@ -262,6 +262,47 @@ async def export_agent( return agent_file_schema.model_dump() +class ExportAgentRequest(BaseModel): + """Request body for POST /export endpoint.""" + + skills: List[SkillSchema] = Field( + default_factory=list, + description="Skills to include in the export. Each skill must have a name and files (including SKILL.md).", + ) + conversation_id: Optional[str] = Field( + None, + description="Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history.", + ) + + +@router.post("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent_with_skills") +async def export_agent_with_skills( + agent_id: str = AgentId, + request: Optional[ExportAgentRequest] = Body(default=None), + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +) -> JSONResponse: + """ + Export the serialized JSON representation of an agent with optional skills. + + This POST endpoint allows including skills in the export by providing them in the request body. + Skills are resolved client-side and passed as SkillSchema objects containing the skill files. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + + # Use defaults if no request body provided + skills = request.skills if request else [] + conversation_id = request.conversation_id if request else None + + agent_file_schema = await server.agent_serialization_manager.export( + agent_ids=[agent_id], + actor=actor, + conversation_id=conversation_id, + skills=skills, + ) + return agent_file_schema.model_dump() + + class ImportedAgentsResponse(BaseModel): """Response model for imported agents""" diff --git a/letta/services/agent_serialization_manager.py b/letta/services/agent_serialization_manager.py index 1947a8ee..900c293b 100644 --- a/letta/services/agent_serialization_manager.py +++ b/letta/services/agent_serialization_manager.py @@ -25,6 +25,7 @@ from letta.schemas.agent_file import ( ImportResult, MCPServerSchema, MessageSchema, + SkillSchema, SourceSchema, ToolSchema, ) @@ -359,7 +360,13 @@ class AgentSerializationManager: logger.error(f"Failed to convert group {group.id}: {e}") raise - async def export(self, agent_ids: List[str], actor: User, conversation_id: Optional[str] = None) -> AgentFileSchema: + async def export( + self, + agent_ids: List[str], + actor: User, + conversation_id: Optional[str] = None, + skills: Optional[List[SkillSchema]] = None, + ) -> AgentFileSchema: """ Export agents and their related entities to AgentFileSchema format. @@ -367,6 +374,8 @@ class AgentSerializationManager: agent_ids: List of agent UUIDs to export conversation_id: Optional conversation ID. If provided, uses the conversation's 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. Returns: AgentFileSchema with all related entities @@ -455,6 +464,7 @@ class AgentSerializationManager: sources=source_schemas, tools=tool_schemas, mcp_servers=mcp_server_schemas, + skills=skills or [], metadata={"revision_id": await get_latest_alembic_revision()}, created_at=datetime.now(timezone.utc), )