瀏覽代碼

Traffic Control: Add new config MAX_CHARS (#1015)

* Add new config MAX_CHARS

* Fix mypy linting issues
Boxuan Li 1 年之前
父節點
當前提交
e0c7492609

+ 28 - 21
agenthub/codeact_agent/codeact_agent.py

@@ -24,7 +24,7 @@ Apart from the standard bash commands, you can also use the following special co
 {COMMAND_DOCS}
 """
     if COMMAND_DOCS is not None
-    else ""
+    else ''
 )
 SYSTEM_MESSAGE = f"""You are a helpful assistant. You will be provided access (as root) to a bash shell to complete user-provided tasks.
 You will be able to execute commands in the bash shell, interact with the file system, install packages, and receive the output of your commands.
@@ -46,27 +46,29 @@ print(math.pi)" > math.py
 {COMMAND_SEGMENT}
 
 When you are done, execute the following to close the shell and end the conversation:
-<execute>exit</execute> 
+<execute>exit</execute>
 """
 
 INVALID_INPUT_MESSAGE = (
     "I don't understand your input. \n"
-    "If you want to execute command, please use <execute> YOUR_COMMAND_HERE </execute>.\n"
-    "If you already completed the task, please exit the shell by generating: <execute> exit </execute>."
+    'If you want to execute command, please use <execute> YOUR_COMMAND_HERE </execute>.\n'
+    'If you already completed the task, please exit the shell by generating: <execute> exit </execute>.'
 )
 
+
 def parse_response(response) -> str:
     action = response.choices[0].message.content
-    if "<execute>" in action and "</execute>" not in action:
-        action += "</execute>"
+    if '<execute>' in action and '</execute>' not in action:
+        action += '</execute>'
     return action
 
+
 class CodeActAgent(Agent):
     """
-    The Code Act Agent is a minimalist agent. 
+    The Code Act Agent is a minimalist agent.
     The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
     """
-    
+
     def __init__(
         self,
         llm: LLM,
@@ -82,7 +84,7 @@ class CodeActAgent(Agent):
 
     def step(self, state: State) -> Action:
         """
-        Performs one step using the Code Act Agent. 
+        Performs one step using the Code Act Agent.
         This includes gathering info on previous steps and prompting the model to make a command to execute.
 
         Parameters:
@@ -97,42 +99,47 @@ class CodeActAgent(Agent):
         """
 
         if len(self.messages) == 0:
-            assert state.plan.main_goal, "Expecting instruction to be set"
+            assert state.plan.main_goal, 'Expecting instruction to be set'
             self.messages = [
-                {"role": "system", "content": SYSTEM_MESSAGE},
-                {"role": "user", "content": state.plan.main_goal},
+                {'role': 'system', 'content': SYSTEM_MESSAGE},
+                {'role': 'user', 'content': state.plan.main_goal},
             ]
         updated_info = state.updated_info
         if updated_info:
             for prev_action, obs in updated_info:
                 assert isinstance(
                     prev_action, (CmdRunAction, AgentEchoAction)
-                ), "Expecting CmdRunAction or AgentEchoAction for Action"
+                ), 'Expecting CmdRunAction or AgentEchoAction for Action'
                 if isinstance(
                     obs, AgentMessageObservation
                 ):  # warning message from itself
-                    self.messages.append({"role": "user", "content": obs.content})
+                    self.messages.append(
+                        {'role': 'user', 'content': obs.content})
                 elif isinstance(obs, CmdOutputObservation):
-                    content = "OBSERVATION:\n" + obs.content
+                    content = 'OBSERVATION:\n' + obs.content
+                    # FIXME: autopep8 and mypy are fighting each other on this line
+                    # autopep8: off
                     content += f"\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]"
-                    self.messages.append({"role": "user", "content": content})
+                    self.messages.append({'role': 'user', 'content': content})
                 else:
                     raise NotImplementedError(
                         f"Unknown observation type: {obs.__class__}"
                     )
         response = self.llm.completion(
             messages=self.messages,
-            stop=["</execute>"],
+            stop=['</execute>'],
             temperature=0.0
         )
         action_str: str = parse_response(response)
-        self.messages.append({"role": "assistant", "content": action_str})
+        state.num_of_chars += sum(len(message['content'])
+                                  for message in self.messages) + len(action_str)
+        self.messages.append({'role': 'assistant', 'content': action_str})
 
-        command = re.search(r"<execute>(.*)</execute>", action_str, re.DOTALL)
+        command = re.search(r'<execute>(.*)</execute>', action_str, re.DOTALL)
         if command is not None:
             # a command was found
             command_group = command.group(1)
-            if command_group.strip() == "exit":
+            if command_group.strip() == 'exit':
                 return AgentFinishAction()
             return CmdRunAction(command=command_group)
             # # execute the code
@@ -149,4 +156,4 @@ class CodeActAgent(Agent):
             )  # warning message to itself
 
     def search_memory(self, query: str) -> List[str]:
-        raise NotImplementedError("Implement this abstract method")
+        raise NotImplementedError('Implement this abstract method')

+ 58 - 56
agenthub/monologue_agent/agent.py

@@ -32,46 +32,46 @@ MAX_MONOLOGUE_LENGTH = 20000
 MAX_OUTPUT_LENGTH = 5000
 
 INITIAL_THOUGHTS = [
-    "I exist!",
-    "Hmm...looks like I can type in a command line prompt",
-    "Looks like I have a web browser too!",
+    'I exist!',
+    'Hmm...looks like I can type in a command line prompt',
+    'Looks like I have a web browser too!',
     "Here's what I want to do: $TASK",
-    "How am I going to get there though?",
-    "It seems like I have some kind of short term memory.",
-    "Each of my thoughts seems to be stored in a JSON array.",
-    "It seems whatever I say next will be added as an object to the list.",
-    "But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.",
-    "Fortunately I have long term memory!",
-    "I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!",
+    'How am I going to get there though?',
+    'It seems like I have some kind of short term memory.',
+    'Each of my thoughts seems to be stored in a JSON array.',
+    'It seems whatever I say next will be added as an object to the list.',
+    'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
+    'Fortunately I have long term memory!',
+    'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
     "Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
     "Let's try it out!",
-    "RECALL what it is I want to do",
+    'RECALL what it is I want to do',
     "Here's what I want to do: $TASK",
-    "How am I going to get there though?",
+    'How am I going to get there though?',
     "Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
     'RUN echo "hello world"',
-    "hello world",
-    "Cool! I bet I can write files too using the write action.",
+    'hello world',
+    'Cool! I bet I can write files too using the write action.',
     "WRITE echo \"console.log('hello world')\" > test.js",
-    "",
+    '',
     "I just created test.js. I'll try and run it now.",
-    "RUN node test.js",
-    "hello world",
-    "It works!",
+    'RUN node test.js',
+    'hello world',
+    'It works!',
     "I'm going to try reading it now using the read action.",
-    "READ test.js",
+    'READ test.js',
     "console.log('hello world')",
-    "Nice! I can read files too!",
-    "And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument",
+    'Nice! I can read files too!',
+    'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
     "Let's try that...",
-    "BROWSE google.com",
+    'BROWSE google.com',
     '<form><input type="text"></input><button type="submit"></button></form>',
-    "I can browse the web too!",
-    "And once I have completed my task, I can use the finish action to stop working.",
+    'I can browse the web too!',
+    'And once I have completed my task, I can use the finish action to stop working.',
     "But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
-    "Very cool. Now to accomplish my task.",
+    'Very cool. Now to accomplish my task.',
     "I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
-    "In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.",
+    'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
     "OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
     "It seems like there might be an existing project here. I should probably start by running `ls` to see what's here.",
 ]
@@ -106,15 +106,15 @@ class MonologueAgent(Agent):
         - event (dict): The event that will be added to monologue and memory
         """
 
-        if "extras" in event and "screenshot" in event["extras"]:
-            del event["extras"]["screenshot"]
+        if 'extras' in event and 'screenshot' in event['extras']:
+            del event['extras']['screenshot']
         if (
-            "args" in event
-            and "output" in event["args"]
-            and len(event["args"]["output"]) > MAX_OUTPUT_LENGTH
+            'args' in event
+            and 'output' in event['args']
+            and len(event['args']['output']) > MAX_OUTPUT_LENGTH
         ):
-            event["args"]["output"] = (
-                event["args"]["output"][:MAX_OUTPUT_LENGTH] + "..."
+            event['args']['output'] = (
+                event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
             )
 
         self.monologue.add_event(event)
@@ -137,51 +137,52 @@ class MonologueAgent(Agent):
         if self._initialized:
             return
 
-        if task is None or task == "":
-            raise ValueError("Instruction must be provided")
+        if task is None or task == '':
+            raise ValueError('Instruction must be provided')
         self.monologue = Monologue()
         self.memory = LongTermMemory()
 
-        output_type = ""
+        output_type = ''
         for thought in INITIAL_THOUGHTS:
-            thought = thought.replace("$TASK", task)
-            if output_type != "":
-                observation: Observation = NullObservation(content="")
+            thought = thought.replace('$TASK', task)
+            if output_type != '':
+                observation: Observation = NullObservation(content='')
                 if output_type == ObservationType.RUN:
                     observation = CmdOutputObservation(
-                        content=thought, command_id=0, command=""
+                        content=thought, command_id=0, command=''
                     )
                 elif output_type == ObservationType.READ:
-                    observation = FileReadObservation(content=thought, path="")
+                    observation = FileReadObservation(content=thought, path='')
                 elif output_type == ObservationType.RECALL:
-                    observation = AgentRecallObservation(content=thought, memories=[])
+                    observation = AgentRecallObservation(
+                        content=thought, memories=[])
                 elif output_type == ObservationType.BROWSE:
                     observation = BrowserOutputObservation(
-                        content=thought, url="", screenshot=""
+                        content=thought, url='', screenshot=''
                     )
                 self._add_event(observation.to_dict())
-                output_type = ""
+                output_type = ''
             else:
                 action: Action = NullAction()
-                if thought.startswith("RUN"):
-                    command = thought.split("RUN ")[1]
+                if thought.startswith('RUN'):
+                    command = thought.split('RUN ')[1]
                     action = CmdRunAction(command)
                     output_type = ActionType.RUN
-                elif thought.startswith("WRITE"):
-                    parts = thought.split("WRITE ")[1].split(" > ")
+                elif thought.startswith('WRITE'):
+                    parts = thought.split('WRITE ')[1].split(' > ')
                     path = parts[1]
                     content = parts[0]
                     action = FileWriteAction(path=path, content=content)
-                elif thought.startswith("READ"):
-                    path = thought.split("READ ")[1]
+                elif thought.startswith('READ'):
+                    path = thought.split('READ ')[1]
                     action = FileReadAction(path=path)
                     output_type = ActionType.READ
-                elif thought.startswith("RECALL"):
-                    query = thought.split("RECALL ")[1]
+                elif thought.startswith('RECALL'):
+                    query = thought.split('RECALL ')[1]
                     action = AgentRecallAction(query=query)
                     output_type = ActionType.RECALL
-                elif thought.startswith("BROWSE"):
-                    url = thought.split("BROWSE ")[1]
+                elif thought.startswith('BROWSE'):
+                    url = thought.split('BROWSE ')[1]
                     action = BrowseURLAction(url=url)
                     output_type = ActionType.BROWSE
                 else:
@@ -211,9 +212,10 @@ class MonologueAgent(Agent):
             self.monologue.get_thoughts(),
             state.background_commands_obs,
         )
-        messages = [{"content": prompt, "role": "user"}]
+        messages = [{'content': prompt, 'role': 'user'}]
         resp = self.llm.completion(messages=messages)
-        action_resp = resp["choices"][0]["message"]["content"]
+        action_resp = resp['choices'][0]['message']['content']
+        state.num_of_chars += len(prompt) + len(action_resp)
         action = prompts.parse_action_response(action_resp)
         self.latest_action = action
         return action

+ 4 - 3
agenthub/planner_agent/agent.py

@@ -7,6 +7,7 @@ from opendevin.llm.llm import LLM
 from opendevin.state import State
 from opendevin.action import Action
 
+
 class PlannerAgent(Agent):
     """
     The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
@@ -24,7 +25,7 @@ class PlannerAgent(Agent):
 
     def step(self, state: State) -> Action:
         """
-        Checks to see if current step is completed, returns AgentFinishAction if True. 
+        Checks to see if current step is completed, returns AgentFinishAction if True.
         Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
 
         Parameters:
@@ -38,12 +39,12 @@ class PlannerAgent(Agent):
         if state.plan.task.state in ['completed', 'verified', 'abandoned']:
             return AgentFinishAction()
         prompt = get_prompt(state.plan, state.history)
-        messages = [{"content": prompt, "role": "user"}]
+        messages = [{'content': prompt, 'role': 'user'}]
         resp = self.llm.completion(messages=messages)
         action_resp = resp['choices'][0]['message']['content']
+        state.num_of_chars += len(prompt) + len(action_resp)
         action = parse_response(action_resp)
         return action
 
     def search_memory(self, query: str) -> List[str]:
         return []
-

+ 1 - 0
frontend/src/types/ConfigType.tsx

@@ -10,6 +10,7 @@ enum ArgConfigType {
   LLM_COOLDOWN_TIME = "LLM_COOLDOWN_TIME",
   DIRECTORY_REWRITE = "DIRECTORY_REWRITE",
   MAX_ITERATIONS = "MAX_ITERATIONS",
+  MAX_CHARS = "MAX_CHARS",
   AGENT = "AGENT",
 
   LANGUAGE = "LANGUAGE",

+ 5 - 1
opendevin/config.py

@@ -22,6 +22,10 @@ DEFAULT_CONFIG: dict = {
     ConfigType.LLM_COOLDOWN_TIME: 1,
     ConfigType.DIRECTORY_REWRITE: '',
     ConfigType.MAX_ITERATIONS: 100,
+    # GPT-4 pricing is $10 per 1M input tokens. Since tokenization happens on LLM side,
+    # we cannot easily count number of tokens, but we can count characters.
+    # Assuming 5 characters per token, 5 million is a reasonable default limit.
+    ConfigType.MAX_CHARS: 5_000_000,
     ConfigType.AGENT: 'MonologueAgent',
     ConfigType.SANDBOX_TYPE: 'ssh',
     ConfigType.DISABLE_COLOR: 'false',
@@ -47,7 +51,7 @@ def get(key: str, required: bool = False):
     """
     value = os.environ.get(key)
     if not value:
-      value = config.get(key)
+        value = config.get(key)
     if not value and required:
         raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.")
     return value

+ 56 - 46
opendevin/controller/agent_controller.py

@@ -15,45 +15,47 @@ from opendevin.action import (
 )
 from opendevin.agent import Agent
 from opendevin.logger import opendevin_logger as logger
+from opendevin.exceptions import MaxCharsExceedError
 from opendevin.observation import Observation, AgentErrorObservation, NullObservation
 from opendevin.plan import Plan
 from opendevin.state import State
 from .command_manager import CommandManager
 
 ColorType = Literal[
-    "red",
-    "green",
-    "yellow",
-    "blue",
-    "magenta",
-    "cyan",
-    "light_grey",
-    "dark_grey",
-    "light_red",
-    "light_green",
-    "light_yellow",
-    "light_blue",
-    "light_magenta",
-    "light_cyan",
-    "white",
+    'red',
+    'green',
+    'yellow',
+    'blue',
+    'magenta',
+    'cyan',
+    'light_grey',
+    'dark_grey',
+    'light_red',
+    'light_green',
+    'light_yellow',
+    'light_blue',
+    'light_magenta',
+    'light_cyan',
+    'white',
 ]
 
 DISABLE_COLOR_PRINTING = (
     config.get('DISABLE_COLOR').lower() == 'true'
 )
-MAX_ITERATIONS = config.get("MAX_ITERATIONS")
+MAX_ITERATIONS = config.get('MAX_ITERATIONS')
+MAX_CHARS = config.get('MAX_CHARS')
 
 
-def print_with_color(text: Any, print_type: str = "INFO"):
+def print_with_color(text: Any, print_type: str = 'INFO'):
     TYPE_TO_COLOR: Mapping[str, ColorType] = {
-        "BACKGROUND LOG": "blue",
-        "ACTION": "green",
-        "OBSERVATION": "yellow",
-        "INFO": "cyan",
-        "ERROR": "red",
-        "PLAN": "light_magenta",
+        'BACKGROUND LOG': 'blue',
+        'ACTION': 'green',
+        'OBSERVATION': 'yellow',
+        'INFO': 'cyan',
+        'ERROR': 'red',
+        'PLAN': 'light_magenta',
     }
-    color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR["INFO"])
+    color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR['INFO'])
     if DISABLE_COLOR_PRINTING:
         print(f'\n{print_type.upper()}:\n{str(text)}', flush=True)
     else:
@@ -76,16 +78,19 @@ class AgentController:
         self,
         agent: Agent,
         workdir: str,
-        sid: str = "",
+        sid: str = '',
         max_iterations: int = MAX_ITERATIONS,
+        max_chars: int = MAX_CHARS,
         container_image: str | None = None,
         callbacks: List[Callable] = [],
     ):
         self.id = sid
         self.agent = agent
         self.max_iterations = max_iterations
+        self.max_chars = max_chars
         self.workdir = workdir
-        self.command_manager = CommandManager(self.id, workdir, container_image)
+        self.command_manager = CommandManager(
+            self.id, workdir, container_image)
         self.callbacks = callbacks
 
     def update_state_for_step(self, i):
@@ -97,9 +102,9 @@ class AgentController:
 
     def add_history(self, action: Action, observation: Observation):
         if not isinstance(action, Action):
-            raise ValueError("action must be an instance of Action")
+            raise ValueError('action must be an instance of Action')
         if not isinstance(observation, Observation):
-            raise ValueError("observation must be an instance of Observation")
+            raise ValueError('observation must be an instance of Observation')
         self.state.history.append((action, observation))
         self.state.updated_info.append((action, observation))
 
@@ -111,40 +116,44 @@ class AgentController:
             try:
                 finished = await self.step(i)
             except Exception as e:
-                logger.error("Error in loop", exc_info=True)
+                logger.error('Error in loop', exc_info=True)
                 raise e
             if finished:
                 break
         if not finished:
-            logger.info("Exited before finishing the task.")
+            logger.info('Exited before finishing the task.')
 
     async def step(self, i: int):
-        print("\n\n==============", flush=True)
-        print("STEP", i, flush=True)
-        print_with_color(self.state.plan.main_goal, "PLAN")
+        print('\n\n==============', flush=True)
+        print('STEP', i, flush=True)
+        print_with_color(self.state.plan.main_goal, 'PLAN')
+
+        if self.state.num_of_chars > self.max_chars:
+            raise MaxCharsExceedError(
+                self.state.num_of_chars, self.max_chars)
 
         log_obs = self.command_manager.get_background_obs()
         for obs in log_obs:
             self.add_history(NullAction(), obs)
             await self._run_callbacks(obs)
-            print_with_color(obs, "BACKGROUND LOG")
+            print_with_color(obs, 'BACKGROUND LOG')
 
         self.update_state_for_step(i)
         action: Action = NullAction()
-        observation: Observation = NullObservation("")
+        observation: Observation = NullObservation('')
         try:
             action = self.agent.step(self.state)
             if action is None:
-                raise ValueError("Agent must return an action")
-            print_with_color(action, "ACTION")
+                raise ValueError('Agent must return an action')
+            print_with_color(action, 'ACTION')
         except Exception as e:
             observation = AgentErrorObservation(str(e))
-            print_with_color(observation, "ERROR")
+            print_with_color(observation, 'ERROR')
             traceback.print_exc()
             # TODO Change to more robust error handling
             if (
-                "The api_key client option must be set" in observation.content
-                or "Incorrect API key provided:" in observation.content
+                'The api_key client option must be set' in observation.content
+                or 'Incorrect API key provided:' in observation.content
             ):
                 raise
         self.update_state_after_step()
@@ -153,22 +162,23 @@ class AgentController:
 
         finished = isinstance(action, AgentFinishAction)
         if finished:
-            print_with_color(action, "INFO")
+            print_with_color(action, 'INFO')
             return True
 
         if isinstance(action, AddTaskAction):
             try:
-                self.state.plan.add_subtask(action.parent, action.goal, action.subtasks)
+                self.state.plan.add_subtask(
+                    action.parent, action.goal, action.subtasks)
             except Exception as e:
                 observation = AgentErrorObservation(str(e))
-                print_with_color(observation, "ERROR")
+                print_with_color(observation, 'ERROR')
                 traceback.print_exc()
         elif isinstance(action, ModifyTaskAction):
             try:
                 self.state.plan.set_subtask_state(action.id, action.state)
             except Exception as e:
                 observation = AgentErrorObservation(str(e))
-                print_with_color(observation, "ERROR")
+                print_with_color(observation, 'ERROR')
                 traceback.print_exc()
 
         if action.executable:
@@ -179,11 +189,11 @@ class AgentController:
                     observation = action.run(self)
             except Exception as e:
                 observation = AgentErrorObservation(str(e))
-                print_with_color(observation, "ERROR")
+                print_with_color(observation, 'ERROR')
                 traceback.print_exc()
 
         if not isinstance(observation, NullObservation):
-            print_with_color(observation, "OBSERVATION")
+            print_with_color(observation, 'OBSERVATION')
 
         self.add_history(action, observation)
         await self._run_callbacks(observation)

+ 9 - 0
opendevin/exceptions.py

@@ -0,0 +1,9 @@
+class MaxCharsExceedError(Exception):
+    def __init__(self, num_of_chars=None, max_chars_limit=None):
+        if num_of_chars is not None and max_chars_limit is not None:
+            # FIXME: autopep8 and mypy are fighting each other on this line
+            # autopep8: off
+            message = f"Number of characters {num_of_chars} exceeds MAX_CHARS limit: {max_chars_limit}"
+        else:
+            message = 'Number of characters exceeds MAX_CHARS limit'
+        super().__init__(message)

+ 13 - 3
opendevin/main.py

@@ -5,6 +5,7 @@ from typing import Type
 
 import agenthub  # noqa F401 (we import this to get the agents registered)
 from opendevin import config
+from opendevin.schema import ConfigType
 from opendevin.agent import Agent
 from opendevin.controller import AgentController
 from opendevin.llm.llm import LLM
@@ -51,17 +52,24 @@ def parse_arguments():
     parser.add_argument(
         '-m',
         '--model-name',
-        default=config.get('LLM_MODEL'),
+        default=config.get(ConfigType.LLM_MODEL),
         type=str,
         help='The (litellm) model name to use',
     )
     parser.add_argument(
         '-i',
         '--max-iterations',
-        default=100,
+        default=config.get(ConfigType.MAX_ITERATIONS),
         type=int,
         help='The maximum number of iterations to run the agent',
     )
+    parser.add_argument(
+        '-n',
+        '--max-chars',
+        default=config.get(ConfigType.MAX_CHARS),
+        type=int,
+        help='The maximum number of characters to send to and receive from LLM per task',
+    )
     return parser.parse_args()
 
 
@@ -81,6 +89,8 @@ async def main():
         raise ValueError(
             'No task provided. Please specify a task through -t, -f.')
 
+    # FIXME: autopep8 and mypy are fighting each other on this line
+    # autopep8: off
     print(
         f'Running agent {args.agent_cls} (model: {args.model_name}, directory: {args.directory}) with task: "{task}"'
     )
@@ -88,7 +98,7 @@ async def main():
     AgentCls: Type[Agent] = Agent.get_cls(args.agent_cls)
     agent = AgentCls(llm=llm)
     controller = AgentController(
-        agent=agent, workdir=args.directory, max_iterations=args.max_iterations
+        agent=agent, workdir=args.directory, max_iterations=args.max_iterations, max_chars=args.max_chars
     )
 
     await controller.start_loop(task)

+ 1 - 0
opendevin/schema/config.py

@@ -15,6 +15,7 @@ class ConfigType(str, Enum):
     LLM_COOLDOWN_TIME = 'LLM_COOLDOWN_TIME'
     DIRECTORY_REWRITE = 'DIRECTORY_REWRITE'
     MAX_ITERATIONS = 'MAX_ITERATIONS'
+    MAX_CHARS = 'MAX_CHARS'
     AGENT = 'AGENT'
     SANDBOX_TYPE = 'SANDBOX_TYPE'
     DISABLE_COLOR = 'DISABLE_COLOR'

+ 2 - 0
opendevin/server/agent/agent.py

@@ -113,6 +113,7 @@ class AgentUnit:
         container_image = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE)
         max_iterations = self.get_arg_or_default(
             args, ConfigType.MAX_ITERATIONS)
+        max_chars = self.get_arg_or_default(args, ConfigType.MAX_CHARS)
 
         if not os.path.exists(directory):
             logger.info(
@@ -127,6 +128,7 @@ class AgentUnit:
                 agent=Agent.get_cls(agent_cls)(llm),
                 workdir=directory,
                 max_iterations=int(max_iterations),
+                max_chars=int(max_chars),
                 container_image=container_image,
                 callbacks=[self.on_agent_event],
             )

+ 7 - 2
opendevin/state.py

@@ -11,10 +11,15 @@ from opendevin.observation import (
     CmdOutputObservation,
 )
 
+
 @dataclass
 class State:
     plan: Plan
     iteration: int = 0
-    background_commands_obs: List[CmdOutputObservation] = field(default_factory=list)
+    # number of characters we have sent to and received from LLM so far for current task
+    num_of_chars: int = 0
+    background_commands_obs: List[CmdOutputObservation] = field(
+        default_factory=list)
     history: List[Tuple[Action, Observation]] = field(default_factory=list)
-    updated_info: List[Tuple[Action, Observation]] = field(default_factory=list)
+    updated_info: List[Tuple[Action, Observation]
+                       ] = field(default_factory=list)