mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-28 00:18:25 -05:00
feat(classic): make CWD the default agent workspace for CLI mode
In CLI mode, agents now work directly in the current directory instead of
being sandboxed to .autogpt/agents/{id}/workspace/. Agent state files are
still stored in .autogpt/agents/{id}/state.json.
Server mode retains the original sandboxed behavior for isolation.
Changes:
- Add workspace_root parameter to FileManagerComponent to detect CLI mode
- Update Agent to pass workspace_root when file_storage is rooted at workspace
- Adjust save_state paths based on mode (CLI uses .autogpt/ prefix)
- Add use_tools field to ActionProposal for parallel tool execution
- Support parallel tool execution in Agent.execute()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ class FileManagerComponent(
|
||||
file_storage: FileStorage,
|
||||
agent_state: BaseAgentSettings,
|
||||
config: Optional[FileManagerConfiguration] = None,
|
||||
workspace_root: Optional[Path] = None,
|
||||
):
|
||||
"""Initialise the FileManagerComponent.
|
||||
Either `agent_id` or `config` must be provided.
|
||||
@@ -61,15 +62,26 @@ class FileManagerComponent(
|
||||
state (BaseAgentSettings): The agent's state.
|
||||
config (FileManagerConfiguration, optional): The configuration for
|
||||
the file manager. Defaults to None.
|
||||
workspace_root (Path, optional): When provided (CLI mode), indicates that
|
||||
file_storage is rooted at the workspace directory. Agent files go to
|
||||
.autogpt/agents/{id}/ and the workspace is the current directory.
|
||||
When None (server mode), uses sandboxed paths under agents/{id}/.
|
||||
"""
|
||||
if not agent_state.agent_id:
|
||||
raise ValueError("Agent must have an ID.")
|
||||
|
||||
self.agent_state = agent_state
|
||||
self._workspace_root = workspace_root
|
||||
|
||||
if not config:
|
||||
storage_path = f"agents/{self.agent_state.agent_id}/"
|
||||
workspace_path = f"agents/{self.agent_state.agent_id}/workspace"
|
||||
if workspace_root:
|
||||
# CLI mode: file_storage root = workspace, agent works in cwd
|
||||
storage_path = f".autogpt/agents/{self.agent_state.agent_id}/"
|
||||
workspace_path = "."
|
||||
else:
|
||||
# Server mode: file_storage root = .autogpt, sandboxed workspace
|
||||
storage_path = f"agents/{self.agent_state.agent_id}/"
|
||||
workspace_path = f"agents/{self.agent_state.agent_id}/workspace"
|
||||
ConfigurableComponent.__init__(
|
||||
self,
|
||||
FileManagerConfiguration(
|
||||
@@ -90,17 +102,26 @@ class FileManagerComponent(
|
||||
async def save_state(self, save_as_id: Optional[str] = None) -> None:
|
||||
"""Save the agent's data and state."""
|
||||
if save_as_id:
|
||||
self._file_storage.make_dir(f"agents/{save_as_id}")
|
||||
# Determine path prefix based on mode
|
||||
if self._workspace_root:
|
||||
# CLI mode: file storage is rooted at workspace, state goes to .autogpt/
|
||||
agents_prefix = ".autogpt/agents"
|
||||
else:
|
||||
# Server mode: file storage is rooted at .autogpt/
|
||||
agents_prefix = "agents"
|
||||
|
||||
self._file_storage.make_dir(f"{agents_prefix}/{save_as_id}")
|
||||
# Save state
|
||||
await self._file_storage.write_file(
|
||||
f"agents/{save_as_id}/{self.STATE_FILE}",
|
||||
f"{agents_prefix}/{save_as_id}/{self.STATE_FILE}",
|
||||
self.agent_state.model_dump_json(),
|
||||
)
|
||||
# Copy workspace
|
||||
self._file_storage.copy(
|
||||
self.config.workspace_path,
|
||||
f"agents/{save_as_id}/workspace",
|
||||
)
|
||||
# Copy workspace (only in server mode, each agent has its own sandbox)
|
||||
if not self._workspace_root:
|
||||
self._file_storage.copy(
|
||||
self.config.workspace_path,
|
||||
f"{agents_prefix}/{save_as_id}/workspace",
|
||||
)
|
||||
else:
|
||||
await self.storage.write_file(
|
||||
self.storage.root / self.STATE_FILE, self.agent_state.model_dump_json()
|
||||
|
||||
@@ -17,12 +17,23 @@ from .utils import ModelWithSummary
|
||||
class ActionProposal(BaseModel):
|
||||
thoughts: str | ModelWithSummary
|
||||
use_tool: AssistantFunctionCall
|
||||
use_tools: Optional[list[AssistantFunctionCall]] = None
|
||||
"""
|
||||
List of tools to execute in parallel. If None, only use_tool is executed.
|
||||
When set, use_tool should be the first item for backwards compatibility.
|
||||
"""
|
||||
|
||||
raw_message: AssistantChatMessage = None # type: ignore
|
||||
"""
|
||||
The message from which the action proposal was parsed. To be set by the parser.
|
||||
"""
|
||||
|
||||
def get_tools(self) -> list[AssistantFunctionCall]:
|
||||
"""Get all tools to execute. Returns use_tools if set, otherwise [use_tool]."""
|
||||
if self.use_tools:
|
||||
return self.use_tools
|
||||
return [self.use_tool]
|
||||
|
||||
@classmethod
|
||||
def model_json_schema(
|
||||
cls,
|
||||
@@ -42,8 +53,11 @@ class ActionProposal(BaseModel):
|
||||
mode=mode,
|
||||
**kwargs,
|
||||
)
|
||||
if "raw_message" in schema["properties"]: # must check because schema is cached
|
||||
# Exclude internal fields from schema (must check because schema is cached)
|
||||
if "raw_message" in schema["properties"]:
|
||||
del schema["properties"]["raw_message"]
|
||||
if "use_tools" in schema["properties"]:
|
||||
del schema["properties"]["use_tools"]
|
||||
return schema
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
@@ -175,7 +176,14 @@ class Agent(BaseAgent[AnyActionProposal], Configurable[AgentSettings]):
|
||||
)
|
||||
if not app_config.noninteractive_mode:
|
||||
self.user_interaction = UserInteractionComponent()
|
||||
self.file_manager = FileManagerComponent(file_storage, settings)
|
||||
# CLI mode: file storage rooted at workspace (not .autogpt)
|
||||
# Agents work directly in cwd; in server mode, they're sandboxed
|
||||
cli_mode = file_storage.root == app_config.workspace
|
||||
self.file_manager = FileManagerComponent(
|
||||
file_storage,
|
||||
settings,
|
||||
workspace_root=app_config.workspace if cli_mode else None,
|
||||
)
|
||||
self.code_executor = CodeExecutorComponent(
|
||||
self.file_manager.workspace,
|
||||
CodeExecutorConfiguration(
|
||||
@@ -352,46 +360,54 @@ class Agent(BaseAgent[AnyActionProposal], Configurable[AgentSettings]):
|
||||
proposal: ActionProposal,
|
||||
user_feedback: str = "",
|
||||
) -> ActionResult:
|
||||
tool = proposal.use_tool
|
||||
# Get all tools to execute (supports parallel execution)
|
||||
tools = proposal.get_tools()
|
||||
|
||||
# Get commands
|
||||
self.commands = await self.run_pipeline(CommandProvider.get_commands)
|
||||
self._remove_disabled_commands()
|
||||
|
||||
# Check permissions before execution
|
||||
# Check permissions for all tools before execution
|
||||
feedback_to_append = None
|
||||
if self.permission_manager:
|
||||
perm_result = self.permission_manager.check_command(
|
||||
tool.name, tool.arguments
|
||||
)
|
||||
if not perm_result.allowed:
|
||||
# Permission denied - pass feedback to agent if provided
|
||||
if perm_result.feedback:
|
||||
return await self.do_not_execute(proposal, perm_result.feedback)
|
||||
return ActionErrorResult(
|
||||
reason=f"Permission denied for command '{tool.name}'",
|
||||
for tool in tools:
|
||||
perm_result = self.permission_manager.check_command(
|
||||
tool.name, tool.arguments
|
||||
)
|
||||
if not perm_result.allowed:
|
||||
# Permission denied - pass feedback to agent if provided
|
||||
if perm_result.feedback:
|
||||
return await self.do_not_execute(proposal, perm_result.feedback)
|
||||
return ActionErrorResult(
|
||||
reason=f"Permission denied for command '{tool.name}'",
|
||||
)
|
||||
# Permission granted - save feedback if any
|
||||
if perm_result.feedback:
|
||||
feedback_to_append = perm_result.feedback
|
||||
|
||||
# Permission granted - execute command, then handle feedback if any
|
||||
feedback_to_append = perm_result.feedback
|
||||
# Execute tool(s)
|
||||
if len(tools) == 1:
|
||||
# Single tool - original behavior
|
||||
tool = tools[0]
|
||||
try:
|
||||
return_value = await self._execute_tool(tool)
|
||||
result = ActionSuccessResult(outputs=return_value)
|
||||
except AgentTerminated:
|
||||
raise
|
||||
except AgentException as e:
|
||||
result = ActionErrorResult.from_exception(e)
|
||||
logger.warning(f"{tool} raised an error: {e}")
|
||||
sentry_sdk.capture_exception(e)
|
||||
else:
|
||||
feedback_to_append = None
|
||||
|
||||
try:
|
||||
return_value = await self._execute_tool(tool)
|
||||
|
||||
result = ActionSuccessResult(outputs=return_value)
|
||||
except AgentTerminated:
|
||||
raise
|
||||
except AgentException as e:
|
||||
result = ActionErrorResult.from_exception(e)
|
||||
logger.warning(f"{tool} raised an error: {e}")
|
||||
sentry_sdk.capture_exception(e)
|
||||
# Multiple tools - execute in parallel
|
||||
logger.info(f"Executing {len(tools)} tools in parallel")
|
||||
result = await self._execute_tools_parallel(tools)
|
||||
|
||||
result_tlength = self.llm_provider.count_tokens(str(result), self.llm.name)
|
||||
if result_tlength > self.send_token_limit // 3:
|
||||
result = ActionErrorResult(
|
||||
reason=f"Command {tool.name} returned too much output. "
|
||||
"Do not execute this command again with the same arguments."
|
||||
reason="Command(s) returned too much output. "
|
||||
"Do not execute these commands again with the same arguments."
|
||||
)
|
||||
|
||||
# Notify ReWOO strategy of execution result for variable tracking
|
||||
@@ -458,6 +474,66 @@ class Agent(BaseAgent[AnyActionProposal], Configurable[AgentSettings]):
|
||||
except Exception as e:
|
||||
raise CommandExecutionError(str(e))
|
||||
|
||||
async def _execute_tools_parallel(
|
||||
self, tools: list[AssistantFunctionCall]
|
||||
) -> ActionResult:
|
||||
"""Execute multiple tools in parallel and combine results.
|
||||
|
||||
Args:
|
||||
tools: List of tool calls to execute in parallel
|
||||
|
||||
Returns:
|
||||
Combined ActionResult with all outputs or errors
|
||||
"""
|
||||
|
||||
async def execute_single(tool: AssistantFunctionCall) -> tuple[str, Any, str]:
|
||||
"""Execute a single tool and return (name, result, error)."""
|
||||
try:
|
||||
result = await self._execute_tool(tool)
|
||||
return (tool.name, result, "")
|
||||
except AgentTerminated:
|
||||
raise
|
||||
except AgentException as e:
|
||||
logger.warning(f"{tool} raised an error: {e}")
|
||||
sentry_sdk.capture_exception(e)
|
||||
return (tool.name, None, str(e))
|
||||
|
||||
# Execute all tools in parallel
|
||||
results = await asyncio.gather(
|
||||
*[execute_single(tool) for tool in tools],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
# Process results
|
||||
outputs: dict[str, Any] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
for i, res in enumerate(results):
|
||||
tool = tools[i]
|
||||
if isinstance(res, BaseException):
|
||||
# Unexpected exception from gather
|
||||
errors.append(f"{tool.name}: {res}")
|
||||
logger.warning(f"{tool} raised unexpected error: {res}")
|
||||
sentry_sdk.capture_exception(res)
|
||||
elif isinstance(res, tuple):
|
||||
name, output, error = res
|
||||
if error:
|
||||
errors.append(f"{name}: {error}")
|
||||
else:
|
||||
outputs[name] = output
|
||||
|
||||
# Return combined result
|
||||
if errors and not outputs:
|
||||
# All failed
|
||||
return ActionErrorResult(reason="; ".join(errors))
|
||||
elif errors:
|
||||
# Partial success - include errors in output
|
||||
outputs["_errors"] = errors
|
||||
return ActionSuccessResult(outputs=outputs)
|
||||
else:
|
||||
# All succeeded
|
||||
return ActionSuccessResult(outputs=outputs)
|
||||
|
||||
def _get_command(self, command_name: str) -> Command:
|
||||
for command in reversed(self.commands):
|
||||
if command_name in command.names:
|
||||
|
||||
Reference in New Issue
Block a user