mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-24 05:26:42 +00:00
Merge branch 'main' into chore/readme
This commit is contained in:
commit
8a76298cc6
12 changed files with 219 additions and 159 deletions
48
.github/workflows/build-view.yml
vendored
48
.github/workflows/build-view.yml
vendored
|
|
@ -20,6 +20,8 @@ jobs:
|
|||
arch: x64
|
||||
- os: windows-latest
|
||||
arch: x64
|
||||
- os: ubuntu-latest
|
||||
arch: x64
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
|
|
@ -46,6 +48,13 @@ jobs:
|
|||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
# Install libfuse2 for Linux AppImage builds
|
||||
- name: Install libfuse2 (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
|
||||
# Step for macOS builds with signing
|
||||
- name: Build Release Files (macOS with signing)
|
||||
if: runner.os == 'macOS'
|
||||
|
|
@ -78,6 +87,19 @@ jobs:
|
|||
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
|
||||
USE_NPM_INSTALL_BUN: 'true'
|
||||
|
||||
# Step for Linux builds
|
||||
- name: Build Release Files (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
timeout-minutes: 90
|
||||
run: npm run build:linux
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
|
||||
VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }}
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
|
||||
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
|
||||
USE_NPM_INSTALL_BUN: 'true'
|
||||
|
||||
- name: Upload Artifact (macOS - dmg only)
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/upload-artifact@v6
|
||||
|
|
@ -95,13 +117,22 @@ jobs:
|
|||
path: |
|
||||
release/*.exe
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload Artifact (Linux - AppImage only)
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: |
|
||||
release/*.AppImage
|
||||
retention-days: 5
|
||||
merge-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create directories
|
||||
run: |
|
||||
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64
|
||||
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64
|
||||
|
||||
# Download all artifacts with correct names
|
||||
- name: Download mac-x64 artifact
|
||||
|
|
@ -122,7 +153,13 @@ jobs:
|
|||
name: release-windows-latest-x64
|
||||
path: temp-win-x64
|
||||
|
||||
# Move only dmg files for macOS and exe files for Windows
|
||||
- name: Download linux-x64 artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-ubuntu-latest-x64
|
||||
path: temp-linux-x64
|
||||
|
||||
# Move only dmg files for macOS, exe files for Windows, and AppImage for Linux
|
||||
- name: Move files to clean folders
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
@ -146,3 +183,10 @@ jobs:
|
|||
else
|
||||
find temp-win-x64 -name "*.exe" -exec mv {} release/win-x64/ \; || true
|
||||
fi
|
||||
|
||||
# linux-x64 - only move AppImage files
|
||||
if [ -d "temp-linux-x64/release" ]; then
|
||||
find temp-linux-x64/release -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
|
||||
else
|
||||
find temp-linux-x64 -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -115,15 +115,12 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
|
|||
@router.post("/chat", name="start chat")
|
||||
@traceroot.trace()
|
||||
async def post(data: Chat, request: Request):
|
||||
request_start_time = time.time()
|
||||
chat_logger.info(
|
||||
"Starting new chat session",
|
||||
extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
|
||||
)
|
||||
|
||||
task_lock = get_or_create_task_lock(data.project_id)
|
||||
# Store request start time in task_lock for downstream timing
|
||||
task_lock.request_start_time = request_start_time
|
||||
|
||||
# Set user-specific environment path for this thread
|
||||
set_user_env_path(data.env_path)
|
||||
|
|
|
|||
|
|
@ -238,18 +238,9 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str:
|
|||
@sync_step
|
||||
@traceroot.trace()
|
||||
async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
||||
import time as time_module
|
||||
|
||||
# === TIMING: step_solve started ===
|
||||
step_solve_start = time_module.time()
|
||||
request_start_time = getattr(task_lock, 'request_start_time', step_solve_start)
|
||||
time_since_request = (step_solve_start - request_start_time) * 1000
|
||||
logger.info(f"⏱️ [TIMING] step_solve started, {time_since_request:.2f}ms since request received")
|
||||
|
||||
start_event_loop = True
|
||||
|
||||
# === TIMING: Task lock initialization ===
|
||||
init_start = time_module.time()
|
||||
# Initialize task_lock attributes
|
||||
if not hasattr(task_lock, 'conversation_history'):
|
||||
task_lock.conversation_history = []
|
||||
if not hasattr(task_lock, 'last_task_result'):
|
||||
|
|
@ -258,24 +249,19 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
task_lock.question_agent = None
|
||||
if not hasattr(task_lock, 'summary_generated'):
|
||||
task_lock.summary_generated = False
|
||||
init_time = (time_module.time() - init_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Task lock attrs initialized in {init_time:.2f}ms")
|
||||
|
||||
# Create or reuse persistent question_agent
|
||||
# === TIMING: question_agent creation ===
|
||||
question_agent_start = time_module.time()
|
||||
if task_lock.question_agent is None:
|
||||
task_lock.question_agent = question_confirm_agent(options)
|
||||
question_agent_time = (time_module.time() - question_agent_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] question_confirm_agent created in {question_agent_time:.2f}ms")
|
||||
else:
|
||||
logger.info(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
|
||||
logger.debug(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
|
||||
|
||||
question_agent = task_lock.question_agent
|
||||
|
||||
# Other variables
|
||||
camel_task = None
|
||||
workforce = None
|
||||
mcp = None
|
||||
last_completed_task_result = "" # Track the last completed task result
|
||||
summary_task_content = "" # Track task summary
|
||||
loop_iteration = 0
|
||||
|
|
@ -312,11 +298,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
logger.info(f"[LIFECYCLE] Breaking out of step_solve loop due to client disconnect")
|
||||
break
|
||||
try:
|
||||
# === TIMING: Waiting for queue item ===
|
||||
queue_wait_start = time_module.time()
|
||||
item = await task_lock.get_queue()
|
||||
queue_wait_time = (time_module.time() - queue_wait_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Got item from queue in {queue_wait_time:.2f}ms, action={item.action}")
|
||||
except Exception as e:
|
||||
logger.error("Error getting item from queue", extra={"project_id": options.project_id, "task_id": options.task_id, "error": str(e)}, exc_info=True)
|
||||
# Continue waiting instead of breaking on queue error
|
||||
|
|
@ -352,25 +334,13 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
})
|
||||
continue
|
||||
|
||||
# === TIMING: Complexity determination ===
|
||||
complexity_start = time_module.time()
|
||||
time_since_request_to_complexity = (complexity_start - request_start_time) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Starting complexity check, {time_since_request_to_complexity:.2f}ms since request")
|
||||
|
||||
# Simplified logic: attachments mean workforce, otherwise let agent decide
|
||||
# Determine task complexity: attachments mean workforce, otherwise let agent decide
|
||||
is_complex_task: bool
|
||||
if len(options.attaches) > 0:
|
||||
# Questions with attachments always need workforce
|
||||
is_complex_task = True
|
||||
logger.info(f"[NEW-QUESTION] Has attachments, treating as complex task")
|
||||
complexity_time = (time_module.time() - complexity_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Complexity check (has attachments) completed in {complexity_time:.2f}ms")
|
||||
else:
|
||||
logger.info(f"[NEW-QUESTION] Calling question_confirm to determine complexity")
|
||||
question_confirm_start = time_module.time()
|
||||
is_complex_task = await question_confirm(question_agent, question, task_lock)
|
||||
question_confirm_time = (time_module.time() - question_confirm_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] question_confirm completed in {question_confirm_time:.2f}ms, is_complex={is_complex_task}")
|
||||
logger.info(f"[NEW-QUESTION] question_confirm result: is_complex={is_complex_task}")
|
||||
|
||||
if not is_complex_task:
|
||||
|
|
@ -409,83 +379,36 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
except Exception as e:
|
||||
logger.error(f"Error cleaning up folder: {e}")
|
||||
else:
|
||||
# === TIMING: Complex task processing ===
|
||||
complex_task_start = time_module.time()
|
||||
time_since_request_to_complex = (complex_task_start - request_start_time) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Complex task processing started, {time_since_request_to_complex:.2f}ms since request")
|
||||
|
||||
logger.info(f"[NEW-QUESTION] 🔧 Complex task, creating workforce and decomposing")
|
||||
logger.info(f"[NEW-QUESTION] Complex task, creating workforce and decomposing")
|
||||
# Update the sync_step with new task_id
|
||||
if hasattr(item, 'new_task_id') and item.new_task_id:
|
||||
set_current_task_id(options.project_id, item.new_task_id)
|
||||
# Reset summary generation flag for new tasks to ensure proper summaries
|
||||
task_lock.summary_generated = False
|
||||
logger.info("[NEW-QUESTION] Reset summary_generated flag for new task", extra={"project_id": options.project_id, "new_task_id": item.new_task_id})
|
||||
|
||||
logger.info(f"[NEW-QUESTION] Sending 'confirmed' SSE to frontend")
|
||||
yield sse_json("confirmed", {"question": question})
|
||||
|
||||
# === TIMING: Context building ===
|
||||
context_build_start = time_module.time()
|
||||
logger.info(f"[NEW-QUESTION] Building context for coordinator")
|
||||
context_for_coordinator = build_context_for_workforce(task_lock, options)
|
||||
context_build_time = (time_module.time() - context_build_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Context building completed in {context_build_time:.2f}ms")
|
||||
|
||||
# Check if workforce exists - if so, reuse it (agents are preserved)
|
||||
# Otherwise create new workforce
|
||||
# Check if workforce exists - if so, reuse it; otherwise create new workforce
|
||||
if workforce is not None:
|
||||
logger.info(f"[NEW-QUESTION] 🔄 Workforce exists (id={id(workforce)}), state={workforce._state.name}, _running={workforce._running}")
|
||||
logger.info(f"[NEW-QUESTION] ✅ Reusing existing workforce with preserved agents")
|
||||
# Workforce is already stopped from skip_task, ready for new decomposition
|
||||
logger.debug(f"[NEW-QUESTION] Reusing existing workforce (id={id(workforce)})")
|
||||
else:
|
||||
# === TIMING: Workforce construction ===
|
||||
workforce_construct_start = time_module.time()
|
||||
logger.info(f"[NEW-QUESTION] 🏭 Creating NEW workforce instance (workforce=None)")
|
||||
logger.info(f"[NEW-QUESTION] Creating NEW workforce instance")
|
||||
(workforce, mcp) = await construct_workforce(options)
|
||||
workforce_construct_time = (time_module.time() - workforce_construct_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Workforce construction completed in {workforce_construct_time:.2f}ms")
|
||||
logger.info(f"[NEW-QUESTION] ✅ NEW Workforce instance created, id={id(workforce)}")
|
||||
for new_agent in options.new_agents:
|
||||
workforce.add_single_agent_worker(
|
||||
format_agent_description(new_agent), await new_agent_model(new_agent, options)
|
||||
)
|
||||
task_lock.status = Status.confirmed
|
||||
|
||||
# === TIMING: Task creation ===
|
||||
task_create_start = time_module.time()
|
||||
# Create camel_task for the question
|
||||
clean_task_content = question + options.summary_prompt
|
||||
camel_task = Task(content=clean_task_content, id=options.task_id)
|
||||
if len(options.attaches) > 0:
|
||||
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
|
||||
|
||||
# If camel_task already exists (from previous paused task), add new question as subtask
|
||||
# Otherwise, create a new camel_task
|
||||
if camel_task is not None:
|
||||
logger.info(f"[NEW-QUESTION] 🔄 camel_task exists (id={camel_task.id}), adding new question as context")
|
||||
# Update the task content with new question
|
||||
clean_task_content = question + options.summary_prompt
|
||||
logger.info(f"[NEW-QUESTION] Updating existing camel_task content with new question")
|
||||
# We keep the existing task structure but update content for new decomposition
|
||||
camel_task = Task(content=clean_task_content, id=options.task_id)
|
||||
if len(options.attaches) > 0:
|
||||
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
|
||||
else:
|
||||
clean_task_content = question + options.summary_prompt
|
||||
logger.info(f"[NEW-QUESTION] Creating NEW camel_task with id={options.task_id}")
|
||||
camel_task = Task(content=clean_task_content, id=options.task_id)
|
||||
if len(options.attaches) > 0:
|
||||
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
|
||||
|
||||
task_create_time = (time_module.time() - task_create_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Task object created in {task_create_time:.2f}ms")
|
||||
|
||||
# === TIMING: Task decomposition start ===
|
||||
decomposition_start = time_module.time()
|
||||
time_since_request_to_decompose = (decomposition_start - request_start_time) * 1000
|
||||
logger.info(f"⏱️ [TIMING] Starting task decomposition, {time_since_request_to_decompose:.2f}ms since request")
|
||||
# Store decomposition start time in task_lock for downstream tracking
|
||||
task_lock.decomposition_start_time = decomposition_start
|
||||
|
||||
# Stream decomposition in background so queue items (decompose_text) are processed immediately
|
||||
logger.info(f"[NEW-QUESTION] 🧩 Starting task decomposition via workforce.eigent_make_sub_tasks")
|
||||
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": "", "first_token_logged": False}
|
||||
# Stream decomposition in background
|
||||
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
|
||||
state_holder: dict[str, Any] = {"sub_tasks": [], "summary_task": ""}
|
||||
|
||||
def on_stream_batch(new_tasks: list[Task], is_final: bool = False):
|
||||
|
|
@ -496,8 +419,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
|
||||
def on_stream_text(chunk):
|
||||
try:
|
||||
# With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
|
||||
# We need to calculate the delta to send only new content to frontend
|
||||
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
|
||||
last_content = stream_state["last_content"]
|
||||
|
||||
|
|
@ -510,12 +431,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
stream_state["last_content"] = accumulated_content
|
||||
|
||||
if delta_content:
|
||||
# === TIMING: Log TTFT (Time to First Token) ===
|
||||
if not stream_state["first_token_logged"]:
|
||||
stream_state["first_token_logged"] = True
|
||||
ttft = (time_module.time() - task_lock.decomposition_start_time) * 1000
|
||||
logger.info(f"⏱️ [TIMING] 🚀 TTFT (Time to First Token): {ttft:.2f}ms - First streaming token received for task decomposition")
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
task_lock.put_queue(
|
||||
ActionDecomposeTextData(
|
||||
|
|
@ -534,10 +449,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
async def run_decomposition():
|
||||
nonlocal camel_task, summary_task_content
|
||||
try:
|
||||
# === TIMING: LLM decomposition call ===
|
||||
llm_decompose_start = time_module.time()
|
||||
decomposition_start_time = getattr(task_lock, 'decomposition_start_time', llm_decompose_start)
|
||||
|
||||
sub_tasks = await asyncio.to_thread(
|
||||
workforce.eigent_make_sub_tasks,
|
||||
camel_task,
|
||||
|
|
@ -546,60 +457,44 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
on_stream_text,
|
||||
)
|
||||
|
||||
llm_decompose_time = (time_module.time() - llm_decompose_start) * 1000
|
||||
total_decompose_time = (time_module.time() - decomposition_start_time) * 1000
|
||||
logger.info(f"⏱️ [TIMING] LLM decomposition completed in {llm_decompose_time:.2f}ms (total decompose phase: {total_decompose_time:.2f}ms)")
|
||||
|
||||
if stream_state["subtasks"]:
|
||||
sub_tasks = stream_state["subtasks"]
|
||||
state_holder["sub_tasks"] = sub_tasks
|
||||
logger.info(f"[NEW-QUESTION] ✅ Task decomposed into {len(sub_tasks)} subtasks")
|
||||
logger.info(f"Task decomposed into {len(sub_tasks)} subtasks")
|
||||
try:
|
||||
setattr(task_lock, "decompose_sub_tasks", sub_tasks)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# === TIMING: Summary generation ===
|
||||
summary_start = time_module.time()
|
||||
logger.info(f"[NEW-QUESTION] Generating task summary")
|
||||
# Generate task summary
|
||||
summary_task_agent = task_summary_agent(options)
|
||||
try:
|
||||
summary_task_content = await asyncio.wait_for(
|
||||
summary_task(summary_task_agent, camel_task), timeout=10
|
||||
)
|
||||
summary_time = (time_module.time() - summary_start) * 1000
|
||||
task_lock.summary_generated = True
|
||||
logger.info(f"⏱️ [TIMING] Summary generation completed in {summary_time:.2f}ms")
|
||||
logger.info("[NEW-QUESTION] ✅ Summary generated successfully", extra={"project_id": options.project_id})
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id})
|
||||
task_lock.summary_generated = True
|
||||
fallback_name = "Task"
|
||||
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
|
||||
if content_preview is None:
|
||||
content_preview = ""
|
||||
summary_task_content = (
|
||||
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
)
|
||||
summary_task_content = f"{fallback_name}|{summary_task_content}"
|
||||
summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
summary_task_content = f"Task|{summary_task_content}"
|
||||
except Exception:
|
||||
task_lock.summary_generated = True
|
||||
fallback_name = "Task"
|
||||
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
|
||||
if content_preview is None:
|
||||
content_preview = ""
|
||||
summary_task_content = (
|
||||
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
)
|
||||
summary_task_content = f"{fallback_name}|{summary_task_content}"
|
||||
summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
summary_task_content = f"Task|{summary_task_content}"
|
||||
|
||||
state_holder["summary_task"] = summary_task_content
|
||||
try:
|
||||
setattr(task_lock, "summary_task_content", summary_task_content)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"[NEW-QUESTION] 📤 Sending to_sub_tasks SSE to frontend (task card)")
|
||||
logger.info(f"[NEW-QUESTION] to_sub_tasks data: task_id={camel_task.id}, summary={summary_task_content[:50]}..., subtasks_count={len(camel_task.subtasks)}")
|
||||
|
||||
payload = {
|
||||
"project_id": options.project_id,
|
||||
"task_id": options.task_id,
|
||||
|
|
@ -609,12 +504,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
"summary_task": summary_task_content,
|
||||
}
|
||||
await task_lock.put_queue(ActionDecomposeProgressData(data=payload))
|
||||
logger.info(f"[NEW-QUESTION] ✅ to_sub_tasks SSE sent")
|
||||
|
||||
# === TIMING: Total time from request to decomposition complete ===
|
||||
request_start = getattr(task_lock, 'request_start_time', decomposition_start_time)
|
||||
total_request_to_decompose = (time_module.time() - request_start) * 1000
|
||||
logger.info(f"⏱️ [TIMING] ===== TOTAL: Request → Decomposition Complete: {total_request_to_decompose:.2f}ms =====")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in background decomposition: {e}", exc_info=True)
|
||||
|
||||
|
|
@ -854,9 +743,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
logger.info(f"[LIFECYCLE] Multi-turn: building context for workforce")
|
||||
context_for_multi_turn = build_context_for_workforce(task_lock, options)
|
||||
|
||||
logger.info(f"[LIFECYCLE] Multi-turn: calling workforce.handle_decompose_append_task for new task decomposition")
|
||||
multi_turn_decompose_start = time_module.time()
|
||||
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": "", "first_token_logged": False, "start_time": multi_turn_decompose_start}
|
||||
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
|
||||
|
||||
def on_stream_batch(new_tasks: list[Task], is_final: bool = False):
|
||||
fresh_tasks = [t for t in new_tasks if t.id not in stream_state["seen_ids"]]
|
||||
|
|
@ -866,8 +753,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
|
||||
def on_stream_text(chunk):
|
||||
try:
|
||||
# With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
|
||||
# We need to calculate the delta to send only new content to frontend
|
||||
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
|
||||
last_content = stream_state["last_content"]
|
||||
|
||||
|
|
@ -879,12 +764,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
stream_state["last_content"] = accumulated_content
|
||||
|
||||
if delta_content:
|
||||
# === TIMING: Log TTFT (Time to First Token) for multi-turn ===
|
||||
if not stream_state["first_token_logged"]:
|
||||
stream_state["first_token_logged"] = True
|
||||
ttft = (time_module.time() - stream_state["start_time"]) * 1000
|
||||
logger.info(f"⏱️ [TIMING] 🚀 TTFT (Time to First Token): {ttft:.2f}ms - First streaming token received for multi-turn task decomposition")
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
task_lock.put_queue(
|
||||
ActionDecomposeTextData(
|
||||
|
|
@ -987,6 +866,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
elif item.action == Action.search_mcp:
|
||||
yield sse_json("search_mcp", item.data)
|
||||
elif item.action == Action.install_mcp:
|
||||
if mcp is None:
|
||||
logger.error(f"Cannot install MCP: mcp agent not initialized for project {options.project_id}")
|
||||
yield sse_json("error", {"message": "MCP agent not initialized. Please start a complex task first."})
|
||||
continue
|
||||
task = asyncio.create_task(install_mcp(mcp, item))
|
||||
task_lock.add_background_task(task)
|
||||
elif item.action == Action.terminal:
|
||||
|
|
|
|||
|
|
@ -64,6 +64,13 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
safe_mode=safe_mode,
|
||||
allowed_commands=allowed_commands,
|
||||
clone_current_env=clone_current_env,
|
||||
install_dependencies=[
|
||||
"pandas",
|
||||
"numpy",
|
||||
"matplotlib",
|
||||
"requests",
|
||||
"openpyxl",
|
||||
],
|
||||
)
|
||||
|
||||
# Auto-register with TaskLock for cleanup when task ends
|
||||
|
|
|
|||
|
|
@ -355,6 +355,7 @@ class Workforce(BaseWorkforce):
|
|||
f"Task {task.id} will not be properly tracked on frontend. "
|
||||
f"Available workers: {[c.node_id for c in self._children if hasattr(c, 'node_id')]}"
|
||||
)
|
||||
else:
|
||||
await task_lock.put_queue(
|
||||
ActionAssignTaskData(
|
||||
action=Action.assign_task,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@
|
|||
}
|
||||
},
|
||||
"win": {
|
||||
"certificateFile": null,
|
||||
"icon": "build/icon.ico",
|
||||
"artifactName": "${productName}.Setup.${version}.exe",
|
||||
"target": [
|
||||
|
|
|
|||
|
|
@ -112,6 +112,32 @@ app.commandLine.appendSwitch('max_old_space_size', '4096');
|
|||
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
|
||||
app.commandLine.appendSwitch('renderer-process-limit', '8');
|
||||
|
||||
// ==================== Anti-fingerprint settings ====================
|
||||
// Disable automation controlled indicator to avoid detection
|
||||
app.commandLine.appendSwitch(
|
||||
'disable-blink-features',
|
||||
'AutomationControlled'
|
||||
);
|
||||
|
||||
// Override User Agent to remove Electron/eigent identifiers
|
||||
// Dynamically generate User Agent based on actual platform and Chrome version
|
||||
const getPlatformUA = () => {
|
||||
// Use actual Chrome version from Electron instead of hardcoded value
|
||||
const chromeVersion = process.versions.chrome || '131.0.0.0';
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
case 'win32':
|
||||
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
case 'linux':
|
||||
return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
default:
|
||||
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
}
|
||||
};
|
||||
const normalUserAgent = getPlatformUA();
|
||||
app.userAgentFallback = normalUserAgent;
|
||||
|
||||
// ==================== protocol privileges ====================
|
||||
// Register custom protocol privileges before app ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
|
|
@ -1827,6 +1853,15 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== Anti-fingerprint: Set User Agent for all sessions ====================
|
||||
// Use the same dynamic User Agent as app.userAgentFallback
|
||||
session.defaultSession.setUserAgent(normalUserAgent);
|
||||
// Also set for the user_login partition used by webviews
|
||||
session.fromPartition('persist:user_login').setUserAgent(normalUserAgent);
|
||||
// And for main_window partition
|
||||
session.fromPartition('persist:main_window').setUserAgent(normalUserAgent);
|
||||
log.info('[ANTI-FINGERPRINT] User Agent set for all sessions');
|
||||
|
||||
// ==================== download handle ====================
|
||||
session.defaultSession.on('will-download', (event, item, webContents) => {
|
||||
item.once('done', (event, state) => {
|
||||
|
|
|
|||
|
|
@ -72,13 +72,108 @@ export class WebViewManager {
|
|||
backgroundThrottling: true,
|
||||
offscreen: false,
|
||||
sandbox: true,
|
||||
disableBlinkFeatures: 'Accelerated2dCanvas',
|
||||
disableBlinkFeatures: 'Accelerated2dCanvas,AutomationControlled',
|
||||
enableBlinkFeatures: 'IdleDetection',
|
||||
autoplayPolicy: 'document-user-activation-required',
|
||||
},
|
||||
})
|
||||
view.webContents.on('did-finish-load', () => {
|
||||
// Inject stealth script to avoid bot detection
|
||||
view.webContents.executeJavaScript(`
|
||||
// Save original values before overriding to maintain consistency
|
||||
const originalLanguages = navigator.languages ? [...navigator.languages] : ['en-US', 'en'];
|
||||
const originalHardwareConcurrency = navigator.hardwareConcurrency || 8;
|
||||
const originalDeviceMemory = navigator.deviceMemory || 8;
|
||||
|
||||
// Hide webdriver property
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => undefined,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Override plugins with proper PluginArray-like behavior
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => {
|
||||
const plugins = {
|
||||
length: 3,
|
||||
0: { name: 'Chrome PDF Plugin', description: 'Portable Document Format', filename: 'internal-pdf-viewer' },
|
||||
1: { name: 'Chrome PDF Viewer', description: '', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
|
||||
2: { name: 'Native Client', description: '', filename: 'internal-nacl-plugin' },
|
||||
item: function(index) { return this[index] || null; },
|
||||
namedItem: function(name) {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (this[i].name === name) return this[i];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
refresh: function() {},
|
||||
[Symbol.iterator]: function* () {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
yield this[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
return plugins;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Use original system languages for consistency with other browser data
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => originalLanguages,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Use original hardwareConcurrency, clamped to common range (4-16) to avoid extreme fingerprints
|
||||
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
||||
get: () => Math.min(Math.max(originalHardwareConcurrency, 4), 16),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Use original deviceMemory, clamped to common range (4-16) to avoid extreme fingerprints
|
||||
Object.defineProperty(navigator, 'deviceMemory', {
|
||||
get: () => Math.min(Math.max(originalDeviceMemory, 4), 16),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Fix WebGL vendor/renderer for both WebGL and WebGL2
|
||||
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
||||
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
||||
if (parameter === 37445) return 'Intel Inc.';
|
||||
if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
|
||||
return getParameter.call(this, parameter);
|
||||
};
|
||||
|
||||
// Also patch WebGL2RenderingContext
|
||||
if (typeof WebGL2RenderingContext !== 'undefined') {
|
||||
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
||||
WebGL2RenderingContext.prototype.getParameter = function(parameter) {
|
||||
if (parameter === 37445) return 'Intel Inc.';
|
||||
if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
|
||||
return getParameter2.call(this, parameter);
|
||||
};
|
||||
}
|
||||
|
||||
// Override chrome runtime - real Chrome has window.chrome but runtime is undefined
|
||||
if (!window.chrome) {
|
||||
window.chrome = {};
|
||||
}
|
||||
// In real Chrome, runtime exists but is undefined outside extensions
|
||||
// Don't set it to an object, that's detectable
|
||||
|
||||
// Hide automation variables
|
||||
const automationVars = ['__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn',
|
||||
'__driver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', 'domAutomation', 'domAutomationController'];
|
||||
automationVars.forEach(v => {
|
||||
Object.defineProperty(window, v, {
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
configurable: true,
|
||||
enumerable: false
|
||||
});
|
||||
});
|
||||
|
||||
// Mouse event handler
|
||||
window.addEventListener('mousedown', (e) => {
|
||||
if (!(e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement)) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ const ToolSelect = forwardRef<
|
|||
key={item.id + item.key + (item.isLocal + "")}
|
||||
className="h-5 bg-button-tertiery-fill-default flex items-center gap-1 w-auto flex-shrink-0 px-xs"
|
||||
>
|
||||
{item.name || item.mcp_name}
|
||||
{item.name || item.mcp_name || item.key || `tool_${item.id}`}
|
||||
<div className="flex items-center justify-center bg-button-secondary-fill-disabled rounded-sm">
|
||||
<X
|
||||
className="w-4 h-4 cursor-pointer text-button-secondary-icon-disabled"
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ export function AddWorker({
|
|||
name: workerName,
|
||||
type: workerName as AgentNameType,
|
||||
log: [],
|
||||
tools: [...selectedTools.map((tool) => tool.name)],
|
||||
tools: [...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`)],
|
||||
activeWebviewIds: [],
|
||||
workerInfo: {
|
||||
name: workerName,
|
||||
|
|
@ -310,7 +310,7 @@ export function AddWorker({
|
|||
type: workerName as AgentNameType,
|
||||
log: [],
|
||||
tools: [
|
||||
...selectedTools.map((tool) => tool?.key || tool?.mcp_name || ""),
|
||||
...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`),
|
||||
],
|
||||
activeWebviewIds: [],
|
||||
workerInfo: {
|
||||
|
|
|
|||
|
|
@ -241,15 +241,14 @@ export default function Workflow({
|
|||
// Merge all agents
|
||||
const allAgents = [...taskAssigning, ...base];
|
||||
// Sort: agents with tasks come first, then agents without tasks
|
||||
taskAssigning = allAgents.sort((a, b) => {
|
||||
const sortedAgents = allAgents.sort((a, b) => {
|
||||
const aHasTasks = a.tasks && a.tasks.length > 0;
|
||||
const bHasTasks = b.tasks && b.tasks.length > 0;
|
||||
if (aHasTasks && !bHasTasks) return -1;
|
||||
if (!aHasTasks && bHasTasks) return 1;
|
||||
return 0; // Keep original order for agents with same task status
|
||||
return 0;
|
||||
});
|
||||
// taskAssigning = taskAssigning.filter((agent) => agent.tasks.length > 0);
|
||||
targetData = taskAssigning.map((agent, index) => {
|
||||
targetData = sortedAgents.map((agent, index) => {
|
||||
const node = targetData.find((node) => node.id === agent.agent_id);
|
||||
if (node) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
</div>
|
||||
<div
|
||||
ref={toolsRef}
|
||||
className="flex-shrink-0 text-text-label text-xs leading-tight min-h-4 font-normal mb-sm pr-3 text-"
|
||||
className="flex-shrink-0 text-text-label text-xs leading-tight min-h-4 font-normal mb-sm pr-3"
|
||||
>
|
||||
{/* {JSON.stringify(data.agent)} */}
|
||||
{agentToolkits[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue