This commit is contained in:
Timothy Jaeryang Baek 2026-11-02 17:49:51 +09:00
parent 2840f0e07b
commit f0cc1bb86e
4 changed files with 88 additions and 36 deletions

View file

@ -2081,10 +2081,15 @@ ENABLE_AUTOMATIONS = os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true'
ENABLE_SUBAGENTS = os.getenv('ENABLE_SUBAGENTS', 'False').lower() == 'true'
SUBAGENTS_BACKGROUND_ENABLED = os.getenv('SUBAGENTS_BACKGROUND_ENABLED', 'False').lower() == 'true'
SUBAGENTS_MAX_CONCURRENT = int(os.getenv('SUBAGENTS_MAX_CONCURRENT', '20'))
SUBAGENTS_MAX_ASYNC = int(os.getenv('SUBAGENTS_MAX_ASYNC', '20'))
SUBAGENTS_MAX_ITERATIONS = int(os.getenv('SUBAGENTS_MAX_ITERATIONS', '30'))
SUBAGENTS_MAX_OUTPUT = int(os.getenv('SUBAGENTS_MAX_OUTPUT', '30000'))
# Leave empty for unlimited.
SUBAGENTS_MAX_CONCURRENT = os.getenv('SUBAGENTS_MAX_CONCURRENT', '')
SUBAGENTS_MAX_CONCURRENT = int(SUBAGENTS_MAX_CONCURRENT) if SUBAGENTS_MAX_CONCURRENT else None
SUBAGENTS_MAX_ASYNC = os.getenv('SUBAGENTS_MAX_ASYNC', '')
SUBAGENTS_MAX_ASYNC = int(SUBAGENTS_MAX_ASYNC) if SUBAGENTS_MAX_ASYNC else None
SUBAGENTS_MAX_ITERATIONS = os.getenv('SUBAGENTS_MAX_ITERATIONS', '')
SUBAGENTS_MAX_ITERATIONS = int(SUBAGENTS_MAX_ITERATIONS) if SUBAGENTS_MAX_ITERATIONS else None
SUBAGENTS_MAX_OUTPUT = os.getenv('SUBAGENTS_MAX_OUTPUT', '')
SUBAGENTS_MAX_OUTPUT = int(SUBAGENTS_MAX_OUTPUT) if SUBAGENTS_MAX_OUTPUT else None
SUBAGENTS_SYSTEM_PROMPT = os.getenv('SUBAGENTS_SYSTEM_PROMPT', '')
AUTOMATION_MAX_COUNT = os.getenv('AUTOMATION_MAX_COUNT', '')

View file

@ -31,7 +31,7 @@ from open_webui.utils.tools import (
set_terminal_servers,
set_tool_servers,
)
from pydantic import BaseModel, ConfigDict, StrictBool
from pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator
router = APIRouter()
@ -799,12 +799,28 @@ async def set_models_config(request: Request, form_data: ModelsConfigForm, user=
class SubagentsConfigForm(BaseModel):
ENABLE_SUBAGENTS: bool
SUBAGENTS_BACKGROUND_ENABLED: bool
SUBAGENTS_MAX_CONCURRENT: int
SUBAGENTS_MAX_ASYNC: int
SUBAGENTS_MAX_ITERATIONS: int
SUBAGENTS_MAX_OUTPUT: int
SUBAGENTS_MAX_CONCURRENT: int | None = Field(default=None, gt=0)
SUBAGENTS_MAX_ASYNC: int | None = Field(default=None, gt=0)
SUBAGENTS_MAX_ITERATIONS: int | None = Field(default=None, gt=0)
SUBAGENTS_MAX_OUTPUT: int | None = Field(default=None, gt=0)
SUBAGENTS_SYSTEM_PROMPT: str
@field_validator(
'SUBAGENTS_MAX_CONCURRENT',
'SUBAGENTS_MAX_ASYNC',
'SUBAGENTS_MAX_ITERATIONS',
'SUBAGENTS_MAX_OUTPUT',
mode='before',
)
@classmethod
def normalize_limit(cls, value):
return None if value == '' else value
@field_validator('SUBAGENTS_MAX_CONCURRENT', 'SUBAGENTS_MAX_ASYNC', mode='before')
@classmethod
def normalize_legacy_unlimited(cls, value):
return None if value == -1 else value
@router.get('/subagents', response_model=SubagentsConfigForm)
async def get_subagents_config(user=Depends(get_admin_user)):

View file

@ -322,13 +322,21 @@ async def delegate(
'subagents.max_output',
'subagents.system_prompt',
)
max_concurrent = int(config.get('subagents.max_concurrent') or 20)
max_async = int(config.get('subagents.max_async') or 20)
max_iterations = int(config.get('subagents.max_iterations') or 30)
max_output = int(config.get('subagents.max_output') or 30_000)
if max_concurrent != -1:
max_concurrent = config.get('subagents.max_concurrent')
max_concurrent = int(max_concurrent) if max_concurrent else None
max_async = config.get('subagents.max_async')
max_async = int(max_async) if max_async else None
max_iterations = config.get('subagents.max_iterations')
max_iterations = int(max_iterations) if max_iterations else None
max_output = config.get('subagents.max_output')
max_output = int(max_output) if max_output else None
if max_concurrent == -1:
max_concurrent = None
elif max_concurrent is not None:
max_concurrent = max(1, max_concurrent)
if max_async != -1:
if max_async == -1:
max_async = None
elif max_async is not None:
max_async = max(1, max_async)
if background and not config.get('subagents.background_enabled'):
@ -406,13 +414,13 @@ async def delegate(
foreground_semaphore = None
if background:
async with _background_lock:
if max_async != -1 and len(_background_active) >= max_async:
if max_async is not None and len(_background_active) >= max_async:
return (
f'Error: Async subagent capacity reached ({max_async} running). '
'Wait for one to finish or increase subagents.max_async.'
)
_background_active.add(delegation_id)
elif max_concurrent != -1:
elif max_concurrent is not None:
if _foreground_semaphore is None:
_foreground_semaphore = asyncio.Semaphore(max_concurrent)
foreground_semaphore = _foreground_semaphore
@ -551,7 +559,7 @@ async def delegate(
for part in item.get('content') or []
if part.get('type') == 'output_text'
)
if len(summary) > max_output:
if max_output is not None and len(summary) > max_output:
summary = f'{summary[:max_output]}\n\n[output truncated]'
error = message.get('error')
if isinstance(error, dict):

View file

@ -15,10 +15,10 @@
let saving = false;
let enabled = false;
let backgroundEnabled = false;
let maxConcurrent = 20;
let maxAsync = 20;
let maxIterations = 30;
let maxOutput = 30000;
let maxConcurrent: number | null = null;
let maxAsync: number | null = null;
let maxIterations: number | null = null;
let maxOutput: number | null = null;
let systemPrompt = '';
onMount(async () => {
@ -26,10 +26,10 @@
const config = await getSubagentsConfig(localStorage.token);
enabled = config?.ENABLE_SUBAGENTS ?? false;
backgroundEnabled = config?.SUBAGENTS_BACKGROUND_ENABLED ?? false;
maxConcurrent = Number(config?.SUBAGENTS_MAX_CONCURRENT) || 20;
maxAsync = Number(config?.SUBAGENTS_MAX_ASYNC) || 20;
maxIterations = Number(config?.SUBAGENTS_MAX_ITERATIONS) || 30;
maxOutput = Number(config?.SUBAGENTS_MAX_OUTPUT) || 30000;
maxConcurrent = config?.SUBAGENTS_MAX_CONCURRENT ?? null;
maxAsync = config?.SUBAGENTS_MAX_ASYNC ?? null;
maxIterations = config?.SUBAGENTS_MAX_ITERATIONS ?? null;
maxOutput = config?.SUBAGENTS_MAX_OUTPUT ?? null;
systemPrompt = config?.SUBAGENTS_SYSTEM_PROMPT ?? '';
} catch (error) {
toast.error(`${error}`);
@ -44,10 +44,10 @@
await setSubagentsConfig(localStorage.token, {
ENABLE_SUBAGENTS: enabled,
SUBAGENTS_BACKGROUND_ENABLED: backgroundEnabled,
SUBAGENTS_MAX_CONCURRENT: maxConcurrent,
SUBAGENTS_MAX_ASYNC: maxAsync,
SUBAGENTS_MAX_ITERATIONS: maxIterations,
SUBAGENTS_MAX_OUTPUT: maxOutput,
SUBAGENTS_MAX_CONCURRENT: maxConcurrent ?? null,
SUBAGENTS_MAX_ASYNC: maxAsync ?? null,
SUBAGENTS_MAX_ITERATIONS: maxIterations ?? null,
SUBAGENTS_MAX_OUTPUT: maxOutput ?? null,
SUBAGENTS_SYSTEM_PROMPT: systemPrompt
});
await appConfig.set(await getBackendConfig());
@ -92,13 +92,21 @@
id="sa-concurrent"
type="number"
bind:value={maxConcurrent}
min="-1"
min="1"
step="1"
aria-describedby="sa-concurrent-help"
class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('simultaneous sub-agents')}
</span>
</div>
<p
id="sa-concurrent-help"
class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600"
>
{$i18n.t('Leave empty for unlimited')}
</p>
</div>
<div>
@ -123,13 +131,18 @@
id="sa-async"
type="number"
bind:value={maxAsync}
min="-1"
min="1"
step="1"
aria-describedby="sa-async-help"
class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('background sub-agents')}
</span>
</div>
<p id="sa-async-help" class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Leave empty for unlimited')}
</p>
</div>
{/if}
@ -143,13 +156,20 @@
type="number"
bind:value={maxIterations}
min="1"
max="100"
step="1"
aria-describedby="sa-iterations-help"
class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('tool loops per sub-agent')}
</span>
</div>
<p
id="sa-iterations-help"
class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600"
>
{$i18n.t('Leave empty for unlimited')}
</p>
</div>
<div>
@ -161,13 +181,16 @@
id="sa-output"
type="number"
bind:value={maxOutput}
min="1000"
max="100000"
step="1000"
min="1"
step="1"
aria-describedby="sa-output-help"
class="h-7 w-20 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">chars</span>
</div>
<p id="sa-output-help" class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Leave empty for unlimited')}
</p>
</div>
<div>