diff --git a/.gitignore b/.gitignore index 3129a866..3ece8cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist/ workspace/ pilot-env/ venv/ +data/ .coverage *.code-workspace diff --git a/core/agents/spec_writer.py b/core/agents/spec_writer.py index c34725b2..2ca25c0d 100644 --- a/core/agents/spec_writer.py +++ b/core/agents/spec_writer.py @@ -183,6 +183,7 @@ class SpecWriter(BaseAgent): ) if user_done_with_description.button == "yes": + await self.ui.send_project_stage({"stage": ProjectStage.SPECS_FINISHED}) break elif user_done_with_description.button == "no": await self.send_message("## What would you like to add?") diff --git a/core/agents/tech_lead.py b/core/agents/tech_lead.py index f5d4f1cc..7b84964d 100644 --- a/core/agents/tech_lead.py +++ b/core/agents/tech_lead.py @@ -21,6 +21,7 @@ from core.log import get_logger from core.telemetry import telemetry from core.templates.registry import PROJECT_TEMPLATES from core.ui.base import ProjectStage, pythagora_source, success_source +from core.utils.text import trim_logs log = get_logger(__name__) @@ -224,8 +225,17 @@ class TechLead(RelevantFilesMixin, BaseAgent): # load the previous state, because in this state we have deleted tasks due to epic being completed! wanted_project_state = await self.state_manager.get_project_state_by_id(self.current_state.prev_state_id) + wanted_project_state.epics[-1]["completed"] = False self.next_state.epics = wanted_project_state.epics + # Trim logs from existing tasks before adding the new task + if wanted_project_state.tasks: + # Trim logs from all existing tasks + for task in wanted_project_state.tasks: + if task.get("description"): + task["description"] = trim_logs(task["description"]) + + # Create tasks list with new task (after trimming logs from existing tasks) self.next_state.tasks = wanted_project_state.tasks + [ { "id": uuid4().hex, @@ -238,8 +248,12 @@ class TechLead(RelevantFilesMixin, BaseAgent): } ] + # Flag tasks as modified so SQLAlchemy knows to save the changes + self.next_state.flag_epics_as_modified() + self.next_state.flag_tasks_as_modified() + await self.ui.send_epics_and_tasks( - self.next_state.current_epic.get("sub_epics", []), + self.next_state.epics[-1].get("sub_epics", []), self.next_state.tasks, ) diff --git a/core/cli/helpers.py b/core/cli/helpers.py index 7fa9e867..81b02a79 100644 --- a/core/cli/helpers.py +++ b/core/cli/helpers.py @@ -45,6 +45,7 @@ from core.ui.base import AgentSource, UIBase, UISource from core.ui.console import PlainConsoleUI from core.ui.ipc_client import IPCClientUI from core.ui.virtual import VirtualUI +from core.utils.text import trim_logs log = get_logger(__name__) @@ -433,36 +434,6 @@ def get_epic_task_number(state, current_task) -> (int, int): return epic_num, task_num -def trim_logs(logs: str) -> str: - """ - Trim logs by removing everything after specific marker phrases. - - This function cuts off the string at the first occurrence of - "Here are the backend logs" or "Here are the frontend logs". - - :param logs: Log text to trim - :return: Trimmed log text with the marker phrase removed - """ - if not logs: - return "" - - # Define marker phrases - markers = ["Here are the backend logs", "Here are the frontend logs"] - - # Find the first occurrence of any marker - index = float("inf") - for marker in markers: - pos = logs.find(marker) - if pos != -1 and pos < index: - index = pos - - # If a marker was found, trim the string - if index != float("inf"): - return logs[:index] - - return logs - - def get_source_for_history(msg_type: Optional[str] = "", question: Optional[str] = ""): if question in [TL_EDIT_DEV_PLAN]: return AgentSource("Tech Lead", "tech-lead") diff --git a/core/db/models/project_state.py b/core/db/models/project_state.py index f41af173..d1e341ae 100644 --- a/core/db/models/project_state.py +++ b/core/db/models/project_state.py @@ -951,7 +951,6 @@ class ProjectState(Base): if task.get("status") == TaskStatus.SKIPPED else "Done", ] - log.debug(task_histories) last_task = {} diff --git a/core/llm/base.py b/core/llm/base.py index 791c70cf..ddf92803 100644 --- a/core/llm/base.py +++ b/core/llm/base.py @@ -10,13 +10,13 @@ import httpx import tiktoken from httpx import AsyncClient -from core.cli.helpers import trim_logs from core.config import LLMConfig, LLMProvider from core.llm.convo import Convo from core.llm.request_log import LLMRequestLog, LLMRequestStatus from core.log import get_logger from core.state.state_manager import StateManager from core.ui.base import UIBase, pythagora_source +from core.utils.text import trim_logs log = get_logger(__name__) tokenizer = tiktoken.get_encoding("cl100k_base") diff --git a/core/state/state_manager.py b/core/state/state_manager.py index f1d246c9..e19427cf 100644 --- a/core/state/state_manager.py +++ b/core/state/state_manager.py @@ -36,6 +36,7 @@ from core.proc.exec_log import ExecLog as ExecLogData from core.telemetry import telemetry from core.ui.base import UIBase from core.ui.base import UserInput as UserInputData +from core.utils.text import trim_logs if TYPE_CHECKING: from core.agents.base import BaseAgent @@ -345,6 +346,27 @@ class StateManager: await state.delete_after() await session.commit() + # TODO: this is a temporary fix to unblock users! + # TODO: REMOVE THIS AFTER 1 WEEK FROM THIS COMMIT + # Process tasks before setting current state - trim logs from task descriptions before current task + if state.tasks and state.current_task: + try: + # Find the current task index + current_task_index = state.tasks.index(state.current_task) + + # Trim logs from all tasks before the current task + for i in range(current_task_index): + task = state.tasks[i] + if task.get("description"): + task["description"] = trim_logs(task["description"]) + + # Flag tasks as modified so SQLAlchemy knows to save the changes + state.flag_tasks_as_modified() + except Exception as e: + # Handle any error during log trimming gracefully + log.warning(f"Error during log trimming: {e}, skipping log trimming") + pass + self.current_session = session self.current_state = state self.branch = state.branch diff --git a/core/utils/__init__.py b/core/utils/__init__.py new file mode 100644 index 00000000..be37f21c --- /dev/null +++ b/core/utils/__init__.py @@ -0,0 +1 @@ +# Utils module for common utility functions diff --git a/core/utils/text.py b/core/utils/text.py new file mode 100644 index 00000000..e70a0d18 --- /dev/null +++ b/core/utils/text.py @@ -0,0 +1,42 @@ +""" +Text processing utility functions. +""" + + +def trim_logs(logs: str) -> str: + """ + Trim logs by removing everything after specific marker phrases. + + This function cuts off the string at the first occurrence of + "Here are the backend logs" or "Here are the frontend logs". + + :param logs: Log text to trim + :return: Trimmed log text with the marker phrase removed + """ + try: + if not logs: + return "" + + # Ensure we have a string + if not isinstance(logs, str): + logs = str(logs) + + # Define marker phrases + markers = ["Here are the backend logs", "Here are the frontend logs"] + + # Find the first occurrence of any marker + index = float("inf") + for marker in markers: + pos = logs.find(marker) + if pos != -1 and pos < index: + index = pos + + # If a marker was found, trim the string + if index != float("inf"): + return logs[:index] + + return logs + + except Exception: + # If anything goes wrong, return the original input as string or empty string + return str(logs) if logs is not None else ""