Skip to content

Commit

Permalink
State: Add local_iteration attribute (All-Hands-AI#2990)
Browse files Browse the repository at this point in the history
* Add local_iteration state attribute

* Fix typos

---------

Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
  • Loading branch information
li-boxuan and xingyaoww authored Jul 18, 2024
1 parent f70c5af commit 9d41314
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 13 deletions.
9 changes: 7 additions & 2 deletions opendevin/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ async def close(self):

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
Expand Down Expand Up @@ -252,7 +253,8 @@ async def start_delegate(self, action: AgentDelegateAction):
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
Expand Down Expand Up @@ -306,6 +308,9 @@ async def _step(self):
# 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()

Expand All @@ -328,7 +333,7 @@ async def _step(self):
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'},
)

Expand Down
50 changes: 50 additions & 0 deletions opendevin/controller/state/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,57 @@ class TrafficControlState(str, Enum):

@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)
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions opendevin/memory/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ def get_last_events(self, n: int) -> 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
Expand Down
1 change: 1 addition & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
26 changes: 16 additions & 10 deletions tests/integration/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@
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',
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)


Expand All @@ -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')
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 9d41314

Please sign in to comment.