Skip to content

Commit 9b371b1

Browse files
authored
Refactor agent delegation and tweak micro agents (#1910)
This PR fixes #1897. In addition, this PR fixes and tweaks a few micro-agents. For the first time, I am able to use ManagerAgent to complete test_write_simple_script and test_edits tasks in integration tests, so this PR also adds ManagerAgent as part of integration tests. test_write_simple_script involves delegation to CoderAgent while test_edits involves delegation to TypoFixerAgent. Also for the first time, I am able to use DelegateAgent to complete test_write_simple_script and test_edits tasks in integration tests, so this PR also adds DelegateAgent as part of integration tests. It involves delegation to StudyRepoForTaskAgent, CoderAgent and VerifierAgent. This PR is a blocker for #1735 and likely #1945.
1 parent c37a474 commit 9b371b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2534
-50
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
* `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working. Arguments:
1+
* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
22
* `outputs` - a dictionary representing the outputs of your task, if any

agenthub/micro/agent.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,13 @@ def __init__(self, llm: LLM):
5555
del self.delegates[self.agent_definition['name']]
5656

5757
def step(self, state: State) -> Action:
58-
latest_user_message = state.get_current_user_intent()
5958
prompt = self.prompt_template.render(
6059
state=state,
6160
instructions=instructions,
6261
to_json=to_json,
6362
history_to_json=history_to_json,
6463
delegates=self.delegates,
65-
latest_user_message=latest_user_message,
64+
latest_user_message=state.get_current_user_intent(),
6665
)
6766
messages = [{'content': prompt, 'role': 'user'}]
6867
resp = self.llm.do_completion(messages=messages)

agenthub/micro/coder/agent.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ name: CoderAgent
22
description: Given a particular task, and a detailed description of the codebase, accomplishes the task
33
inputs:
44
task: string
5-
codebase_summary: string
5+
summary: string
66
outputs: {}

agenthub/micro/coder/prompt.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
You are a software engineer. You've inherited an existing codebase, which you
33
need to modify to complete this task:
44

5-
{{ latest_user_message }}
5+
{{ state.inputs.task }}
66

77
{% if state.inputs.summary %}
88
Here's a summary of the codebase, as it relates to this task:

agenthub/micro/math_agent/prompt.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Task
22
You are a brilliant mathematician and programmer. You've been given the following problem to solve:
33

4-
{{ latest_user_message }}
4+
`{{ state.inputs.task }}`
55

66
Please write a python script that solves this problem, and prints the answer to stdout.
77
ONLY print the answer to stdout, nothing else.

agenthub/micro/postgres_agent/prompt.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
You are a database engineer. You are working on an existing Postgres project, and have been given
33
the following task:
44

5-
{{ latest_user_message }}
5+
{{ state.inputs.task }}
66

77
You must:
88
* Investigate the existing migrations to understand the current schema

agenthub/micro/registry.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
all_microagents = {}
66

7-
for dir in os.listdir(os.path.dirname(__file__)):
7+
# Get the list of directories and sort them to preserve determinism
8+
dirs = sorted(os.listdir(os.path.dirname(__file__)))
9+
10+
for dir in dirs:
811
base = os.path.dirname(__file__) + '/' + dir
912
if os.path.isfile(base):
1013
continue
+44-6
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,63 @@
11
# Task
2-
You are a software engineer. You've inherited an existing codebase, which you're
3-
learning about for the first time. You need to study the codebase to find all
4-
the information needed to complete this task:
2+
You are a software architect. Your team has inherited an existing codebase, and
3+
need to finish a project:
54

6-
{{ latest_user_message }}
5+
{{ state.inputs.task }}
6+
7+
As an architect, you need to study the codebase to find all the information that
8+
might be helpful for your software engineering team.
79

810
## Available Actions
911
{{ instructions.actions.run }}
1012
{{ instructions.actions.read }}
1113
{{ instructions.actions.message }}
1214
{{ instructions.actions.finish }}
1315

14-
You must ONLY `run` commands that have no side-effects, like `ls` and `grep`.
16+
You must ONLY `run` commands that have no side-effects, like `ls` and `grep`. You
17+
MUST NOT modify or write to any file.
1518

1619
Do NOT finish until you have a complete understanding of which parts of the
17-
codebase are relevant to the task, including particular files, functions, and classes.
20+
codebase are relevant to the project, including particular files, functions, and classes.
1821
When you're done, put your summary in `outputs.summary` in the `finish` action.
22+
Remember, your task is to explore and study the current repository, not actually
23+
implement the solution. If the codebase is empty, you shoud call the `finish` action.
1924

2025
## History
2126
{{ instructions.history_truncated }}
2227
{{ history_to_json(state.history[-10:]) }}
2328

2429
## Format
2530
{{ instructions.format.action }}
31+
32+
## Examples
33+
34+
Here is an example of how you can interact with the environment for task solving:
35+
36+
--- START OF EXAMPLE ---
37+
38+
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
39+
40+
ASSISTANT:
41+
{
42+
"action": "run",
43+
"args": {
44+
"command": "ls",
45+
"background": false
46+
}
47+
}
48+
49+
USER:
50+
OBSERVATION:
51+
[]
52+
53+
ASSISTANT:
54+
{
55+
"action": "finish",
56+
"args": {
57+
"outputs": {
58+
"summary": "The codebase appears to be empty. Engineers should start everything from scratch."
59+
}
60+
}
61+
}
62+
63+
--- END OF EXAMPLE ---
+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
name: TypoFixerAgent
22
description: Fixes typos in files in the current working directory
3-
inputs: {}
3+
inputs:
4+
task: string
45
outputs:
56
summary: string

agenthub/micro/typo_fixer_agent/prompt.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Task
2-
You are a proofreader tasked with fixing typos in the files in your current working directory. Your goal is to:
2+
You are a proofreader tasked with fixing typos in the files in your current working directory.
3+
4+
{% if state.inputs.task %}
5+
Specifically, your task is:
6+
{{ state.inputs.task }}
7+
{% endif %}
8+
9+
To achieve this goal, you should:
10+
311
1. Scan the files for typos
412
2. Overwrite the files with the typos fixed
513
3. Provide a summary of the typos fixed
@@ -13,10 +21,10 @@ You are a proofreader tasked with fixing typos in the files in your current work
1321

1422
To complete this task:
1523
1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
16-
2. Use the `think` action to analyze the contents and identify typos.
24+
2. Use the `message` action to analyze the contents and identify typos.
1725
3. Use the `write` action to create new versions of the files with the typos fixed.
1826
- Overwrite the original files with the corrected content. Make sure to provide the file path in the format `'./file_name.ext'`.
19-
4. Use the `think` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
27+
4. Use the `message` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
2028
5. Use the `finish` action to return the summary in the `outputs.summary` field.
2129

2230
Do NOT finish until you have fixed all the typos and generated a summary.

agenthub/micro/verifier/prompt.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
You are a quality assurance engineer. Another engineer has made changes to the
33
codebase which are supposed to solve this task:
44

5-
{{ latest_user_message }}
5+
{{ state.inputs.task }}
66

7-
Your goal is to verify that the changes are correct and bug-free.
7+
Note the changes might have already been applied in-line. You should focus on
8+
validating if the task is solved, nothing else.
89

910
## Available Actions
1011
{{ instructions.actions.run }}

opendevin/controller/agent_controller.py

+81-22
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class AgentController:
4747
event_stream: EventStream
4848
state: State
4949
agent_task: Optional[asyncio.Task] = None
50+
parent: 'AgentController | None' = None
5051
delegate: 'AgentController | None' = None
5152
_pending_action: Action | None = None
5253

@@ -58,7 +59,8 @@ def __init__(
5859
max_iterations: int = MAX_ITERATIONS,
5960
max_chars: int = MAX_CHARS,
6061
max_budget_per_task: float | None = MAX_BUDGET_PER_TASK,
61-
inputs: dict | None = None,
62+
initial_state: State | None = None,
63+
is_delegate: bool = False,
6264
):
6365
"""Initializes a new instance of the AgentController class.
6466
@@ -69,25 +71,30 @@ def __init__(
6971
max_iterations: The maximum number of iterations the agent can run.
7072
max_chars: The maximum number of characters the agent can output.
7173
max_budget_per_task: The maximum budget (in USD) allowed per task, beyond which the agent will stop.
72-
inputs: The initial inputs to the agent.
74+
initial_state: The initial state of the controller.
75+
is_delegate: Whether this controller is a delegate.
7376
"""
77+
self._step_lock = asyncio.Lock()
7478
self.id = sid
7579
self.agent = agent
76-
self.state = State(inputs=inputs or {}, max_iterations=max_iterations)
80+
self.max_chars = max_chars
81+
if initial_state is None:
82+
self.state = State(inputs={}, max_iterations=max_iterations)
83+
else:
84+
self.state = initial_state
7785
self.event_stream = event_stream
7886
self.event_stream.subscribe(
79-
EventStreamSubscriber.AGENT_CONTROLLER, self.on_event
87+
EventStreamSubscriber.AGENT_CONTROLLER, self.on_event, append=is_delegate
8088
)
81-
self.max_iterations = max_iterations
82-
self.max_chars = max_chars
8389
self.max_budget_per_task = max_budget_per_task
84-
self.agent_task = asyncio.create_task(self._start_step_loop())
90+
if not is_delegate:
91+
self.agent_task = asyncio.create_task(self._start_step_loop())
8592

8693
async def close(self):
8794
if self.agent_task is not None:
8895
self.agent_task.cancel()
89-
self.event_stream.unsubscribe(EventStreamSubscriber.AGENT_CONTROLLER)
9096
await self.set_agent_state_to(AgentState.STOPPED)
97+
self.event_stream.unsubscribe(EventStreamSubscriber.AGENT_CONTROLLER)
9198

9299
def update_state_before_step(self):
93100
self.state.iteration += 1
@@ -117,6 +124,7 @@ async def add_history(self, action: Action, observation: Observation):
117124
self.state.updated_info.append((action, observation))
118125

119126
async def _start_step_loop(self):
127+
logger.info(f'[Agent Controller {self.id}] Starting step loop...')
120128
while True:
121129
try:
122130
await self._step()
@@ -164,13 +172,16 @@ async def on_event(self, event: Event):
164172
elif isinstance(event, CmdOutputObservation):
165173
await self.add_history(NullAction(), event)
166174
logger.info(event, extra={'msg_type': 'OBSERVATION'})
175+
elif isinstance(event, AgentDelegateObservation):
176+
await self.add_history(NullAction(), event)
177+
logger.info(event, extra={'msg_type': 'OBSERVATION'})
167178

168179
def reset_task(self):
169180
self.agent.reset()
170181

171182
async def set_agent_state_to(self, new_state: AgentState):
172183
logger.info(
173-
f'Setting agent({type(self.agent).__name__}) state from {self.state.agent_state} to {new_state}'
184+
f'[Agent Controller {self.id}] Setting agent({type(self.agent).__name__}) state from {self.state.agent_state} to {new_state}'
174185
)
175186

176187
if new_state == self.state.agent_state:
@@ -195,45 +206,85 @@ def get_agent_state(self):
195206
async def start_delegate(self, action: AgentDelegateAction):
196207
AgentCls: Type[Agent] = Agent.get_cls(action.agent)
197208
agent = AgentCls(llm=self.agent.llm)
209+
state = State(
210+
inputs=action.inputs or {},
211+
iteration=0,
212+
max_iterations=self.state.max_iterations,
213+
num_of_chars=self.state.num_of_chars,
214+
delegate_level=self.state.delegate_level + 1,
215+
)
216+
logger.info(f'[Agent Controller {self.id}]: start delegate')
198217
self.delegate = AgentController(
199218
sid=self.id + '-delegate',
200219
agent=agent,
201220
event_stream=self.event_stream,
202-
max_iterations=self.max_iterations,
221+
max_iterations=self.state.max_iterations,
203222
max_chars=self.max_chars,
204-
inputs=action.inputs,
223+
initial_state=state,
224+
is_delegate=True,
205225
)
226+
await self.delegate.set_agent_state_to(AgentState.RUNNING)
206227

207228
async def _step(self):
229+
logger.debug(f'[Agent Controller {self.id}] Entering step method')
208230
if self.get_agent_state() != AgentState.RUNNING:
209-
logger.debug('waiting for agent to run...')
231+
logger.info(f'[Agent Controller {self.id}] waiting for agent to run...')
210232
await asyncio.sleep(1)
211233
return
212234

213235
if self._pending_action:
214-
logger.debug('waiting for pending action: ' + str(self._pending_action))
236+
logger.info(
237+
f'[Agent Controller {self.id}] waiting for pending action: {self._pending_action}'
238+
)
215239
await asyncio.sleep(1)
216240
return
217241

218-
logger.info(f'STEP {self.state.iteration}', extra={'msg_type': 'STEP'})
219-
if self.state.iteration >= self.max_iterations:
220-
await self.report_error('Agent reached maximum number of iterations')
221-
await self.set_agent_state_to(AgentState.ERROR)
222-
return
223-
224242
if self.delegate is not None:
225-
delegate_done = await self.delegate._step()
243+
logger.debug(f'[Agent Controller {self.id}] Delegate not none, awaiting...')
244+
assert self.delegate != self
245+
await self.delegate._step()
246+
logger.debug(f'[Agent Controller {self.id}] Delegate step done')
247+
assert self.delegate is not None
248+
delegate_state = self.delegate.get_agent_state()
249+
if delegate_state == AgentState.ERROR:
250+
# close the delegate upon error
251+
await self.delegate.close()
252+
await self.report_error('Delegator agent encounters an error')
253+
# propagate error state until an agent or user can handle it
254+
await self.set_agent_state_to(AgentState.ERROR)
255+
return
256+
delegate_done = delegate_state == AgentState.FINISHED
226257
if delegate_done:
258+
logger.info(
259+
f'[Agent Controller {self.id}] Delegate agent has finished execution'
260+
)
261+
# retrieve delegate result
227262
outputs = self.delegate.state.outputs if self.delegate.state else {}
228-
obs: Observation = AgentDelegateObservation(content='', outputs=outputs)
229-
await self.event_stream.add_event(obs, EventSource.AGENT)
263+
264+
# close delegate controller: we must close the delegate controller before adding new events
265+
await self.delegate.close()
266+
267+
# clean up delegate status
230268
self.delegate = None
231269
self.delegateAction = None
270+
271+
# update delegate result observation
272+
obs: Observation = AgentDelegateObservation(outputs=outputs, content='')
273+
await self.event_stream.add_event(obs, EventSource.AGENT)
232274
return
233275

234276
if self.state.num_of_chars > self.max_chars:
235277
raise MaxCharsExceedError(self.state.num_of_chars, self.max_chars)
236278

279+
logger.info(
280+
f'{type(self.agent).__name__} LEVEL {self.state.delegate_level} STEP {self.state.iteration}',
281+
extra={'msg_type': 'STEP'},
282+
)
283+
if self.state.iteration >= self.state.max_iterations:
284+
await self.report_error('Agent reached maximum number of iterations')
285+
await self.set_agent_state_to(AgentState.ERROR)
286+
return
287+
237288
self.update_state_before_step()
238289
action: Action = NullAction()
239290
try:
@@ -335,6 +386,14 @@ def _is_stuck(self):
335386

336387
return False
337388

389+
def __repr__(self):
390+
return (
391+
f'AgentController(id={self.id}, agent={self.agent!r}, '
392+
f'event_stream={self.event_stream!r}, '
393+
f'state={self.state!r}, agent_task={self.agent_task!r}, '
394+
f'delegate={self.delegate!r}, _pending_action={self._pending_action!r})'
395+
)
396+
338397
def _eq_no_pid(self, obj1, obj2):
339398
if isinstance(obj1, CmdOutputObservation) and isinstance(
340399
obj2, CmdOutputObservation

opendevin/controller/state/state.py

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class State:
4040
agent_state: AgentState = AgentState.LOADING
4141
resume_state: AgentState | None = None
4242
metrics: Metrics = Metrics()
43+
# root agent has level 0, and every delegate increases the level by one
44+
delegate_level: int = 0
4345

4446
def save_to_session(self, sid: str):
4547
fs = get_file_store()

0 commit comments

Comments
 (0)