Преглед изворни кода

Bypass MAX_ITERATIONS and MAX_BUDGET_PER_TASK on web GUI (#2697)

Closes #1493

Introduced TRAFFIC_CONTROL_STATE to allow OpenDevin to switch between normal traffic limiting mode and temporarily disabled mode.
Boxuan Li пре 1 година
родитељ
комит
8dae1f9307
2 измењених фајлова са 54 додато и 13 уклоњено
  1. 40 13
      opendevin/controller/agent_controller.py
  2. 14 0
      opendevin/controller/state/state.py

+ 40 - 13
opendevin/controller/agent_controller.py

@@ -3,7 +3,7 @@ import traceback
 from typing import Optional, Type
 
 from opendevin.controller.agent import Agent
-from opendevin.controller.state.state import State
+from opendevin.controller.state.state import TRAFFIC_CONTROL_STATE, State
 from opendevin.core.config import config
 from opendevin.core.exceptions import (
     LLMMalformedActionError,
@@ -37,6 +37,10 @@ from opendevin.events.observation import (
 
 MAX_ITERATIONS = config.max_iterations
 MAX_BUDGET_PER_TASK = config.max_budget_per_task
+# note: RESUME is only available on web GUI
+TRAFFIC_CONTROL_REMINDER = (
+    "Please click on resume button if you'd like to continue, or start a new task."
+)
 
 
 class AgentController:
@@ -194,6 +198,14 @@ class AgentController:
         if new_state == self.state.agent_state:
             return
 
+        if (
+            self.state.agent_state == AgentState.PAUSED
+            and new_state == AgentState.RUNNING
+            and self.state.traffic_control_state == TRAFFIC_CONTROL_STATE.THROTTLING
+        ):
+            # user intends to interrupt traffic control and let the task resume temporarily
+            self.state.traffic_control_state = TRAFFIC_CONTROL_STATE.PAUSED
+
         self.state.agent_state = new_state
         if new_state == AgentState.STOPPED or new_state == AgentState.ERROR:
             self.reset_task()
@@ -208,6 +220,8 @@ class AgentController:
 
     def get_agent_state(self):
         """Returns the current state of the agent task."""
+        if self.delegate is not None:
+            return self.delegate.get_agent_state()
         return self.state.agent_state
 
     async def start_delegate(self, action: AgentDelegateAction):
@@ -293,22 +307,35 @@ class AgentController:
             f'{self.agent.name} LEVEL {self.state.delegate_level} STEP {self.state.iteration}',
             extra={'msg_type': 'STEP'},
         )
-        if self.state.iteration >= self.state.max_iterations:
-            await self.report_error('Agent reached maximum number of iterations')
-            await self.set_agent_state_to(AgentState.ERROR)
-            return
 
-        if self.max_budget_per_task is not None:
-            current_cost = self.state.metrics.accumulated_cost
-            if current_cost > self.max_budget_per_task:
+        if self.state.iteration >= self.state.max_iterations:
+            if self.state.traffic_control_state == TRAFFIC_CONTROL_STATE.PAUSED:
+                logger.info(
+                    'Hitting traffic control, temporarily resume upon user request'
+                )
+                self.state.traffic_control_state = TRAFFIC_CONTROL_STATE.NORMAL
+            else:
+                self.state.traffic_control_state = TRAFFIC_CONTROL_STATE.THROTTLING
                 await self.report_error(
-                    f'Task budget exceeded. Current cost: {current_cost:.2f}, Max budget: {self.max_budget_per_task:.2f}'
+                    f'Agent reached maximum number of iterations, task paused. {TRAFFIC_CONTROL_REMINDER}'
                 )
-                await self.set_agent_state_to(AgentState.ERROR)
+                await self.set_agent_state_to(AgentState.PAUSED)
                 return
-
-        if self.state.agent_state == AgentState.ERROR:
-            return
+        elif self.max_budget_per_task is not None:
+            current_cost = self.state.metrics.accumulated_cost
+            if current_cost > self.max_budget_per_task:
+                if self.state.traffic_control_state == TRAFFIC_CONTROL_STATE.PAUSED:
+                    logger.info(
+                        'Hitting traffic control, temporarily resume upon user request'
+                    )
+                    self.state.traffic_control_state = TRAFFIC_CONTROL_STATE.NORMAL
+                else:
+                    self.state.traffic_control_state = TRAFFIC_CONTROL_STATE.THROTTLING
+                    await self.report_error(
+                        f'Task budget exceeded. Current cost: {current_cost:.2f}, Max budget: {self.max_budget_per_task:.2f}, task paused. {TRAFFIC_CONTROL_REMINDER}'
+                    )
+                    await self.set_agent_state_to(AgentState.PAUSED)
+                    return
 
         self.update_state_before_step()
         action: Action = NullAction()

+ 14 - 0
opendevin/controller/state/state.py

@@ -1,6 +1,7 @@
 import base64
 import pickle
 from dataclasses import dataclass, field
+from enum import Enum
 
 from opendevin.controller.state.task import RootTask
 from opendevin.core.logger import opendevin_logger as logger
@@ -16,6 +17,18 @@ from opendevin.events.observation import (
 )
 from opendevin.storage import get_file_store
 
+
+class TRAFFIC_CONTROL_STATE(str, Enum):
+    # default state, no rate limiting
+    NORMAL = 'normal'
+
+    # task paused due to traffic control
+    THROTTLING = 'throttling'
+
+    # traffic control is temporarily paused
+    PAUSED = 'paused'
+
+
 RESUMABLE_STATES = [
     AgentState.RUNNING,
     AgentState.PAUSED,
@@ -37,6 +50,7 @@ class State:
     last_error: str | None = None
     agent_state: AgentState = AgentState.LOADING
     resume_state: AgentState | None = None
+    traffic_control_state: TRAFFIC_CONTROL_STATE = TRAFFIC_CONTROL_STATE.NORMAL
     metrics: Metrics = Metrics()
     # root agent has level 0, and every delegate increases the level by one
     delegate_level: int = 0