Compare commits
No commits in common. "2766a8ab571733c0e53a499bf00c5403f6bc99c7" and "a6775598dc87480b93dafc35a432fddaa05ac463" have entirely different histories.
2766a8ab57
...
a6775598dc
@ -14,11 +14,6 @@ SUB_AGENT_SERVICE_BASE_URL = os.environ.get("SUB_AGENT_SERVICE_URL", "http://127
|
|||||||
SUB_AGENT_DEFAULT_TIMEOUT = int(os.environ.get("SUB_AGENT_DEFAULT_TIMEOUT", "180")) # 秒
|
SUB_AGENT_DEFAULT_TIMEOUT = int(os.environ.get("SUB_AGENT_DEFAULT_TIMEOUT", "180")) # 秒
|
||||||
SUB_AGENT_STATUS_POLL_INTERVAL = float(os.environ.get("SUB_AGENT_STATUS_POLL_INTERVAL", "2.0"))
|
SUB_AGENT_STATUS_POLL_INTERVAL = float(os.environ.get("SUB_AGENT_STATUS_POLL_INTERVAL", "2.0"))
|
||||||
|
|
||||||
# 子智能体进程自身的网络权限(与工具级 HOST_SANDBOX_NETWORK_PERMISSION 独立)。
|
|
||||||
# 子智能体需要调用模型 API,因此默认给予完整网络;工具级网络限制仍由
|
|
||||||
# HOST_SANDBOX_NETWORK_PERMISSION 控制,并由主进程对命令单独加沙箱。
|
|
||||||
SUB_AGENT_HOST_NETWORK_PERMISSION = os.environ.get("SUB_AGENT_HOST_NETWORK_PERMISSION", "full")
|
|
||||||
|
|
||||||
# 存储与并发限制
|
# 存储与并发限制
|
||||||
# 子智能体任务目录与状态文件属于运行态数据,跟随 DATA_DIR(默认 ~/.agents/<mode>/data)。
|
# 子智能体任务目录与状态文件属于运行态数据,跟随 DATA_DIR(默认 ~/.agents/<mode>/data)。
|
||||||
SUB_AGENT_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), f"{DATA_DIR}/sub_agent_tasks")
|
SUB_AGENT_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), f"{DATA_DIR}/sub_agent_tasks")
|
||||||
@ -39,5 +34,4 @@ __all__ = [
|
|||||||
"SUB_AGENT_STATE_FILE",
|
"SUB_AGENT_STATE_FILE",
|
||||||
"SUB_AGENT_MAX_ACTIVE",
|
"SUB_AGENT_MAX_ACTIVE",
|
||||||
"SUB_AGENT_MODELS_CONFIG_FILE",
|
"SUB_AGENT_MODELS_CONFIG_FILE",
|
||||||
"SUB_AGENT_HOST_NETWORK_PERMISSION",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -654,7 +654,8 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
"execution_mode": self.get_execution_mode(),
|
"execution_mode": self.get_execution_mode(),
|
||||||
"pending_permission_mode": None,
|
"pending_permission_mode": None,
|
||||||
"pending_execution_mode": None,
|
"pending_execution_mode": None,
|
||||||
# frozen_*_prompt 不在创建时预设,由第一次 build_messages 根据当时的实际模式懒加载并冻结
|
"frozen_permission_prompt": self._build_permission_mode_message() or "",
|
||||||
|
"frozen_execution_prompt": self._build_execution_mode_message() or "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
print(f"{OUTPUT_FORMATS['info']} 新建对话: {conversation_id}")
|
print(f"{OUTPUT_FORMATS['info']} 新建对话: {conversation_id}")
|
||||||
|
|||||||
@ -211,7 +211,7 @@ class MainTerminalContextMixin:
|
|||||||
cm = getattr(self, "context_manager", None)
|
cm = getattr(self, "context_manager", None)
|
||||||
meta = getattr(cm, "conversation_metadata", {}) if cm else {}
|
meta = getattr(cm, "conversation_metadata", {}) if cm else {}
|
||||||
cached = meta.get(key) if isinstance(meta, dict) else None
|
cached = meta.get(key) if isinstance(meta, dict) else None
|
||||||
if isinstance(cached, str) and cached:
|
if isinstance(cached, str):
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
built = builder() or ""
|
built = builder() or ""
|
||||||
|
|||||||
@ -174,7 +174,8 @@ class WebTerminal(MainTerminal):
|
|||||||
"execution_mode": self.get_execution_mode() if hasattr(self, "get_execution_mode") else "sandbox",
|
"execution_mode": self.get_execution_mode() if hasattr(self, "get_execution_mode") else "sandbox",
|
||||||
"pending_permission_mode": None,
|
"pending_permission_mode": None,
|
||||||
"pending_execution_mode": None,
|
"pending_execution_mode": None,
|
||||||
# frozen_*_prompt 不在创建时预设,由第一次 build_messages 根据当时的实际模式懒加载并冻结
|
"frozen_permission_prompt": self._build_permission_mode_message() or "",
|
||||||
|
"frozen_execution_prompt": self._build_execution_mode_message() or "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -17,12 +17,7 @@ const { applyUsage, normalizeUsagePayload } = require('../utils/token_usage');
|
|||||||
// ── stdout 输出 ──
|
// ── stdout 输出 ──
|
||||||
function emit(type, data) {
|
function emit(type, data) {
|
||||||
const line = JSON.stringify({ type, ...data });
|
const line = JSON.stringify({ type, ...data });
|
||||||
try {
|
process.stdout.write(line + '\n');
|
||||||
// 使用同步写并刷新,避免管道缓冲导致主进程不能实时读到进度
|
|
||||||
fs.writeSync(1, line + '\n');
|
|
||||||
} catch (err) {
|
|
||||||
process.stdout.write(line + '\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 解析命令行参数 ──
|
// ── 解析命令行参数 ──
|
||||||
|
|||||||
@ -16,7 +16,6 @@ from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING
|
|||||||
from config import (
|
from config import (
|
||||||
OUTPUT_FORMATS,
|
OUTPUT_FORMATS,
|
||||||
SUB_AGENT_DEFAULT_TIMEOUT,
|
SUB_AGENT_DEFAULT_TIMEOUT,
|
||||||
SUB_AGENT_HOST_NETWORK_PERMISSION,
|
|
||||||
SUB_AGENT_MAX_ACTIVE,
|
SUB_AGENT_MAX_ACTIVE,
|
||||||
SUB_AGENT_MODELS_CONFIG_FILE,
|
SUB_AGENT_MODELS_CONFIG_FILE,
|
||||||
SUB_AGENT_STATE_FILE,
|
SUB_AGENT_STATE_FILE,
|
||||||
@ -189,10 +188,7 @@ class SubAgentManager:
|
|||||||
return {"success": False, "error": "宿主机子智能体执行被拒绝:HOST_SANDBOX_ENABLED=0"}
|
return {"success": False, "error": "宿主机子智能体执行被拒绝:HOST_SANDBOX_ENABLED=0"}
|
||||||
try:
|
try:
|
||||||
command_str = self._join_shell_words(cmd)
|
command_str = self._join_shell_words(cmd)
|
||||||
# 子智能体进程需要调用模型 API,使用独立的网络权限(默认 full)。
|
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||||
# 其内部执行的 run_command 等命令仍通过 HOST_SANDBOX_NETWORK_PERMISSION
|
|
||||||
# 保持工具级网络限制。
|
|
||||||
network_permission = SUB_AGENT_HOST_NETWORK_PERMISSION
|
|
||||||
plan = build_host_sandbox_plan(
|
plan = build_host_sandbox_plan(
|
||||||
command_str, self.project_path, env, network_permission=network_permission
|
command_str, self.project_path, env, network_permission=network_permission
|
||||||
)
|
)
|
||||||
@ -817,32 +813,15 @@ class SubAgentManager:
|
|||||||
if self.state_file.exists():
|
if self.state_file.exists():
|
||||||
try:
|
try:
|
||||||
data = json.loads(self.state_file.read_text(encoding="utf-8"))
|
data = json.loads(self.state_file.read_text(encoding="utf-8"))
|
||||||
loaded_tasks = data.get("tasks", {})
|
self.tasks = data.get("tasks", {})
|
||||||
loaded_agents = data.get("conversation_agents", {})
|
self.conversation_agents = data.get("conversation_agents", {})
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.warning("子智能体状态文件损坏,已忽略。")
|
logger.warning("子智能体状态文件损坏,已忽略。")
|
||||||
self.tasks = {}
|
self.tasks = {}
|
||||||
self.conversation_agents = {}
|
self.conversation_agents = {}
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
self.tasks = {}
|
self.tasks = {}
|
||||||
self.conversation_agents = {}
|
self.conversation_agents = {}
|
||||||
return
|
|
||||||
|
|
||||||
# 与已加载的运行态任务合并,避免覆盖仅存在于内存中的状态
|
|
||||||
# (例如 _stdout_lines、notified 等运行时标记)。
|
|
||||||
runtime_only_keys = {"_stdout_lines"}
|
|
||||||
merged_tasks: Dict[str, Dict] = {}
|
|
||||||
for task_id, task in loaded_tasks.items():
|
|
||||||
existing = self.tasks.get(task_id)
|
|
||||||
if existing:
|
|
||||||
for key in runtime_only_keys:
|
|
||||||
if key in existing:
|
|
||||||
task[key] = existing[key]
|
|
||||||
merged_tasks[task_id] = task
|
|
||||||
self.tasks = merged_tasks
|
|
||||||
self.conversation_agents = loaded_agents
|
|
||||||
|
|
||||||
if self.tasks:
|
if self.tasks:
|
||||||
migrated = False
|
migrated = False
|
||||||
for task in self.tasks.values():
|
for task in self.tasks.values():
|
||||||
|
|||||||
@ -1457,12 +1457,11 @@ def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserW
|
|||||||
progress_file = str(Path(task_root) / "progress.jsonl")
|
progress_file = str(Path(task_root) / "progress.jsonl")
|
||||||
|
|
||||||
entries: List[Dict[str, Any]] = []
|
entries: List[Dict[str, Any]] = []
|
||||||
# 默认返回足够多的记录,确保历史工具调用完整展示;同时保留上限防止异常大文件拖垮响应。
|
limit = request.args.get("limit", "200")
|
||||||
limit = request.args.get("limit", "100000")
|
|
||||||
try:
|
try:
|
||||||
limit_num = max(1, min(int(limit), 100000))
|
limit_num = max(1, min(int(limit), 1000))
|
||||||
except Exception:
|
except Exception:
|
||||||
limit_num = 100000
|
limit_num = 200
|
||||||
|
|
||||||
if progress_file and Path(progress_file).exists():
|
if progress_file and Path(progress_file).exists():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -111,6 +111,11 @@ const buildText = (entry: ActivityEntry) => {
|
|||||||
return `${tool || '工具'}`;
|
return `${tool || '工具'}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isTerminalStatus = (status?: string) => {
|
||||||
|
const normalized = (status || '').toString().toLowerCase();
|
||||||
|
return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
const canStop = computed(() => {
|
const canStop = computed(() => {
|
||||||
if (!activeAgent.value?.task_id) return false;
|
if (!activeAgent.value?.task_id) return false;
|
||||||
return !isTerminalStatus(activeAgent.value.status);
|
return !isTerminalStatus(activeAgent.value.status);
|
||||||
@ -132,49 +137,26 @@ const handleStop = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTerminalStatus = (status?: string) => {
|
|
||||||
const normalized = (status || '').toString().toLowerCase();
|
|
||||||
return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized);
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayItems = computed(() => {
|
const displayItems = computed(() => {
|
||||||
const entries = activityEntries.value || [];
|
const order: string[] = [];
|
||||||
const groups: { key: string; entry: ActivityEntry }[] = [];
|
const map = new Map<string, ActivityEntry>();
|
||||||
|
for (const entry of activityEntries.value || []) {
|
||||||
for (let i = 0; i < entries.length; i++) {
|
|
||||||
const entry = entries[i];
|
|
||||||
if (!entry) continue;
|
if (!entry) continue;
|
||||||
|
const key = entry.id || `${entry.tool || 'tool'}-${entry.ts || order.length}`;
|
||||||
const baseKey = entry.id || `${entry.tool || 'tool'}-${entry.ts || i}`;
|
if (!map.has(key)) {
|
||||||
const lastGroup = groups[groups.length - 1];
|
map.set(key, { ...entry });
|
||||||
|
order.push(key);
|
||||||
// 同一个 id 的相邻进度更新(running -> completed)合并为一次工具调用。
|
} else {
|
||||||
// 如果上一个同 id 的组已经进入终态,则后续同 id 的记录视为新的工具调用。
|
map.set(key, { ...map.get(key), ...entry });
|
||||||
if (
|
|
||||||
lastGroup &&
|
|
||||||
(lastGroup.entry.id === entry.id || lastGroup.key === baseKey) &&
|
|
||||||
!isTerminalStatus(lastGroup.entry.status)
|
|
||||||
) {
|
|
||||||
lastGroup.entry = { ...lastGroup.entry, ...entry };
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一 key:id 重复时加上序号
|
|
||||||
let key = baseKey;
|
|
||||||
let suffix = 0;
|
|
||||||
while (groups.some((g) => g.key === key)) {
|
|
||||||
suffix++;
|
|
||||||
key = `${baseKey}--${suffix}`;
|
|
||||||
}
|
|
||||||
groups.push({ key, entry: { ...entry } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups.map((group) => {
|
return order.map((key) => {
|
||||||
const item = group.entry;
|
const item = map.get(key) || {};
|
||||||
const state = normalizeStatus(item.status);
|
const state = normalizeStatus(item.status);
|
||||||
const stateLabel = state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中';
|
const stateLabel = state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中';
|
||||||
return {
|
return {
|
||||||
key: group.key,
|
key,
|
||||||
state,
|
state,
|
||||||
stateLabel,
|
stateLabel,
|
||||||
text: buildText(item)
|
text: buildText(item)
|
||||||
|
|||||||
@ -199,9 +199,7 @@ function renderSessionLog() {
|
|||||||
console.log('[TerminalPanel] renderSessionLog:', activeSession.value, 'len:', log.length, 'ready:', _historyReady.value[activeSession.value], 'hydrated:', sessionHydrated.value[activeSession.value]);
|
console.log('[TerminalPanel] renderSessionLog:', activeSession.value, 'len:', log.length, 'ready:', _historyReady.value[activeSession.value], 'hydrated:', sessionHydrated.value[activeSession.value]);
|
||||||
term.clear();
|
term.clear();
|
||||||
if (log) {
|
if (log) {
|
||||||
term.write(log, () => {
|
term.write(log);
|
||||||
term?.scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ interface ResizeContext {
|
|||||||
isResizing?: boolean;
|
isResizing?: boolean;
|
||||||
resizingPanel?: PanelKey | null;
|
resizingPanel?: PanelKey | null;
|
||||||
rightCollapsed?: boolean;
|
rightCollapsed?: boolean;
|
||||||
terminalPanelOpen?: boolean;
|
|
||||||
gitChangesPanelOpen?: boolean;
|
gitChangesPanelOpen?: boolean;
|
||||||
rightWidth?: number;
|
rightWidth?: number;
|
||||||
minPanelWidth?: number;
|
minPanelWidth?: number;
|
||||||
@ -20,8 +19,8 @@ function clamp(value: number, min: number, max: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function startResize(ctx: ResizeContext, panel: PanelKey, event: MouseEvent) {
|
export function startResize(ctx: ResizeContext, panel: PanelKey, event: MouseEvent) {
|
||||||
// 右侧面板(终端 / Git)只有在至少一个打开时才允许拖拽调整宽度
|
// 如果右侧面板(审批面板)已折叠,禁用拖拽边缘展开
|
||||||
if (panel === 'right' && !ctx.terminalPanelOpen && !ctx.gitChangesPanelOpen) {
|
if (panel === 'right' && ctx.rightCollapsed && !ctx.gitChangesPanelOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export const useSubAgentStore = defineStore('subAgent', {
|
|||||||
if (!taskId) return;
|
if (!taskId) return;
|
||||||
this.activityLoading = true;
|
this.activityLoading = true;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=100000`);
|
const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=200`);
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error(await resp.text());
|
throw new Error(await resp.text());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -512,8 +512,8 @@ class ConversationManager:
|
|||||||
"execution_mode": "sandbox",
|
"execution_mode": "sandbox",
|
||||||
"pending_permission_mode": None,
|
"pending_permission_mode": None,
|
||||||
"pending_execution_mode": None,
|
"pending_execution_mode": None,
|
||||||
"frozen_permission_prompt": None,
|
"frozen_permission_prompt": "",
|
||||||
"frozen_execution_prompt": None,
|
"frozen_execution_prompt": "",
|
||||||
"has_images": has_images,
|
"has_images": has_images,
|
||||||
"has_videos": has_videos,
|
"has_videos": has_videos,
|
||||||
# 首次对话尚未生成文件树快照,待首次用户消息时填充
|
# 首次对话尚未生成文件树快照,待首次用户消息时填充
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user