diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index d86716ef..8b5935e7 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -12,7 +12,7 @@ body:
id: version
attributes:
label: What version of eigent are you using?
- placeholder: E.g., 0.0.72
+ placeholder: E.g., 0.0.73
validations:
required: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 00000000..c66e115b
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,103 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL Advanced"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: '42 22 * * 5'
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: actions
+ build-mode: none
+ - language: javascript-typescript
+ build-mode: none
+ - language: python
+ build-mode: none
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Add any setup steps before running the `github/codeql-action/init` action.
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
+ # or others). This is typically only required for manual builds.
+ # - name: Setup runtime (example)
+ # uses: actions/setup-example@v1
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - name: Run manual build steps
+ if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/backend/app/component/model_validation.py b/backend/app/component/model_validation.py
index f1692a57..d734ab92 100644
--- a/backend/app/component/model_validation.py
+++ b/backend/app/component/model_validation.py
@@ -37,5 +37,6 @@ def create_agent(
system_message="You are a helpful assistant that must use the tool get_website_content to get the content of a website.",
model=model,
tools=[get_website_content],
+ step_timeout=900,
)
return agent
diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py
index 69c8244f..b8a2e73e 100644
--- a/backend/app/utils/agent.py
+++ b/backend/app/utils/agent.py
@@ -107,6 +107,7 @@ class ListenChatAgent(ChatAgent):
pause_event: asyncio.Event | None = None,
prune_tool_calls_from_memory: bool = False,
enable_snapshot_clean: bool = False,
+ step_timeout: float | None = 900,
) -> None:
super().__init__(
system_message=system_message,
@@ -128,6 +129,7 @@ class ListenChatAgent(ChatAgent):
pause_event=pause_event,
prune_tool_calls_from_memory=prune_tool_calls_from_memory,
enable_snapshot_clean=enable_snapshot_clean,
+ step_timeout=step_timeout,
)
self.api_task_id = api_task_id
self.agent_name = agent_name
@@ -287,19 +289,25 @@ class ListenChatAgent(ChatAgent):
if asyncio.iscoroutinefunction(tool.func):
# For async functions, we need to use the async execution path
return asyncio.run(self._aexecute_tool(tool_call_request))
- elif hasattr(tool.func, "__wrapped__"):
- with set_process_task(self.process_task_id):
- return super()._execute_tool(tool_call_request)
- else:
- args = tool_call_request.args
- tool_call_id = tool_call_request.tool_call_id
- try:
- task_lock = get_task_lock(self.api_task_id)
- toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit"
- traceroot_logger.debug(
- f"Agent {self.agent_name} executing tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}"
- )
+ # Handle all sync tools ourselves to maintain ContextVar context
+ args = tool_call_request.args
+ tool_call_id = tool_call_request.tool_call_id
+
+ # Check if tool is wrapped by @listen_toolkit decorator
+ # If so, the decorator will handle activate/deactivate events
+ has_listen_decorator = hasattr(tool.func, "__wrapped__")
+
+ try:
+ task_lock = get_task_lock(self.api_task_id)
+
+ toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit"
+ traceroot_logger.debug(
+ f"Agent {self.agent_name} executing tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}"
+ )
+
+ # Only send activate event if tool is NOT wrapped by @listen_toolkit
+ if not has_listen_decorator:
asyncio.create_task(
task_lock.put_queue(
ActionActivateToolkitData(
@@ -313,29 +321,33 @@ class ListenChatAgent(ChatAgent):
)
)
)
+ # Set process_task context for all tool executions
+ with set_process_task(self.process_task_id):
raw_result = tool(**args)
- traceroot_logger.debug(f"Tool {func_name} executed successfully")
- if self.mask_tool_output:
- self._secure_result_store[tool_call_id] = raw_result
- result = (
- "[The tool has been executed successfully, but the output"
- " from the tool is masked. You can move forward]"
- )
- mask_flag = True
+ traceroot_logger.debug(f"Tool {func_name} executed successfully")
+ if self.mask_tool_output:
+ self._secure_result_store[tool_call_id] = raw_result
+ result = (
+ "[The tool has been executed successfully, but the output"
+ " from the tool is masked. You can move forward]"
+ )
+ mask_flag = True
+ else:
+ result = raw_result
+ mask_flag = False
+ # Prepare result message with truncation
+ if isinstance(result, str):
+ result_msg = result
+ else:
+ result_str = repr(result)
+ MAX_RESULT_LENGTH = 500
+ if len(result_str) > MAX_RESULT_LENGTH:
+ result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)"
else:
- result = raw_result
- mask_flag = False
- # Prepare result message with truncation
- if isinstance(result, str):
- result_msg = result
- else:
- result_str = repr(result)
- MAX_RESULT_LENGTH = 500
- if len(result_str) > MAX_RESULT_LENGTH:
- result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)"
- else:
- result_msg = result_str
+ result_msg = result_str
+ # Only send deactivate event if tool is NOT wrapped by @listen_toolkit
+ if not has_listen_decorator:
asyncio.create_task(
task_lock.put_queue(
ActionDeactivateToolkitData(
@@ -349,31 +361,40 @@ class ListenChatAgent(ChatAgent):
)
)
)
- except Exception as e:
- # Capture the error message to prevent framework crash
- error_msg = f"Error executing tool '{func_name}': {e!s}"
- result = f"Tool execution failed: {error_msg}"
- mask_flag = False
- traceroot_logger.error(f"Tool execution failed for {func_name}: {e}", exc_info=True)
+ except Exception as e:
+ # Capture the error message to prevent framework crash
+ error_msg = f"Error executing tool '{func_name}': {e!s}"
+ result = f"Tool execution failed: {error_msg}"
+ mask_flag = False
+ traceroot_logger.error(f"Tool execution failed for {func_name}: {e}", exc_info=True)
- return self._record_tool_calling(func_name, args, result, tool_call_id, mask_output=mask_flag)
+ return self._record_tool_calling(
+ func_name, args, result, tool_call_id,
+ mask_output=mask_flag,
+ extra_content=tool_call_request.extra_content,
+ )
@traceroot.trace()
async def _aexecute_tool(self, tool_call_request: ToolCallRequest) -> ToolCallingRecord:
func_name = tool_call_request.tool_name
tool: FunctionTool = self._internal_tools[func_name]
- if hasattr(tool.func, "__wrapped__"):
- with set_process_task(self.process_task_id):
- return await super()._aexecute_tool(tool_call_request)
- else:
- args = tool_call_request.args
- tool_call_id = tool_call_request.tool_call_id
- task_lock = get_task_lock(self.api_task_id)
- toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit"
- traceroot_logger.info(
- f"Agent {self.agent_name} executing async tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}"
- )
+ # Always handle tool execution ourselves to maintain ContextVar context
+ args = tool_call_request.args
+ tool_call_id = tool_call_request.tool_call_id
+ task_lock = get_task_lock(self.api_task_id)
+
+ # Check if tool is wrapped by @listen_toolkit decorator
+ # If so, the decorator will handle activate/deactivate events
+ has_listen_decorator = hasattr(tool.func, "__wrapped__")
+
+ toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit"
+ traceroot_logger.info(
+ f"Agent {self.agent_name} executing async tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}"
+ )
+
+ # Only send activate event if tool is NOT wrapped by @listen_toolkit
+ if not has_listen_decorator:
await task_lock.put_queue(
ActionActivateToolkitData(
data={
@@ -385,7 +406,9 @@ class ListenChatAgent(ChatAgent):
},
)
)
- try:
+ try:
+ # Set process_task context for all tool executions
+ with set_process_task(self.process_task_id):
# Try different invocation paths in order of preference
if hasattr(tool, "func") and hasattr(tool.func, "async_call"):
# Case: FunctionTool wrapping an MCP tool
@@ -404,29 +427,32 @@ class ListenChatAgent(ChatAgent):
result = await tool(**args)
else:
- # Fallback: synchronous call
+ # Fallback: synchronous call - call directly in current context
+ # DO NOT use run_in_executor to preserve ContextVar
result = tool(**args)
# Handle case where synchronous call returns a coroutine
if asyncio.iscoroutine(result):
result = await result
- except Exception as e:
- # Capture the error message to prevent framework crash
- error_msg = f"Error executing async tool '{func_name}': {e!s}"
- result = {"error": error_msg}
- traceroot_logger.error(f"Async tool execution failed for {func_name}: {e}", exc_info=True)
+ except Exception as e:
+ # Capture the error message to prevent framework crash
+ error_msg = f"Error executing async tool '{func_name}': {e!s}"
+ result = {"error": error_msg}
+ traceroot_logger.error(f"Async tool execution failed for {func_name}: {e}", exc_info=True)
- # Prepare result message with truncation
- if isinstance(result, str):
- result_msg = result
+ # Prepare result message with truncation
+ if isinstance(result, str):
+ result_msg = result
+ else:
+ result_str = repr(result)
+ MAX_RESULT_LENGTH = 500
+ if len(result_str) > MAX_RESULT_LENGTH:
+ result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)"
else:
- result_str = repr(result)
- MAX_RESULT_LENGTH = 500
- if len(result_str) > MAX_RESULT_LENGTH:
- result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)"
- else:
- result_msg = result_str
+ result_msg = result_str
+ # Only send deactivate event if tool is NOT wrapped by @listen_toolkit
+ if not has_listen_decorator:
await task_lock.put_queue(
ActionDeactivateToolkitData(
data={
@@ -438,7 +464,10 @@ class ListenChatAgent(ChatAgent):
},
)
)
- return self._record_tool_calling(func_name, args, result, tool_call_id)
+ return self._record_tool_calling(
+ func_name, args, result, tool_call_id,
+ extra_content=tool_call_request.extra_content,
+ )
@traceroot.trace()
def clone(self, with_memory: bool = False) -> ChatAgent:
@@ -468,6 +497,7 @@ class ListenChatAgent(ChatAgent):
mask_tool_output=self.mask_tool_output,
pause_event=self.pause_event,
prune_tool_calls_from_memory=self.prune_tool_calls_from_memory,
+ step_timeout=self.step_timeout,
)
new_agent.process_task_id = self.process_task_id
@@ -746,7 +776,7 @@ def search_agent(options: Chat):
"browser_enter",
"browser_visit_page",
"browser_scroll",
- "browser_get_som_screenshot",
+ # "browser_get_som_screenshot",
],
)
@@ -869,9 +899,6 @@ Your approach depends on available search tools:
interactive elements, not the full page text. To see more content on
long pages, Navigate with `browser_click`, `browser_back`, and
`browser_forward`. Manage multiple pages with `browser_switch_tab`.
-- **Analysis**: Use `browser_get_som_screenshot` to understand the page
- layout and identify interactive elements. Since this is a heavy
- operation, only use it when visual analysis is necessary.
- **Interaction**: Use `browser_type` to fill out forms and
`browser_enter` to submit or confirm search.
diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py
index 7e98713b..eb52e8ad 100644
--- a/backend/app/utils/listen/toolkit_listen.py
+++ b/backend/app/utils/listen/toolkit_listen.py
@@ -27,6 +27,13 @@ def _safe_put_queue(task_lock, data):
task = asyncio.create_task(task_lock.put_queue(data))
if hasattr(task_lock, "add_background_task"):
task_lock.add_background_task(task)
+ # Add done callback to handle any exceptions and prevent warnings
+ def handle_task_result(t):
+ try:
+ t.result() # This will raise any exception that occurred
+ except Exception as e:
+ logger.error(f"[listen_toolkit] Background task failed: {e}")
+ task.add_done_callback(handle_task_result)
except RuntimeError:
# No running event loop, we need to handle this differently
try:
diff --git a/backend/app/utils/toolkit/file_write_toolkit.py b/backend/app/utils/toolkit/file_write_toolkit.py
index 210ffc9c..375f9468 100644
--- a/backend/app/utils/toolkit/file_write_toolkit.py
+++ b/backend/app/utils/toolkit/file_write_toolkit.py
@@ -5,7 +5,7 @@ from camel.toolkits import FileToolkit as BaseFileToolkit
from app.component.environment import env
from app.service.task import process_task
from app.service.task import ActionWriteFileData, Agents, get_task_lock
-from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
+from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit, _safe_put_queue
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@@ -46,12 +46,15 @@ class FileToolkit(BaseFileToolkit, AbstractToolkit):
res = super().write_to_file(title, content, filename, encoding, use_latex)
if "Content successfully written to file: " in res:
task_lock = get_task_lock(self.api_task_id)
- asyncio.create_task(
- task_lock.put_queue(
- ActionWriteFileData(
- process_task_id=process_task.get(),
- data=res.replace("Content successfully written to file: ", ""),
- )
+ # Capture ContextVar value before creating async task
+ current_process_task_id = process_task.get("")
+
+ # Use _safe_put_queue to handle both sync and async contexts
+ _safe_put_queue(
+ task_lock,
+ ActionWriteFileData(
+ process_task_id=current_process_task_id,
+ data=res.replace("Content successfully written to file: ", ""),
)
)
return res
diff --git a/backend/app/utils/toolkit/human_toolkit.py b/backend/app/utils/toolkit/human_toolkit.py
index edd43a98..e76a45fb 100644
--- a/backend/app/utils/toolkit/human_toolkit.py
+++ b/backend/app/utils/toolkit/human_toolkit.py
@@ -107,12 +107,16 @@ class HumanToolkit(BaseToolkit, AbstractToolkit):
print(message_attachment)
logger.info(f"\nAgent Message:\n{message_title} {message_description} {message_attachment}")
task_lock = get_task_lock(self.api_task_id)
- asyncio.create_task(
- task_lock.put_queue(
- ActionNoticeData(
- process_task_id=process_task.get(""),
- data=f"{message_description}",
- )
+ # Capture ContextVar value before creating async task
+ current_process_task_id = process_task.get("")
+
+ # Use _safe_put_queue to handle both sync and async contexts
+ from app.utils.listen.toolkit_listen import _safe_put_queue
+ _safe_put_queue(
+ task_lock,
+ ActionNoticeData(
+ process_task_id=current_process_task_id,
+ data=f"{message_description}",
)
)
diff --git a/backend/app/utils/toolkit/pptx_toolkit.py b/backend/app/utils/toolkit/pptx_toolkit.py
index 0e341d24..ae52c176 100644
--- a/backend/app/utils/toolkit/pptx_toolkit.py
+++ b/backend/app/utils/toolkit/pptx_toolkit.py
@@ -4,7 +4,7 @@ from camel.toolkits import PPTXToolkit as BasePPTXToolkit
from app.component.environment import env
from app.service.task import ActionWriteFileData, Agents, get_task_lock
-from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
+from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit, _safe_put_queue
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from app.service.task import process_task
@@ -39,7 +39,12 @@ class PPTXToolkit(BasePPTXToolkit, AbstractToolkit):
res = super().create_presentation(content, filename, template)
if "PowerPoint presentation successfully created" in res:
task_lock = get_task_lock(self.api_task_id)
- asyncio.create_task(
- task_lock.put_queue(ActionWriteFileData(process_task_id=process_task.get(), data=str(file_path)))
+ # Capture ContextVar value before creating async task
+ current_process_task_id = process_task.get("")
+
+ # Use _safe_put_queue to handle both sync and async contexts
+ _safe_put_queue(
+ task_lock,
+ ActionWriteFileData(process_task_id=current_process_task_id, data=str(file_path))
)
return res
diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py
index 4e6472c5..52ee19f3 100644
--- a/backend/app/utils/toolkit/terminal_toolkit.py
+++ b/backend/app/utils/toolkit/terminal_toolkit.py
@@ -134,6 +134,27 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
exc_info=True
)
+ def shell_exec(self, id: str, command: str, block: bool = True) -> str:
+ r"""Executes a shell command in blocking or non-blocking mode.
+
+ Args:
+ id (str): A unique identifier for the command's session.
+ command (str): The shell command to execute.
+ block (bool, optional): Determines the execution mode. Defaults to True.
+
+ Returns:
+ str: The output of the command execution.
+ """
+ result = super().shell_exec(id, command, block)
+
+ # If the command executed successfully but returned empty output,
+ # provide a clear success message to help the AI agent understand
+ # that the command completed without error.
+ if block and result == "":
+ return "Command executed successfully (no output)."
+
+ return result
+
@classmethod
def shutdown(cls):
if cls._thread_pool:
diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py
index c9793ec1..4833d1b2 100644
--- a/backend/app/utils/workforce.py
+++ b/backend/app/utils/workforce.py
@@ -6,6 +6,7 @@ from camel.societies.workforce.workforce import (
WorkforceState,
DEFAULT_WORKER_POOL_SIZE,
)
+from camel.societies.workforce.utils import FailureHandlingConfig
from camel.societies.workforce.task_channel import TaskChannel
from camel.societies.workforce.base import BaseNode
from camel.societies.workforce.utils import TaskAssignResult
@@ -58,6 +59,9 @@ class Workforce(BaseWorkforce):
graceful_shutdown_timeout=graceful_shutdown_timeout,
share_memory=share_memory,
use_structured_output_handler=use_structured_output_handler,
+ failure_handling_config=FailureHandlingConfig(
+ enabled_strategies=["retry", "replan"],
+ ),
)
logger.info(f"[WF-LIFECYCLE] ✅ Workforce.__init__ COMPLETED, id={id(self)}")
@@ -217,8 +221,8 @@ class Workforce(BaseWorkforce):
return subtasks
async def _find_assignee(self, tasks: List[Task]) -> TaskAssignResult:
- # Task assignment phase: send "waiting for execution" notification
- # to the frontend, and send "start execution" notification when the
+ # Task assignment phase: send "waiting for execution" notification
+ # to the frontend, and send "start execution" notification when the
# task actually begins execution
assigned = await super()._find_assignee(tasks)
@@ -238,6 +242,18 @@ class Workforce(BaseWorkforce):
content = ""
else:
content = task_obj.content
+
+ # Skip sending notification if this is a retry/replan for an already assigned task
+ # This prevents the frontend from showing "Reassigned" when a task is being retried
+ # with the same or different worker due to failure recovery
+ if task_obj and task_obj.assigned_worker_id:
+ logger.debug(
+ f"[WF] ASSIGN Skip notification for task {item.task_id}: "
+ f"already has assigned_worker_id={task_obj.assigned_worker_id}, "
+ f"new assignee={item.assignee_id} (retry/replan scenario)"
+ )
+ continue
+
# Asynchronously send waiting notification
task = asyncio.create_task(
task_lock.put_queue(
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 4b4dc445..30a01a15 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -3,9 +3,9 @@ name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
-requires-python = "==3.10.16"
+requires-python = ">=3.10,<3.11"
dependencies = [
- "camel-ai[eigent]==0.2.80a3",
+ "camel-ai[eigent] @ git+https://github.com/camel-ai/camel.git@feature/workforce-configurable-fail-handling",
"fastapi>=0.115.12",
"fastapi-babel>=1.0.0",
"uvicorn[standard]>=0.34.2",
diff --git a/backend/uv.lock b/backend/uv.lock
index 972efe55..74a5e62d 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -1,6 +1,6 @@
version = 1
revision = 2
-requires-python = "==3.10.16"
+requires-python = "==3.10.*"
[[package]]
name = "aiofiles"
@@ -249,7 +249,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
- { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.80a3" },
+ { name = "camel-ai", extras = ["eigent"], git = "https://github.com/camel-ai/camel.git?rev=feature%2Fworkforce-configurable-fail-handling" },
{ name = "debugpy", specifier = ">=1.8.17" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastapi-babel", specifier = ">=1.0.0" },
@@ -333,8 +333,8 @@ wheels = [
[[package]]
name = "camel-ai"
-version = "0.2.80a3"
-source = { registry = "https://pypi.org/simple" }
+version = "0.2.82.dev1"
+source = { git = "https://github.com/camel-ai/camel.git?rev=feature%2Fworkforce-configurable-fail-handling#d84dbec44ecb26add2638857059e2f92d896d308" }
dependencies = [
{ name = "astor" },
{ name = "colorama" },
@@ -349,10 +349,6 @@ dependencies = [
{ name = "tiktoken" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1c/72/691a6126e062b5c5a24b7c5a116f690be1b50127a359c7d53d13435d27ef/camel_ai-0.2.80a3.tar.gz", hash = "sha256:edda7cb0466a63c4d8f92ae4ea2d11ee6946b69146336176346db3227de747d9", size = 1013184, upload-time = "2025-11-20T18:52:25.016Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/46/2c/a1203a2fa8e432deb4e6a7740a4dff532b79e0129bbf2e60626fca8f4271/camel_ai-0.2.80a3-py3-none-any.whl", hash = "sha256:2f398e648edae57bf23372a9cc812c82bf64f007178cdaf7ba113f2fde6761a6", size = 1473469, upload-time = "2025-11-20T18:52:22.853Z" },
-]
[package.optional-dependencies]
eigent = [
diff --git a/docs/core/models/gemini.md b/docs/core/models/gemini.md
new file mode 100644
index 00000000..76198777
--- /dev/null
+++ b/docs/core/models/gemini.md
@@ -0,0 +1,48 @@
+---
+title: "Gemini"
+description: "This guide walks you through setting up your Google Gemini API key within Eigent to enable the Gemini model for your AI workforce."
+---
+
+### Prerequisites
+
+- **Get your API Key:** If you haven't already, generate a key at [Google AI Studio](https://aistudio.google.com/).
+- **Copy the Key:** Keep your API key ready to paste.
+
+### Configuration Steps
+
+**1. Access Application Settings**
+
+- Launch Eigent and navigate to the **Home Page**.
+- Click on the **Settings** tab (usually located in the sidebar or top navigation).
+
+
+
+**2. Locate Model Configuration**
+
+- In the Settings menu, find and select the **Models** section.
+- Scroll down to the **Custom Model** area.
+- Look for the **Gemini Config** card.
+-
+
+
+
+**3. Enter API Details** Click on the Gemini Config card and fill in the following fields:
+
+- **API Key:** Paste the key you generated from Google AI Studio.
+- **API Host:** Enter the appropriate API endpoint host (e.g., `generativelanguage.googleapis.com`).
+- **Model Type:** Enter the specific model version you wish to use.
+ - _Example:_ `gemini-3-pro-preview`
+- **Save:** Click the **Save** button to apply your changes.
+
+
+
+**4. Set as Default & Verify**
+
+- Once saved, the **"Set as Default"** button on the Gemini Config card will be selected/active.
+- **You are ready to go.** Your Eigent agents can now utilize the Gemini model.
+
+
+
+---
+
+> **Video Tutorial:** Prefer a visual guide? **Watch the full configuration video here**
\ No newline at end of file
diff --git a/docs/core/models.md b/docs/core/models/local-model.md
similarity index 57%
rename from docs/core/models.md
rename to docs/core/models/local-model.md
index 3a66a999..21024bee 100644
--- a/docs/core/models.md
+++ b/docs/core/models/local-model.md
@@ -1,33 +1,9 @@
---
-title: Models
-description: Configure and deploy your preferred LLM models with Eigent.
-icon: server
+title: "Models (Local Model)"
+description: "Configure and deploy your preferred LLM models with Eigent."
+icon: "server"
---
-Eigent supports flexible integration and deployment of top LLMs and multimodal models. You can follow the steps below to set up your preferred LLM models.
-
-1. Click Settings
-
-
-
-2. Close Eigent Cloud Version
-
-
-
-3. Configure your APIKEY and Model Type
-
-
-
-4. Configure the Google Search toolkit
-
-
-
-
-
-You can refer to the following document for detailed information on how to configure **GOOGLE_API_KEY** and **SEARCH_ENGINE_ID :** https://developers.google.com/custom-search/v1/overview
-
-Now,start enjoying Eigent!
-
## **Self-Host Model**
1. Configure your self-host model
@@ -71,8 +47,7 @@ ollama pull qwen2.5:7b

-
-You can refer to the following document for detailed information on how to configure **GOOGLE_API_KEY** and **SEARCH_ENGINE_ID :** https://developers.google.com/custom-search/v1/overview
+
You can refer to the following document for detailed information on how to configure **GOOGLE_API_KEY** and **SEARCH_ENGINE_ID :** https://developers.google.com/custom-search/v1/overview
## **API KEY Reference**
diff --git a/docs/docs.json b/docs/docs.json
index 2c48f997..7da1ed62 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -47,24 +47,35 @@
"groups": [
{
"group": "Get Started",
+ "icon": "rocket",
"pages": [
"/get_started/welcome",
- "/get_started/installation",
+ "/get_started/installation",
"/get_started/quick_start"
]
},
{
"group": "Core",
+ "icon": "key",
"pages": [
"/core/concepts",
"/core/workforce",
- "/core/models",
+ {
+ "group": "Models",
+ "icon": "brain",
+ "expanded": true,
+ "pages": [
+ "/core/models/gemini",
+ "/core/models/local-model"
+ ]
+ },
"/core/tools",
"/core/workers"
]
},
{
"group": "Troubleshooting",
+ "icon": "question-circle",
"pages": [
"/troubleshooting/support",
"/troubleshooting/bug"
diff --git a/docs/images/gemini_1.png b/docs/images/gemini_1.png
new file mode 100644
index 00000000..82c82249
Binary files /dev/null and b/docs/images/gemini_1.png differ
diff --git a/docs/images/gemini_2.png b/docs/images/gemini_2.png
new file mode 100644
index 00000000..66ba5d75
Binary files /dev/null and b/docs/images/gemini_2.png differ
diff --git a/docs/images/gemini_3.png b/docs/images/gemini_3.png
new file mode 100644
index 00000000..5acdc4d9
Binary files /dev/null and b/docs/images/gemini_3.png differ
diff --git a/docs/images/gemini_4.png b/docs/images/gemini_4.png
new file mode 100644
index 00000000..2975f2e6
Binary files /dev/null and b/docs/images/gemini_4.png differ
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 10dbee40..fa62d7fa 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -1316,14 +1316,6 @@ async function createWindow() {
});
});
} else {
- // REMOVED: Previously this block would directly set initState='done' when installation
- // was already complete, bypassing the backend readiness check.
- //
- // This caused a critical bug where:
- // 1. Frontend would show immediately (initState='done')
- // 2. Backend would still be starting (10-15 seconds)
- // 3. Users could interact before backend was ready, causing connection errors
- //
// The proper flow is now handled by useInstallationSetup.ts with dual-check mechanism:
// 1. Installation complete event → installationCompleted.current = true
// 2. Backend ready event → backendReady.current = true
@@ -1354,6 +1346,9 @@ async function createWindow() {
log.info('Window is ready, processing queued protocol URLs...');
processQueuedProtocolUrls();
+ // Wait for React components to mount and register event listeners
+ await new Promise(resolve => setTimeout(resolve, 500));
+
// Now check and install dependencies
let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win });
if (!res.success) {
diff --git a/electron/main/init.ts b/electron/main/init.ts
index b23c61c1..1ee74eaa 100644
--- a/electron/main/init.ts
+++ b/electron/main/init.ts
@@ -1,4 +1,4 @@
-import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, isBinaryExists, runInstallScript } from "./utils/process";
+import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName } from "./utils/process";
import { spawn, exec } from 'child_process'
import log from 'electron-log'
import fs from 'fs'
@@ -21,16 +21,16 @@ export function getMainWindow(): BrowserWindow | null {
export async function checkToolInstalled() {
return new Promise(async (resolve, reject) => {
if (!(await isBinaryExists('uv'))) {
- resolve({success: false, message: "uv doesn't exist"})
+ resolve({ success: false, message: "uv doesn't exist" })
return
}
if (!(await isBinaryExists('bun'))) {
- resolve({success: false, message: "Bun doesn't exist"})
+ resolve({ success: false, message: "Bun doesn't exist" })
return
}
- resolve({success: true, message: "Tools exist already"})
+ resolve({ success: true, message: "Tools exist already" })
})
}
@@ -159,12 +159,13 @@ export async function startBackend(setPort?: (port: number) => void): Promise void): Promise void): Promise {
try {
process.kill(-proc.pid, 'SIGKILL');
- } catch (e) {}
+ } catch (e) { }
}, 1000);
} catch (e) {
log.error(`Failed to kill process group: ${e}`);
diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts
index aa662a9f..84efae6d 100644
--- a/electron/main/install-deps.ts
+++ b/electron/main/install-deps.ts
@@ -3,7 +3,7 @@ import path from 'node:path'
import log from 'electron-log'
import { getMainWindow } from './init'
import fs from 'node:fs'
-import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process'
+import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process'
import { spawn } from 'child_process'
import { safeMainWindowSend } from './utils/safeWebContentsSend'
import os from 'node:os'
@@ -242,7 +242,6 @@ class InstallLogs {
constructor(extraArgs:string[], version: string) {
console.log('start install dependencies', extraArgs, 'version:', version)
- const venvPath = getVenvPath(version);
this.version = version;
this.node_process = spawn(uv_path, [
@@ -253,10 +252,7 @@ class InstallLogs {
cwd: backendPath,
env: {
...process.env,
- UV_TOOL_DIR: getCachePath('uv_tool'),
- UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
- UV_PROJECT_ENVIRONMENT: venvPath,
- UV_HTTP_TIMEOUT: '180', // 3 minutes timeout
+ ...getUvEnv(version),
}
})
}
diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts
index 66f502d7..7488f332 100644
--- a/electron/main/utils/process.ts
+++ b/electron/main/utils/process.ts
@@ -142,3 +142,49 @@ export async function isBinaryExists(name: string): Promise {
return await fs.existsSync(cmd)
}
+
+/**
+ * Get unified UV environment variables for consistent Python environment management.
+ * This ensures both installation and runtime use the same paths.
+ * @param version - The app version for venv path
+ * @returns Environment variables for UV commands
+ */
+export function getUvEnv(version: string): Record {
+ return {
+ UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
+ UV_TOOL_DIR: getCachePath('uv_tool'),
+ UV_PROJECT_ENVIRONMENT: getVenvPath(version),
+ UV_HTTP_TIMEOUT: '300',
+ }
+}
+
+export async function killProcessByName(name: string): Promise {
+ const platform = process.platform
+ try {
+ if (platform === 'win32') {
+ await new Promise((resolve, reject) => {
+ // /F = force, /IM = image name
+ const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`])
+ cmd.on('close', (code) => {
+ // code 0 = success, code 128 = process not found (which is fine)
+ if (code === 0 || code === 128) resolve()
+ else reject(new Error(`taskkill exited with code ${code}`))
+ })
+ cmd.on('error', reject)
+ })
+ } else {
+ await new Promise((resolve, reject) => {
+ const cmd = spawn('pkill', ['-9', name])
+ cmd.on('close', (code) => {
+ // code 0 = success, code 1 = no process found (which is fine)
+ if (code === 0 || code === 1) resolve()
+ else reject(new Error(`pkill exited with code ${code}`))
+ })
+ cmd.on('error', reject)
+ })
+ }
+ } catch (err) {
+ // Ignore errors, just best effort
+ log.warn(`Failed to kill process ${name}:`, err)
+ }
+}
diff --git a/package.json b/package.json
index f84e7612..2c72eff8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eigent",
- "version": "0.0.72",
+ "version": "0.0.73",
"main": "dist-electron/main/index.js",
"description": "Eigent",
"author": "Eigent.AI",
@@ -133,4 +133,4 @@
"engines": {
"node": ">=18.0.0 <23.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js
index fa1ffb00..735d69bf 100644
--- a/resources/scripts/install-bun.js
+++ b/resources/scripts/install-bun.js
@@ -53,7 +53,9 @@ async function downloadBunBinary(bun_download_url,platform, arch, version = DEFA
const downloadUrl = `${bun_download_url}/bun-v${version}/${packageName}`
const tempdir = os.tmpdir()
// Create a temporary file for the downloaded binary
- const tempFilename = path.join(tempdir, packageName)
+ // Use unique temp filename to avoid race conditions when multiple processes install simultaneously
+ const uniqueSuffix = `${process.pid}-${Date.now()}`
+ const tempFilename = path.join(tempdir, `${uniqueSuffix}-${packageName}`)
try {
console.log(`Downloading bun ${version} for ${platformKey}...`)
diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js
index 057c2898..b3588908 100644
--- a/resources/scripts/install-uv.js
+++ b/resources/scripts/install-uv.js
@@ -66,7 +66,9 @@ async function downloadUvBinary(
// Download URL for the specific binary
const downloadUrl = `${uv_download_url}/${version}/${packageName}`;
const tempdir = os.tmpdir();
- const tempFilename = path.join(tempdir, packageName);
+ // Use unique temp filename to avoid race conditions when multiple processes install simultaneously
+ const uniqueSuffix = `${process.pid}-${Date.now()}`;
+ const tempFilename = path.join(tempdir, `${uniqueSuffix}-${packageName}`);
try {
console.log(`Downloading uv ${version} for ${platformKey}...`);
diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts
index 4654cd72..11850756 100644
--- a/src/hooks/useInstallationSetup.ts
+++ b/src/hooks/useInstallationSetup.ts
@@ -155,15 +155,6 @@ export const useInstallationSetup = () => {
if (installationStatus.success && installationStatus.isInstalling) {
startInstallation();
- } else if (initState !== 'done' && toolResult) {
- if (toolResult.success && !toolResult.isInstalled) {
- console.log('[useInstallationSetup] Tools missing and not installing. Starting installation...');
- try {
- await performInstallation();
- } catch (installError) {
- console.error('[useInstallationSetup] Installation failed:', installError);
- }
- }
}
} catch (err) {
console.error('[useInstallationSetup] Failed to check installation status:', err);