diff --git a/backend/agent/api.py b/backend/agent/api.py
index 0d290e66..a3db33ea 100644
--- a/backend/agent/api.py
+++ b/backend/agent/api.py
@@ -529,13 +529,10 @@ async def start_agent(
logger.error(f"Error loading workflow {workflow_id} for thread {thread_id}: {e}")
# Continue with existing agent config if workflow loading fails
- # Update thread's agent_id if a different agent was explicitly requested
+ # Don't update thread's agent_id since threads are now agent-agnostic
+ # The agent selection is handled per message/agent run
if body.agent_id and body.agent_id != thread_agent_id and agent_config:
- try:
- await client.table('threads').update({"agent_id": agent_config['agent_id']}).eq('thread_id', thread_id).execute()
- logger.info(f"Updated thread {thread_id} to use agent {agent_config['agent_id']}")
- except Exception as e:
- logger.warning(f"Failed to update thread agent_id: {e}")
+ logger.info(f"Using agent {agent_config['agent_id']} for this agent run (thread remains agent-agnostic)")
can_use, model_message, allowed_models = await can_use_model(client, account_id, model_name)
if not can_use:
@@ -570,7 +567,9 @@ async def start_agent(
agent_run = await client.table('agent_runs').insert({
"thread_id": thread_id, "status": "running",
- "started_at": datetime.now(timezone.utc).isoformat()
+ "started_at": datetime.now(timezone.utc).isoformat(),
+ "agent_id": agent_config.get('agent_id') if agent_config else None,
+ "agent_version_id": agent_config.get('current_version_id') if agent_config else None
}).execute()
agent_run_id = agent_run.data[0]['id']
structlog.contextvars.bind_contextvars(
@@ -648,7 +647,8 @@ async def get_agent_run(agent_run_id: str, user_id: str = Depends(get_current_us
@router.get("/thread/{thread_id}/agent", response_model=ThreadAgentResponse)
async def get_thread_agent(thread_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
- """Get the agent details for a specific thread."""
+ """Get the agent details for a specific thread. Since threads are now agent-agnostic,
+ this returns the most recently used agent or the default agent."""
structlog.contextvars.bind_contextvars(
thread_id=thread_id,
)
@@ -667,47 +667,79 @@ async def get_thread_agent(thread_id: str, user_id: str = Depends(get_current_us
thread_agent_id = thread_data.get('agent_id')
account_id = thread_data.get('account_id')
- # If no agent_id is set in the thread, try to get the default agent
- effective_agent_id = thread_agent_id
- agent_source = "thread"
+ effective_agent_id = None
+ agent_source = "none"
- if not effective_agent_id:
- # No agent set in thread, get default agent for the account
+ # First, try to get the most recently used agent from agent_runs
+ recent_agent_result = await client.table('agent_runs').select('agent_id', 'agent_version_id').eq('thread_id', thread_id).not_.is_('agent_id', 'null').order('created_at', desc=True).limit(1).execute()
+ if recent_agent_result.data:
+ effective_agent_id = recent_agent_result.data[0]['agent_id']
+ recent_version_id = recent_agent_result.data[0].get('agent_version_id')
+ agent_source = "recent"
+ logger.info(f"Found most recently used agent: {effective_agent_id} (version: {recent_version_id})")
+
+ # If no recent agent, fall back to thread default agent
+ elif thread_agent_id:
+ effective_agent_id = thread_agent_id
+ agent_source = "thread"
+ logger.info(f"Using thread default agent: {effective_agent_id}")
+
+ # If no thread agent, try to get the default agent for the account
+ else:
default_agent_result = await client.table('agents').select('agent_id').eq('account_id', account_id).eq('is_default', True).execute()
if default_agent_result.data:
effective_agent_id = default_agent_result.data[0]['agent_id']
agent_source = "default"
- else:
- # No default agent found
- return {
- "agent": None,
- "source": "none",
- "message": "No agent configured for this thread"
- }
+ logger.info(f"Using account default agent: {effective_agent_id}")
- # Fetch the agent details
- agent_result = await client.table('agents').select('*').eq('agent_id', effective_agent_id).eq('account_id', account_id).execute()
+ # If still no agent found
+ if not effective_agent_id:
+ return {
+ "agent": None,
+ "source": "none",
+ "message": "No agent configured for this thread. Threads are agent-agnostic - you can select any agent."
+ }
+
+ # Fetch the agent details with version information
+ agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('agent_id', effective_agent_id).eq('account_id', account_id).execute()
if not agent_result.data:
# Agent was deleted or doesn't exist
return {
"agent": None,
"source": "missing",
- "message": f"Agent {effective_agent_id} not found or was deleted"
+ "message": f"Agent {effective_agent_id} not found or was deleted. You can select a different agent."
}
agent_data = agent_result.data[0]
+ # Use version data if available, otherwise fall back to agent data (for backward compatibility)
+ if agent_data.get('agent_versions'):
+ version_data = agent_data['agent_versions']
+ # Use the version data for the response
+ system_prompt = version_data['system_prompt']
+ configured_mcps = version_data.get('configured_mcps', [])
+ custom_mcps = version_data.get('custom_mcps', [])
+ agentpress_tools = version_data.get('agentpress_tools', {})
+ logger.info(f"Using agent {agent_data['name']} version {version_data.get('version_name', 'v1')}")
+ else:
+ # Backward compatibility - use agent data directly
+ system_prompt = agent_data['system_prompt']
+ configured_mcps = agent_data.get('configured_mcps', [])
+ custom_mcps = agent_data.get('custom_mcps', [])
+ agentpress_tools = agent_data.get('agentpress_tools', {})
+ logger.info(f"Using agent {agent_data['name']} - no version data (backward compatibility)")
+
return {
"agent": AgentResponse(
agent_id=agent_data['agent_id'],
account_id=agent_data['account_id'],
name=agent_data['name'],
description=agent_data.get('description'),
- system_prompt=agent_data['system_prompt'],
- configured_mcps=agent_data.get('configured_mcps', []),
- custom_mcps=agent_data.get('custom_mcps', []),
- agentpress_tools=agent_data.get('agentpress_tools', {}),
+ system_prompt=system_prompt,
+ configured_mcps=configured_mcps,
+ custom_mcps=custom_mcps,
+ agentpress_tools=agentpress_tools,
is_default=agent_data.get('is_default', False),
is_public=agent_data.get('is_public', False),
marketplace_published_at=agent_data.get('marketplace_published_at'),
@@ -716,10 +748,12 @@ async def get_thread_agent(thread_id: str, user_id: str = Depends(get_current_us
avatar=agent_data.get('avatar'),
avatar_color=agent_data.get('avatar_color'),
created_at=agent_data['created_at'],
- updated_at=agent_data['updated_at']
+ updated_at=agent_data['updated_at'],
+ current_version_id=agent_data.get('current_version_id'),
+ version_count=agent_data.get('version_count', 1)
),
"source": agent_source,
- "message": f"Using {agent_source} agent: {agent_data['name']}"
+ "message": f"Using {agent_source} agent: {agent_data['name']}. Threads are agent-agnostic - you can change agents anytime."
}
except HTTPException:
@@ -1091,10 +1125,10 @@ async def initiate_agent_with_files(
account_id=account_id,
)
- # Store the agent_id in the thread if we have one
+ # Don't store agent_id in thread since threads are now agent-agnostic
+ # The agent selection will be handled per message/agent run
if agent_config:
- thread_data["agent_id"] = agent_config['agent_id']
- logger.info(f"Storing agent_id {agent_config['agent_id']} in thread")
+ logger.info(f"Using agent {agent_config['agent_id']} for this conversation (thread remains agent-agnostic)")
structlog.contextvars.bind_contextvars(
agent_id=agent_config['agent_id'],
)
@@ -1186,7 +1220,9 @@ async def initiate_agent_with_files(
# 6. Start Agent Run
agent_run = await client.table('agent_runs').insert({
"thread_id": thread_id, "status": "running",
- "started_at": datetime.now(timezone.utc).isoformat()
+ "started_at": datetime.now(timezone.utc).isoformat(),
+ "agent_id": agent_config.get('agent_id') if agent_config else None,
+ "agent_version_id": agent_config.get('current_version_id') if agent_config else None
}).execute()
agent_run_id = agent_run.data[0]['id']
logger.info(f"Created new agent run: {agent_run_id}")
@@ -1834,6 +1870,7 @@ class MarketplaceAgent(BaseModel):
creator_name: str
avatar: Optional[str]
avatar_color: Optional[str]
+ is_kortix_team: Optional[bool] = False
class MarketplaceAgentsResponse(BaseModel):
agents: List[MarketplaceAgent]
@@ -1902,6 +1939,25 @@ async def get_marketplace_agents(
if has_more:
total_pages = page + 1
+ # Add Kortix team identification
+ kortix_team_creators = [
+ 'kortix', 'kortix team', 'suna team', 'official', 'kortix official'
+ ]
+
+ for agent in agents_data:
+ creator_name = agent.get('creator_name', '').lower()
+ agent['is_kortix_team'] = any(
+ kortix_creator in creator_name
+ for kortix_creator in kortix_team_creators
+ )
+
+ agents_data = sorted(agents_data, key=lambda x: (
+ not x.get('is_kortix_team', False),
+ -x.get('download_count', 0) if sort_by == "most_downloaded" else 0,
+ x.get('name', '').lower() if sort_by == "name" else '',
+ -(datetime.fromisoformat(x.get('marketplace_published_at', x.get('created_at', ''))).timestamp()) if sort_by == "newest" else 0
+ ))
+
logger.info(f"Found {len(agents_data)} marketplace agents (page {page}, estimated {total_pages} pages)")
return {
"agents": agents_data,
diff --git a/backend/agent/run.py b/backend/agent/run.py
index 8d396cde..5fe6cd76 100644
--- a/backend/agent/run.py
+++ b/backend/agent/run.py
@@ -57,7 +57,7 @@ async def run_agent(
if not trace:
trace = langfuse.trace(name="run_agent", session_id=thread_id, metadata={"project_id": project_id})
- thread_manager = ThreadManager(trace=trace, is_agent_builder=is_agent_builder, target_agent_id=target_agent_id)
+ thread_manager = ThreadManager(trace=trace, is_agent_builder=is_agent_builder, target_agent_id=target_agent_id, agent_config=agent_config)
client = await thread_manager.db.client
diff --git a/backend/agentpress/response_processor.py b/backend/agentpress/response_processor.py
index 62c118e5..68cd6bdc 100644
--- a/backend/agentpress/response_processor.py
+++ b/backend/agentpress/response_processor.py
@@ -86,13 +86,14 @@ class ProcessorConfig:
class ResponseProcessor:
"""Processes LLM responses, extracting and executing tool calls."""
- def __init__(self, tool_registry: ToolRegistry, add_message_callback: Callable, trace: Optional[StatefulTraceClient] = None, is_agent_builder: bool = False, target_agent_id: Optional[str] = None):
+ def __init__(self, tool_registry: ToolRegistry, add_message_callback: Callable, trace: Optional[StatefulTraceClient] = None, is_agent_builder: bool = False, target_agent_id: Optional[str] = None, agent_config: Optional[dict] = None):
"""Initialize the ResponseProcessor.
Args:
tool_registry: Registry of available tools
add_message_callback: Callback function to add messages to the thread.
MUST return the full saved message object (dict) or None.
+ agent_config: Optional agent configuration with version information
"""
self.tool_registry = tool_registry
self.add_message = add_message_callback
@@ -103,6 +104,7 @@ class ResponseProcessor:
self.xml_parser = XMLToolParser(strict_mode=False)
self.is_agent_builder = is_agent_builder
self.target_agent_id = target_agent_id
+ self.agent_config = agent_config
async def _yield_message(self, message_obj: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Helper to yield a message with proper formatting.
@@ -112,6 +114,32 @@ class ResponseProcessor:
if message_obj:
return format_for_yield(message_obj)
+ async def _add_message_with_agent_info(
+ self,
+ thread_id: str,
+ type: str,
+ content: Union[Dict[str, Any], List[Any], str],
+ is_llm_message: bool = False,
+ metadata: Optional[Dict[str, Any]] = None
+ ):
+ """Helper to add a message with agent version information if available."""
+ agent_id = None
+ agent_version_id = None
+
+ if self.agent_config:
+ agent_id = self.agent_config.get('agent_id')
+ agent_version_id = self.agent_config.get('current_version_id')
+
+ return await self.add_message(
+ thread_id=thread_id,
+ type=type,
+ content=content,
+ is_llm_message=is_llm_message,
+ metadata=metadata,
+ agent_id=agent_id,
+ agent_version_id=agent_version_id
+ )
+
async def process_streaming_response(
self,
llm_response: AsyncGenerator,
@@ -492,7 +520,7 @@ class ResponseProcessor:
"tool_calls": complete_native_tool_calls or None
}
- last_assistant_message_object = await self.add_message(
+ last_assistant_message_object = await self._add_message_with_agent_info(
thread_id=thread_id, type="assistant", content=message_data,
is_llm_message=True, metadata={"thread_run_id": thread_run_id}
)
@@ -879,7 +907,7 @@ class ResponseProcessor:
# --- SAVE and YIELD Final Assistant Message ---
message_data = {"role": "assistant", "content": content, "tool_calls": native_tool_calls_for_message or None}
- assistant_message_object = await self.add_message(
+ assistant_message_object = await self._add_message_with_agent_info(
thread_id=thread_id, type="assistant", content=message_data,
is_llm_message=True, metadata={"thread_run_id": thread_run_id}
)
diff --git a/backend/agentpress/thread_manager.py b/backend/agentpress/thread_manager.py
index db1e08e6..2eb963af 100644
--- a/backend/agentpress/thread_manager.py
+++ b/backend/agentpress/thread_manager.py
@@ -38,19 +38,21 @@ class ThreadManager:
XML-based tool execution patterns.
"""
- def __init__(self, trace: Optional[StatefulTraceClient] = None, is_agent_builder: bool = False, target_agent_id: Optional[str] = None):
+ def __init__(self, trace: Optional[StatefulTraceClient] = None, is_agent_builder: bool = False, target_agent_id: Optional[str] = None, agent_config: Optional[dict] = None):
"""Initialize ThreadManager.
Args:
trace: Optional trace client for logging
is_agent_builder: Whether this is an agent builder session
target_agent_id: ID of the agent being built (if in agent builder mode)
+ agent_config: Optional agent configuration with version information
"""
self.db = DBConnection()
self.tool_registry = ToolRegistry()
self.trace = trace
self.is_agent_builder = is_agent_builder
self.target_agent_id = target_agent_id
+ self.agent_config = agent_config
if not self.trace:
self.trace = langfuse.trace(name="anonymous:thread_manager")
self.response_processor = ResponseProcessor(
@@ -58,7 +60,8 @@ class ThreadManager:
add_message_callback=self.add_message,
trace=self.trace,
is_agent_builder=self.is_agent_builder,
- target_agent_id=self.target_agent_id
+ target_agent_id=self.target_agent_id,
+ agent_config=self.agent_config
)
self.context_manager = ContextManager()
@@ -345,7 +348,9 @@ class ThreadManager:
type: str,
content: Union[Dict[str, Any], List[Any], str],
is_llm_message: bool = False,
- metadata: Optional[Dict[str, Any]] = None
+ metadata: Optional[Dict[str, Any]] = None,
+ agent_id: Optional[str] = None,
+ agent_version_id: Optional[str] = None
):
"""Add a message to the thread in the database.
@@ -358,8 +363,10 @@ class ThreadManager:
Defaults to False (user message).
metadata: Optional dictionary for additional message metadata.
Defaults to None, stored as an empty JSONB object if None.
+ agent_id: Optional ID of the agent associated with this message.
+ agent_version_id: Optional ID of the specific agent version used.
"""
- logger.debug(f"Adding message of type '{type}' to thread {thread_id}")
+ logger.debug(f"Adding message of type '{type}' to thread {thread_id} (agent: {agent_id}, version: {agent_version_id})")
client = await self.db.client
# Prepare data for insertion
@@ -370,6 +377,12 @@ class ThreadManager:
'is_llm_message': is_llm_message,
'metadata': metadata or {},
}
+
+ # Add agent information if provided
+ if agent_id:
+ data_to_insert['agent_id'] = agent_id
+ if agent_version_id:
+ data_to_insert['agent_version_id'] = agent_version_id
try:
# Add returning='representation' to get the inserted row data including the id
diff --git a/backend/mcp_local/secure_api.py b/backend/mcp_local/secure_api.py
index 67d63a7e..82d35a87 100644
--- a/backend/mcp_local/secure_api.py
+++ b/backend/mcp_local/secure_api.py
@@ -114,6 +114,7 @@ class TemplateResponse(BaseModel):
creator_name: Optional[str] = None
avatar: Optional[str]
avatar_color: Optional[str]
+ is_kortix_team: Optional[bool] = False
class InstallationResponse(BaseModel):
"""Response model for template installation"""
diff --git a/backend/mcp_local/template_manager.py b/backend/mcp_local/template_manager.py
index 262b731b..4b270049 100644
--- a/backend/mcp_local/template_manager.py
+++ b/backend/mcp_local/template_manager.py
@@ -71,24 +71,18 @@ class TemplateManager:
tags: Optional[List[str]] = None
) -> str:
"""
- Create an agent template from an existing agent, stripping all credentials
+ Create a secure template from an existing agent
- Args:
- agent_id: ID of the existing agent
- creator_id: ID of the user creating the template
- make_public: Whether to make the template public immediately
- tags: Optional tags for the template
-
- Returns:
- template_id: ID of the created template
+ This extracts the agent configuration and creates a template with
+ MCP requirements (without credentials) that can be safely shared
"""
- logger.info(f"Creating template from agent {agent_id}")
+ logger.info(f"Creating template from agent {agent_id} for user {creator_id}")
try:
client = await db.client
- # Get the existing agent with current version
- agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('agent_id', agent_id).execute()
+ # Get the agent
+ agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).execute()
if not agent_result.data:
raise ValueError("Agent not found")
@@ -96,61 +90,63 @@ class TemplateManager:
# Verify ownership
if agent['account_id'] != creator_id:
- raise ValueError("Access denied: not agent owner")
+ raise ValueError("Access denied - you can only create templates from your own agents")
- # Extract MCP requirements (remove credentials)
+ # Extract MCP requirements from agent configuration
mcp_requirements = []
- # Process configured_mcps
- for mcp_config in agent.get('configured_mcps', []):
- requirement = {
- 'qualified_name': mcp_config.get('qualifiedName'),
- 'display_name': mcp_config.get('name'),
- 'enabled_tools': mcp_config.get('enabledTools', []),
- 'required_config': list(mcp_config.get('config', {}).keys())
- }
- mcp_requirements.append(requirement)
+ # Process configured_mcps (regular MCP servers)
+ for mcp in agent.get('configured_mcps', []):
+ if isinstance(mcp, dict) and 'qualifiedName' in mcp:
+ # Extract required config keys from the config
+ config_keys = list(mcp.get('config', {}).keys())
+
+ requirement = {
+ 'qualified_name': mcp['qualifiedName'],
+ 'display_name': mcp.get('name', mcp['qualifiedName']),
+ 'enabled_tools': mcp.get('enabledTools', []),
+ 'required_config': config_keys
+ }
+ mcp_requirements.append(requirement)
- # Process custom_mcps
+ # Process custom_mcps (custom MCP servers)
for custom_mcp in agent.get('custom_mcps', []):
- custom_type = custom_mcp.get('customType', custom_mcp.get('type', 'sse'))
- requirement = {
- 'qualified_name': f"custom_{custom_type}_{custom_mcp['name'].replace(' ', '_').lower()}",
- 'display_name': custom_mcp['name'],
- 'enabled_tools': custom_mcp.get('enabledTools', []),
- 'required_config': list(custom_mcp.get('config', {}).keys()),
- 'custom_type': custom_type
- }
- logger.info(f"Created custom MCP requirement: {requirement}")
- mcp_requirements.append(requirement)
+ if isinstance(custom_mcp, dict) and 'name' in custom_mcp:
+ # Extract required config keys from the config
+ config_keys = list(custom_mcp.get('config', {}).keys())
+
+ requirement = {
+ 'qualified_name': custom_mcp['name'].lower().replace(' ', '_'),
+ 'display_name': custom_mcp['name'],
+ 'enabled_tools': custom_mcp.get('enabledTools', []),
+ 'required_config': config_keys,
+ 'custom_type': custom_mcp.get('type', 'http') # Default to http
+ }
+ mcp_requirements.append(requirement)
- # Use version data if available, otherwise fall back to agent data
- version_data = agent.get('agent_versions', {})
- if version_data:
- system_prompt = version_data.get('system_prompt', agent['system_prompt'])
- agentpress_tools = version_data.get('agentpress_tools', agent.get('agentpress_tools', {}))
- version_name = version_data.get('version_name', 'v1')
- else:
- system_prompt = agent['system_prompt']
- agentpress_tools = agent.get('agentpress_tools', {})
- version_name = 'v1'
+ kortix_team_account_ids = [
+ 'xxxxxxxx',
+ ]
- # Create template
+ is_kortix_team = creator_id in kortix_team_account_ids
+
+ # Create the template
template_data = {
'creator_id': creator_id,
'name': agent['name'],
'description': agent.get('description'),
- 'system_prompt': system_prompt,
+ 'system_prompt': agent['system_prompt'],
'mcp_requirements': mcp_requirements,
- 'agentpress_tools': agentpress_tools,
+ 'agentpress_tools': agent.get('agentpress_tools', {}),
'tags': tags or [],
'is_public': make_public,
+ 'is_kortix_team': is_kortix_team,
'avatar': agent.get('avatar'),
'avatar_color': agent.get('avatar_color'),
'metadata': {
'source_agent_id': agent_id,
'source_version_id': agent.get('current_version_id'),
- 'source_version_name': version_name
+ 'source_version_name': agent.get('current_version', {}).get('version_name', 'v1.0')
}
}
@@ -163,7 +159,7 @@ class TemplateManager:
raise ValueError("Failed to create template")
template_id = result.data[0]['template_id']
- logger.info(f"Successfully created template {template_id} from agent {agent_id}")
+ logger.info(f"Successfully created template {template_id} from agent {agent_id} with is_kortix_team={is_kortix_team}")
return template_id
@@ -554,10 +550,19 @@ class TemplateManager:
if not template or template.creator_id != creator_id:
raise ValueError("Template not found or access denied")
+ # Check if this is a Kortix team account
+ kortix_team_account_ids = [
+ 'bc14b70b-2edf-473c-95be-5f5b109d6553', # Your Kortix team account ID
+ # Add more Kortix team account IDs here as needed
+ ]
+
+ is_kortix_team = template.creator_id in kortix_team_account_ids
+
# Update template
update_data = {
'is_public': True,
- 'marketplace_published_at': datetime.now(timezone.utc).isoformat()
+ 'marketplace_published_at': datetime.now(timezone.utc).isoformat(),
+ 'is_kortix_team': is_kortix_team # Set based on account
}
if tags:
@@ -568,6 +573,7 @@ class TemplateManager:
.eq('template_id', template_id)\
.execute()
+ logger.info(f"Published template {template_id} with is_kortix_team={is_kortix_team}")
return len(result.data) > 0
except Exception as e:
@@ -615,6 +621,7 @@ class TemplateManager:
query = client.table('agent_templates')\
.select('*')\
.eq('is_public', True)\
+ .order('is_kortix_team', desc=True)\
.order('marketplace_published_at', desc=True)\
.range(offset, offset + limit - 1)
@@ -628,6 +635,9 @@ class TemplateManager:
templates = []
for template_data in result.data:
+ # Use the database field for is_kortix_team
+ is_kortix_team = template_data.get('is_kortix_team', False)
+
templates.append({
'template_id': template_data['template_id'],
'name': template_data['name'],
@@ -639,11 +649,13 @@ class TemplateManager:
'download_count': template_data.get('download_count', 0),
'marketplace_published_at': template_data.get('marketplace_published_at'),
'created_at': template_data['created_at'],
- 'creator_name': 'Anonymous',
+ 'creator_name': 'Kortix Team' if is_kortix_team else 'Community',
'avatar': template_data.get('avatar'),
- 'avatar_color': template_data.get('avatar_color')
+ 'avatar_color': template_data.get('avatar_color'),
+ 'is_kortix_team': is_kortix_team
})
+ # Templates are already sorted by database query (Kortix team first, then by date)
return templates
except Exception as e:
diff --git a/backend/supabase/migrations/20250601000000_add_thread_metadata.sql b/backend/supabase/migrations/20250601000000_add_thread_metadata.sql
index d86e7c12..84d15923 100644
--- a/backend/supabase/migrations/20250601000000_add_thread_metadata.sql
+++ b/backend/supabase/migrations/20250601000000_add_thread_metadata.sql
@@ -5,4 +5,17 @@ ALTER TABLE threads ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb;
CREATE INDEX IF NOT EXISTS idx_threads_metadata ON threads USING GIN (metadata);
-- Comment on the column
-COMMENT ON COLUMN threads.metadata IS 'Stores additional thread context like agent builder mode and target agent';
\ No newline at end of file
+COMMENT ON COLUMN threads.metadata IS 'Stores additional thread context like agent builder mode and target agent';
+
+-- Add agent_id to messages table to support per-message agent selection
+ALTER TABLE messages ADD COLUMN IF NOT EXISTS agent_id UUID REFERENCES agents(agent_id) ON DELETE SET NULL;
+
+-- Create index for message agent queries
+CREATE INDEX IF NOT EXISTS idx_messages_agent_id ON messages(agent_id);
+
+-- Comment on the new column
+COMMENT ON COLUMN messages.agent_id IS 'ID of the agent that generated this message. For user messages, this represents the agent that should respond to this message.';
+
+-- Make thread agent_id nullable to allow agent-agnostic threads
+-- This is already nullable from the existing migration, but we'll add a comment
+COMMENT ON COLUMN threads.agent_id IS 'Optional default agent for the thread. If NULL, agent can be selected per message.';
\ No newline at end of file
diff --git a/backend/supabase/migrations/20250626092143_agent_agnostic_thread.sql b/backend/supabase/migrations/20250626092143_agent_agnostic_thread.sql
new file mode 100644
index 00000000..0af214ff
--- /dev/null
+++ b/backend/supabase/migrations/20250626092143_agent_agnostic_thread.sql
@@ -0,0 +1,33 @@
+-- Migration: Make threads agent-agnostic with proper agent versioning support
+-- This migration enables per-message agent selection with version tracking
+
+BEGIN;
+
+-- Add agent version tracking to messages table
+ALTER TABLE messages ADD COLUMN IF NOT EXISTS agent_id UUID REFERENCES agents(agent_id) ON DELETE SET NULL;
+ALTER TABLE messages ADD COLUMN IF NOT EXISTS agent_version_id UUID REFERENCES agent_versions(version_id) ON DELETE SET NULL;
+
+-- Create indexes for message agent queries
+CREATE INDEX IF NOT EXISTS idx_messages_agent_id ON messages(agent_id);
+CREATE INDEX IF NOT EXISTS idx_messages_agent_version_id ON messages(agent_version_id);
+
+-- Comments on the new columns
+COMMENT ON COLUMN messages.agent_id IS 'ID of the agent that generated this message. For user messages, this represents the agent that should respond to this message.';
+COMMENT ON COLUMN messages.agent_version_id IS 'Specific version of the agent used for this message. This is the actual configuration that was active.';
+
+-- Update comment on thread agent_id to reflect new agent-agnostic approach
+COMMENT ON COLUMN threads.agent_id IS 'Optional default agent for the thread. If NULL, agent can be selected per message. Threads are now agent-agnostic.';
+
+-- Add agent version tracking to agent_runs
+ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS agent_id UUID REFERENCES agents(agent_id) ON DELETE SET NULL;
+ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS agent_version_id UUID REFERENCES agent_versions(version_id) ON DELETE SET NULL;
+
+-- Create indexes for agent run queries
+CREATE INDEX IF NOT EXISTS idx_agent_runs_agent_id ON agent_runs(agent_id);
+CREATE INDEX IF NOT EXISTS idx_agent_runs_agent_version_id ON agent_runs(agent_version_id);
+
+-- Comments on the agent_runs columns
+COMMENT ON COLUMN agent_runs.agent_id IS 'ID of the agent used for this specific agent run.';
+COMMENT ON COLUMN agent_runs.agent_version_id IS 'Specific version of the agent used for this run. This tracks the exact configuration.';
+
+COMMIT;
\ No newline at end of file
diff --git a/backend/supabase/migrations/20250626114642_kortix_team_agents.sql b/backend/supabase/migrations/20250626114642_kortix_team_agents.sql
new file mode 100644
index 00000000..25f4a4be
--- /dev/null
+++ b/backend/supabase/migrations/20250626114642_kortix_team_agents.sql
@@ -0,0 +1,15 @@
+-- Migration: Add is_kortix_team field to agent_templates
+-- This migration adds support for marking templates as Kortix team templates
+
+BEGIN;
+
+-- Add is_kortix_team column to agent_templates table
+ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS is_kortix_team BOOLEAN DEFAULT false;
+
+-- Create index for better performance
+CREATE INDEX IF NOT EXISTS idx_agent_templates_is_kortix_team ON agent_templates(is_kortix_team);
+
+-- Add comment
+COMMENT ON COLUMN agent_templates.is_kortix_team IS 'Indicates if this template is created by the Kortix team (official templates)';
+
+COMMIT;
\ No newline at end of file
diff --git a/backend/utils/config.py b/backend/utils/config.py
index bca8d582..1bc42f81 100644
--- a/backend/utils/config.py
+++ b/backend/utils/config.py
@@ -170,7 +170,7 @@ class Configuration:
return self.STRIPE_TIER_200_1000_YEARLY_ID_PROD
# LLM API keys
- ANTHROPIC_API_KEY: str = None
+ ANTHROPIC_API_KEY: Optional[str] = None
OPENAI_API_KEY: Optional[str] = None
GROQ_API_KEY: Optional[str] = None
OPENROUTER_API_KEY: Optional[str] = None
diff --git a/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx b/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx
index 67fde4c2..021bdd7a 100644
--- a/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx
+++ b/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx
@@ -118,8 +118,6 @@ const AgentModal = ({ agent, isOpen, onClose, onCustomize, onChat, onPublish, on
Chat
-
- {/* Marketplace Actions */}