Understanding AI Agent Hosting Models
When you build an AI agent—whether it's a LangChain-based reasoning engine, a custom GPT wrapper with tool access, or a multi-step autonomous workflow—you eventually face a critical decision: where and how should this agent live? The hosting model you choose directly impacts latency, cost, scalability, security, and how easily you can iterate. There are three dominant paradigms: managed platforms, self-hosted infrastructure, and serverless functions. Each carries distinct trade-offs that become magnified when you're dealing with long-running LLM calls, stateful conversations, and tool orchestration.
This tutorial walks through each hosting model in depth, provides complete, runnable code examples for deploying the same AI agent across all three, and lays out best practices so you can make an informed choice for your production workloads.
What Defines an AI Agent Hosting Environment?
Unlike a traditional REST API that responds in milliseconds, an AI agent often needs to sustain open connections for tens of seconds or even minutes. It may chain multiple LLM calls, invoke external APIs, wait for human approval steps, or manage a persistent memory store. The hosting environment must handle:
- Long-running processes without timeout restrictions
- State management across multiple invocations
- Secret storage for API keys and tool credentials
- Streaming responses (SSE or WebSocket) for real-time UX
- Horizontal scaling when hundreds of agents run concurrently
Your choice of hosting determines how gracefully you can satisfy these requirements. Let's examine each model through the lens of a concrete AI agent: a simple research assistant that takes a user query, performs a web search via a tool, summarizes the results, and returns the answer. We'll deploy this exact agent three different ways.
The Reference AI Agent
Before we compare hosting, here is the agent we'll be deploying. It uses OpenAI's SDK with a custom tool for web search. This code is self-contained and will serve as our baseline.
# agent_core.py
import os
import json
import asyncio
from openai import OpenAI
import httpx
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# Tool definition
def search_web(query: str) -> str:
"""Search the web for a query and return a summary."""
# Simulated search - replace with real API like SerpAPI or Tavily
results = [
f"Result for '{query}': AI agents are autonomous programs that use LLMs to plan and execute tasks.",
f"Additional context: Hosting options include managed, self-hosted, and serverless architectures."
]
return "\n".join(results[:2])
TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information on a given query",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}
}
}
]
async def run_agent(user_query: str) -> str:
"""Run the agent loop: call LLM, execute tools, return final answer."""
messages = [
{"role": "system", "content": "You are a helpful research assistant. Use the search_web tool to find information, then synthesize a concise answer."},
{"role": "user", "content": user_query}
]
# Agent loop - up to 3 tool calls
for _ in range(3):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
if tool_call.function.name == "search_web":
args = json.loads(tool_call.function.arguments)
result = search_web(args["query"])
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Continue loop
continue
else:
# No tool calls - this is the final answer
return message.content
return "Agent reached maximum tool call iterations."
# Synchronous wrapper for simpler deployment
def run_agent_sync(user_query: str) -> str:
return asyncio.run(run_agent(user_query))
This agent is intentionally straightforward—a single query triggers a loop of LLM reasoning and tool execution. In production, you might add memory, multi-step planning, or human-in-the-loop pauses. The hosting implications scale with that complexity.
Model 1: Managed AI Agent Platforms
What It Is
Managed platforms abstract away infrastructure completely. You provide your agent logic (often as a Python function or a configuration file), and the platform handles provisioning, scaling, monitoring, secrets management, and API exposure. Examples include Modal, Replit Agent Hosting, LangChain Cloud, Steamship, and Fly.io Machines (when used with their managed runtime). These platforms typically charge based on compute seconds, with generous free tiers for experimentation.
Why It Matters
- Zero DevOps overhead: No Dockerfiles, no Kubernetes, no load balancer configuration.
- Built-in long-running support: Many serverless platforms cap at 30–60 seconds; managed agent platforms expect runs of 5+ minutes.
- Integrated observability: Logs, traces, and cost breakdowns per agent invocation come out of the box.
- GPU access: If your agent uses local models, managed platforms often provide on-demand GPU instances.
Complete Deployment Example (Modal)
Modal lets you decorate Python functions and deploy them as scalable endpoints. The platform keeps your function "warm" and handles incoming HTTP requests. Here's how to deploy our research agent:
# deploy_managed_modal.py
import modal
from agent_core import run_agent_sync
# Create a Modal app
app = modal.App("research-agent-managed")
# Define a container image with dependencies
image = modal.Image.debian_slim().pip_install(
"openai",
"httpx"
)
# Mount the agent code so it's available inside the container
AGENT_CODE = modal.Mount.from_local_file(
"./agent_core.py",
remote_path="/root/agent_core.py"
)
@app.function(
image=image,
mounts=[AGENT_CODE],
secrets=[modal.Secret.from_name("openai-api-key")],
# Allow up to 5 minutes for agent execution
timeout=300,
# Keep a container warm for low latency
container_idle_timeout=600
)
@modal.web_endpoint(method="POST")
def run_research_agent(request: dict):
"""
Managed endpoint that runs the agent.
Request body: {"query": "What are AI agents?"}
"""
query = request.get("query", "")
if not query:
return {"error": "Missing 'query' field"}, 400
result = run_agent_sync(query)
return {"answer": result}
# Deploy with: modal deploy deploy_managed_modal.py
# The CLI will output a public URL like:
# https://your-org--research-agent-managed-run-research-agent.modal.run
With this single file and one CLI command, you get a production-grade HTTPS endpoint. Modal handles TLS termination, request queuing, autoscaling (up to hundreds of containers), and log streaming. The timeout=300 parameter is critical—it tells the platform to allow up to 5 minutes per invocation, which accommodates multiple LLM round-trips. The container_idle_timeout keeps a warm instance ready, avoiding cold starts of 2–5 seconds on subsequent requests.
Best Practices for Managed Hosting
- Set generous timeouts: Always check the platform's default timeout. Many default to 30 seconds, which will kill your agent mid-reasoning. Explicitly set 300–900 seconds.
- Use platform-native secrets: Never hardcode API keys. Bind secrets at the function level so they're injected as environment variables at runtime.
- Monitor cost per invocation: Managed platforms charge per compute second. Log the duration of each agent run so you can spot unexpectedly expensive queries.
- Leverage cold-start mitigation: If your platform supports "keep warm" instances, enable them for latency-sensitive endpoints. The cost is minimal compared to user-perceived slowness.
- Version your deployments: Use the platform's aliasing or versioning features to run staging and production deployments side by side.
Model 2: Self-Hosted Infrastructure
What It Is
Self-hosting means you run your agent on infrastructure you control: a VPS (DigitalOcean, AWS EC2, Hetzner), a Kubernetes cluster, or even on-premises hardware. You package the agent as a web service (typically FastAPI or Flask), wrap it in a Docker container, and manage the full stack yourself—load balancers, TLS certificates, monitoring, log aggregation, and scaling policies.
Why It Matters
- Full control: You choose the exact Python version, system libraries, and network configuration.
- No vendor lock-in: Your Docker image runs identically on any cloud or on-premises environment.
- Predictable pricing: A VPS costs a fixed monthly amount regardless of request volume (until you saturate it).
- Compliance and data sovereignty: For regulated industries, keeping data within your own VPC is non-negotiable.
Complete Deployment Example (FastAPI + Docker on a VPS)
We'll build a production-ready FastAPI service with streaming support, wrap it in Docker, and deploy it to a Linux VPS behind Nginx.
# server.py - FastAPI service for the research agent
import os
import sys
import asyncio
import json
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from pydantic import BaseModel
from openai import OpenAI
# Add parent to path for agent_core import
sys.path.insert(0, "/app")
from agent_core import run_agent
app = FastAPI(title="Research Agent API", version="1.0.0")
class QueryRequest(BaseModel):
query: str
stream: bool = False
class QueryResponse(BaseModel):
answer: str
tool_calls_made: int
@app.on_event("startup")
async def startup_event():
"""Verify API key is present on boot."""
if not os.environ.get("OPENAI_API_KEY"):
raise RuntimeError("OPENAI_API_KEY environment variable is required")
print("Agent server started successfully")
@app.post("/agent/run", response_model=QueryResponse)
async def run_agent_endpoint(request: QueryRequest):
"""Run the agent synchronously and return the result."""
try:
result = await run_agent(request.query)
return QueryResponse(answer=result, tool_calls_made=0)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/agent/stream")
async def stream_agent_endpoint(request: QueryRequest):
"""Stream the agent's reasoning steps as Server-Sent Events."""
async def event_generator():
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
messages = [
{"role": "system", "content": "You are a helpful research assistant."},
{"role": "user", "content": request.query}
]
tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}
}
]
yield f"data: {json.dumps({'event': 'started', 'query': request.query})}\n\n"
for _ in range(3):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
stream=True
)
# Collect streaming chunks
tool_calls_buffer = {}
content_buffer = ""
for chunk in response:
delta = chunk.choices[0].delta
if delta.content:
content_buffer += delta.content
yield f"data: {json.dumps({'event': 'thought', 'text': delta.content})}\n\n"
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
if idx not in tool_calls_buffer:
tool_calls_buffer[idx] = {"id": "", "name": "", "arguments": ""}
if tc.id:
tool_calls_buffer[idx]["id"] = tc.id
if tc.function and tc.function.name:
tool_calls_buffer[idx]["name"] = tc.function.name
if tc.function and tc.function.arguments:
tool_calls_buffer[idx]["arguments"] += tc.function.arguments
if tool_calls_buffer:
# Execute tools
for tc_data in tool_calls_buffer.values():
yield f"data: {json.dumps({'event': 'tool_call', 'name': tc_data['name'], 'args': tc_data['arguments']})}\n\n"
# Simulate tool result
result = f"Search results for the query..."
messages.append({"role": "tool", "tool_call_id": tc_data['id'], "content": result})
continue
else:
yield f"data: {json.dumps({'event': 'done', 'answer': content_buffer})}\n\n"
break
yield "data: {\"event\": \"finished\"}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Disable Nginx buffering for SSE
}
)
@app.get("/health")
async def health_check():
return {"status": "healthy", "uptime": "ok"}
Now the Dockerfile that packages everything:
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY agent_core.py .
COPY server.py .
# Create non-root user for security
RUN useradd -m -s /bin/bash agentuser && chown -R agentuser:agentuser /app
USER agentuser
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "2"]
The requirements file:
# requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.6
openai==1.55.0
httpx==0.28.1
pydantic==2.10.0
Deployment commands for a VPS running Ubuntu 22.04:
# On your local machine - build and push the image
docker build -t research-agent:v1 .
docker save research-agent:v1 | gzip > research-agent.tar.gz
# Copy to VPS
scp research-agent.tar.gz user@your-vps:/home/user/
# On the VPS - load and run
sudo docker load < research-agent.tar.gz
# Create a .env file with your API key
echo "OPENAI_API_KEY=sk-your-key-here" > /home/user/agent.env
# Run the container with restart policy
sudo docker run -d \
--name research-agent \
--env-file /home/user/agent.env \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
research-agent:v1
# Set up Nginx as reverse proxy
sudo apt install -y nginx certbot python3-certbot-nginx
# Create Nginx config
sudo tee /etc/nginx/sites-available/research-agent << 'EOF'
server {
listen 80;
server_name agent.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Critical for SSE streaming
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/research-agent /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Get HTTPS certificate
sudo certbot --nginx -d agent.yourdomain.com --non-interactive --agree-tos
Best Practices for Self-Hosting
- Use a reverse proxy: Nginx or Caddy handles TLS, rate limiting, and buffering control. Disable proxy buffering for SSE streams as shown above.
- Run as non-root user: The Dockerfile explicitly creates an
agentuseraccount. Never run containers as root in production. - Implement health checks: The Docker HEALTHCHECK directive and the
/healthendpoint allow orchestration tools to detect and restart unhealthy instances. - Set resource limits: Use Docker's
--memoryand--cpusflags to prevent a runaway agent from consuming all VPS resources. - Centralize logs: Configure Docker's logging driver to ship logs to Loki, CloudWatch, or a simple rotating file on the host.
- Plan for state: If your agent needs persistent memory across invocations, mount a Docker volume for a SQLite database or connect to a managed Postgres instance.
- Automate deployments: Use a simple shell script or GitHub Actions to build, push, and SSH-deploy new versions. Manual SCP works for prototypes but doesn't scale.
Model 3: Serverless Functions
What It Is
Serverless platforms like AWS Lambda, Google Cloud Functions, Azure Container Apps, or Cloudflare Workers execute your code in ephemeral containers that spin up on demand and disappear after the response. You pay only for the milliseconds of actual execution. The platform handles scaling, load balancing, and availability automatically.
Why It Matters
- Cost efficiency for sporadic workloads: If your agent receives 50 requests per day, you pay almost nothing.
- Automatic scaling to zero: No idle compute costs. The function sleeps when unused.
- Deep cloud integration: Native access to cloud services like S3, DynamoDB, and SQS simplifies building agent pipelines.
The Serverless Challenge: Timeouts and State
Standard serverless functions have hard timeout limits: AWS Lambda caps at 15 minutes (900 seconds), but many providers like Cloudflare Workers cap at 30 seconds. For AI agents that need multiple LLM round-trips, these limits are dangerous. Additionally, serverless functions are stateless by design—you cannot rely on in-memory state between invocations. This fundamentally changes how you architect the agent.
Complete Deployment Example (AWS Lambda + Function URL)
We'll deploy the agent to AWS Lambda using a container image (not the ZIP upload method, because our dependencies exceed the 250 MB limit). We'll expose it via a Lambda Function URL with streaming support.
# lambda_handler.py - Entry point for AWS Lambda
import os
import json
import asyncio
from openai import OpenAI
# Lambda runs in a single-threaded environment by default.
# We'll use the synchronous version of our agent.
from agent_core import run_agent_sync
def lambda_handler(event, context):
"""
AWS Lambda handler for the research agent.
Event format follows API Gateway / Function URL conventions.
"""
try:
# Parse the request body
body = json.loads(event.get("body", "{}"))
query = body.get("query", "")
if not query:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "Missing 'query' field"})
}
# Run the agent synchronously
result = run_agent_sync(query)
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
"body": json.dumps({"answer": result})
}
except Exception as e:
print(f"Agent execution failed: {str(e)}")
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": f"Agent error: {str(e)}"})
}
# For streaming responses, we need a different approach.
# Lambda Function URLs support response streaming via the InvokeWithResponseStream API.
# Here's a streaming-compatible handler using the runtime's built-in stream support:
def lambda_handler_streaming(event, context):
"""
Streaming handler that flushes chunks as they arrive.
Requires: AWS Lambda runtime with streaming support (Python 3.12+)
"""
import io
body = json.loads(event.get("body", "{}"))
query = body.get("query", "What is an AI agent?")
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
messages = [
{"role": "system", "content": "You are a helpful research assistant."},
{"role": "user", "content": query}
]
# Use streaming response from OpenAI
response_stream = client.chat.completions.create(
model="gpt-4o",
messages=messages,
stream=True
)
# Build a streaming response
def stream_generator():
yield json.dumps({"event": "started"}) + "\n"
for chunk in response_stream:
if chunk.choices[0].delta.content:
yield json.dumps({
"event": "thought",
"text": chunk.choices[0].delta.content
}) + "\n"
yield json.dumps({"event": "done"}) + "\n"
# Return a streaming response
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/x-ndjson",
"Transfer-Encoding": "chunked"
},
"body": stream_generator()
}
Dockerfile optimized for Lambda:
# Dockerfile.lambda
FROM public.ecr.aws/lambda/python:3.12
WORKDIR ${LAMBDA_TASK_ROOT}
# Copy application code
COPY agent_core.py ${LAMBDA_TASK_ROOT}/agent_core.py
COPY lambda_handler.py ${LAMBDA_TASK_ROOT}/lambda_handler.py
# Install dependencies
COPY requirements-lambda.txt .
RUN pip install --no-cache-dir -r requirements-lambda.txt
# Lambda uses the CMD to locate the handler
CMD ["lambda_handler.lambda_handler"]
# requirements-lambda.txt
openai==1.55.0
httpx==0.28.1
Deployment script using AWS CLI:
#!/bin/bash
# deploy_lambda.sh - Deploy the agent to AWS Lambda
AWS_REGION="us-east-1"
FUNCTION_NAME="research-agent-lambda"
ROLE_ARN="arn:aws:iam::123456789012:role/lambda-execution-role"
# 1. Build the Docker image for Lambda
docker build -t research-agent-lambda:v1 -f Dockerfile.lambda .
# 2. Create an ECR repository (skip if exists)
aws ecr create-repository \
--repository-name research-agent-lambda \
--region $AWS_REGION 2>/dev/null || true
# 3. Authenticate Docker to ECR
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.$AWS_REGION.amazonaws.com
# 4. Tag and push the image
docker tag research-agent-lambda:v1 \
123456789012.dkr.ecr.$AWS_REGION.amazonaws.com/research-agent-lambda:v1
docker push 123456789012.dkr.ecr.$AWS_REGION.amazonaws.com/research-agent-lambda:v1
# 5. Create or update the Lambda function
aws lambda create-function \
--function-name $FUNCTION_NAME \
--package-type Image \
--code ImageUri=123456789012.dkr.ecr.$AWS_REGION.amazonaws.com/research-agent-lambda:v1 \
--role $ROLE_ARN \
--timeout 900 \
--memory-size 2048 \
--environment-variables "OPENAI_API_KEY=sk-your-key-here" \
--region $AWS_REGION 2>/dev/null || \
aws lambda update-function-code \
--function-name $FUNCTION_NAME \
--image-uri 123456789012.dkr.ecr.$AWS_REGION.amazonaws.com/research-agent-lambda:v1 \
--region $AWS_REGION
# 6. Create a Function URL (public HTTPS endpoint)
aws lambda create-url-config \
--function-name $FUNCTION_NAME \
--auth-type NONE \
--invoke-mode RESPONSE_STREAM \
--region $AWS_REGION
# 7. Get the Function URL
aws lambda get-url-config \
--function-name $FUNCTION_NAME \
--region $AWS_REGION \
--query 'FunctionUrl' --output text
The critical detail here is --timeout 900—we're setting the Lambda timeout to the maximum 15 minutes. For agents that may run longer, Lambda alone won't suffice; you'd need to decompose the agent into a step function workflow. Also note --invoke-mode RESPONSE_STREAM which enables streaming responses through the Function URL, essential for real-time agent feedback.
Best Practices for Serverless AI Agents
- Max out the timeout: Set Lambda timeout to 900 seconds (or your provider's maximum). If your agent can exceed this, break it into smaller steps orchestrated by AWS Step Functions or Temporal.
- Use provisioned concurrency for cold starts: Lambda cold starts (especially with container images) can take 3–8 seconds. Provisioned concurrency keeps instances warm, though it eliminates the "scale to zero" cost benefit.
- Externalize state: Never rely on in-memory state. Persist conversation history, agent memory, and intermediate results in DynamoDB or S3. Each invocation should be reconstructable from durable storage.
- Monitor memory usage: AI agents with large context windows can consume 1–2 GB of RAM. Set
--memory-sizeappropriately—Lambda charges proportionally to allocated memory. - Handle graceful degradation: If the Lambda is about to time out (check
context.get_remaining_time_in_millis()), save partial results and return them rather than crashing silently. - Use asynchronous invocation for fire-and-forget agents: If the caller doesn't need an immediate response, invoke the Lambda asynchronously and have it write results to a webhook or database.
Side-by-Side Comparison
Here is a summary table to help you choose based on your specific constraints:
| Criterion | Managed | Self-Hosted | Serverless |
|---|---|---|---|
| Setup Time | Minutes (single CLI command) | Hours (Docker, Nginx, TLS, monitoring) | 30–60 minutes (ECR push, IAM roles) |
| Max Execution Time | Configurable, typically 15 min+ | Unlimited (set by you) | Provider-limited (15 min on Lambda, 30s on Workers) |
| Cold Start Latency | 2–5s (mitigated by warm pools) | 0s (always running) | 3–8s (mitigated by provisioned concurrency) |
| Cost Model | Per compute second + idle warm fee | Fixed monthly (VPS/instance cost) | Per millisecond of execution only |
| Scaling Limit | Platform-defined (often 100+ concurrent) | Hardware-limited (you scale manually) | Cloud-defined (1000+ concurrent for Lambda) |
| State Management | Platform-provided (Redis, DB, or disk mounts) | Full control (volumes, external DBs) | Must be fully externalized |
| Streaming Support | First-class (SSE, WebSocket) | Full control (Nginx tuning required) | Varies (Lambda supports streaming; Workers limited) |
| Data Sovereignty | Data flows through platform infra | Complete control over data path | Data stays within your cloud VPC |
When to Choose Each Model
Choose Managed when you're an individual developer or a small team that wants to ship an AI agent quickly without building infrastructure. Managed platforms excel at developer experience and time-to-market. They're also ideal when you need GPU access for local models but don't want to manage CUDA drivers and container registries yourself.
Choose Self-Hosted when you have strict compliance requirements, need guaranteed uptime with no cold starts, or expect consistent high traffic that makes fixed-cost VPS more economical than per-request billing. Self-hosting also makes sense when your agent requires specialized hardware, custom network configurations, or integration with on-premises systems.
Choose Serverless when your agent workload is sporadic (bursts of requests with long idle periods), you need massive horizontal scaling without manual intervention, or you're already deeply invested in a cloud ecosystem and want tight integration with services like SQS, DynamoDB, and Step Functions. Serverless works best for agents that complete within the timeout limit and don't require persistent in-memory state.
Hybrid Architectures: The Pragmatic Reality
In production, many teams end up with a hybrid approach. A common pattern is:
- A self-hosted or managed long-running service handles the core agent loop, maintaining WebSocket connections to the frontend.
- Serverless functions handle auxiliary tasks: webhook ingestion, tool execution for simple lookups, or post-processing of agent outputs.
- A managed queue (S