mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 07:18:10 -05:00
State: Add local_iteration attribute (#2990)
* Add local_iteration state attribute * Fix typos --------- Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
This commit is contained in:
@@ -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'},
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user