mirror of
https://github.com/open-webui/computer.git
synced 2026-11-15 02:15:44 +03:00
refac
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount } from 'svelte';
|
||||
import { getAdminConfig, updateConfig } from '$lib/apis/admin';
|
||||
import { t } from '$lib/i18n';
|
||||
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
|
||||
let enabled = $state(false);
|
||||
let maxConcurrent = $state(3);
|
||||
let maxIterations = $state(30);
|
||||
let maxOutput = $state(30000);
|
||||
let systemPrompt = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await getAdminConfig();
|
||||
enabled = config['subagents.enabled'] === true || config['subagents.enabled'] === 'true';
|
||||
maxConcurrent = Number(config['subagents.max_concurrent']) || 3;
|
||||
maxIterations = Number(config['subagents.max_iterations']) || 30;
|
||||
maxOutput = Number(config['subagents.max_output']) || 30000;
|
||||
systemPrompt = (config['subagents.system_prompt'] as string) || '';
|
||||
} catch {
|
||||
toast.error($t('admin.failedToLoadConfig'));
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
|
||||
async function save() {
|
||||
saving = true;
|
||||
try {
|
||||
await updateConfig({
|
||||
'subagents.enabled': enabled,
|
||||
'subagents.max_concurrent': maxConcurrent,
|
||||
'subagents.max_iterations': maxIterations,
|
||||
'subagents.max_output': maxOutput,
|
||||
'subagents.system_prompt': systemPrompt
|
||||
});
|
||||
toast.success($t('settings.saved'));
|
||||
} catch {
|
||||
toast.error($t('admin.failedToSave'));
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col min-h-full">
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$t('admin.subagents')}</h2>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-8"><Spinner size={16} /></div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label class="flex items-center justify-between cursor-pointer">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{$t('admin.subagentsEnabled')}</span>
|
||||
<ToggleSwitch value={enabled} onchange={(v) => { enabled = v; }} />
|
||||
</label>
|
||||
<p class="text-[11px] text-gray-400 dark:text-gray-600 -mt-1">
|
||||
{$t('admin.subagentsHint')}
|
||||
</p>
|
||||
|
||||
{#if enabled}
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 dark:text-gray-400" for="sa-concurrent">{$t('admin.subagentsMaxConcurrent')}</label>
|
||||
<div class="flex items-center gap-1.5 mt-1">
|
||||
<input id="sa-concurrent" type="number" bind:value={maxConcurrent} min="1" max="10"
|
||||
class="w-16 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors" />
|
||||
<span class="text-[11px] text-gray-400 dark:text-gray-600">{$t('admin.subagentsMaxConcurrentHint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 dark:text-gray-400" for="sa-iterations">{$t('admin.subagentsMaxIterations')}</label>
|
||||
<div class="flex items-center gap-1.5 mt-1">
|
||||
<input id="sa-iterations" type="number" bind:value={maxIterations} min="1" max="100"
|
||||
class="w-16 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors" />
|
||||
<span class="text-[11px] text-gray-400 dark:text-gray-600">{$t('admin.subagentsMaxIterationsHint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 dark:text-gray-400" for="sa-output">{$t('admin.subagentsMaxOutput')}</label>
|
||||
<div class="flex items-center gap-1.5 mt-1">
|
||||
<input id="sa-output" type="number" bind:value={maxOutput} min="1000" max="100000" step="1000"
|
||||
class="w-20 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors" />
|
||||
<span class="text-[11px] text-gray-400 dark:text-gray-600">chars</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 dark:text-gray-400" for="sa-prompt">{$t('admin.subagentsSystemPrompt')}</label>
|
||||
<textarea id="sa-prompt" bind:value={systemPrompt} rows="4"
|
||||
placeholder={$t('admin.subagentsSystemPromptPlaceholder')}
|
||||
class="w-full mt-1 px-2 py-1.5 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors resize-y font-mono" />
|
||||
<p class="text-[11px] text-gray-400 dark:text-gray-600 mt-0.5">{$t('admin.subagentsSystemPromptHint')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Save -->
|
||||
<div class="mt-auto pt-6 flex justify-end">
|
||||
<button
|
||||
class="text-[13px] text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-100 disabled:opacity-50"
|
||||
onclick={() => save()}
|
||||
disabled={saving}
|
||||
>{$t('settings.save')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -13,6 +13,7 @@
|
||||
import AudioSettings from './Admin/AudioSettings.svelte';
|
||||
import AdminWeb from './Admin/Web.svelte';
|
||||
import ToolServers from './Admin/ToolServers.svelte';
|
||||
import Subagents from './Admin/Subagents.svelte';
|
||||
import { session } from '$lib/session';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
@@ -28,7 +29,8 @@
|
||||
| 'gateway'
|
||||
| 'audio'
|
||||
| 'web'
|
||||
| 'toolservers';
|
||||
| 'toolservers'
|
||||
| 'subagents';
|
||||
|
||||
interface Props {
|
||||
onclose: () => void;
|
||||
@@ -56,7 +58,8 @@
|
||||
{ id: 'gateway', label: $t('admin.gateway.tab'), icon: 'gateway' },
|
||||
{ id: 'audio', label: $t('admin.audio.title'), icon: 'microphone' },
|
||||
{ id: 'web', label: $t('admin.web'), icon: 'globe' },
|
||||
{ id: 'toolservers', label: $t('admin.toolServers'), icon: 'plug' }
|
||||
{ id: 'toolservers', label: $t('admin.toolServers'), icon: 'plug' },
|
||||
{ id: 'subagents', label: $t('admin.subagents'), icon: 'user' }
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -137,6 +140,8 @@
|
||||
<AdminWeb />
|
||||
{:else if activeTab === 'toolservers'}
|
||||
<ToolServers />
|
||||
{:else if activeTab === 'subagents'}
|
||||
<Subagents />
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -178,6 +178,10 @@
|
||||
return _t('chat.tool.fetchUrlFallback');
|
||||
}
|
||||
}
|
||||
case 'delegate_task': {
|
||||
const t = args.task || '?';
|
||||
return `Sub-agent: "${t.length > 60 ? t.slice(0, 60) + '…' : t}"`;
|
||||
}
|
||||
default: {
|
||||
// External tool: {server_id}_{tool_name} → "tool_name (server_id)"
|
||||
const idx = name.indexOf('_');
|
||||
|
||||
@@ -680,6 +680,18 @@
|
||||
"toolServers.updated": "Tool server updated",
|
||||
"toolServers.deleted": "Tool server deleted",
|
||||
"toolServers.saveFailed": "Failed to save tool server",
|
||||
"toolServers.deleteFailed": "Failed to delete tool server"
|
||||
"toolServers.deleteFailed": "Failed to delete tool server",
|
||||
|
||||
"admin.subagents": "Sub-agents",
|
||||
"admin.subagentsEnabled": "Enable sub-agents",
|
||||
"admin.subagentsHint": "Allow the AI to delegate tasks to sub-agents. Each sub-agent creates a real chat with full tool access. Uses additional LLM calls.",
|
||||
"admin.subagentsMaxConcurrent": "Max concurrent",
|
||||
"admin.subagentsMaxConcurrentHint": "simultaneous sub-agents",
|
||||
"admin.subagentsMaxIterations": "Max iterations",
|
||||
"admin.subagentsMaxIterationsHint": "tool loops per sub-agent",
|
||||
"admin.subagentsMaxOutput": "Max output",
|
||||
"admin.subagentsSystemPrompt": "System prompt",
|
||||
"admin.subagentsSystemPromptPlaceholder": "You are a sub-agent...",
|
||||
"admin.subagentsSystemPromptHint": "Leave empty for the built-in default."
|
||||
}
|
||||
|
||||
|
||||
+172
-105
@@ -861,8 +861,12 @@ async def run_chat_task(
|
||||
if not skills:
|
||||
tools = [t for t in tools if t["name"] != "view_skill"]
|
||||
|
||||
# Parse $skill-name mentions from the user message to auto-activate skills
|
||||
# Strip delegate_task from sub-agent chats (depth limit = 1)
|
||||
chat_obj = await Chat.get_by_id(chat_id)
|
||||
if chat_obj and (chat_obj.meta or {}).get("subagent"):
|
||||
tools = [t for t in tools if t["name"] != "delegate_task"]
|
||||
|
||||
# Parse $skill-name mentions from the user message to auto-activate skills
|
||||
chat_params = (chat_obj.meta or {}).get("params", {}) if chat_obj else {}
|
||||
attached_skill_ids: list[str] = []
|
||||
if skills and messages:
|
||||
@@ -1024,6 +1028,7 @@ async def run_chat_task(
|
||||
)
|
||||
|
||||
restart = False
|
||||
pending_calls: list[dict] = [] # Collect tool calls from this response
|
||||
|
||||
async for event in stream:
|
||||
if event["type"] == "text_delta":
|
||||
@@ -1033,93 +1038,8 @@ async def run_chat_task(
|
||||
_sync_state()
|
||||
|
||||
elif event["type"] == "tool_call":
|
||||
# Flush any text before the tool call
|
||||
flushed_item = _flush_text()
|
||||
|
||||
name = event["name"]
|
||||
tool = ALL_TOOLS.get(name)
|
||||
item = {
|
||||
"type": "function_call",
|
||||
"id": str(uuid.uuid4()),
|
||||
"call_id": event["call_id"],
|
||||
"fc_id": event.get("id", ""),
|
||||
"name": name,
|
||||
"arguments": event["arguments"],
|
||||
}
|
||||
|
||||
should_auto = approval_mode == "full" or (
|
||||
approval_mode == "auto" and tool and tool["auto"]
|
||||
)
|
||||
|
||||
if should_auto:
|
||||
# Show tool call in progress BEFORE execution
|
||||
item["status"] = "in_progress"
|
||||
output_items.append(item)
|
||||
if flushed_item:
|
||||
await emit(output=flushed_item)
|
||||
await emit(output=item)
|
||||
_sync_state()
|
||||
|
||||
if name == "create_artifact":
|
||||
result = await create_artifact(
|
||||
**event["arguments"], workspace=workspace
|
||||
)
|
||||
else:
|
||||
result = await execute_tool(
|
||||
name,
|
||||
event["arguments"],
|
||||
{
|
||||
"workspace": workspace,
|
||||
"user_id": user_id,
|
||||
"model_id": model,
|
||||
"chat_id": chat_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Update status to completed
|
||||
item["status"] = "completed"
|
||||
result_item = {
|
||||
"type": "function_call_output",
|
||||
"call_id": event["call_id"],
|
||||
"output": result,
|
||||
}
|
||||
output_items.append(result_item)
|
||||
await emit(output=item)
|
||||
await emit(output=result_item)
|
||||
_sync_state()
|
||||
|
||||
# Artifact UI card: detect create_artifact or create_file with artifact_type
|
||||
artifact_item = build_artifact_item(name, event["arguments"], result)
|
||||
if artifact_item:
|
||||
output_items.append(artifact_item)
|
||||
await emit(output=artifact_item)
|
||||
_sync_state()
|
||||
|
||||
# Persist intermediate state so content survives crashes/errors
|
||||
await ChatMessage.update(message_id, content=content, output=output_items)
|
||||
|
||||
# Append to messages for next iteration
|
||||
_append_tool_to_messages(messages, event, result, provider)
|
||||
new_messages_since += 2 # tool_call + tool_result
|
||||
restart = True
|
||||
break
|
||||
|
||||
else:
|
||||
# Needs approval, persist and stop
|
||||
item["status"] = "pending"
|
||||
output_items.append(item)
|
||||
await ChatMessage.update(
|
||||
message_id,
|
||||
content=content,
|
||||
output=output_items,
|
||||
done=False,
|
||||
)
|
||||
if flushed_item:
|
||||
await emit(output=flushed_item)
|
||||
await emit(output=item)
|
||||
_task_state.pop(message_id, None)
|
||||
await emit(done=True)
|
||||
return
|
||||
# Collect tool call — don't execute yet
|
||||
pending_calls.append(event)
|
||||
|
||||
elif event["type"] == "usage":
|
||||
_flush_text()
|
||||
@@ -1130,28 +1050,175 @@ async def run_chat_task(
|
||||
)
|
||||
last_usage = usage
|
||||
new_messages_since = 0
|
||||
logger.info(
|
||||
"[task %s] save (usage): content=%d chars, output=%d items, types=%s",
|
||||
message_id[:8],
|
||||
len(content),
|
||||
len(output_items),
|
||||
[i.get("type") for i in output_items],
|
||||
)
|
||||
await ChatMessage.update(
|
||||
message_id,
|
||||
content=content,
|
||||
output=output_items,
|
||||
usage=usage,
|
||||
done=True,
|
||||
)
|
||||
_task_state.pop(message_id, None)
|
||||
await _emit_done()
|
||||
return
|
||||
|
||||
if not pending_calls:
|
||||
# No tool calls — final response, we're done
|
||||
logger.info(
|
||||
"[task %s] save (usage): content=%d chars, output=%d items, types=%s",
|
||||
message_id[:8],
|
||||
len(content),
|
||||
len(output_items),
|
||||
[i.get("type") for i in output_items],
|
||||
)
|
||||
await ChatMessage.update(
|
||||
message_id,
|
||||
content=content,
|
||||
output=output_items,
|
||||
usage=usage,
|
||||
done=True,
|
||||
)
|
||||
_task_state.pop(message_id, None)
|
||||
await _emit_done()
|
||||
return
|
||||
|
||||
elif event["type"] == "done":
|
||||
# Stream ended without explicit usage
|
||||
pass
|
||||
|
||||
# ── Process collected tool calls ────────────────────
|
||||
if pending_calls:
|
||||
flushed_item = _flush_text()
|
||||
|
||||
tool_ctx = {
|
||||
"workspace": workspace,
|
||||
"user_id": user_id,
|
||||
"model_id": model,
|
||||
"chat_id": chat_id,
|
||||
"connection": connection,
|
||||
}
|
||||
|
||||
# Check if any call needs approval
|
||||
needs_approval = None
|
||||
for tc in pending_calls:
|
||||
name = tc["name"]
|
||||
tool = ALL_TOOLS.get(name)
|
||||
should_auto = approval_mode == "full" or (
|
||||
approval_mode == "auto" and tool and tool["auto"]
|
||||
)
|
||||
if not should_auto:
|
||||
needs_approval = tc
|
||||
break
|
||||
|
||||
if needs_approval:
|
||||
# First non-auto tool stops the loop for approval
|
||||
tc = needs_approval
|
||||
item = {
|
||||
"type": "function_call",
|
||||
"id": str(uuid.uuid4()),
|
||||
"call_id": tc["call_id"],
|
||||
"fc_id": tc.get("id", ""),
|
||||
"name": tc["name"],
|
||||
"arguments": tc["arguments"],
|
||||
"status": "pending",
|
||||
}
|
||||
output_items.append(item)
|
||||
await ChatMessage.update(
|
||||
message_id,
|
||||
content=content,
|
||||
output=output_items,
|
||||
done=False,
|
||||
)
|
||||
if flushed_item:
|
||||
await emit(output=flushed_item)
|
||||
await emit(output=item)
|
||||
_task_state.pop(message_id, None)
|
||||
await emit(done=True)
|
||||
return
|
||||
|
||||
# All calls are auto-approved — build UI items
|
||||
call_items: list[tuple[dict, dict]] = [] # (event, ui_item)
|
||||
for tc in pending_calls:
|
||||
item = {
|
||||
"type": "function_call",
|
||||
"id": str(uuid.uuid4()),
|
||||
"call_id": tc["call_id"],
|
||||
"fc_id": tc.get("id", ""),
|
||||
"name": tc["name"],
|
||||
"arguments": tc["arguments"],
|
||||
"status": "in_progress",
|
||||
}
|
||||
output_items.append(item)
|
||||
call_items.append((tc, item))
|
||||
|
||||
# Emit all as in_progress
|
||||
if flushed_item:
|
||||
await emit(output=flushed_item)
|
||||
for _, item in call_items:
|
||||
await emit(output=item)
|
||||
_sync_state()
|
||||
|
||||
# Separate delegate_task (concurrent) from others (sequential)
|
||||
delegate_indices = [i for i, (tc, _) in enumerate(call_items) if tc["name"] == "delegate_task"]
|
||||
other_indices = [i for i, (tc, _) in enumerate(call_items) if tc["name"] != "delegate_task"]
|
||||
|
||||
# Execute non-delegate tools sequentially first
|
||||
for idx in other_indices:
|
||||
tc, item = call_items[idx]
|
||||
if tc["name"] == "create_artifact":
|
||||
result = await create_artifact(**tc["arguments"], workspace=workspace)
|
||||
else:
|
||||
result = await execute_tool(tc["name"], tc["arguments"], tool_ctx)
|
||||
|
||||
item["status"] = "completed"
|
||||
result_item = {
|
||||
"type": "function_call_output",
|
||||
"call_id": tc["call_id"],
|
||||
"output": result,
|
||||
}
|
||||
output_items.append(result_item)
|
||||
await emit(output=item)
|
||||
await emit(output=result_item)
|
||||
_sync_state()
|
||||
|
||||
artifact_item = build_artifact_item(tc["name"], tc["arguments"], result)
|
||||
if artifact_item:
|
||||
output_items.append(artifact_item)
|
||||
await emit(output=artifact_item)
|
||||
_sync_state()
|
||||
|
||||
_append_tool_to_messages(messages, tc, result, provider)
|
||||
new_messages_since += 2
|
||||
|
||||
# Execute delegate_task calls concurrently, emit each as it completes
|
||||
if delegate_indices:
|
||||
# Create tasks, mapping task → index
|
||||
inflight: dict[asyncio.Task, int] = {}
|
||||
for idx in delegate_indices:
|
||||
tc, _ = call_items[idx]
|
||||
task = asyncio.create_task(
|
||||
execute_tool(tc["name"], tc["arguments"], tool_ctx)
|
||||
)
|
||||
inflight[task] = idx
|
||||
|
||||
while inflight:
|
||||
done_set, _ = await asyncio.wait(
|
||||
inflight.keys(), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
for task in done_set:
|
||||
idx = inflight.pop(task)
|
||||
tc, item = call_items[idx]
|
||||
try:
|
||||
result = task.result()
|
||||
except Exception as e:
|
||||
result = f"Error: {e}"
|
||||
|
||||
item["status"] = "completed"
|
||||
result_item = {
|
||||
"type": "function_call_output",
|
||||
"call_id": tc["call_id"],
|
||||
"output": result,
|
||||
}
|
||||
output_items.append(result_item)
|
||||
await emit(output=item)
|
||||
await emit(output=result_item)
|
||||
_sync_state()
|
||||
_append_tool_to_messages(messages, tc, result, provider)
|
||||
new_messages_since += 2
|
||||
|
||||
# Persist after all tool calls
|
||||
await ChatMessage.update(message_id, content=content, output=output_items)
|
||||
restart = True
|
||||
|
||||
if not restart:
|
||||
flushed_item = _flush_text()
|
||||
if flushed_item:
|
||||
|
||||
+144
-1
@@ -1208,8 +1208,149 @@ BROWSER_TOOLS: dict[str, dict] = {
|
||||
"browser_evaluate": {"fn": browser_evaluate, "auto": False},
|
||||
}
|
||||
|
||||
|
||||
# ── Sub-agent ───────────────────────────────────────────────
|
||||
|
||||
_DEFAULT_SUBAGENT_SYSTEM = """You are a sub-agent working on a specific task assigned by the lead agent.
|
||||
|
||||
You have full access to the workspace — you can read, write, edit files, and run commands.
|
||||
Focus exclusively on your assigned task. Do NOT work on anything outside your scope.
|
||||
|
||||
When done, end with a clear summary:
|
||||
- What you did
|
||||
- What files you changed (if any)
|
||||
- Any issues or open questions
|
||||
"""
|
||||
|
||||
_subagent_semaphore: asyncio.Semaphore | None = None
|
||||
|
||||
|
||||
async def _get_subagent_config() -> dict:
|
||||
"""Load sub-agent settings from config with defaults."""
|
||||
from cptr.models import Config
|
||||
|
||||
return {
|
||||
"max_concurrent": int(await Config.get("subagents.max_concurrent") or 3),
|
||||
"max_iterations": int(await Config.get("subagents.max_iterations") or 30),
|
||||
"max_output": int(await Config.get("subagents.max_output") or 30_000),
|
||||
"system_prompt": (await Config.get("subagents.system_prompt"))
|
||||
or _DEFAULT_SUBAGENT_SYSTEM,
|
||||
}
|
||||
|
||||
|
||||
def _truncate_output(text: str, max_chars: int) -> str:
|
||||
"""Truncate text to max_chars, appending a note if truncated."""
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[:max_chars] + "\n\n[output truncated]"
|
||||
|
||||
|
||||
async def delegate_task(
|
||||
task: str,
|
||||
context: str = "",
|
||||
*,
|
||||
__context__: dict,
|
||||
) -> str:
|
||||
"""Delegate a task to a sub-agent. The sub-agent has full access to read, write, edit files, and run commands. Use for parallel work — call multiple times in one response to run tasks concurrently.
|
||||
|
||||
:param task: What the sub-agent should do.
|
||||
:param context: Optional context (e.g. relevant file paths, decisions made so far).
|
||||
"""
|
||||
global _subagent_semaphore
|
||||
config = await _get_subagent_config()
|
||||
if _subagent_semaphore is None:
|
||||
_subagent_semaphore = asyncio.Semaphore(config["max_concurrent"])
|
||||
|
||||
async with _subagent_semaphore:
|
||||
return await _run_subagent_chat(
|
||||
task=task,
|
||||
context=context,
|
||||
workspace=__context__["workspace"],
|
||||
connection=__context__["connection"],
|
||||
model=__context__["model_id"],
|
||||
user_id=__context__["user_id"],
|
||||
parent_chat_id=__context__["chat_id"],
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
async def _run_subagent_chat(
|
||||
task: str,
|
||||
context: str,
|
||||
workspace: str,
|
||||
connection: dict,
|
||||
model: str,
|
||||
user_id: str,
|
||||
parent_chat_id: str,
|
||||
config: dict,
|
||||
) -> str:
|
||||
"""Create a real chat and run the agent loop on it."""
|
||||
from cptr.models import Chat, ChatMessage
|
||||
from cptr.utils.chat_task import run_chat_task
|
||||
from cptr.utils.config import now_ms
|
||||
|
||||
# Build the user message content
|
||||
user_content = f"{task}\n\n## Context\n{context}" if context else task
|
||||
|
||||
# Create a real chat, marked as a sub-agent
|
||||
chat = await Chat.create(
|
||||
user_id=user_id,
|
||||
title=f"Sub-agent: {task[:60]}",
|
||||
meta={
|
||||
"workspace": workspace,
|
||||
"subagent": True,
|
||||
"parent_chat_id": parent_chat_id,
|
||||
"params": {
|
||||
"tool_approval_mode": "full", # auto-approve all tools
|
||||
},
|
||||
},
|
||||
created_at=now_ms(),
|
||||
)
|
||||
|
||||
# Create user message
|
||||
user_msg = await ChatMessage.create(
|
||||
chat_id=chat.id,
|
||||
role="user",
|
||||
content=user_content,
|
||||
created_at=now_ms(),
|
||||
)
|
||||
|
||||
# Create empty assistant message
|
||||
assistant_msg = await ChatMessage.create(
|
||||
chat_id=chat.id,
|
||||
role="assistant",
|
||||
content="",
|
||||
parent_id=user_msg.id,
|
||||
model=model,
|
||||
done=False,
|
||||
created_at=now_ms(),
|
||||
)
|
||||
|
||||
await Chat.update_current_message(chat.id, assistant_msg.id, now_ms())
|
||||
|
||||
# Run the SAME agent loop as a normal chat — no special code
|
||||
await run_chat_task(
|
||||
message_id=assistant_msg.id,
|
||||
chat_id=chat.id,
|
||||
user_id=user_id,
|
||||
connection=connection,
|
||||
workspace=workspace,
|
||||
model=model,
|
||||
)
|
||||
|
||||
# Read back the completed message
|
||||
result_msg = await ChatMessage.get_by_id(assistant_msg.id)
|
||||
output = result_msg.content if result_msg else "Sub-agent produced no output."
|
||||
|
||||
return _truncate_output(output, config["max_output"])
|
||||
|
||||
|
||||
SUBAGENT_TOOLS: dict[str, dict] = {
|
||||
"delegate_task": {"fn": delegate_task, "auto": True},
|
||||
}
|
||||
|
||||
# Combined lookup for execution and approval (always available regardless of config)
|
||||
ALL_TOOLS: dict[str, dict] = {**TOOLS, **BROWSER_TOOLS}
|
||||
ALL_TOOLS: dict[str, dict] = {**TOOLS, **BROWSER_TOOLS, **SUBAGENT_TOOLS}
|
||||
|
||||
|
||||
# ── External tool servers ───────────────────────────────────
|
||||
@@ -1424,6 +1565,8 @@ async def get_tool_list() -> list[dict]:
|
||||
|
||||
if (await Config.get("browser.enabled")) in (True, "true", "1"):
|
||||
tools.update(BROWSER_TOOLS)
|
||||
if (await Config.get("subagents.enabled")) in (True, "true", "1"):
|
||||
tools.update(SUBAGENT_TOOLS)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user