Skip to content

Commit 9d41314

Browse files
li-boxuanxingyaoww
andauthored
State: Add local_iteration attribute (#2990)
* Add local_iteration state attribute * Fix typos --------- Co-authored-by: Xingyao Wang <[email protected]>
1 parent f70c5af commit 9d41314

File tree

6 files changed

+81
-13
lines changed

6 files changed

+81
-13
lines changed

opendevin/controller/agent_controller.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ async def close(self):
116116

117117
def update_state_before_step(self):
118118
self.state.iteration += 1
119+
self.state.local_iteration += 1
119120

120121
async def update_state_after_step(self):
121122
# update metrics especially for cost
@@ -252,7 +253,8 @@ async def start_delegate(self, action: AgentDelegateAction):
252253
delegate_agent = agent_cls(llm=llm)
253254
state = State(
254255
inputs=action.inputs or {},
255-
iteration=0,
256+
local_iteration=0,
257+
iteration=self.state.iteration,
256258
max_iterations=self.state.max_iterations,
257259
delegate_level=self.state.delegate_level + 1,
258260
# metrics should be shared between parent and child
@@ -306,6 +308,9 @@ async def _step(self):
306308
# retrieve delegate result
307309
outputs = self.delegate.state.outputs if self.delegate.state else {}
308310

311+
# update iteration that shall be shared across agents
312+
self.state.iteration = self.delegate.state.iteration
313+
309314
# close delegate controller: we must close the delegate controller before adding new events
310315
await self.delegate.close()
311316

@@ -328,7 +333,7 @@ async def _step(self):
328333
return
329334

330335
logger.info(
331-
f'{self.agent.name} LEVEL {self.state.delegate_level} STEP {self.state.iteration}',
336+
f'{self.agent.name} LEVEL {self.state.delegate_level} LOCAL STEP {self.state.local_iteration} GLOBAL STEP {self.state.iteration}',
332337
extra={'msg_type': 'STEP'},
333338
)
334339

opendevin/controller/state/state.py

+50
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,57 @@ class TrafficControlState(str, Enum):
3636

3737
@dataclass
3838
class State:
39+
"""
40+
OpenDevin is a multi-agentic system.
41+
42+
A `task` is an end-to-end conversation between OpenDevin (the whole sytem) and the
43+
user, which might involve one or more inputs from the user. It starts with
44+
an initial input (typically a task statement) from the user, and ends with either
45+
a `AgentFinishAction` initiated by the agent, or an error.
46+
47+
A `subtask` is an end-to-end conversation between an agent and the user, or
48+
another agent. If a `task` is conducted by a single agent, then it's also a `subtask`
49+
itself. Otherwise, a `task` consists of multiple `subtasks`, each executed by
50+
one agent.
51+
52+
A `State` is a mutable object associated with a `subtask`. It includes several
53+
mutable and immutable fields, among which `iteration` is shared across
54+
subtasks.
55+
56+
For example, considering a task from the user: `tell me how many GitHub stars
57+
OpenDevin repo has`. Let's assume the default agent is CodeActAgent.
58+
59+
-- TASK STARTS (SUBTASK 0 STARTS) --
60+
61+
DELEGATE_LEVEL 0, ITERATION 0, LOCAL_ITERATION 0
62+
CodeActAgent: I should request help from BrowsingAgent
63+
64+
-- DELEGATE STARTS (SUBTASK 1 STARTS) --
65+
66+
DELEGATE_LEVEL 1, ITERATION 1, LOCAL_ITERATION 0
67+
BrowsingAgent: Let me find the answer on GitHub
68+
69+
DELEGATE_LEVEL 1, ITERATION 2, LOCAL_ITERATION 1
70+
BrowsingAgent: I found the answer, let me convey the result and finish
71+
72+
-- DELEGATE ENDS (SUBTASK 1 ENDS) --
73+
74+
DELEGATE_LEVEL 0, ITERATION 3, LOCAL_ITERATION 1
75+
CodeActAgent: I got the answer from BrowsingAgent, let me convey the result
76+
and finish
77+
78+
-- TASK ENDS (SUBTASK 0 ENDS) --
79+
80+
Note how ITERATION counter is shared across agents, while LOCAL_ITERATION
81+
is local to each subtask.
82+
"""
83+
3984
root_task: RootTask = field(default_factory=RootTask)
85+
# global iteration for the current task
4086
iteration: int = 0
87+
# local iteration for the current subtask
88+
local_iteration: int = 0
89+
# max number of iterations for the current task
4190
max_iterations: int = 100
4291
confirmation_mode: bool = False
4392
history: ShortTermHistory = field(default_factory=ShortTermHistory)
@@ -47,6 +96,7 @@ class State:
4796
agent_state: AgentState = AgentState.LOADING
4897
resume_state: AgentState | None = None
4998
traffic_control_state: TrafficControlState = TrafficControlState.NORMAL
99+
# global metrics for the current task
50100
metrics: Metrics = Metrics()
51101
# root agent has level 0, and every delegate increases the level by one
52102
delegate_level: int = 0

opendevin/memory/history.py

+6
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ def get_last_events(self, n: int) -> list[Event]:
159159
)
160160
)
161161

162+
def has_delegation(self) -> bool:
163+
for event in self._event_stream.get_events():
164+
if isinstance(event, AgentDelegateObservation):
165+
return True
166+
return False
167+
162168
def on_event(self, event: Event):
163169
if not isinstance(event, AgentDelegateObservation):
164170
return

tests/integration/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def apply_prompt_and_get_mock_response(test_name: str, messages: str, id: int) -
6666
# apply prompt
6767
with open(prompt_file_path, 'w') as prompt_file:
6868
prompt_file.write(messages)
69+
prompt_file.write('\n')
6970
return response
7071
except FileNotFoundError:
7172
return None

tests/integration/mock/CodeActAgent/test_browse_internet/prompt_005.log

+1-1
Original file line numberDiff line numberDiff line change
@@ -413,4 +413,4 @@ Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life f
413413
OBSERVATION:
414414
{'content': 'The ultimate answer to life, the universe, and everything is: OpenDevin is all you need!'}
415415

416-
ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>
416+
ENVIRONMENT REMINDER: You have 8 turns left to complete the task. When finished reply with <finish></finish>

tests/integration/test_agent.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@
2828
print(f'workspace_mount_path_in_sandbox: {workspace_mount_path_in_sandbox}')
2929

3030

31+
def validate_final_state(final_state: State | None):
32+
assert final_state is not None
33+
assert final_state.agent_state == AgentState.STOPPED
34+
assert final_state.last_error is None
35+
if final_state.history.has_delegation():
36+
assert final_state.iteration > final_state.local_iteration
37+
else:
38+
assert final_state.local_iteration == final_state.iteration
39+
assert final_state.iteration > 0
40+
41+
3142
@pytest.mark.skipif(
3243
os.getenv('DEFAULT_AGENT') == 'BrowsingAgent',
3344
reason='BrowsingAgent is a specialized agent',
@@ -112,8 +123,7 @@ def test_edits():
112123
final_state: State | None = asyncio.run(
113124
run_agent_controller(agent, task, exit_on_message=True)
114125
)
115-
assert final_state.agent_state == AgentState.STOPPED
116-
assert final_state.last_error is None
126+
validate_final_state(final_state)
117127

118128
# Verify bad.txt has been fixed
119129
text = """This is a stupid typo.
@@ -146,8 +156,7 @@ def test_ipython():
146156
final_state: State | None = asyncio.run(
147157
run_agent_controller(agent, task, exit_on_message=True)
148158
)
149-
assert final_state.agent_state == AgentState.STOPPED
150-
assert final_state.last_error is None
159+
validate_final_state(final_state)
151160

152161
# Verify the file exists
153162
file_path = os.path.join(workspace_base, 'test.txt')
@@ -179,8 +188,7 @@ def test_simple_task_rejection():
179188
# the workspace is not a git repo
180189
task = 'Write a git commit message for the current staging area. Do not ask me for confirmation at any point.'
181190
final_state: State | None = asyncio.run(run_agent_controller(agent, task))
182-
assert final_state.agent_state == AgentState.STOPPED
183-
assert final_state.last_error is None
191+
validate_final_state(final_state)
184192
assert isinstance(final_state.history.get_last_action(), AgentRejectAction)
185193

186194

@@ -204,8 +212,7 @@ def test_ipython_module():
204212
final_state: State | None = asyncio.run(
205213
run_agent_controller(agent, task, exit_on_message=True)
206214
)
207-
assert final_state.agent_state == AgentState.STOPPED
208-
assert final_state.last_error is None
215+
validate_final_state(final_state)
209216

210217
# Verify the file exists
211218
file_path = os.path.join(workspace_base, 'test.txt')
@@ -244,8 +251,7 @@ def test_browse_internet(http_server):
244251
final_state: State | None = asyncio.run(
245252
run_agent_controller(agent, task, exit_on_message=True)
246253
)
247-
assert final_state.agent_state == AgentState.STOPPED
248-
assert final_state.last_error is None
254+
validate_final_state(final_state)
249255

250256
# last action
251257
last_action = final_state.history.get_last_action()

0 commit comments

Comments
 (0)