Skip to content

Commit 58cfb0e

Browse files
committed
Merge branch 'main' into ui-polishing
2 parents cf9d768 + b84b184 commit 58cfb0e

File tree

36 files changed

+2176
-1957
lines changed

36 files changed

+2176
-1957
lines changed

.cursor/rules/suna-project.mdc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ project/
7070
- **LLM Integration**: LiteLLM for multi-provider support, structured prompts
7171
- **Tool System**: Dual schema decorators (OpenAPI + XML), consistent ToolResult
7272
- **Real-time**: Supabase subscriptions for live updates
73-
- **Background Jobs**: Dramatiq for async processing, QStash for scheduling
7473

7574
## Key Technologies
7675

CONTRIBUTING.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ Before contributing, ensure you have access to:
4141
- Daytona account (for agent execution)
4242
- Tavily API key (for search)
4343
- Firecrawl API key (for web scraping)
44-
- QStash account (for background jobs)
4544

4645
**Optional:**
4746

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ The setup process includes:
9999
- Setting up Daytona for secure agent execution
100100
- Integrating with LLM providers (Anthropic, OpenAI, OpenRouter, etc.)
101101
- Configuring web search and scraping capabilities (Tavily, Firecrawl)
102-
- Setting up QStash for background job processing and workflows
103102
- Configuring webhook handling for automated tasks
104103
- Optional integrations (RapidAPI for data providers)
105104

@@ -147,14 +146,13 @@ We welcome contributions from the community! Please see our [Contributing Guide]
147146
### Technologies
148147

149148
- [Daytona](https://daytona.io/) - Secure agent execution environment
150-
- [Supabase](https://supabase.com/) - Database and authentication
149+
- [Supabase](https://supabase.com/) - Database, Cron, and Authentication
151150
- [Playwright](https://playwright.dev/) - Browser automation
152151
- [OpenAI](https://openai.com/) - LLM provider
153152
- [Anthropic](https://www.anthropic.com/) - LLM provider
154153
- [Morph](https://morphllm.com/) - For AI-powered code editing
155154
- [Tavily](https://tavily.com/) - Search capabilities
156155
- [Firecrawl](https://firecrawl.dev/) - Web scraping capabilities
157-
- [QStash](https://upstash.com/qstash) - Background job processing and workflows
158156
- [RapidAPI](https://rapidapi.com/) - API services
159157
- Custom MCP servers - Extend functionality with custom tools
160158

backend/.env.example

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ SMITHERY_API_KEY=
5454

5555
MCP_CREDENTIAL_ENCRYPTION_KEY=
5656

57-
QSTASH_URL="https://qstash.upstash.io"
58-
QSTASH_TOKEN=""
59-
QSTASH_CURRENT_SIGNING_KEY=""
60-
QSTASH_NEXT_SIGNING_KEY=""
61-
6257
WEBHOOK_BASE_URL=""
6358

6459
# Optional

backend/README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,6 @@ DAYTONA_API_KEY=your-daytona-key
9696
DAYTONA_SERVER_URL=https://app.daytona.io/api
9797
DAYTONA_TARGET=us
9898

99-
# Background Job Processing (Required)
100-
QSTASH_URL=https://qstash.upstash.io
101-
QSTASH_TOKEN=your-qstash-token
102-
QSTASH_CURRENT_SIGNING_KEY=your-current-signing-key
103-
QSTASH_NEXT_SIGNING_KEY=your-next-signing-key
10499
WEBHOOK_BASE_URL=https://yourdomain.com
105100

106101
# MCP Configuration

backend/agent/api.py

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ async def start_agent(
294294
body: AgentStartRequest = Body(...),
295295
user_id: str = Depends(get_current_user_id_from_jwt)
296296
):
297-
"""Start an agent for a specific thread in the background."""
297+
"""Start an agent for a specific thread in the background"""
298298
structlog.contextvars.bind_contextvars(
299299
thread_id=thread_id,
300300
)
@@ -321,7 +321,9 @@ async def start_agent(
321321
client = await db.client
322322

323323
await verify_thread_access(client, thread_id, user_id)
324+
324325
thread_result = await client.table('threads').select('project_id', 'account_id', 'metadata').eq('thread_id', thread_id).execute()
326+
325327
if not thread_result.data:
326328
raise HTTPException(status_code=404, detail="Thread not found")
327329
thread_data = thread_result.data[0]
@@ -349,7 +351,7 @@ async def start_agent(
349351
logger.info(f"[AGENT LOAD] Agent loading flow:")
350352
logger.info(f" - body.agent_id: {body.agent_id}")
351353
logger.info(f" - effective_agent_id: {effective_agent_id}")
352-
354+
353355
if effective_agent_id:
354356
logger.info(f"[AGENT LOAD] Querying for agent: {effective_agent_id}")
355357
# Get agent
@@ -390,7 +392,7 @@ async def start_agent(
390392
source = "request" if body.agent_id else "fallback"
391393
else:
392394
logger.info(f"[AGENT LOAD] No effective_agent_id, will try default agent")
393-
395+
394396
if not agent_config:
395397
logger.info(f"[AGENT LOAD] No agent config yet, querying for default agent")
396398
default_agent_result = await client.table('agents').select('*').eq('account_id', account_id).eq('is_default', True).execute()
@@ -424,22 +426,25 @@ async def start_agent(
424426
logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) - no version data")
425427
else:
426428
logger.warning(f"[AGENT LOAD] No default agent found for account {account_id}")
427-
429+
428430
logger.info(f"[AGENT LOAD] Final agent_config: {agent_config is not None}")
429431
if agent_config:
430432
logger.info(f"[AGENT LOAD] Agent config keys: {list(agent_config.keys())}")
431433
logger.info(f"Using agent {agent_config['agent_id']} for this agent run (thread remains agent-agnostic)")
432434

433435
can_use, model_message, allowed_models = await can_use_model(client, account_id, model_name)
436+
434437
if not can_use:
435438
raise HTTPException(status_code=403, detail={"message": model_message, "allowed_models": allowed_models})
436439

437440
can_run, message, subscription = await check_billing_status(client, account_id)
441+
438442
if not can_run:
439443
raise HTTPException(status_code=402, detail={"message": message, "subscription": subscription})
440444

441445
# Check agent run limit (maximum parallel runs in past 24 hours)
442446
limit_check = await check_agent_run_limit(client, account_id)
447+
443448
if not limit_check['can_start']:
444449
error_detail = {
445450
"message": f"Maximum of {config.MAX_PARALLEL_AGENT_RUNS} parallel agent runs allowed within 24 hours. You currently have {limit_check['running_count']} running.",
@@ -450,23 +455,6 @@ async def start_agent(
450455
logger.warning(f"Agent run limit exceeded for account {account_id}: {limit_check['running_count']} running agents")
451456
raise HTTPException(status_code=429, detail=error_detail)
452457

453-
try:
454-
project_result = await client.table('projects').select('*').eq('project_id', project_id).execute()
455-
if not project_result.data:
456-
raise HTTPException(status_code=404, detail="Project not found")
457-
458-
project_data = project_result.data[0]
459-
sandbox_info = project_data.get('sandbox', {})
460-
if not sandbox_info.get('id'):
461-
raise HTTPException(status_code=404, detail="No sandbox found for this project")
462-
463-
sandbox_id = sandbox_info['id']
464-
sandbox = await get_or_start_sandbox(sandbox_id)
465-
logger.info(f"Successfully started sandbox {sandbox_id} for project {project_id}")
466-
except Exception as e:
467-
logger.error(f"Failed to start sandbox for project {project_id}: {str(e)}")
468-
raise HTTPException(status_code=500, detail=f"Failed to initialize sandbox: {str(e)}")
469-
470458
agent_run = await client.table('agent_runs').insert({
471459
"thread_id": thread_id, "status": "running",
472460
"started_at": datetime.now(timezone.utc).isoformat(),
@@ -479,6 +467,7 @@ async def start_agent(
479467
"enable_context_manager": body.enable_context_manager
480468
}
481469
}).execute()
470+
482471
agent_run_id = agent_run.data[0]['id']
483472
structlog.contextvars.bind_contextvars(
484473
agent_run_id=agent_run_id,
@@ -1084,47 +1073,56 @@ async def initiate_agent_with_files(
10841073
project_id = project.data[0]['project_id']
10851074
logger.info(f"Created new project: {project_id}")
10861075

1087-
# 2. Create Sandbox
1076+
# 2. Create Sandbox (lazy): only create now if files were uploaded and need the
1077+
# sandbox immediately. Otherwise leave sandbox creation to `_ensure_sandbox()`
1078+
# which will create it lazily when tools require it.
10881079
sandbox_id = None
1089-
try:
1090-
sandbox_pass = str(uuid.uuid4())
1091-
sandbox = await create_sandbox(sandbox_pass, project_id)
1092-
sandbox_id = sandbox.id
1093-
logger.info(f"Created new sandbox {sandbox_id} for project {project_id}")
1094-
1095-
# Get preview links
1096-
vnc_link = await sandbox.get_preview_link(6080)
1097-
website_link = await sandbox.get_preview_link(8080)
1098-
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link).split("url='")[1].split("'")[0]
1099-
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
1100-
token = None
1101-
if hasattr(vnc_link, 'token'):
1102-
token = vnc_link.token
1103-
elif "token='" in str(vnc_link):
1104-
token = str(vnc_link).split("token='")[1].split("'")[0]
1105-
except Exception as e:
1106-
logger.error(f"Error creating sandbox: {str(e)}")
1107-
await client.table('projects').delete().eq('project_id', project_id).execute()
1108-
if sandbox_id:
1109-
try: await delete_sandbox(sandbox_id)
1110-
except Exception as e: pass
1111-
raise Exception("Failed to create sandbox")
1112-
1080+
sandbox = None
1081+
sandbox_pass = None
1082+
vnc_url = None
1083+
website_url = None
1084+
token = None
11131085

1114-
# Update project with sandbox info
1115-
update_result = await client.table('projects').update({
1116-
'sandbox': {
1117-
'id': sandbox_id, 'pass': sandbox_pass, 'vnc_preview': vnc_url,
1118-
'sandbox_url': website_url, 'token': token
1119-
}
1120-
}).eq('project_id', project_id).execute()
1086+
if files:
1087+
try:
1088+
sandbox_pass = str(uuid.uuid4())
1089+
sandbox = await create_sandbox(sandbox_pass, project_id)
1090+
sandbox_id = sandbox.id
1091+
logger.info(f"Created new sandbox {sandbox_id} for project {project_id}")
1092+
1093+
# Get preview links
1094+
vnc_link = await sandbox.get_preview_link(6080)
1095+
website_link = await sandbox.get_preview_link(8080)
1096+
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link).split("url='")[1].split("'")[0]
1097+
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
1098+
token = None
1099+
if hasattr(vnc_link, 'token'):
1100+
token = vnc_link.token
1101+
elif "token='" in str(vnc_link):
1102+
token = str(vnc_link).split("token='")[1].split("'")[0]
1103+
1104+
# Update project with sandbox info
1105+
update_result = await client.table('projects').update({
1106+
'sandbox': {
1107+
'id': sandbox_id, 'pass': sandbox_pass, 'vnc_preview': vnc_url,
1108+
'sandbox_url': website_url, 'token': token
1109+
}
1110+
}).eq('project_id', project_id).execute()
11211111

1122-
if not update_result.data:
1123-
logger.error(f"Failed to update project {project_id} with new sandbox {sandbox_id}")
1124-
if sandbox_id:
1125-
try: await delete_sandbox(sandbox_id)
1126-
except Exception as e: logger.error(f"Error deleting sandbox: {str(e)}")
1127-
raise Exception("Database update failed")
1112+
if not update_result.data:
1113+
logger.error(f"Failed to update project {project_id} with new sandbox {sandbox_id}")
1114+
if sandbox_id:
1115+
try: await delete_sandbox(sandbox_id)
1116+
except Exception as e: logger.error(f"Error deleting sandbox: {str(e)}")
1117+
raise Exception("Database update failed")
1118+
except Exception as e:
1119+
logger.error(f"Error creating sandbox: {str(e)}")
1120+
await client.table('projects').delete().eq('project_id', project_id).execute()
1121+
if sandbox_id:
1122+
try: await delete_sandbox(sandbox_id)
1123+
except Exception:
1124+
pass
1125+
raise Exception("Failed to create sandbox")
11281126

11291127
# 3. Create Thread
11301128
thread_data = {

backend/agent/run.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,10 @@ async def setup(self):
390390
project_data = project.data[0]
391391
sandbox_info = project_data.get('sandbox', {})
392392
if not sandbox_info.get('id'):
393-
raise ValueError(f"No sandbox found for project {self.config.project_id}")
393+
# Sandbox is created lazily by tools when required. Do not fail setup
394+
# if no sandbox is present — tools will call `_ensure_sandbox()`
395+
# which will create and persist the sandbox metadata when needed.
396+
logger.info(f"No sandbox found for project {self.config.project_id}; will create lazily when needed")
394397

395398
async def setup_tools(self):
396399
tool_manager = ToolManager(self.thread_manager, self.config.project_id, self.config.thread_id)

backend/agent/utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
from typing import Optional, List, Dict, Any
33
from datetime import datetime, timezone, timedelta
4+
from utils.cache import Cache
45
from utils.logger import logger
56
from utils.config import config
67
from services import redis
@@ -88,6 +89,10 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]:
8889
Dict with 'can_start' (bool), 'running_count' (int), 'running_thread_ids' (list)
8990
"""
9091
try:
92+
result = await Cache.get(f"agent_run_limit:{account_id}")
93+
if result:
94+
return result
95+
9196
# Calculate 24 hours ago
9297
twenty_four_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
9398
twenty_four_hours_ago_iso = twenty_four_hours_ago.isoformat()
@@ -117,17 +122,19 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]:
117122

118123
logger.info(f"Account {account_id} has {running_count} running agent runs in the past 24 hours")
119124

120-
return {
125+
result = {
121126
'can_start': running_count < config.MAX_PARALLEL_AGENT_RUNS,
122127
'running_count': running_count,
123128
'running_thread_ids': running_thread_ids
124129
}
125-
130+
await Cache.set(f"agent_run_limit:{account_id}", result)
131+
return result
132+
126133
except Exception as e:
127134
logger.error(f"Error checking agent run limit for account {account_id}: {str(e)}")
128135
# In case of error, allow the run to proceed but log the error
129136
return {
130137
'can_start': True,
131138
'running_count': 0,
132139
'running_thread_ids': []
133-
}
140+
}

backend/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ dependencies = [
5858
"cryptography>=41.0.0",
5959
"apscheduler>=3.10.0",
6060
"croniter>=1.4.0",
61-
"qstash>=2.0.0",
6261
"structlog==25.4.0",
6362
"PyPDF2==3.0.1",
6463
"python-docx==1.1.0",

0 commit comments

Comments
 (0)