mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Enable E2B `auto_resume` lifecycle option and reduce the safety-net timeout from 3 hours to 5 minutes. Currently, if the explicit per-turn `pause_sandbox_direct()` call fails (process crash, network issue, fire-and-forget task cancellation), the sandbox keeps running for up to **3 hours** before the safety-net timeout fires. With this change, worst-case billing drops to **5 minutes**. ### Changes - Add `auto_resume: True` to sandbox lifecycle config — paused sandboxes wake transparently on SDK activity - Reduce `e2b_sandbox_timeout` default from 10800s (3h) → 300s (5min) - Add `e2b_sandbox_auto_resume` config field (default: `True`) - Guard: `auto_resume` only added when `on_timeout == "pause"` ### What doesn't change - Explicit per-turn `pause_sandbox_direct()` remains the primary mechanism - `connect()` / `_try_reconnect()` flow unchanged - Redis key management unchanged - No latency impact (resume is ~1-2s regardless of trigger) ### Risk Very low — `auto_resume` is additive. If it doesn't work as advertised, `connect()` still resumes paused sandboxes exactly as before. Ref: https://e2b.dev/docs/sandbox/auto-resume Linear: SECRT-2118 --- Co-authored-by: Zamil Majdy (@majdyz) <zamil.majdy@agpt.co>
233 lines
8.7 KiB
Python
233 lines
8.7 KiB
Python
"""Configuration management for chat system."""
|
|
|
|
import os
|
|
from typing import Literal
|
|
|
|
from pydantic import Field, field_validator
|
|
from pydantic_settings import BaseSettings
|
|
|
|
from backend.util.clients import OPENROUTER_BASE_URL
|
|
|
|
|
|
class ChatConfig(BaseSettings):
|
|
"""Configuration for the chat system."""
|
|
|
|
# OpenAI API Configuration
|
|
model: str = Field(
|
|
default="anthropic/claude-opus-4.6", description="Default model to use"
|
|
)
|
|
title_model: str = Field(
|
|
default="openai/gpt-4o-mini",
|
|
description="Model to use for generating session titles (should be fast/cheap)",
|
|
)
|
|
api_key: str | None = Field(default=None, description="OpenAI API key")
|
|
base_url: str | None = Field(
|
|
default=OPENROUTER_BASE_URL,
|
|
description="Base URL for API (e.g., for OpenRouter)",
|
|
)
|
|
|
|
# Session TTL Configuration - 12 hours
|
|
session_ttl: int = Field(default=43200, description="Session TTL in seconds")
|
|
|
|
max_agent_runs: int = Field(default=30, description="Maximum number of agent runs")
|
|
max_agent_schedules: int = Field(
|
|
default=30, description="Maximum number of agent schedules"
|
|
)
|
|
|
|
# Stream registry configuration for SSE reconnection
|
|
stream_ttl: int = Field(
|
|
default=3600,
|
|
description="TTL in seconds for stream data in Redis (1 hour)",
|
|
)
|
|
stream_lock_ttl: int = Field(
|
|
default=120,
|
|
description="TTL in seconds for stream lock (2 minutes). Short timeout allows "
|
|
"reconnection after refresh/crash without long waits.",
|
|
)
|
|
stream_max_length: int = Field(
|
|
default=10000,
|
|
description="Maximum number of messages to store per stream",
|
|
)
|
|
|
|
# Redis key prefixes for stream registry
|
|
session_meta_prefix: str = Field(
|
|
default="chat:task:meta:",
|
|
description="Prefix for session metadata hash keys",
|
|
)
|
|
turn_stream_prefix: str = Field(
|
|
default="chat:stream:",
|
|
description="Prefix for turn message stream keys",
|
|
)
|
|
|
|
# Langfuse Prompt Management Configuration
|
|
# Note: Langfuse credentials are in Settings().secrets (settings.py)
|
|
langfuse_prompt_name: str = Field(
|
|
default="CoPilot Prompt",
|
|
description="Name of the prompt in Langfuse to fetch",
|
|
)
|
|
langfuse_prompt_cache_ttl: int = Field(
|
|
default=300,
|
|
description="Cache TTL in seconds for Langfuse prompt (0 to disable caching)",
|
|
)
|
|
|
|
# Claude Agent SDK Configuration
|
|
use_claude_agent_sdk: bool = Field(
|
|
default=True,
|
|
description="Use Claude Agent SDK (True) or OpenAI-compatible LLM baseline (False)",
|
|
)
|
|
claude_agent_model: str | None = Field(
|
|
default=None,
|
|
description="Model for the Claude Agent SDK path. If None, derives from "
|
|
"the `model` field by stripping the OpenRouter provider prefix.",
|
|
)
|
|
claude_agent_max_buffer_size: int = Field(
|
|
default=10 * 1024 * 1024, # 10MB (default SDK is 1MB)
|
|
description="Max buffer size in bytes for Claude Agent SDK JSON message parsing. "
|
|
"Increase if tool outputs exceed the limit.",
|
|
)
|
|
claude_agent_max_subtasks: int = Field(
|
|
default=10,
|
|
description="Max number of concurrent sub-agent Tasks the SDK can run per session.",
|
|
)
|
|
claude_agent_use_resume: bool = Field(
|
|
default=True,
|
|
description="Use --resume for multi-turn conversations instead of "
|
|
"history compression. Falls back to compression when unavailable.",
|
|
)
|
|
use_claude_code_subscription: bool = Field(
|
|
default=False,
|
|
description="For personal/dev use: use Claude Code CLI subscription auth instead of API keys. Requires `claude login` on the host. Only works with SDK mode.",
|
|
)
|
|
|
|
# E2B Sandbox Configuration
|
|
use_e2b_sandbox: bool = Field(
|
|
default=True,
|
|
description="Use E2B cloud sandboxes for persistent bash/python execution. "
|
|
"When enabled, bash_exec routes commands to E2B and SDK file tools "
|
|
"operate directly on the sandbox via E2B's filesystem API.",
|
|
)
|
|
e2b_api_key: str | None = Field(
|
|
default=None,
|
|
description="E2B API key. Falls back to E2B_API_KEY environment variable.",
|
|
)
|
|
e2b_sandbox_template: str = Field(
|
|
default="base",
|
|
description="E2B sandbox template to use for copilot sessions.",
|
|
)
|
|
e2b_sandbox_timeout: int = Field(
|
|
default=300, # 5 min safety net — explicit per-turn pause is the primary mechanism
|
|
description="E2B sandbox running-time timeout (seconds). "
|
|
"E2B timeout is wall-clock (not idle). Explicit per-turn pause is the primary "
|
|
"mechanism; this is the safety net.",
|
|
)
|
|
e2b_sandbox_on_timeout: Literal["kill", "pause"] = Field(
|
|
default="pause",
|
|
description="E2B lifecycle action on timeout: 'pause' (default, free) or 'kill'.",
|
|
)
|
|
|
|
@property
|
|
def e2b_active(self) -> bool:
|
|
"""True when E2B is enabled and the API key is present.
|
|
|
|
Single source of truth for "should we use E2B right now?".
|
|
Prefer this over combining ``use_e2b_sandbox`` and ``e2b_api_key``
|
|
separately at call sites.
|
|
"""
|
|
return self.use_e2b_sandbox and bool(self.e2b_api_key)
|
|
|
|
@property
|
|
def active_e2b_api_key(self) -> str | None:
|
|
"""Return the E2B API key when E2B is enabled and configured, else None.
|
|
|
|
Combines the ``use_e2b_sandbox`` flag check and key presence into one.
|
|
Use in callers::
|
|
|
|
if api_key := config.active_e2b_api_key:
|
|
# E2B is active; api_key is narrowed to str
|
|
"""
|
|
return self.e2b_api_key if self.e2b_active else None
|
|
|
|
@field_validator("use_e2b_sandbox", mode="before")
|
|
@classmethod
|
|
def get_use_e2b_sandbox(cls, v):
|
|
"""Get use_e2b_sandbox from environment if not provided."""
|
|
env_val = os.getenv("CHAT_USE_E2B_SANDBOX", "").lower()
|
|
if env_val:
|
|
return env_val in ("true", "1", "yes", "on")
|
|
return True if v is None else v
|
|
|
|
@field_validator("e2b_api_key", mode="before")
|
|
@classmethod
|
|
def get_e2b_api_key(cls, v):
|
|
"""Get E2B API key from environment if not provided."""
|
|
if not v:
|
|
v = os.getenv("CHAT_E2B_API_KEY") or os.getenv("E2B_API_KEY")
|
|
return v
|
|
|
|
@field_validator("api_key", mode="before")
|
|
@classmethod
|
|
def get_api_key(cls, v):
|
|
"""Get API key from environment if not provided."""
|
|
if not v:
|
|
# Try to get from environment variables
|
|
# First check for CHAT_API_KEY (Pydantic prefix)
|
|
v = os.getenv("CHAT_API_KEY")
|
|
if not v:
|
|
# Fall back to OPEN_ROUTER_API_KEY
|
|
v = os.getenv("OPEN_ROUTER_API_KEY")
|
|
if not v:
|
|
# Fall back to OPENAI_API_KEY
|
|
v = os.getenv("OPENAI_API_KEY")
|
|
# Note: ANTHROPIC_API_KEY is intentionally NOT included here.
|
|
# The SDK CLI picks it up from the env directly. Including it
|
|
# would pair it with the OpenRouter base_url, causing auth failures.
|
|
return v
|
|
|
|
@field_validator("base_url", mode="before")
|
|
@classmethod
|
|
def get_base_url(cls, v):
|
|
"""Get base URL from environment if not provided."""
|
|
if not v:
|
|
# Check for OpenRouter or custom base URL
|
|
v = os.getenv("CHAT_BASE_URL")
|
|
if not v:
|
|
v = os.getenv("OPENROUTER_BASE_URL")
|
|
if not v:
|
|
v = os.getenv("OPENAI_BASE_URL")
|
|
if not v:
|
|
v = OPENROUTER_BASE_URL
|
|
return v
|
|
|
|
@field_validator("use_claude_agent_sdk", mode="before")
|
|
@classmethod
|
|
def get_use_claude_agent_sdk(cls, v):
|
|
"""Get use_claude_agent_sdk from environment if not provided."""
|
|
# Check environment variable - default to True if not set
|
|
env_val = os.getenv("CHAT_USE_CLAUDE_AGENT_SDK", "").lower()
|
|
if env_val:
|
|
return env_val in ("true", "1", "yes", "on")
|
|
# Default to True (SDK enabled by default)
|
|
return True if v is None else v
|
|
|
|
@field_validator("use_claude_code_subscription", mode="before")
|
|
@classmethod
|
|
def get_use_claude_code_subscription(cls, v):
|
|
"""Get use_claude_code_subscription from environment if not provided."""
|
|
env_val = os.getenv("CHAT_USE_CLAUDE_CODE_SUBSCRIPTION", "").lower()
|
|
if env_val:
|
|
return env_val in ("true", "1", "yes", "on")
|
|
return False if v is None else v
|
|
|
|
# Prompt paths for different contexts
|
|
PROMPT_PATHS: dict[str, str] = {
|
|
"default": "prompts/chat_system.md",
|
|
"onboarding": "prompts/onboarding_system.md",
|
|
}
|
|
|
|
class Config:
|
|
"""Pydantic config."""
|
|
|
|
env_file = ".env"
|
|
env_file_encoding = "utf-8"
|
|
extra = "ignore" # Ignore extra environment variables
|