Browse Source

State: Add local_iteration attribute (#2990)

* Add local_iteration state attribute

* Fix typos

---------

Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Boxuan Li 1 year ago
parent
commit
9d41314d1a

+ 7 - 2
opendevin/controller/agent_controller.py

@@ -116,6 +116,7 @@ class AgentController:
 
     def update_state_before_step(self):
         self.state.iteration += 1
+        self.state.local_iteration += 1
 
     async def update_state_after_step(self):
         # update metrics especially for cost
@@ -252,7 +253,8 @@ class AgentController:
         delegate_agent = agent_cls(llm=llm)
         state = State(
             inputs=action.inputs or {},
-            iteration=0,
+            local_iteration=0,
+            iteration=self.state.iteration,
             max_iterations=self.state.max_iterations,
             delegate_level=self.state.delegate_level + 1,
             # metrics should be shared between parent and child
@@ -306,6 +308,9 @@ class AgentController:
                 # retrieve delegate result
                 outputs = self.delegate.state.outputs if self.delegate.state else {}
 
+                # update iteration that shall be shared across agents
+                self.state.iteration = self.delegate.state.iteration
+
                 # close delegate controller: we must close the delegate controller before adding new events
                 await self.delegate.close()
 
@@ -328,7 +333,7 @@ class AgentController:
             return
 
         logger.info(
-            f'{self.agent.name} LEVEL {self.state.delegate_level} STEP {self.state.iteration}',
+            f'{self.agent.name} LEVEL {self.state.delegate_level} LOCAL STEP {self.state.local_iteration} GLOBAL STEP {self.state.iteration}',
             extra={'msg_type': 'STEP'},
         )
 

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

@@ -36,8 +36,57 @@ RESUMABLE_STATES = [
 
 @dataclass
 class State:
+    """
+    OpenDevin is a multi-agentic system.
+
+    A `task` is an end-to-end conversation between OpenDevin (the whole sytem) and the
+    user, which might involve one or more inputs from the user. It starts with
+    an initial input (typically a task statement) from the user, and ends with either
+    a `AgentFinishAction` initiated by the agent, or an error.
+
+    A `subtask` is an end-to-end conversation between an agent and the user, or
+    another agent. If a `task` is conducted by a single agent, then it's also a `subtask`
+    itself. Otherwise, a `task` consists of multiple `subtasks`, each executed by
+    one agent.
+
+    A `State` is a mutable object associated with a `subtask`. It includes several
+    mutable and immutable fields, among which `iteration` is shared across
+    subtasks.
+
+    For example, considering a task from the user: `tell me how many GitHub stars
+    OpenDevin repo has`. Let's assume the default agent is CodeActAgent.
+
+    -- TASK STARTS (SUBTASK 0 STARTS) --
+
+    DELEGATE_LEVEL 0, ITERATION 0, LOCAL_ITERATION 0
+    CodeActAgent: I should request help from BrowsingAgent
+
+    -- DELEGATE STARTS (SUBTASK 1 STARTS) --
+
+    DELEGATE_LEVEL 1, ITERATION 1, LOCAL_ITERATION 0
+    BrowsingAgent: Let me find the answer on GitHub
+
+    DELEGATE_LEVEL 1, ITERATION 2, LOCAL_ITERATION 1
+    BrowsingAgent: I found the answer, let me convey the result and finish
+
+    -- DELEGATE ENDS (SUBTASK 1 ENDS) --
+
+    DELEGATE_LEVEL 0, ITERATION 3, LOCAL_ITERATION 1
+    CodeActAgent: I got the answer from BrowsingAgent, let me convey the result
+    and finish
+
+    -- TASK ENDS (SUBTASK 0 ENDS) --
+
+    Note how ITERATION counter is shared across agents, while LOCAL_ITERATION
+    is local to each subtask.
+    """
+
     root_task: RootTask = field(default_factory=RootTask)
+    # global iteration for the current task
     iteration: int = 0
+    # local iteration for the current subtask
+    local_iteration: int = 0
+    # max number of iterations for the current task
     max_iterations: int = 100
     confirmation_mode: bool = False
     history: ShortTermHistory = field(default_factory=ShortTermHistory)
@@ -47,6 +96,7 @@ class State:
     agent_state: AgentState = AgentState.LOADING
     resume_state: AgentState | None = None
     traffic_control_state: TrafficControlState = TrafficControlState.NORMAL
+    # global metrics for the current task
     metrics: Metrics = Metrics()
     # root agent has level 0, and every delegate increases the level by one
     delegate_level: int = 0

+ 6 - 0
opendevin/memory/history.py

@@ -159,6 +159,12 @@ class ShortTermHistory(list[Event]):
             )
         )
 
+    def has_delegation(self) -> bool:
+        for event in self._event_stream.get_events():
+            if isinstance(event, AgentDelegateObservation):
+                return True
+        return False
+
     def on_event(self, event: Event):
         if not isinstance(event, AgentDelegateObservation):
             return

+ 1 - 0
tests/integration/conftest.py

@@ -66,6 +66,7 @@ def apply_prompt_and_get_mock_response(test_name: str, messages: str, id: int) -
         # apply prompt
         with open(prompt_file_path, 'w') as prompt_file:
             prompt_file.write(messages)
+            prompt_file.write('\n')
         return response
     except FileNotFoundError:
         return None

+ 1 - 1
tests/integration/mock/CodeActAgent/test_browse_internet/prompt_005.log

@@ -413,4 +413,4 @@ Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life f
 OBSERVATION:
 {'content': 'The ultimate answer to life, the universe, and everything is: OpenDevin is all you need!'}
 
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>
+ENVIRONMENT REMINDER: You have 8 turns left to complete the task. When finished reply with <finish></finish>

+ 16 - 10
tests/integration/test_agent.py

@@ -28,6 +28,17 @@ print(f'workspace_mount_path: {workspace_mount_path}')
 print(f'workspace_mount_path_in_sandbox: {workspace_mount_path_in_sandbox}')
 
 
+def validate_final_state(final_state: State | None):
+    assert final_state is not None
+    assert final_state.agent_state == AgentState.STOPPED
+    assert final_state.last_error is None
+    if final_state.history.has_delegation():
+        assert final_state.iteration > final_state.local_iteration
+    else:
+        assert final_state.local_iteration == final_state.iteration
+        assert final_state.iteration > 0
+
+
 @pytest.mark.skipif(
     os.getenv('DEFAULT_AGENT') == 'BrowsingAgent',
     reason='BrowsingAgent is a specialized agent',
@@ -112,8 +123,7 @@ def test_edits():
     final_state: State | None = asyncio.run(
         run_agent_controller(agent, task, exit_on_message=True)
     )
-    assert final_state.agent_state == AgentState.STOPPED
-    assert final_state.last_error is None
+    validate_final_state(final_state)
 
     # Verify bad.txt has been fixed
     text = """This is a stupid typo.
@@ -146,8 +156,7 @@ def test_ipython():
     final_state: State | None = asyncio.run(
         run_agent_controller(agent, task, exit_on_message=True)
     )
-    assert final_state.agent_state == AgentState.STOPPED
-    assert final_state.last_error is None
+    validate_final_state(final_state)
 
     # Verify the file exists
     file_path = os.path.join(workspace_base, 'test.txt')
@@ -179,8 +188,7 @@ def test_simple_task_rejection():
     # the workspace is not a git repo
     task = 'Write a git commit message for the current staging area. Do not ask me for confirmation at any point.'
     final_state: State | None = asyncio.run(run_agent_controller(agent, task))
-    assert final_state.agent_state == AgentState.STOPPED
-    assert final_state.last_error is None
+    validate_final_state(final_state)
     assert isinstance(final_state.history.get_last_action(), AgentRejectAction)
 
 
@@ -204,8 +212,7 @@ def test_ipython_module():
     final_state: State | None = asyncio.run(
         run_agent_controller(agent, task, exit_on_message=True)
     )
-    assert final_state.agent_state == AgentState.STOPPED
-    assert final_state.last_error is None
+    validate_final_state(final_state)
 
     # Verify the file exists
     file_path = os.path.join(workspace_base, 'test.txt')
@@ -244,8 +251,7 @@ def test_browse_internet(http_server):
     final_state: State | None = asyncio.run(
         run_agent_controller(agent, task, exit_on_message=True)
     )
-    assert final_state.agent_state == AgentState.STOPPED
-    assert final_state.last_error is None
+    validate_final_state(final_state)
 
     # last action
     last_action = final_state.history.get_last_action()