From b7aa3fbcde206dd844b40487a2133be97beac797 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 2 Oct 2025 22:03:41 +0100 Subject: [PATCH 001/138] chore(ui): This commit notes stream wrapper TODOs --- codetide/agents/tide/ui/stream_processor.py | 1 + codetide/agents/tide/ui/utils.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 652afbd..933f092 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -161,6 +161,7 @@ async def _process_inside_block(self) -> bool: marker_len = len(self.current_config.end_marker) if len(self.buffer) >= marker_len: stream_content = self.buffer[:-marker_len+1] + ### TODO target step cannot receive stream_token or can it? Maybe we could just build a wrapper aound custom element if stream_content and self.current_config.target_step: await self.current_config.target_step.stream_token(stream_content) self.buffer = self.buffer[-marker_len+1:] diff --git a/codetide/agents/tide/ui/utils.py b/codetide/agents/tide/ui/utils.py index 6d71c3a..2344d0e 100644 --- a/codetide/agents/tide/ui/utils.py +++ b/codetide/agents/tide/ui/utils.py @@ -95,3 +95,9 @@ async def send_reasoning_msg(loading_msg :cl.message, context_msg :cl.Message, a await context_msg.send() return True + +### Wrap thus send_reasoning_msg into a custom object which receives a loading_msg a context_msg and a st +### should also receive a dict with arguments (props) to be used internaly when calling stream_token (which will always receive a string) +### include stream_token method +### do not remove laoding message for now +### start with expanded template with wave animation and placeholder From 21d900545d0c9ea3e977b8f9fe76dd4f5a13511a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 2 Oct 2025 22:05:36 +0100 Subject: [PATCH 002/138] feat(ui): This commit adds ticket and reasoning demos --- codetide/agents/tide/ui/app.py | 103 ++++++++- .../tide/ui/public/elements/LinearTicket.jsx | 53 +++++ .../ui/public/elements/ReasoningExplorer.jsx | 199 ++++++++++++++++++ 3 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 codetide/agents/tide/ui/public/elements/LinearTicket.jsx create mode 100644 codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 411f981..91ad611 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -124,6 +124,88 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): except Exception as e: exception = e +# Example 1: Partial data with reasoning steps, context and modify identifiers, not finished +example1 = { + "reasoning_steps": [ + { + "header": "Initial Analysis", + "content": "I'm examining the problem statement to understand the requirements and constraints.", + "candidate_identifiers": ["problem_id", "requirements", "constraints"] + }, + { + "header": "Solution Approach", + "content": "Based on the analysis, I'll implement a solution using a divide-and-conquer strategy.", + "candidate_identifiers": ["algorithm", "divide_conquer", "implementation"] + } + ], + "context_identifiers": ["user_context", "system_requirements", "api_documentation"], + "modify_identifiers": ["configuration_settings", "user_preferences"], + "finished": False +} + +# Example 2: Complete data with all fields populated and finished as true +example2 = { + "reasoning_steps": [ + { + "header": "Problem Identification", + "content": "The issue appears to be related to memory management in the application.", + "candidate_identifiers": ["memory_leak", "heap_overflow", "garbage_collection"] + }, + { + "header": "Root Cause Analysis", + "content": "After thorough investigation, I've identified that objects are not being properly deallocated.", + "candidate_identifiers": ["object_lifecycle", "destructor", "reference_counting"] + }, + { + "header": "Solution Implementation", + "content": "I'll implement a custom memory pool to better manage object allocation and deallocation.", + "candidate_identifiers": ["memory_pool", "allocation_strategy", "deallocation"] + } + ], + "context_identifiers": ["application_logs", "performance_metrics", "system_architecture"], + "modify_identifiers": ["memory_management_module", "allocation_policies"], + "finished": True +} + +# Example 3: Only reasoning steps with no other data +example3 = { + "reasoning_steps": [ + { + "header": "Data Collection", + "content": "Gathering relevant data from various sources to build our dataset.", + "candidate_identifiers": ["data_sources", "extraction_methods", "validation"] + }, + { + "header": "Data Processing", + "content": "Cleaning and transforming the raw data to make it suitable for analysis.", + "candidate_identifiers": ["data_cleaning", "transformation", "normalization"] + }, + { + "header": "Model Training", + "content": "Training the machine learning model using the processed dataset.", + "candidate_identifiers": ["ml_algorithm", "hyperparameters", "training_split"] + }, + { + "header": "Model Evaluation", + "content": "Evaluating the model's performance using various metrics.", + "candidate_identifiers": ["accuracy", "precision", "recall", "f1_score"] + } + ], + "context_identifiers": [], + "modify_identifiers": [], + "finished": False +} + +async def get_ticket(): + """Pretending to fetch data from linear""" + return { + "title": "Fix Authentication Bug", + "status": "in-progress", + "assignee": "Sarah Chen", + "deadline": "2025-01-15", + "tags": ["security", "high-priority", "backend"] + } + @cl.on_chat_start async def start_chat(): # TODO think of fast way to initialize and get settings @@ -135,6 +217,25 @@ async def start_chat(): await cl.context.emitter.set_commands(AgentTideUi.commands) cl.user_session.set("chat_history", []) + props = await get_ticket() + + ticket_element = cl.CustomElement(name="LinearTicket", props=props) + # Store the element if we want to update it server side at a later stage. + cl.user_session.set("ticket_el", ticket_element) + + await cl.Message(content="Here is the ticket information!", elements=[ticket_element]).send() + + await asyncio.sleep(2) + ticket_element.props["title"] = "Could not Fix Authentication Bug" + await ticket_element.update() + + card_element = cl.CustomElement(name="ReasoningExplorer", props=example1) + await cl.Message(content="", elements=[card_element]).send() + + await asyncio.sleep(2) + card_element.props.update(example2) + await card_element.update() + @cl.set_starters async def set_starters(): return [cl.Starter(**kwargs) for kwargs in STARTERS] @@ -362,7 +463,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option start_wrapper="\n```shell\n", end_wrapper="\n```\n", target_step=msg - ), + ) ], global_fallback_msg=msg ) diff --git a/codetide/agents/tide/ui/public/elements/LinearTicket.jsx b/codetide/agents/tide/ui/public/elements/LinearTicket.jsx new file mode 100644 index 0000000..acb7ec5 --- /dev/null +++ b/codetide/agents/tide/ui/public/elements/LinearTicket.jsx @@ -0,0 +1,53 @@ +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Clock, User, Tag } from "lucide-react" + +export default function TicketStatusCard() { + const getProgressValue = (status) => { + const progress = { + 'open': 25, + 'in-progress': 50, + 'resolved': 75, + 'closed': 100 + } + return progress[status] || 0 + } + + return ( + + +
+ + {props.title || 'Untitled Ticket'} + + + {props.status || 'Unknown'} + +
+
+ +
+ + +
+
+ + {props.assignee || 'Unassigned'} +
+
+ + {props.deadline || 'No deadline'} +
+
+ + {props.tags?.join(', ') || 'No tags'} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx new file mode 100644 index 0000000..1349447 --- /dev/null +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -0,0 +1,199 @@ +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { ChevronDown, ChevronRight, Loader2, Brain, FileText, Edit } from "lucide-react" +import { useState } from "react" +import { cn } from "@/lib/utils" + +export default function ReasoningStepsCard() { + // Set default values for props +// const { +// reasoning_steps = [], +// context_identifiers = [], +// modify_identifiers = [], +// finished = false +// } = props; + + const [expandedSteps, setExpandedSteps] = useState({}); + const [expandedContext, setExpandedContext] = useState(false); + const [expandedModify, setExpandedModify] = useState(false); + const [expandedAll, setExpandedAll] = useState(!props.finished); + + const toggleStep = (index) => { + setExpandedSteps(prev => ({ + ...prev, + [index]: !prev[index] + })); + }; + + const toggleContext = () => { + setExpandedContext(!expandedContext); + }; + + const toggleModify = () => { + setExpandedModify(!expandedModify); + }; + + const toggleAll = () => { + setExpandedAll(!expandedAll); + }; + + // Check if all props are empty or undefined + const hasNoData = ( + props.reasoning_steps.length === 0 && + props.context_identifiers.length === 0 && + props.modify_identifiers.length === 0 + ); + + // If no data, show loading animation + if (hasNoData) { + return ( + + +
+ + Analyzing... +
+
+
+ ); + } + + return ( + + +
+ + Reasoning Process + +
+ {props.finished && ( + + Completed + + )} + +
+
+
+ {expandedAll && ( + + {/* Reasoning Steps */} + {props.reasoning_steps.length > 0 && ( +
+ {props.reasoning_steps.map((step, index) => ( +
+ {index < props.reasoning_steps.length - 1 && ( +
+ )} + + +
+
+ +
+
+ + {step.header} + +
+ +
+
+ +

{step.content}

+ {expandedSteps[index] && step.candidate_identifiers && step.candidate_identifiers.length > 0 && ( +
+

Candidate Identifiers:

+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))} +
+
+ )} +
+
+
+ ))} +
+ )} + + {/* Context Identifiers */} + {props.context_identifiers.length > 0 && ( + + +
+
+ + + Context Identifiers + +
+ +
+
+ {expandedContext && ( + +
+ {props.context_identifiers.map((id, index) => ( + + {id} + + ))} +
+
+ )} +
+ )} + + {/* Modify Identifiers */} + {props.modify_identifiers.length > 0 && ( + + +
+
+ + + Modify Identifiers + +
+ +
+
+ {expandedModify && ( + +
+ {props.modify_identifiers.map((id, index) => ( + + {id} + + ))} +
+
+ )} +
+ )} +
+ )} +
+ ); +} \ No newline at end of file From dbb323cc3397fe8053f712e77139f13285c10ec0 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 2 Oct 2025 22:29:54 +0100 Subject: [PATCH 003/138] refactor(ui): This commit refactors reasoning explorer layout --- .../ui/public/elements/ReasoningExplorer.jsx | 90 +++++++++---------- 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 1349447..89a5957 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -2,17 +2,8 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { ChevronDown, ChevronRight, Loader2, Brain, FileText, Edit } from "lucide-react" import { useState } from "react" -import { cn } from "@/lib/utils" export default function ReasoningStepsCard() { - // Set default values for props -// const { -// reasoning_steps = [], -// context_identifiers = [], -// modify_identifiers = [], -// finished = false -// } = props; - const [expandedSteps, setExpandedSteps] = useState({}); const [expandedContext, setExpandedContext] = useState(false); const [expandedModify, setExpandedModify] = useState(false); @@ -37,14 +28,12 @@ export default function ReasoningStepsCard() { setExpandedAll(!expandedAll); }; - // Check if all props are empty or undefined const hasNoData = ( props.reasoning_steps.length === 0 && props.context_identifiers.length === 0 && props.modify_identifiers.length === 0 ); - // If no data, show loading animation if (hasNoData) { return ( @@ -60,7 +49,7 @@ export default function ReasoningStepsCard() { return ( - +
Reasoning Process @@ -81,47 +70,48 @@ export default function ReasoningStepsCard() { {/* Reasoning Steps */} {props.reasoning_steps.length > 0 && ( -
+
{props.reasoning_steps.map((step, index) => ( -
- {index < props.reasoning_steps.length - 1 && ( -
- )} - - -
-
- -
-
- - {step.header} - -
- +
+ {/* Icon and line */} +
+
+ +
+ {index < props.reasoning_steps.length - 1 && ( +
+ )} +
+ + {/* Content */} +
+
+
+

+ {step.header} +

+

{step.content}

- - -

{step.content}

- {expandedSteps[index] && step.candidate_identifiers && step.candidate_identifiers.length > 0 && ( -
-

Candidate Identifiers:

-
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} -
+ +
+ {expandedSteps[index] && step.candidate_identifiers && step.candidate_identifiers.length > 0 && ( +
+

Candidate Identifiers:

+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))}
- )} - - +
+ )} +
))}
From 9b8b1283f1d2e1487b8d2490c686fcc73858a7c5 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 2 Oct 2025 23:47:56 +0100 Subject: [PATCH 004/138] feat(ui): This commit adds loader icon --- .../tide/ui/public/elements/ReasoningExplorer.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 89a5957..9832315 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -51,9 +51,14 @@ export default function ReasoningStepsCard() {
- - Reasoning Process - +
+ + Reasoning Process + + {!props.finished && ( + + )} +
{props.finished && ( From c83da848c989ec0764fad10d5cdff8d958b8f14b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 2 Oct 2025 23:54:30 +0100 Subject: [PATCH 005/138] chore(ui,utils): This commit outlines reasoning todo updates --- codetide/agents/tide/ui/app.py | 3 +++ codetide/agents/tide/ui/utils.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 91ad611..86d7dd5 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -472,6 +472,9 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option is_reasonig_sent = False loop = run_concurrent_tasks(agent_tide_ui, codeIdentifiers) async for chunk in loop: + ### TODO update this to check FROM AGENT TIDE if reasoning is being ran and if so we need + ### to send is finished true to custom element when the next STREAM_START_TOKEN_arrives + if chunk == STREAM_START_TOKEN: is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) continue diff --git a/codetide/agents/tide/ui/utils.py b/codetide/agents/tide/ui/utils.py index 2344d0e..cd9e1cb 100644 --- a/codetide/agents/tide/ui/utils.py +++ b/codetide/agents/tide/ui/utils.py @@ -101,3 +101,6 @@ async def send_reasoning_msg(loading_msg :cl.message, context_msg :cl.Message, a ### include stream_token method ### do not remove laoding message for now ### start with expanded template with wave animation and placeholder +### custom obj should preserve props and update them with new args, markerconfig should be update to include args per +### config as well as possibility to dump only once filled and convert to type i.e json loads to list / dict by is_obj prooperty) +### dumping only when buffer is complete should be handled at streamprocessor level From a913a92761112b5622cd9f3861161a784fd11ac8 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 21:43:19 +0100 Subject: [PATCH 006/138] refactor: This commit streamlines explorer ui/ids --- .../ui/public/elements/ReasoningExplorer.jsx | 277 +++++++++--------- 1 file changed, 145 insertions(+), 132 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 9832315..f771ac4 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -1,38 +1,32 @@ -import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { ChevronDown, ChevronRight, Loader2, Brain, FileText, Edit } from "lucide-react" -import { useState } from "react" +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ChevronDown, ChevronRight, Loader2, Brain, Layers } from "lucide-react"; +import { useState } from "react"; export default function ReasoningStepsCard() { const [expandedSteps, setExpandedSteps] = useState({}); - const [expandedContext, setExpandedContext] = useState(false); - const [expandedModify, setExpandedModify] = useState(false); const [expandedAll, setExpandedAll] = useState(!props.finished); + const [expandedIdentifiers, setExpandedIdentifiers] = useState(false); const toggleStep = (index) => { - setExpandedSteps(prev => ({ + setExpandedSteps((prev) => ({ ...prev, - [index]: !prev[index] + [index]: !prev[index], })); }; - const toggleContext = () => { - setExpandedContext(!expandedContext); - }; - - const toggleModify = () => { - setExpandedModify(!expandedModify); - }; - const toggleAll = () => { setExpandedAll(!expandedAll); }; - const hasNoData = ( + const toggleIdentifiers = () => { + setExpandedIdentifiers(!expandedIdentifiers); + }; + + const hasNoData = props.reasoning_steps.length === 0 && props.context_identifiers.length === 0 && - props.modify_identifiers.length === 0 - ); + props.modify_identifiers.length === 0; if (hasNoData) { return ( @@ -49,6 +43,7 @@ export default function ReasoningStepsCard() { return ( + {/* Header */}
@@ -59,136 +54,154 @@ export default function ReasoningStepsCard() { )}
-
- {props.finished && ( - - Completed - + -
+
+ + {/* Timeline */} {expandedAll && ( - + {/* Reasoning Steps */} - {props.reasoning_steps.length > 0 && ( -
- {props.reasoning_steps.map((step, index) => ( -
- {/* Icon and line */} -
-
- -
- {index < props.reasoning_steps.length - 1 && ( -
- )} -
- - {/* Content */} -
-
-
-

- {step.header} -

-

{step.content}

-
- -
- {expandedSteps[index] && step.candidate_identifiers && step.candidate_identifiers.length > 0 && ( -
-

Candidate Identifiers:

-
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} -
-
- )} -
+ {props.reasoning_steps.map((step, index) => ( +
+ {/* Timeline Icon + Connector */} +
+
+
- ))} -
- )} + {index < props.reasoning_steps.length - 1 && ( +
+ )} +
- {/* Context Identifiers */} - {props.context_identifiers.length > 0 && ( - - -
-
- - - Context Identifiers - + {/* Step Content */} +
+
+
+

+ {step.header} +

+

+ {step.content} +

-
- - {expandedContext && ( - -
- {props.context_identifiers.map((id, index) => ( - - {id} - - ))} -
-
- )} - - )} - {/* Modify Identifiers */} - {props.modify_identifiers.length > 0 && ( - - -
-
- - - Modify Identifiers - + {/* Expanded candidate identifiers */} + {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( +
+

+ Candidate Identifiers: +

+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))} +
- + )} +
+
+ ))} + + {/* Unified Context + Modify Identifiers */} + {(props.context_identifiers.length > 0 || + props.modify_identifiers.length > 0) && ( +
+
+
+ +

+ Additional Identifiers +

+
+ +
+ + {expandedIdentifiers && ( +
+ {/* Context Identifiers */} + {props.context_identifiers.length > 0 && ( +
+

+ Context Identifiers: +

+
+ {props.context_identifiers.map((id, index) => ( + + {id} + + ))} +
+
+ )} + + {/* Modify Identifiers */} + {props.modify_identifiers.length > 0 && ( +
+

+ Modify Identifiers: +

+
+ {props.modify_identifiers.map((id, index) => ( + + {id} + + ))} +
+
+ )}
- - {expandedModify && ( - -
- {props.modify_identifiers.map((id, index) => ( - - {id} - - ))} -
-
)} - +
)} )}
); -} \ No newline at end of file +} From 248222ca070da805c2b559b05d9ca5b6c3c9bb7f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 21:44:17 +0100 Subject: [PATCH 007/138] refactor(ui): This commit streamlines reasoning UI From 172cd6d1c06838c139b4a549363606c49ee7c71b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 22:56:38 +0100 Subject: [PATCH 008/138] feat(ui): This commit adds full-mode field-aware streaming --- codetide/agents/tide/ui/stream_processor.py | 205 ++++++++++++++++++-- 1 file changed, 191 insertions(+), 14 deletions(-) diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 933f092..18c2c7c 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -1,5 +1,82 @@ -from typing import Optional, List, NamedTuple +from typing import Dict, Literal, Optional, List, NamedTuple, Union +from dataclasses import dataclass import chainlit as cl +import re + +class CustomElementStep: + ... + + def stream_token(self, content :str)->str: + pass + +@dataclass +class ExtractedFields: + """Container for extracted field data from a marker block.""" + raw_content: str + fields: Dict[str, any] + +class FieldExtractor: + """Handles extraction of structured fields from marker content.""" + + def __init__(self, field_patterns: Dict[str, str]): + """ + Initialize with field extraction patterns. + + Args: + field_patterns: Dict mapping field names to regex patterns. + Patterns should have named groups or return the match. + """ + self.field_patterns = { + name: re.compile(pattern, re.MULTILINE | re.DOTALL) + for name, pattern in field_patterns.items() + } + + def extract(self, content: str) -> ExtractedFields: + """ + Extract all configured fields from content. + + Args: + content: Raw text content between markers + + Returns: + ExtractedFields object with parsed data + """ + fields = {} + + for field_name, pattern in self.field_patterns.items(): + match = pattern.search(content) + if match: + # If pattern has named groups, use them + if match.groupdict(): + fields[field_name] = match.groupdict() + # Otherwise use the first group or full match + elif match.groups(): + fields[field_name] = match.group(1).strip() + else: + fields[field_name] = match.group(0).strip() + else: + fields[field_name] = None + + return ExtractedFields(raw_content=content, fields=fields) + + def extract_list(self, content: str, field_name: str) -> List[str]: + """ + Extract a list of items (e.g., candidate_identifiers). + + Args: + content: Raw text content + field_name: Name of the field containing list items + + Returns: + List of extracted strings + """ + pattern = self.field_patterns.get(field_name) + if not pattern: + return [] + + matches = pattern.findall(content) + return [m.strip() if isinstance(m, str) else m[0].strip() + for m in matches if m] class MarkerConfig(NamedTuple): """Configuration for a single marker pair.""" @@ -7,8 +84,24 @@ class MarkerConfig(NamedTuple): end_marker: str start_wrapper: str = "" end_wrapper: str = "" - target_step: Optional[cl.Step] = None + target_step: Optional[Union[cl.Step, CustomElementStep]] = None fallback_msg: Optional[cl.Message] = None + stream_mode: Literal["chunk", "full"] = "chunk" + field_extractor: Optional[FieldExtractor] = None + + def process_content(self, content: str) -> Union[str, ExtractedFields]: + """ + Process content, extracting fields if field_extractor is configured. + + Args: + content: Raw content between markers + + Returns: + ExtractedFields if extractor configured, otherwise raw string + """ + if self.field_extractor: + return self.field_extractor.extract(content) + return content class StreamProcessor: """ @@ -33,6 +126,7 @@ def __init__( self.buffer = "" self.current_config = None # Currently active marker config self.current_config_index = None + self.accumulated_content = "" # For full mode with field extractor def __init_single__( self, @@ -137,6 +231,7 @@ async def _process_outside_block(self) -> bool: self.current_config = earliest_config self.current_config_index = earliest_config_index + self.accumulated_content = "" # Reset accumulator for full mode # Remove everything up to and including the begin marker self.buffer = self.buffer[earliest_idx + len(earliest_config.begin_marker):] @@ -156,21 +251,46 @@ async def _process_inside_block(self) -> bool: return False idx = self.buffer.find(self.current_config.end_marker) + + # Check if we're in full mode with field extractor + is_full_mode = (self.current_config.stream_mode == "full" and + self.current_config.field_extractor is not None) + if idx == -1: - # No end marker found, stream everything except potential partial marker + # No end marker found marker_len = len(self.current_config.end_marker) if len(self.buffer) >= marker_len: stream_content = self.buffer[:-marker_len+1] - ### TODO target step cannot receive stream_token or can it? Maybe we could just build a wrapper aound custom element - if stream_content and self.current_config.target_step: - await self.current_config.target_step.stream_token(stream_content) + + if is_full_mode: + # Accumulate content for processing at the end + self.accumulated_content += stream_content + else: + # Stream immediately in chunk mode + if stream_content and self.current_config.target_step: + await self.current_config.target_step.stream_token(stream_content) + self.buffer = self.buffer[-marker_len+1:] return False else: # Found end marker - if idx > 0 and self.current_config.target_step: - # Stream content before the end marker to target step - await self.current_config.target_step.stream_token(self.buffer[:idx]) + block_content = self.buffer[:idx] + + if is_full_mode: + # Add final content to accumulator + self.accumulated_content += block_content + + # Process the complete content with field extractor + extracted = self.current_config.process_content(self.accumulated_content) + + # Stream the processed result + if self.current_config.target_step: + processed_output = self._format_extracted_fields(extracted) + await self.current_config.target_step.stream_token(processed_output) + else: + # Stream content before the end marker in chunk mode + if block_content and self.current_config.target_step: + await self.current_config.target_step.stream_token(block_content) # Close the special block if self.current_config.target_step and self.current_config.end_wrapper: @@ -179,6 +299,7 @@ async def _process_inside_block(self) -> bool: self.buffer = self.buffer[idx + len(self.current_config.end_marker):] self.current_config = None self.current_config_index = None + self.accumulated_content = "" # Clear accumulator # Remove everything up to and including the end marker if self.buffer.startswith('\n'): @@ -186,6 +307,47 @@ async def _process_inside_block(self) -> bool: return True + def _format_extracted_fields(self, extracted: Union[str, ExtractedFields]) -> str: + """ + Format extracted fields for streaming output. + + Args: + extracted: Either raw string or ExtractedFields object + + Returns: + Formatted string representation + """ + if isinstance(extracted, str): + return extracted + + # Format ExtractedFields into a readable output + output_parts = [] + + for field_name, field_value in extracted.fields.items(): + if field_value is None: + continue + + # Handle list fields specially (like candidate_identifiers) + if field_name in self.current_config.field_extractor.field_patterns: + # Try to extract as list + list_items = self.current_config.field_extractor.extract_list( + extracted.raw_content, + field_name + ) + if list_items: + output_parts.append(f"**{field_name}**:") + for item in list_items: + output_parts.append(f" - {item}") + continue + + # Handle regular fields + if isinstance(field_value, dict): + output_parts.append(f"**{field_name}**: {field_value}") + else: + output_parts.append(f"**{field_name}**: {field_value}") + + return "\n".join(output_parts) if output_parts else extracted.raw_content + def _get_fallback_msg(self) -> Optional[cl.Message]: """Get the appropriate fallback message.""" return self.global_fallback_msg @@ -196,10 +358,24 @@ async def finalize(self) -> None: """ if self.buffer: if self.current_config is not None: - if self.current_config.target_step: - await self.current_config.target_step.stream_token(self.buffer) - if self.current_config.end_wrapper: - await self.current_config.target_step.stream_token(self.current_config.end_wrapper) + is_full_mode = (self.current_config.stream_mode == "full" and + self.current_config.field_extractor is not None) + + if is_full_mode: + # Process accumulated content with field extractor + self.accumulated_content += self.buffer + extracted = self.current_config.process_content(self.accumulated_content) + + if self.current_config.target_step: + processed_output = self._format_extracted_fields(extracted) + await self.current_config.target_step.stream_token(processed_output) + else: + # Stream remaining content in chunk mode + if self.current_config.target_step: + await self.current_config.target_step.stream_token(self.buffer) + + if self.current_config.target_step and self.current_config.end_wrapper: + await self.current_config.target_step.stream_token(self.current_config.end_wrapper) else: fallback_msg = self._get_fallback_msg() if fallback_msg: @@ -209,4 +385,5 @@ async def finalize(self) -> None: # Reset state self.buffer = "" self.current_config = None - self.current_config_index = None \ No newline at end of file + self.current_config_index = None + self.accumulated_content = "" From 4264ca2429aac42670de5b8f1c0ad84e144dda8a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 22:58:52 +0100 Subject: [PATCH 009/138] test(stream-processor): This commit adds stream processor tests --- tests/agents/tide/test_stream_processor.py | 423 +++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 tests/agents/tide/test_stream_processor.py diff --git a/tests/agents/tide/test_stream_processor.py b/tests/agents/tide/test_stream_processor.py new file mode 100644 index 0000000..5831e2d --- /dev/null +++ b/tests/agents/tide/test_stream_processor.py @@ -0,0 +1,423 @@ +""" +Pytest test suite for StreamProcessor with field extraction in full mode. + +Run with: pytest test_stream_processor.py -v -s +""" +import pytest +from typing import List +from codetide.agents.tide.ui.stream_processor import ExtractedFields, MarkerConfig, FieldExtractor, StreamProcessor + + +# Mock classes to simulate chainlit behavior +class MockStep: + """Mock Step class for testing.""" + + def __init__(self, name: str): + self.name = name + self.content = [] + + async def stream_token(self, content: str): + """Simulate streaming a token.""" + self.content.append(content) + print(f"[{self.name}] Streamed: {content}") + + def get_full_content(self) -> str: + """Get all streamed content.""" + return "".join(self.content) + + def clear(self): + """Clear content for next test.""" + self.content = [] + + +class MockMessage: + """Mock Message class for testing.""" + + def __init__(self, name: str = "fallback"): + self.name = name + self.content = [] + + async def stream_token(self, content: str): + """Simulate streaming a token.""" + self.content.append(content) + print(f"[{self.name}] Streamed: {content}") + + async def send(self): + """Simulate sending the message.""" + print(f"[{self.name}] Message sent!") + + def get_full_content(self) -> str: + """Get all streamed content.""" + return "".join(self.content) + + def clear(self): + """Clear content for next test.""" + self.content = [] + + +# Fixtures +@pytest.fixture +def field_patterns(): + """Field patterns for reasoning blocks.""" + return { + "header": r"\*\*([^*]+)\*\*(?=\s*\n\s*\*\*content\*\*)", + "content": r"\*\*content\*\*:\s*(.+?)(?=\s*\*\*candidate_identifiers\*\*|$)", + "candidate_identifiers": r"^\s*-\s*(.+?)$" + } + + +@pytest.fixture +def reasoning_step(): + """Mock step for reasoning blocks.""" + return MockStep("Reasoning Block") + + +@pytest.fixture +def code_step(): + """Mock step for code blocks.""" + return MockStep("Code Block") + + +@pytest.fixture +def fallback_msg(): + """Mock fallback message.""" + return MockMessage("Fallback") + + +@pytest.fixture +def sample_stream(): + """Sample streaming content with reasoning blocks.""" + return """Some initial content before reasoning. + +*** Begin Reasoning +**Update Authentication Module** +**content**: brief summary of the logic behind this task and the files to look into and why +**candidate_identifiers**: + - src.auth.authenticate.AuthHandler.verify_token + - src.auth.models.User + - config/auth_config.yaml +*** End Reasoning + +Some content between blocks. + +*** Begin Reasoning +**Refactor Database Layer** +**content**: Need to update the database connection pooling to handle increased load +**candidate_identifiers**: + - src.database.connection.ConnectionPool + - src.database.query_builder.QueryBuilder + - tests/database/test_connection.py +*** End Reasoning + +Final content after reasoning blocks. +""" + + +@pytest.fixture +def mixed_content_stream(): + """Sample stream with both reasoning and code blocks.""" + return """Here's my analysis: + +*** Begin Reasoning +**Analyze Code Structure** +**content**: Review the existing code architecture and identify areas for improvement +**candidate_identifiers**: + - src.main.Application + - src.config.settings +*** End Reasoning + +Now here's the implementation: + +```python +def process_data(items): + return [item * 2 for item in items] +``` + +That's the solution! +""" + + +# Helper functions +def split_into_chunks(text: str, chunk_size: int = 30) -> List[str]: + """Split text into chunks for simulating streaming.""" + return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)] + + +# Tests +@pytest.mark.asyncio +async def test_full_mode_field_extraction(field_patterns, reasoning_step, fallback_msg, sample_stream): + """Test that full mode accumulates content and processes it only at end marker.""" + + + # Setup + extractor = FieldExtractor(field_patterns) + reasoning_config = MarkerConfig( + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + start_wrapper="## Processing Reasoning Block\n\n", + end_wrapper="\n\n---\n", + target_step=reasoning_step, + stream_mode="full", + field_extractor=extractor + ) + + processor = StreamProcessor( + marker_configs=[reasoning_config], + global_fallback_msg=fallback_msg + ) + + # Process stream in chunks + chunks = split_into_chunks(sample_stream, chunk_size=30) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Assertions + reasoning_content = reasoning_step.get_full_content() + fallback_content = fallback_msg.get_full_content() + + # Should have processed 2 reasoning blocks + assert reasoning_content.count("## Processing Reasoning Block") == 2 + assert reasoning_content.count("---") == 2 + + # Should have extracted headers + assert "Update Authentication Module" in reasoning_content + assert "Refactor Database Layer" in reasoning_content + + # Should have extracted candidate_identifiers + assert "src.auth.authenticate.AuthHandler.verify_token" in reasoning_content + assert "src.database.connection.ConnectionPool" in reasoning_content + + # Fallback should have content outside markers + assert "Some initial content before reasoning" in fallback_content + assert "Some content between blocks" in fallback_content + assert "Final content after reasoning blocks" in fallback_content + + print("\n✓ Full mode field extraction test passed!") + + +@pytest.mark.asyncio +async def test_chunk_mode_immediate_streaming(code_step, fallback_msg): + """Test that chunk mode streams content immediately without accumulation.""" + + # Setup + code_config = MarkerConfig( + begin_marker="```python", + end_marker="```", + start_wrapper="```python\n", + end_wrapper="\n```", + target_step=code_step, + stream_mode="chunk" + ) + + processor = StreamProcessor( + marker_configs=[code_config], + global_fallback_msg=fallback_msg + ) + + # Process stream + test_stream = """Some text before. + +```python +def hello(): + print("world") +``` + +Some text after. +""" + + chunks = split_into_chunks(test_stream, chunk_size=20) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Assertions + code_content = code_step.get_full_content() + + assert "```python\n" in code_content + assert "def hello():" in code_content + assert 'print("world")' in code_content + assert "\n```" in code_content + + print("\n✓ Chunk mode immediate streaming test passed!") + + +@pytest.mark.asyncio +async def test_multiple_configs_mixed_modes( + field_patterns, reasoning_step, code_step, fallback_msg, mixed_content_stream +): + """Test multiple marker configs with different streaming modes.""" + + # Setup reasoning config (full mode) + reasoning_extractor = FieldExtractor(field_patterns) + reasoning_config = MarkerConfig( + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + target_step=reasoning_step, + stream_mode="full", + field_extractor=reasoning_extractor + ) + + # Setup code config (chunk mode) + code_config = MarkerConfig( + begin_marker="```python", + end_marker="```", + start_wrapper="```python\n", + end_wrapper="\n```", + target_step=code_step, + stream_mode="chunk" + ) + + processor = StreamProcessor( + marker_configs=[reasoning_config, code_config], + global_fallback_msg=fallback_msg + ) + + # Process stream + chunks = split_into_chunks(mixed_content_stream, chunk_size=25) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Assertions + reasoning_content = reasoning_step.get_full_content() + code_content = code_step.get_full_content() + fallback_content = fallback_msg.get_full_content() + + # Reasoning should be processed with field extraction + assert "Analyze Code Structure" in reasoning_content + assert "src.main.Application" in reasoning_content + + # Code should be streamed as-is + assert "def process_data(items):" in code_content + assert "return [item * 2 for item in items]" in code_content + + # Fallback should have content outside both markers + assert "Here's my analysis:" in fallback_content + assert "Now here's the implementation:" in fallback_content + assert "That's the solution!" in fallback_content + + print("\n✓ Multiple configs mixed modes test passed!") + + +@pytest.mark.asyncio +async def test_field_extractor_list_extraction(field_patterns): + """Test that list extraction works correctly for candidate_identifiers.""" + + + extractor = FieldExtractor(field_patterns) + + test_content = """**Task Header** +**content**: Some description here +**candidate_identifiers**: + - src.module.Class.method + - src.another.module.function + - config/settings.yaml +""" + + # Extract fields + extracted = extractor.extract(test_content) + + # Extract list specifically + identifiers = extractor.extract_list(test_content, "candidate_identifiers") + + # Assertions + assert isinstance(extracted, ExtractedFields) + assert extracted.fields["header"] == "Task Header" + assert "Some description here" in extracted.fields["content"] + + assert len(identifiers) == 3 + assert "src.module.Class.method" in identifiers + assert "src.another.module.function" in identifiers + assert "config/settings.yaml" in identifiers + + print("\n✓ Field extractor list extraction test passed!") + + +@pytest.mark.asyncio +async def test_incomplete_block_handling(field_patterns, reasoning_step, fallback_msg): + """Test that incomplete blocks are handled properly in finalize.""" + + extractor = FieldExtractor(field_patterns) + reasoning_config = MarkerConfig( + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + target_step=reasoning_step, + stream_mode="full", + field_extractor=extractor + ) + + processor = StreamProcessor( + marker_configs=[reasoning_config], + global_fallback_msg=fallback_msg + ) + + # Stream with incomplete block (no end marker) + incomplete_stream = """Some content. + +*** Begin Reasoning +**Incomplete Task** +**content**: This block never closes +**candidate_identifiers**: + - src.test.module +""" + + chunks = split_into_chunks(incomplete_stream, chunk_size=25) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Should still process the incomplete block in finalize + reasoning_content = reasoning_step.get_full_content() + assert "Incomplete Task" in reasoning_content + assert "src.test.module" in reasoning_content + + print("\n✓ Incomplete block handling test passed!") + + +@pytest.mark.asyncio +async def test_no_field_extractor_full_mode(reasoning_step, fallback_msg): + """Test full mode without field extractor (should stream raw content).""" + + reasoning_config = MarkerConfig( + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + target_step=reasoning_step, + stream_mode="full", + field_extractor=None # No extractor + ) + + processor = StreamProcessor( + marker_configs=[reasoning_config], + global_fallback_msg=fallback_msg + ) + + test_stream = """ +*** Begin Reasoning +This is raw content without structured fields. +Just plain text. +*** End Reasoning +""" + + chunks = split_into_chunks(test_stream, chunk_size=25) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + reasoning_content = reasoning_step.get_full_content() + + # Should stream raw content without formatting + assert "This is raw content without structured fields." in reasoning_content + assert "Just plain text." in reasoning_content + + print("\n✓ Full mode without field extractor test passed!") + + +if __name__ == "__main__": + # Run with pytest + pytest.main([__file__, "-v", "-s"]) \ No newline at end of file From 86d9b211cd327267b83f293e06642b58605faa2c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 22:59:27 +0100 Subject: [PATCH 010/138] docs(ui): This commit adds reasoning templates --- codetide/agents/tide/ui/app.py | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 86d7dd5..292eacb 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -143,6 +143,37 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): "finished": False } +""" +*** Begin Reasoning +1. **first task header** + **content**: brief summary of the logic behind this task and the files to look into and why + **candidate_identifiers**: + - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to use as context +*** End Reasoning +*** Begin Reasoning +2. **first task header** + **content**: brief summary of the logic behind this task and the files to look into and why + **candidate_identifiers**: + - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to modify or update +*** End Reasoning +""" +### use current expansion logic here then move to the next one once all possible candidate_identifiers have been found + +""" +*** Begin Summary +summary of the reasoning steps so far +*** End Summary + +*** Begin Context Identifiers + +*** End Context Identifiers + +*** Begin Modify Identifiers + +*** End Modify Identifiers + +""" + # Example 2: Complete data with all fields populated and finished as true example2 = { "reasoning_steps": [ @@ -437,6 +468,9 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option context_msg = cl.Message(content="", author="AgentTide") msg = cl.Message(content="", author="Agent Tide") + + # ReasoningCustomElementStep = CustomElementStep() + async with cl.Step("ApplyPatch", type="tool") as diff_step: await diff_step.remove() @@ -463,7 +497,12 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option start_wrapper="\n```shell\n", end_wrapper="\n```\n", target_step=msg - ) + ), + # MarkerConfig( + # begin_marker="*** Begin Reasoning", + # end_marker="*** End Reasoning", + # target_step=CustomElement + # ) ], global_fallback_msg=msg ) From d3a223f285b0b33c3f299f9a26be08b66794490a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 23:32:11 +0100 Subject: [PATCH 011/138] feat(ui): This commit adds custom element field streaming --- codetide/agents/tide/ui/stream_processor.py | 131 ++++++++++++++++++-- 1 file changed, 120 insertions(+), 11 deletions(-) diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 18c2c7c..3e1563c 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -1,19 +1,122 @@ -from typing import Dict, Literal, Optional, List, NamedTuple, Union from dataclasses import dataclass +from typing import Dict, Literal, Optional, List, NamedTuple, Union import chainlit as cl import re -class CustomElementStep: - ... - def stream_token(self, content :str)->str: - pass +class CustomElementStep: + """Step that streams extracted fields to a Chainlit CustomElement.""" + + def __init__(self, element: cl.CustomElement, props_schema: Dict[str, type]): + """ + Initialize CustomElementStep with element and props schema. + + Args: + element: Chainlit CustomElement to update + props_schema: Dict mapping marker_id to expected type (list, str, dict, etc.) + e.g., {"reasoning": list, "summary": str, "metadata": dict} + """ + self.element = element + self.props_schema = props_schema + self.props = self._initialize_props() + + def _initialize_props(self) -> Dict[str, any]: + """Initialize props based on schema with appropriate empty values.""" + initialized = {} + for marker_id, prop_type in self.props_schema.items(): + if prop_type is list: + initialized[marker_id] = [] + elif prop_type is str: + initialized[marker_id] = "" + elif prop_type is dict: + initialized[marker_id] = {} + elif prop_type is set: + initialized[marker_id] = set() + elif prop_type is int: + initialized[marker_id] = 0 + elif prop_type is float: + initialized[marker_id] = 0.0 + elif prop_type is bool: + initialized[marker_id] = False + else: + # For custom types, try to instantiate with no args + try: + initialized[marker_id] = prop_type() + except Exception: + initialized[marker_id] = None + return initialized + + async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: + """ + Stream content to the custom element by updating props. + + Args: + content: Either a string (for raw content) or ExtractedFields.fields dict + """ + if isinstance(content, str): + # Raw string content - append to default prop if exists + if "content" in self.props and isinstance(self.props["content"], str): + self.props["content"] += content + return + + # Handle ExtractedFields dict + if not isinstance(content, dict): + return + + # content should have 'marker_id' and 'fields' + marker_id = content.get("marker_id") + fields = content.get("fields", {}) + + if not marker_id or marker_id not in self.props: + return + + # Update prop based on its type + prop_type = self.props_schema.get(marker_id) + current_value = self.props[marker_id] + + if prop_type is list: + # Append fields dict to list + if isinstance(current_value, list): + self.props[marker_id].append(fields) + elif prop_type is str: + # Concatenate string representation of fields + if isinstance(current_value, str): + formatted = self._format_fields_as_string(fields) + self.props[marker_id] += formatted + elif prop_type is dict: + # Merge fields into dict + if isinstance(current_value, dict): + self.props[marker_id].update(fields) + + # Update the element + self.element.props.update(self.props) + await self.element.update() + + def _format_fields_as_string(self, fields: Dict[str, any]) -> str: + """Format fields dict as a readable string.""" + parts = [] + for key, value in fields.items(): + if value is not None: + if isinstance(value, list): + items = "\n ".join(f"- {item}" for item in value) + parts.append(f"**{key}**:\n {items}") + else: + parts.append(f"**{key}**: {value}") + return "\n".join(parts) + "\n\n" @dataclass class ExtractedFields: """Container for extracted field data from a marker block.""" + marker_id: str raw_content: str fields: Dict[str, any] + + def to_dict(self) -> Dict[str, any]: + """Convert to dict for streaming to CustomElementStep.""" + return { + "marker_id": self.marker_id, + "fields": self.fields + } class FieldExtractor: """Handles extraction of structured fields from marker content.""" @@ -31,12 +134,13 @@ def __init__(self, field_patterns: Dict[str, str]): for name, pattern in field_patterns.items() } - def extract(self, content: str) -> ExtractedFields: + def extract(self, content: str, marker_id: str = "") -> ExtractedFields: """ Extract all configured fields from content. Args: content: Raw text content between markers + marker_id: Identifier for the marker config Returns: ExtractedFields object with parsed data @@ -57,7 +161,7 @@ def extract(self, content: str) -> ExtractedFields: else: fields[field_name] = None - return ExtractedFields(raw_content=content, fields=fields) + return ExtractedFields(marker_id=marker_id, raw_content=content, fields=fields) def extract_list(self, content: str, field_name: str) -> List[str]: """ @@ -82,6 +186,7 @@ class MarkerConfig(NamedTuple): """Configuration for a single marker pair.""" begin_marker: str end_marker: str + marker_id: str = "" start_wrapper: str = "" end_wrapper: str = "" target_step: Optional[Union[cl.Step, CustomElementStep]] = None @@ -100,7 +205,7 @@ def process_content(self, content: str) -> Union[str, ExtractedFields]: ExtractedFields if extractor configured, otherwise raw string """ if self.field_extractor: - return self.field_extractor.extract(content) + return self.field_extractor.extract(content, self.marker_id) return content class StreamProcessor: @@ -307,7 +412,7 @@ async def _process_inside_block(self) -> bool: return True - def _format_extracted_fields(self, extracted: Union[str, ExtractedFields]) -> str: + def _format_extracted_fields(self, extracted: Union[str, ExtractedFields]) -> Union[str, Dict[str, any]]: """ Format extracted fields for streaming output. @@ -315,12 +420,16 @@ def _format_extracted_fields(self, extracted: Union[str, ExtractedFields]) -> st extracted: Either raw string or ExtractedFields object Returns: - Formatted string representation + Formatted string for regular steps or dict for CustomElementStep """ if isinstance(extracted, str): return extracted - # Format ExtractedFields into a readable output + # If target is CustomElementStep, return dict format + if isinstance(self.current_config.target_step, CustomElementStep): + return extracted.to_dict() + + # For regular steps, format as string output_parts = [] for field_name, field_value in extracted.fields.items(): From dc4c65d0854550d1e5ea606638677d45876439d4 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 7 Oct 2025 23:33:13 +0100 Subject: [PATCH 012/138] test(stream): This commit adds custom element streaming tests --- tests/agents/tide/test_stream_processor.py | 245 ++++++++++++++++++++- 1 file changed, 238 insertions(+), 7 deletions(-) diff --git a/tests/agents/tide/test_stream_processor.py b/tests/agents/tide/test_stream_processor.py index 5831e2d..f0a06b0 100644 --- a/tests/agents/tide/test_stream_processor.py +++ b/tests/agents/tide/test_stream_processor.py @@ -5,7 +5,13 @@ """ import pytest from typing import List -from codetide.agents.tide.ui.stream_processor import ExtractedFields, MarkerConfig, FieldExtractor, StreamProcessor +from codetide.agents.tide.ui.stream_processor import ( + ExtractedFields, + MarkerConfig, + FieldExtractor, + StreamProcessor, + CustomElementStep +) # Mock classes to simulate chainlit behavior @@ -55,6 +61,21 @@ def clear(self): self.content = [] +class MockCustomElement: + """Mock CustomElement class for testing.""" + + def __init__(self, name: str): + self.name = name + self.props = {} + self.update_count = 0 + + async def update(self): + """Simulate updating the element.""" + self.update_count += 1 + print(f"[{self.name}] Updated (count: {self.update_count})") + print(f"[{self.name}] Current props: {self.props}") + + # Fixtures @pytest.fixture def field_patterns(): @@ -84,6 +105,12 @@ def fallback_msg(): return MockMessage("Fallback") +@pytest.fixture +def mock_custom_element(): + """Mock custom element for testing.""" + return MockCustomElement("ReasoningDisplay") + + @pytest.fixture def sample_stream(): """Sample streaming content with reasoning blocks.""" @@ -147,13 +174,13 @@ def split_into_chunks(text: str, chunk_size: int = 30) -> List[str]: @pytest.mark.asyncio async def test_full_mode_field_extraction(field_patterns, reasoning_step, fallback_msg, sample_stream): """Test that full mode accumulates content and processes it only at end marker.""" - # Setup extractor = FieldExtractor(field_patterns) reasoning_config = MarkerConfig( begin_marker="*** Begin Reasoning", end_marker="*** End Reasoning", + marker_id="reasoning", start_wrapper="## Processing Reasoning Block\n\n", end_wrapper="\n\n---\n", target_step=reasoning_step, @@ -197,14 +224,189 @@ async def test_full_mode_field_extraction(field_patterns, reasoning_step, fallba print("\n✓ Full mode field extraction test passed!") +@pytest.mark.asyncio +async def test_custom_element_step_list_accumulation(field_patterns, mock_custom_element, fallback_msg, sample_stream): + """Test CustomElementStep accumulating extracted fields in a list.""" + + # Setup CustomElementStep + props_schema = { + "reasoning": list, # Will accumulate reasoning blocks as list + } + custom_step = CustomElementStep(mock_custom_element, props_schema) + + # Setup extractor and config + extractor = FieldExtractor(field_patterns) + reasoning_config = MarkerConfig( + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + marker_id="reasoning", # Matches props_schema key + target_step=custom_step, + stream_mode="full", + field_extractor=extractor + ) + + processor = StreamProcessor( + marker_configs=[reasoning_config], + global_fallback_msg=fallback_msg + ) + + # Process stream + chunks = split_into_chunks(sample_stream, chunk_size=30) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Assertions + assert mock_custom_element.update_count == 2 # Updated twice (one per block) + assert "reasoning" in mock_custom_element.props + + reasoning_list = mock_custom_element.props["reasoning"] + assert isinstance(reasoning_list, list) + assert len(reasoning_list) == 2 + + # Check first block + first_block = reasoning_list[0] + assert first_block["header"] == "Update Authentication Module" + assert "brief summary" in first_block["content"] + + # Check second block + second_block = reasoning_list[1] + assert second_block["header"] == "Refactor Database Layer" + assert "database connection pooling" in second_block["content"] + + print("\n✓ CustomElementStep list accumulation test passed!") + + +@pytest.mark.asyncio +async def test_custom_element_step_string_concatenation(field_patterns, mock_custom_element, fallback_msg): + """Test CustomElementStep concatenating extracted fields as string.""" + + # Setup CustomElementStep with string type + props_schema = { + "reasoning_text": str, + } + custom_step = CustomElementStep(mock_custom_element, props_schema) + + # Setup extractor and config + extractor = FieldExtractor(field_patterns) + reasoning_config = MarkerConfig( + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + marker_id="reasoning_text", # Matches props_schema key + target_step=custom_step, + stream_mode="full", + field_extractor=extractor + ) + + processor = StreamProcessor( + marker_configs=[reasoning_config], + global_fallback_msg=fallback_msg + ) + + # Process stream with single block + test_stream = """ +*** Begin Reasoning +**First Task** +**content**: This is the first task description +**candidate_identifiers**: + - src.module.Class +*** End Reasoning + +*** Begin Reasoning +**Second Task** +**content**: This is the second task description +**candidate_identifiers**: + - src.another.Class +*** End Reasoning +""" + + chunks = split_into_chunks(test_stream, chunk_size=30) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Assertions + assert mock_custom_element.update_count == 2 + assert "reasoning_text" in mock_custom_element.props + + reasoning_text = mock_custom_element.props["reasoning_text"] + assert isinstance(reasoning_text, str) + + # Both blocks should be concatenated + assert "First Task" in reasoning_text + assert "Second Task" in reasoning_text + assert "src.module.Class" in reasoning_text + assert "src.another.Class" in reasoning_text + + print("\n✓ CustomElementStep string concatenation test passed!") + + +@pytest.mark.asyncio +async def test_custom_element_step_dict_merging(mock_custom_element, fallback_msg): + """Test CustomElementStep merging extracted fields into dict.""" + + # Setup CustomElementStep with dict type + props_schema = { + "metadata": dict, + } + custom_step = CustomElementStep(mock_custom_element, props_schema) + + # Simple field patterns for metadata + field_patterns = { + "status": r"status:\s*(\w+)", + "count": r"count:\s*(\d+)", + } + + extractor = FieldExtractor(field_patterns) + metadata_config = MarkerConfig( + begin_marker="### Begin Metadata", + end_marker="### End Metadata", + marker_id="metadata", + target_step=custom_step, + stream_mode="full", + field_extractor=extractor + ) + + processor = StreamProcessor( + marker_configs=[metadata_config], + global_fallback_msg=fallback_msg + ) + + # Process stream + test_stream = """ +### Begin Metadata +status: active +count: 42 +### End Metadata +""" + + chunks = split_into_chunks(test_stream, chunk_size=20) + for chunk in chunks: + await processor.process_chunk(chunk) + + await processor.finalize() + + # Assertions + assert "metadata" in mock_custom_element.props + metadata = mock_custom_element.props["metadata"] + assert isinstance(metadata, dict) + assert metadata.get("status") == "active" + assert metadata.get("count") == "42" + + print("\n✓ CustomElementStep dict merging test passed!") + + @pytest.mark.asyncio async def test_chunk_mode_immediate_streaming(code_step, fallback_msg): """Test that chunk mode streams content immediately without accumulation.""" - + # Setup code_config = MarkerConfig( begin_marker="```python", end_marker="```", + marker_id="code", start_wrapper="```python\n", end_wrapper="\n```", target_step=code_step, @@ -249,12 +451,13 @@ async def test_multiple_configs_mixed_modes( field_patterns, reasoning_step, code_step, fallback_msg, mixed_content_stream ): """Test multiple marker configs with different streaming modes.""" - + # Setup reasoning config (full mode) reasoning_extractor = FieldExtractor(field_patterns) reasoning_config = MarkerConfig( begin_marker="*** Begin Reasoning", end_marker="*** End Reasoning", + marker_id="reasoning", target_step=reasoning_step, stream_mode="full", field_extractor=reasoning_extractor @@ -264,6 +467,7 @@ async def test_multiple_configs_mixed_modes( code_config = MarkerConfig( begin_marker="```python", end_marker="```", + marker_id="code", start_wrapper="```python\n", end_wrapper="\n```", target_step=code_step, @@ -306,7 +510,6 @@ async def test_multiple_configs_mixed_modes( @pytest.mark.asyncio async def test_field_extractor_list_extraction(field_patterns): """Test that list extraction works correctly for candidate_identifiers.""" - extractor = FieldExtractor(field_patterns) @@ -319,13 +522,14 @@ async def test_field_extractor_list_extraction(field_patterns): """ # Extract fields - extracted = extractor.extract(test_content) + extracted = extractor.extract(test_content, marker_id="test") # Extract list specifically identifiers = extractor.extract_list(test_content, "candidate_identifiers") # Assertions assert isinstance(extracted, ExtractedFields) + assert extracted.marker_id == "test" assert extracted.fields["header"] == "Task Header" assert "Some description here" in extracted.fields["content"] @@ -345,6 +549,7 @@ async def test_incomplete_block_handling(field_patterns, reasoning_step, fallbac reasoning_config = MarkerConfig( begin_marker="*** Begin Reasoning", end_marker="*** End Reasoning", + marker_id="reasoning", target_step=reasoning_step, stream_mode="full", field_extractor=extractor @@ -382,10 +587,11 @@ async def test_incomplete_block_handling(field_patterns, reasoning_step, fallbac @pytest.mark.asyncio async def test_no_field_extractor_full_mode(reasoning_step, fallback_msg): """Test full mode without field extractor (should stream raw content).""" - + reasoning_config = MarkerConfig( begin_marker="*** Begin Reasoning", end_marker="*** End Reasoning", + marker_id="reasoning", target_step=reasoning_step, stream_mode="full", field_extractor=None # No extractor @@ -418,6 +624,31 @@ async def test_no_field_extractor_full_mode(reasoning_step, fallback_msg): print("\n✓ Full mode without field extractor test passed!") +@pytest.mark.asyncio +async def test_extracted_fields_to_dict(): + """Test ExtractedFields.to_dict() method.""" + + fields_data = { + "header": "Test Header", + "content": "Test content", + "items": ["item1", "item2"] + } + + extracted = ExtractedFields( + marker_id="test_marker", + raw_content="raw text", + fields=fields_data + ) + + result = extracted.to_dict() + + assert result["marker_id"] == "test_marker" + assert result["fields"] == fields_data + assert "raw_content" not in result # to_dict should not include raw_content + + print("\n✓ ExtractedFields.to_dict() test passed!") + + if __name__ == "__main__": # Run with pytest pytest.main([__file__, "-v", "-s"]) \ No newline at end of file From 271d95a13fde03c6de5473230be337ae127f3adc Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 8 Oct 2025 22:06:31 +0100 Subject: [PATCH 013/138] feat(ui): This commit adds finished reasoning summary title --- .../agents/tide/ui/public/elements/ReasoningExplorer.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index f771ac4..03d2bfb 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -48,7 +48,7 @@ export default function ReasoningStepsCard() {
- Reasoning Process + {props.finished ? "Reasoning Completed" : "Reasoning ..."} {!props.finished && ( @@ -65,6 +65,11 @@ export default function ReasoningStepsCard() { )}
+ {props.finished && props.summary && ( +
+ {props.summary} +
+ )} {/* Timeline */} @@ -72,7 +77,7 @@ export default function ReasoningStepsCard() { {/* Reasoning Steps */} {props.reasoning_steps.map((step, index) => ( -
+
{/* Timeline Icon + Connector */}
From 3c6fed28ad51bf449101201bf8e99842169f8ced Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 8 Oct 2025 23:25:16 +0100 Subject: [PATCH 014/138] refactor(prompts): extend tide agent prompts with candidate gathering and final selection phases --- codetide/agents/tide/prompts.py | 268 +++++++++++++++++++++++++++++++- 1 file changed, 267 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 99b279f..ac495fb 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -562,4 +562,270 @@ - **Focus on obvious patterns**: Look for clear naming matches with user request **REMEMBER**: This is rapid identifier selection based on educated guessing from file/directory structure. Your job is to quickly identify likely relevant files based on naming patterns and organization. Make reasonable assumptions and avoid perfectionist analysis. Speed and decisiveness over exhaustive exploration. -""" \ No newline at end of file +""" + +GATHER_CANDIDATES_PROMPT = """ +You are Agent **Tide**, operating in **Candidate Gathering Mode** on **{DATE}**. + +**SUPPORTED_LANGUAGES** are: {SUPPORTED_LANGUAGES} + +**ABSOLUTE PROHIBITION - NEVER UNDER ANY CIRCUMSTANCE:** +- Answer or address the user request directly or indirectly +- Provide solutions, suggestions, or advice about the user's problem +- View or analyze file contents +- Check implementation details inside files +- Verify inter-file dependencies +- Write solutions or code modifications +- Access actual identifier definitions +- Use markdown formatting, bold text, italics, headers, code blocks, or any special formatting whatsoever + +**YOUR SOLE PURPOSE:** Explore repository structure to identify ALL potential candidate identifiers through iterative tree expansion. + +**PHASE 1 MISSION:** Gather comprehensive candidate identifiers by expanding the repository tree strategically. + +**Current State:** +- **Repository tree**: {TREE_STATE} +- **User request**: Analyze to determine relevant areas +- **Previous iteration context**: {ACCUMULATED_CONTEXT} +- **Iteration number**: {ITERATION_COUNT} + +**EXPLORATION STRATEGY:** + +1. **Analyze User Request**: Identify key functional areas, components, and concepts mentioned +2. **Scan Current Tree**: Look for directories and files that match these areas +3. **Identify Gaps**: Determine what's collapsed or hidden that could contain relevant code +4. **Strategic Expansion**: Request expansion of promising directories to reveal file structure + +**REASONING OUTPUT FORMAT:** + +For each relevant area or task you identify, output: + +``` +*** Begin Reasoning +**Task**: [Brief task description based on user request] +**Rationale**: [Why this area is relevant, which files/directories look promising based on naming] +**Candidate Identifiers**: + - [fully.qualified.identifier or path/to/file.ext] + - [another.identifier.or.path] +*** End Reasoning +``` + +**IDENTIFIER SELECTION RULES:** + +1. **Language-Based Selection:** + - For files in **SUPPORTED_LANGUAGES**: Return code identifiers (functions, classes, methods) using dot notation + - For files NOT in SUPPORTED_LANGUAGES: Return file paths only + - Do NOT include package names, imports, or external dependencies + +2. **Candidate Types:** + - Include identifiers for functions, classes, methods, variables, or attributes you believe exist in the codebase + - Include file paths for non-code files that might be relevant + - Make educated guesses based on file/directory naming patterns + +3. **Accumulation Approach:** + - Add NEW candidates not yet in {ACCUMULATED_CONTEXT} + - Build comprehensive coverage across iterations + - Don't worry about over-inclusion - Phase 2 will filter + +**EXPANSION DECISION:** + +After outputting reasoning blocks, decide on expansion: + +``` +*** Begin Expand Paths +[path/to/directory/] +[another/path/] +*** End Expand Paths +``` + +**Expand When:** +- Core directories related to user request are collapsed +- Cannot see file names in obviously relevant areas +- Need to explore subdirectories to find specific components +- File structure is insufficient to identify candidates + +**SUFFICIENCY ASSESSMENT:** + +Evaluate if you have gathered enough candidates: + +**SUFFICIENT (TRUE) when:** +- All major functional areas from user request have been explored +- Relevant directories are expanded enough to see file organization +- Can identify files/identifiers for all key tasks implied by request +- Further expansion unlikely to reveal significantly new relevant areas + +**INSUFFICIENT (FALSE) when:** +- Core directories mentioned in request are still collapsed +- Cannot identify candidates for obvious tasks from user request +- Major functional areas are hidden or unexplored +- Expansion would likely reveal important relevant files + +Output: +``` +ENOUGH_IDENTIFIERS: [TRUE|FALSE] +``` + +**HISTORY ASSESSMENT:** + +Determine if more conversation history is needed: + +**NEED HISTORY (FALSE) when:** +- Current user message is self-contained +- Request is clear without additional context +- No references to previous discussion +- First iteration only + +**NEED HISTORY (TRUE) when:** +- User references "it", "that", "the earlier discussion" +- Request builds on previous conversation +- Context from earlier messages would clarify intent +- Ambiguous request that prior messages might resolve + +Output: +``` +ENOUGH_HISTORY: [TRUE|FALSE] +``` + +**COMPLETE OUTPUT STRUCTURE:** + +[Plain text reasoning paragraph explaining your analysis approach] + +*** Begin Reasoning +**Task**: [task name] +**Rationale**: [explanation] +**Candidate Identifiers**: + - [identifier or path] +*** End Reasoning + +[Additional reasoning blocks as needed] + +*** Begin Expand Paths +[paths to expand, one per line, or empty] +*** End Expand Paths + +ENOUGH_IDENTIFIERS: [TRUE|FALSE] +ENOUGH_HISTORY: [TRUE|FALSE] + +**GUIDELINES:** +- Be thorough in gathering candidates - Phase 2 will refine +- Trust file naming patterns to identify likely relevant code +- Expand strategically to maximize information per iteration +- Aim to complete gathering in 2-3 iterations maximum +- Make decisive sufficiency assessments +""" + +# Phase 2: Final Selection and Classification Prompt +FINALIZE_IDENTIFIERS_PROMPT = """ +You are Agent **Tide**, operating in **Final Selection Mode** on **{DATE}**. + +**SUPPORTED_LANGUAGES** are: {SUPPORTED_LANGUAGES} + +**ABSOLUTE PROHIBITION - NEVER UNDER ANY CIRCUMSTANCE:** +- Answer or address the user request directly or indirectly +- Provide solutions, suggestions, or advice +- View or analyze file contents +- Write code or modifications +- Use markdown formatting, bold text, italics, headers, code blocks, or any special formatting + +**YOUR SOLE PURPOSE:** Review all gathered candidates and make final classification decisions. + +**PHASE 2 MISSION:** +1. Synthesize reasoning from Phase 1 +2. Classify candidates into Context vs Modify identifiers +3. Determine operation mode for downstream processing + +**INPUT ANALYSIS:** + +You are receiving: +- **User request**: {USER_REQUEST} +- **All Phase 1 reasoning**: Complete exploration and candidate gathering +- **Candidate pool**: {ALL_CANDIDATES} + +**CLASSIFICATION RULES:** + +**Context Identifiers:** +- Functions, classes, methods, variables that provide understanding +- Supporting code that won't be directly modified +- Base classes, utilities, configuration files +- Dependencies needed to understand modification scope +- Code that defines interfaces or contracts + +**Modify Identifiers:** +- Functions, classes, methods, variables requiring direct changes +- Code that implements the specific behavior to be altered +- Files where new code will be added +- Entities that must be updated to satisfy user request + +**CRITICAL CONSTRAINTS:** +- Only include actual code elements (functions, classes, methods, variables, attributes) +- NEVER include package names, import statements, or external dependencies +- NEVER include identifiers that represent only imports or modules without concrete definitions +- Even if present in candidate pool, exclude non-code-element identifiers + +**OPERATION MODE DETERMINATION:** + +Analyze the user request and determine appropriate processing mode(s): + +**STANDARD:** +- General questions, explanations, documentation +- Conceptual discussions about code +- No code modification or planning required +- Information retrieval tasks + +**PLAN_STEPS:** +- Multi-step implementation required +- Complex feature additions +- Refactoring across multiple files +- Architecture changes +- Requires sequential task planning + +**PATCH_CODE:** +- Direct code modifications needed +- Bug fixes, updates, improvements +- Concrete implementation tasks +- Writing or editing specific code + +**Mode can be combined**: `STANDARD+PLAN_STEPS`, `PLAN_STEPS+PATCH_CODE`, etc. + +**OUTPUT FORMAT:** + +[Plain text summary paragraph synthesizing Phase 1 exploration and your final decisions] + +*** Begin Summary +[Comprehensive summary of the reasoning from Phase 1, highlighting key areas identified, exploration path taken, and rationale for final identifier selection] +*** End Summary + +*** Begin Context Identifiers +[identifier.one] +[identifier.two] +[path/to/file.ext] +*** End Context Identifiers + +*** Begin Modify Identifiers +[identifier.to.modify] +[another.identifier] +*** End Modify Identifiers + +OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE|combinations] + +**DECISION GUIDELINES:** + +**For Context vs Modify:** +- When uncertain, prefer Context (safer for providing understanding) +- If request says "use X to do Y", X is Context, Y-related code is Modify +- If request is about understanding/explaining, most/all are Context +- If request is about changing behavior, focus on Modify + +**For Operation Mode:** +- Default to STANDARD for questions and explanations +- Use PLAN_STEPS when request implies "add feature", "implement", "refactor" +- Use PATCH_CODE when request is "fix", "update", "change this specific thing" +- Combine when both planning AND implementation are clearly needed + +**QUALITY CHECKS:** +- Verify all identifiers are actual code elements, not imports/packages +- Ensure Modify identifiers align with what will actually change +- Confirm Context identifiers provide necessary understanding +- Check Operation Mode matches request intent +- Keep identifier lists focused and relevant +""" From 48e51ca06ec3003389af5ab6d979b16a81ed10a8 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 8 Oct 2025 23:25:44 +0100 Subject: [PATCH 015/138] refactor(agent): implement two-phase identifier resolution and improve context extraction in agent loop --- codetide/agents/tide/agent.py | 278 ++++++++++++++++++++++------------ 1 file changed, 177 insertions(+), 101 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 6d03d42..f53c9c0 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -1,3 +1,4 @@ +import json from codetide import CodeTide from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file, parse_patch_blocks from ...core.defaults import DEFAULT_STORAGE_PATH @@ -5,7 +6,7 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -34,6 +35,8 @@ import pygit2 import os +ROUND_FINISHED = "" + class AgentTide(BaseModel): llm :Llm tide :CodeTide @@ -126,11 +129,170 @@ def _clean_history(self): message = self.history[i] if isinstance(message, dict): self.history[i] = message.get("content" ,"") + + async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdentifiers=None, TODAY :str=None): + """ + Two-phase identifier resolution: + Phase 1: Gather candidates through iterative tree expansion + Phase 2: Classify and finalize identifiers with operation mode + """ + # Initialize tracking + last_message = self.history[-1] if self.history else "" + matches = autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] + + self._context_identifier_window.append(set(matches)) + if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: + self._context_identifier_window.pop(0) + + window_identifiers = set() + for s in self._context_identifier_window: + window_identifiers.update(s) + + initial_identifiers = set(codeIdentifiers) if codeIdentifiers else set() + initial_identifiers.update(window_identifiers) + + # ===== PHASE 1: CANDIDATE GATHERING ===== + candidate_pool = set() + all_reasoning = [] + iteration_count = 0 + max_iterations = 3 + repo_tree = None + expand_paths = ["./"] + enough_identifiers = False + expanded_history = list(self.history)[-3:] # Track expanded history + + while not enough_identifiers and iteration_count < max_iterations: + iteration_count += 1 + + # Get current repo tree state + if repo_tree is None or iteration_count > 1: + repo_tree = await self.get_repo_tree_from_user_prompt( + expanded_history, + include_modules=bool(iteration_count > 1), + expand_paths=expand_paths + ) + + # Prepare accumulated context + accumulated_context = "\n".join(sorted(candidate_pool)) if candidate_pool else "None yet" + + # Phase 1 LLM call + phase1_response = await self.llm.acomplete( + expanded_history, + system_prompt=[GATHER_CANDIDATES_PROMPT.format( + DATE=TODAY, + SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, + TREE_STATE="Current view" if iteration_count == 1 else "Expanded view", + ACCUMULATED_CONTEXT=accumulated_context, + ITERATION_COUNT=iteration_count + )], + prefix_prompt=repo_tree, + stream=True + ) + + print(f"Phase 1 Iteration {iteration_count}: {phase1_response}") + + # Parse Phase 1 response + reasoning_blocks = parse_blocks(phase1_response, block_word="Reasoning", multiple=True) + expand_paths_block = parse_blocks(phase1_response, block_word="Expand Paths", multiple=False) + + # Extract and accumulate candidates from reasoning blocks + for reasoning in reasoning_blocks: + all_reasoning.append(reasoning) + # Extract candidate identifiers from reasoning block + if "**Candidate Identifiers**:" in reasoning or "**candidate_identifiers**:" in reasoning.lower(): + lines = reasoning.split('\n') + capture = False + for line in lines: + if "candidate" in line.lower() and "identifier" in line.lower(): + capture = True + continue + if capture and line.strip().startswith('-'): + ident = line.strip().lstrip('-').strip() + if ident := self.get_valid_identifier(autocomplete, ident): + candidate_pool.add(ident) + + # Check if we need to expand more + if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper(): + enough_identifiers = True + + # Check if we need more history + if "ENOUGH_HISTORY: FALSE" in phase1_response.upper() and iteration_count == 1: + # Load more history for next iteration + # TODO this should be imcremental i.e starting += 2 each time! + expanded_history = self.history[-5:] if len(self.history) > 1 else self.history + + # Parse expansion paths for next iteration + if expand_paths_block and not enough_identifiers: + expand_paths = [ + path.strip() for path in expand_paths_block.strip().split('\n') + if path.strip() and self.get_valid_identifier(autocomplete, path.strip()) + ] + else: + expand_paths = [] + + # ===== PHASE 2: FINAL SELECTION AND CLASSIFICATION ===== + + # Prepare Phase 2 input + all_reasoning_text = "\n\n".join(all_reasoning) + all_candidates_text = "\n".join(sorted(candidate_pool)) + + phase2_response = await self.llm.acomplete( + expanded_history, + system_prompt=[FINALIZE_IDENTIFIERS_PROMPT.format( + DATE=TODAY, + SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, + USER_REQUEST=last_message, + ALL_CANDIDATES=all_candidates_text + )], + prefix_prompt=f"Phase 1 Exploration Results:\n\n{all_reasoning_text}", + stream=True + ) + + print(f"Phase 2 Final Selection: {phase2_response}") + + # Parse Phase 2 response + summary = parse_blocks(phase2_response, block_word="Summary", multiple=False) + context_identifiers = parse_blocks(phase2_response, block_word="Context Identifiers", multiple=False) + modify_identifiers = parse_blocks(phase2_response, block_word="Modify Identifiers", multiple=False) + + # Extract operation mode + operation_mode = "STANDARD" # default + if "OPERATION_MODE:" in phase2_response: + mode_line = [line for line in phase2_response.split('\n') if 'OPERATION_MODE:' in line] + if mode_line: + operation_mode = mode_line[0].split('OPERATION_MODE:')[1].strip() + + # Process final identifiers + final_context = set() + final_modify = set() + + if context_identifiers: + for ident in context_identifiers.strip().split('\n'): + if ident := self.get_valid_identifier(autocomplete, ident.strip()): + final_context.add(ident) + + if modify_identifiers: + for ident in modify_identifiers.strip().split('\n'): + if ident := self.get_valid_identifier(autocomplete, ident.strip()): + final_modify.add(ident) + + return { + "matches": matches, + "context_identifiers": list(final_context), + "modify_identifiers": self.tide._as_file_paths(list(final_modify)), + "operation_mode": operation_mode, + "summary": summary, + "expanded_history": expanded_history, # Make available for downstream + "all_reasoning": all_reasoning_text, + "iteration_count": iteration_count + } async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): TODAY = date.today() await self.tide.check_for_updates(serialize=True, include_cached_ids=True) + print("Finished check for updates") self._clean_history() + print("Finished clean history") # Initialize the context identifier window if not present if self._context_identifier_window is None: @@ -138,9 +300,10 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): codeContext = None if self._skip_context_retrieval: - ... + expanded_history = self.history[-1] else: autocomplete = AutoComplete(self.tide.cached_ids) + print(f"{autocomplete=}") if self._direct_mode: self.contextIdentifiers = None # Only extract matches from the last message @@ -153,105 +316,17 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self._context_identifier_window.append(set(exact_matches)) if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: self._context_identifier_window.pop(0) + expanded_history = self.history[-5:] + operation_mode = "STANDARD, PATH_CODE" + ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history else: - # Only extract matches from the last message - last_message = self.history[-1] if self.history else "" - matches = autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] - print(f"{matches=}") - # Update the context identifier window - self._context_identifier_window.append(set(matches)) - if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: - self._context_identifier_window.pop(0) - # Combine identifiers from the last N interactions - window_identifiers = set() - for s in self._context_identifier_window: - window_identifiers.update(s) - # If codeIdentifiers is passed, include them as well - identifiers_accum = set(codeIdentifiers) if codeIdentifiers else set() - identifiers_accum.update(window_identifiers) - modify_accum = set() - reasoning_accum = [] - repo_tree = None - smart_search_attempts = 0 - max_smart_search_attempts = 3 - done = False - previous_reason = None - - while not done: - expand_paths = ["./"] - # 1. SmartCodeSearch to filter repo tree - if repo_tree is None or smart_search_attempts > 0: - repo_history = self.history - if previous_reason: - repo_history += [previous_reason] - - repo_tree = await self.get_repo_tree_from_user_prompt(self.history, include_modules=bool(smart_search_attempts), expand_paths=expand_paths) - - # 2. Single LLM call with unified prompt - # Pass accumulated identifiers for context if this isn't the first iteration - accumulated_context = "\n".join( - sorted((identifiers_accum or set()) | (modify_accum or set())) - ) if (identifiers_accum or modify_accum) else "" - - unified_response = await self.llm.acomplete( - self.history, - system_prompt=[GET_CODE_IDENTIFIERS_UNIFIED_PROMPT.format( - DATE=TODAY, - SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, - IDENTIFIERS=accumulated_context - )], - prefix_prompt=repo_tree, - stream=False - ) - - # Parse the unified response - contextIdentifiers = parse_blocks(unified_response, block_word="Context Identifiers", multiple=False) - modifyIdentifiers = parse_blocks(unified_response, block_word="Modify Identifiers", multiple=False) - expandPaths = parse_blocks(unified_response, block_word="Expand Paths", multiple=False) - - # Extract reasoning (everything before the first "*** Begin") - reasoning_parts = unified_response.split("*** Begin") - if reasoning_parts: - reasoning_accum.append(reasoning_parts[0].strip()) - previous_reason = reasoning_accum[-1] - - # Accumulate identifiers - if contextIdentifiers: - if smart_search_attempts == 0: - identifiers_accum = set() - for ident in contextIdentifiers.splitlines(): - if ident := self.get_valid_identifier(autocomplete, ident.strip()): - identifiers_accum.add(ident) - - if modifyIdentifiers: - for ident in modifyIdentifiers.splitlines(): - if ident := self.get_valid_identifier(autocomplete, ident.strip()): - modify_accum.add(ident.strip()) - - if expandPaths: - expand_paths = [ - path for ident in expandPaths if (path := self.get_valid_identifier(autocomplete, ident.strip())) - ] - - # Check if we have enough identifiers (unified prompt includes this decision) - if "ENOUGH_IDENTIFIERS: TRUE" in unified_response.upper(): - done = True - else: - smart_search_attempts += 1 - if smart_search_attempts >= max_smart_search_attempts: - done = True - - # Finalize identifiers - self.reasoning = "\n\n".join(reasoning_accum) - self.contextIdentifiers = list(identifiers_accum) if identifiers_accum else None - self.modifyIdentifiers = list(modify_accum) if modify_accum else None - - codeIdentifiers = self.contextIdentifiers or [] - if self.modifyIdentifiers: - self.modifyIdentifiers = self.tide._as_file_paths(self.modifyIdentifiers) - codeIdentifiers.extend(self.modifyIdentifiers) - # TODO preserve passed identifiers by the user - codeIdentifiers += matches + reasoning_output = await self.get_identifiers_two_phase(autocomplete, codeIdentifiers, TODAY) + print(json.dumps(reasoning_output, indent=4)) + + codeIdentifiers = reasoning_output.get("context_identifiers", []) + reasoning_output.get("modify_identifiers", []) + matches = reasoning_output.get("matches") + operation_mode = reasoning_output.get("operation_mode") + expanded_history = reasoning_output.get("expanded_history") # --- End Unified Identifier Retrieval --- if codeIdentifiers: @@ -268,7 +343,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self._last_code_context = codeContext await delete_file(self.patch_path) response = await self.llm.acomplete( - self.history, + expanded_history, system_prompt=[ AGENT_TIDE_SYSTEM_PROMPT.format(DATE=TODAY), STEPS_SYSTEM_PROMPT.format(DATE=TODAY), @@ -300,6 +375,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): response = response.replace(f"*** Begin Patch\n{patch}*** End Patch", "") self.history.append(response) + await self.llm.logger_fn(ROUND_FINISHED) @staticmethod async def get_git_diff_staged_simple(directory: str) -> str: From 31c8d9e9c7a88418d3683fdd85a923de68e6944b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 8 Oct 2025 23:26:35 +0100 Subject: [PATCH 016/138] refactor(ui): update agent_loop stream handling and reasoning UI integration This commit refactors the agent_loop in codetide/agents/tide/ui/app.py by removing the loading message display, replacing direct STREAM_START_TOKEN checks with a more general SPECIAL_TOKENS filter, and integrating a new ReasoningExplorer custom element with enhanced marker configurations for reasoning steps, summaries, and identifier tracking. It also imports and uses ROUND_FINISHED token for stream completion handling, cleans up commented-out ticket UI code, and adjusts stream processing logic to better support reasoning step updates and UI synchronization. --- codetide/agents/tide/ui/app.py | 136 +++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 41 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 292eacb..fc485af 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -9,7 +9,7 @@ from aicore.config import Config from aicore.llm import Llm, LlmConfig from aicore.models import AuthenticationError, ModelError - from aicore.const import STREAM_END_TOKEN, STREAM_START_TOKEN#, REASONING_START_TOKEN, REASONING_STOP_TOKEN + from aicore.const import SPECIAL_TOKENS # STREAM_END_TOKEN, STREAM_START_TOKEN#, REASONING_START_TOKEN, REASONING_STOP_TOKEN from codetide.agents.tide.ui.utils import process_thread, send_reasoning_msg from codetide.agents.tide.ui.persistance import check_docker, launch_postgres from codetide.agents.tide.ui.stream_processor import StreamProcessor, MarkerConfig @@ -29,6 +29,8 @@ "Install it with: pip install codetide[agents-ui]" ) from e +from codetide.agents.tide.agent import ROUND_FINISHED +from codetide.agents.tide.ui.stream_processor import CustomElementStep, FieldExtractor from codetide.agents.tide.ui.defaults import AICORE_CONFIG_EXAMPLE, EXCEPTION_MESSAGE, MISSING_CONFIG_MESSAGE from codetide.agents.tide.defaults import DEFAULT_AGENT_TIDE_LLM_CONFIG_PATH from codetide.core.defaults import DEFAULT_ENCODING @@ -140,24 +142,26 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): ], "context_identifiers": ["user_context", "system_requirements", "api_documentation"], "modify_identifiers": ["configuration_settings", "user_preferences"], + "summary": "no summary yet", "finished": False } """ *** Begin Reasoning -1. **first task header** - **content**: brief summary of the logic behind this task and the files to look into and why - **candidate_identifiers**: - - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to use as context +**first task header** +**content**: brief summary of the logic behind this task and the files to look into and why +**candidate_identifiers**: + - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to use as context *** End Reasoning *** Begin Reasoning -2. **first task header** - **content**: brief summary of the logic behind this task and the files to look into and why - **candidate_identifiers**: - - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to modify or update +**first task header** +**content**: brief summary of the logic behind this task and the files to look into and why +**candidate_identifiers**: + - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to modify or update *** End Reasoning """ ### use current expansion logic here then move to the next one once all possible candidate_identifiers have been found +### decide here together with expand_paths if we need to expand history i.e load older messages """ *** Begin Summary @@ -195,6 +199,7 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): ], "context_identifiers": ["application_logs", "performance_metrics", "system_architecture"], "modify_identifiers": ["memory_management_module", "allocation_policies"], + "summary": "This is the final reasoning summary", "finished": True } @@ -248,17 +253,17 @@ async def start_chat(): await cl.context.emitter.set_commands(AgentTideUi.commands) cl.user_session.set("chat_history", []) - props = await get_ticket() + # props = await get_ticket() - ticket_element = cl.CustomElement(name="LinearTicket", props=props) - # Store the element if we want to update it server side at a later stage. - cl.user_session.set("ticket_el", ticket_element) + # ticket_element = cl.CustomElement(name="LinearTicket", props=props) + # # Store the element if we want to update it server side at a later stage. + # cl.user_session.set("ticket_el", ticket_element) - await cl.Message(content="Here is the ticket information!", elements=[ticket_element]).send() + # await cl.Message(content="Here is the ticket information!", elements=[ticket_element]).send() - await asyncio.sleep(2) - ticket_element.props["title"] = "Could not Fix Authentication Bug" - await ticket_element.update() + # await asyncio.sleep(2) + # ticket_element.props["title"] = "Could not Fix Authentication Bug" + # await ticket_element.update() card_element = cl.CustomElement(name="ReasoningExplorer", props=example1) await cl.Message(content="", elements=[card_element]).send() @@ -438,19 +443,19 @@ async def on_reject_patch(action :cl.Action): @cl.on_message async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Optional[list] = None, agent_tide_ui :Optional[AgentTideUi]=None): - loading_msg = await cl.Message( - content="", - elements=[ - cl.CustomElement( - name="LoadingMessage", - props={ - "messages": ["Working", "Syncing CodeTide", "Thinking", "Looking for context"], - "interval": 1500, # 1.5 seconds between messages - "showIcon": True - } - ) - ] - ).send() + # loading_msg = await cl.Message( + # content="", + # elements=[ + # cl.CustomElement( + # name="LoadingMessage", + # props={ + # "messages": ["Working", "Syncing CodeTide", "Thinking", "Looking for context"], + # "interval": 1500, # 1.5 seconds between messages + # "showIcon": True + # } + # ) + # ] + # ).send() if agent_tide_ui is None: agent_tide_ui = await loadAgentTideUi() @@ -466,7 +471,26 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option chat_history.append({"role": "user", "content": message.content}) await agent_tide_ui.add_to_history(message.content) - context_msg = cl.Message(content="", author="AgentTide") + context_msg = cl.Message(content="", author="AgentTide").send() + reasoning_element = cl.CustomElement(name="ReasoningExplorer", props={ + "reasoning_steps": [], + "summary": "", + "context_identifiers": [], # amrker + "modify_identifiers": [], + "finished": False + }) + ### TODO this needs to receive the message as well to call update + reasoning_step = CustomElementStep( + element=reasoning_element, + props_schema = { + "reasoning_steps": list, # Will accumulate reasoning blocks as list + "summary": str, + "context_identifiers": list, + "modify_identifiers": list + } + ) + + msg = cl.Message(content="", author="Agent Tide") # ReasoningCustomElementStep = CustomElementStep() @@ -497,12 +521,41 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option start_wrapper="\n```shell\n", end_wrapper="\n```\n", target_step=msg + ), + MarkerConfig( + marker_id="reasoning_steps", + begin_marker="*** Begin Reasoning", + end_marker="*** End Reasoning", + target_step=reasoning_step, + stream_mode="full", + field_extractor=FieldExtractor({ + "header": r"\*\*([^*]+)\*\*(?=\s*\n\s*\*\*content\*\*)", + "content": r"\*\*content\*\*:\s*(.+?)(?=\s*\*\*candidate_identifiers\*\*|$)", + "candidate_identifiers": r"^\s*-\s*(.+?)$" + }) + ), + MarkerConfig( + marker_id="summary", + begin_marker="*** Begin Summary", + end_marker="*** End Summary", + target_step=reasoning_step, + stream_mode="full" + ), + MarkerConfig( + marker_id="context_identifiers", + begin_marker="*** Begin Context Identifiers", + end_marker="*** End Context Identifiers", + target_step=reasoning_step, + stream_mode="full" + ), + MarkerConfig( + marker_id="modify_identifiers", + begin_marker="*** Begin Modify Identifiers", + end_marker="*** End Modify Identifiers", + target_step=reasoning_step, + stream_mode="full" ), - # MarkerConfig( - # begin_marker="*** Begin Reasoning", - # end_marker="*** End Reasoning", - # target_step=CustomElement - # ) + ], global_fallback_msg=msg ) @@ -514,14 +567,15 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option ### TODO update this to check FROM AGENT TIDE if reasoning is being ran and if so we need ### to send is finished true to custom element when the next STREAM_START_TOKEN_arrives - if chunk == STREAM_START_TOKEN: - is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) + if chunk in SPECIAL_TOKENS: continue + # is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) + # continue - elif not is_reasonig_sent: - is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) + # elif not is_reasonig_sent: + # is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) - elif chunk == STREAM_END_TOKEN: + elif chunk == ROUND_FINISHED: # Handle any remaining content await stream_processor.finalize() await asyncio.sleep(0.5) From 8a53edeb9ea84d480c3734fb4d563e6deb332f07 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 9 Oct 2025 00:02:34 +0100 Subject: [PATCH 017/138] refactor(ui): enhance CustomElementStep prop updates with smart merging logic --- codetide/agents/tide/ui/stream_processor.py | 87 ++++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 3e1563c..9af81d6 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -46,6 +46,72 @@ def _initialize_props(self) -> Dict[str, any]: initialized[marker_id] = None return initialized + def _smart_update_props(self, updates: Dict[str, any]) -> None: + """ + Update props dict based on the type of each value. + - list: append new items + - str: concatenate strings + - dict: merge/update dictionaries + - set: union sets + - int/float: add values + - bool: logical OR + - other: replace value + + Args: + updates: Dictionary of prop updates to apply + """ + for key, new_value in updates.items(): + if key not in self.props: + # If key doesn't exist, just set it + self.props[key] = new_value + continue + + current_value = self.props[key] + prop_type = self.props_schema.get(key) + + # Handle based on type + if prop_type is list or isinstance(current_value, list): + if isinstance(new_value, list): + self.props[key].extend(new_value) + else: + self.props[key].append(new_value) + + elif prop_type is str or isinstance(current_value, str): + if isinstance(new_value, str): + self.props[key] += new_value + else: + self.props[key] += str(new_value) + + elif prop_type is dict or isinstance(current_value, dict): + if isinstance(new_value, dict): + self.props[key].update(new_value) + else: + # Can't merge non-dict into dict, replace instead + self.props[key] = new_value + + elif prop_type is set or isinstance(current_value, set): + if isinstance(new_value, set): + self.props[key] = self.props[key].union(new_value) + elif isinstance(new_value, (list, tuple)): + self.props[key].update(new_value) + else: + self.props[key].add(new_value) + + elif prop_type in (int, float) or isinstance(current_value, (int, float)): + if isinstance(new_value, (int, float)): + self.props[key] += new_value + else: + self.props[key] = new_value + + elif prop_type is bool or isinstance(current_value, bool): + if isinstance(new_value, bool): + self.props[key] = current_value or new_value + else: + self.props[key] = bool(new_value) + else: + # Default: replace value + self.props[key] = new_value + async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: """ Stream content to the custom element by updating props. @@ -55,8 +121,8 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: """ if isinstance(content, str): # Raw string content - append to default prop if exists - if "content" in self.props and isinstance(self.props["content"], str): - self.props["content"] += content + if "content" in self.props: + self._smart_update_props({"content": content}) return # Handle ExtractedFields dict @@ -68,28 +134,25 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: fields = content.get("fields", {}) if not marker_id or marker_id not in self.props: + print(f"{marker_id=} not in {self.props.keys()=}") return # Update prop based on its type prop_type = self.props_schema.get(marker_id) - current_value = self.props[marker_id] if prop_type is list: # Append fields dict to list - if isinstance(current_value, list): - self.props[marker_id].append(fields) + self._smart_update_props({marker_id: fields}) elif prop_type is str: # Concatenate string representation of fields - if isinstance(current_value, str): - formatted = self._format_fields_as_string(fields) - self.props[marker_id] += formatted + formatted = self._format_fields_as_string(fields) + self._smart_update_props({marker_id: formatted}) elif prop_type is dict: # Merge fields into dict - if isinstance(current_value, dict): - self.props[marker_id].update(fields) + self._smart_update_props({marker_id: fields}) - # Update the element - self.element.props.update(self.props) + # Update the element using smart update + self._smart_update_props(self.props) await self.element.update() def _format_fields_as_string(self, fields: Dict[str, any]) -> str: From 39640ad5261052118cea03c8dd0425622475452b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 9 Oct 2025 00:11:40 +0100 Subject: [PATCH 018/138] fix(ui): correct reasoning element initialization and message sending in agent loop --- codetide/agents/tide/ui/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index fc485af..1978315 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -470,8 +470,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option chat_history.append({"role": "user", "content": message.content}) await agent_tide_ui.add_to_history(message.content) - - context_msg = cl.Message(content="", author="AgentTide").send() + reasoning_element = cl.CustomElement(name="ReasoningExplorer", props={ "reasoning_steps": [], "summary": "", @@ -479,6 +478,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "modify_identifiers": [], "finished": False }) + context_msg = cl.Message(content="", author="AgentTide", elements=reasoning_element).send() ### TODO this needs to receive the message as well to call update reasoning_step = CustomElementStep( element=reasoning_element, @@ -554,8 +554,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option end_marker="*** End Modify Identifiers", target_step=reasoning_step, stream_mode="full" - ), - + # TODO add support to ignore global_fallback_message + ) ], global_fallback_msg=msg ) From 50bc1beb79e1a07940796ea00dd9c261fc9ecd6e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 9 Oct 2025 20:43:14 +0100 Subject: [PATCH 019/138] feat(agent,ui): add reasoning start/finish markers and handle in UI stream processing --- codetide/agents/tide/agent.py | 6 +++++- codetide/agents/tide/ui/app.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index f53c9c0..4f1a3b5 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -36,6 +36,8 @@ import os ROUND_FINISHED = "" +REASONING_STARTED = "" +REASONING_FINISHED = "" class AgentTide(BaseModel): llm :Llm @@ -319,8 +321,10 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): expanded_history = self.history[-5:] operation_mode = "STANDARD, PATH_CODE" ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history - else: + else: + await self.llm.logger_fn(REASONING_STARTED) reasoning_output = await self.get_identifiers_two_phase(autocomplete, codeIdentifiers, TODAY) + await self.llm.logger_fn(REASONING_FINISHED) print(json.dumps(reasoning_output, indent=4)) codeIdentifiers = reasoning_output.get("context_identifiers", []) + reasoning_output.get("modify_identifiers", []) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 1978315..b49ded6 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -29,7 +29,7 @@ "Install it with: pip install codetide[agents-ui]" ) from e -from codetide.agents.tide.agent import ROUND_FINISHED +from codetide.agents.tide.agent import REASONING_FINISHED, REASONING_STARTED, ROUND_FINISHED from codetide.agents.tide.ui.stream_processor import CustomElementStep, FieldExtractor from codetide.agents.tide.ui.defaults import AICORE_CONFIG_EXAMPLE, EXCEPTION_MESSAGE, MISSING_CONFIG_MESSAGE from codetide.agents.tide.defaults import DEFAULT_AGENT_TIDE_LLM_CONFIG_PATH @@ -554,7 +554,6 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option end_marker="*** End Modify Identifiers", target_step=reasoning_step, stream_mode="full" - # TODO add support to ignore global_fallback_message ) ], global_fallback_msg=msg @@ -574,7 +573,16 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option # elif not is_reasonig_sent: # is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) - + elif chunk == REASONING_STARTED: + stream_processor.global_fallback_msg = None + stream_processor.buffer = "" + stream_processor.accumulated_content = "" + + elif chunk == REASONING_FINISHED: + stream_processor.global_fallback_msg = msg + stream_processor.buffer = "" + stream_processor.accumulated_content = "" + elif chunk == ROUND_FINISHED: # Handle any remaining content await stream_processor.finalize() From 4bdb3eee4679708b38a6503cb5c2fa3913151bdb Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 9 Oct 2025 20:46:58 +0100 Subject: [PATCH 020/138] fix(ui): handle control chunk cases properly in agent loop to avoid processing errors --- codetide/agents/tide/ui/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index b49ded6..9efe67b 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -577,11 +577,13 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option stream_processor.global_fallback_msg = None stream_processor.buffer = "" stream_processor.accumulated_content = "" + continue elif chunk == REASONING_FINISHED: stream_processor.global_fallback_msg = msg stream_processor.buffer = "" stream_processor.accumulated_content = "" + continue elif chunk == ROUND_FINISHED: # Handle any remaining content From af395e0a5c64ec29a3753065fc1d0e0c36ac282b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 9 Oct 2025 22:12:15 +0100 Subject: [PATCH 021/138] fix(ui): await message send and optimize CustomElementStep prop updates --- codetide/agents/tide/ui/app.py | 2 +- codetide/agents/tide/ui/stream_processor.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 9efe67b..2002659 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -478,7 +478,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "modify_identifiers": [], "finished": False }) - context_msg = cl.Message(content="", author="AgentTide", elements=reasoning_element).send() + context_msg = await cl.Message(content="", author="AgentTide", elements=[reasoning_element]).send() ### TODO this needs to receive the message as well to call update reasoning_step = CustomElementStep( element=reasoning_element, diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 9af81d6..e43dbb7 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -61,11 +61,6 @@ def _smart_update_props(self, updates: Dict[str, any]) -> None: updates: Dictionary of prop updates to apply """ for key, new_value in updates.items(): - if key not in self.props: - # If key doesn't exist, just set it - self.props[key] = new_value - continue - current_value = self.props[key] prop_type = self.props_schema.get(key) @@ -151,8 +146,7 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: # Merge fields into dict self._smart_update_props({marker_id: fields}) - # Update the element using smart update - self._smart_update_props(self.props) + self.element.props.update(self.props) await self.element.update() def _format_fields_as_string(self, fields: Dict[str, any]) -> str: From bc26e614a616dbe585dd61003c40c6ff1b1f1f57 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 9 Oct 2025 22:24:34 +0100 Subject: [PATCH 022/138] refactor(ui): enhance field extraction with schema support and update stream processing --- codetide/agents/tide/ui/app.py | 2 +- codetide/agents/tide/ui/stream_processor.py | 123 +++++++++++++++----- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 2002659..7bcba2d 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -531,7 +531,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option field_extractor=FieldExtractor({ "header": r"\*\*([^*]+)\*\*(?=\s*\n\s*\*\*content\*\*)", "content": r"\*\*content\*\*:\s*(.+?)(?=\s*\*\*candidate_identifiers\*\*|$)", - "candidate_identifiers": r"^\s*-\s*(.+?)$" + "candidate_identifiers": {"pattern": "^\s*-\s*(.+?)$", "schema": list} }) ), MarkerConfig( diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index e43dbb7..1bc8f7e 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict, Literal, Optional, List, NamedTuple, Union +from typing import Any, Dict, Literal, Optional, List, NamedTuple, Type, Union import chainlit as cl import re @@ -114,16 +114,14 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: Args: content: Either a string (for raw content) or ExtractedFields.fields dict """ - if isinstance(content, str): - # Raw string content - append to default prop if exists - if "content" in self.props: - self._smart_update_props({"content": content}) - return - + # Handle ExtractedFields dict if not isinstance(content, dict): return + + print(f"UPDATING STREAM TOKEN {type(content)=}") + # content should have 'marker_id' and 'fields' marker_id = content.get("marker_id") fields = content.get("fields", {}) @@ -177,20 +175,42 @@ def to_dict(self) -> Dict[str, any]: class FieldExtractor: """Handles extraction of structured fields from marker content.""" - - def __init__(self, field_patterns: Dict[str, str]): + + def __init__(self, field_patterns: Dict[str, Union[str, Dict[str, Any]]]): """ Initialize with field extraction patterns. Args: - field_patterns: Dict mapping field names to regex patterns. - Patterns should have named groups or return the match. + field_patterns: Dict mapping field names to either: + - str: regex pattern (returns string by default) + - dict: {"pattern": str, "schema": type} where schema can be list, str, int, etc. + + Examples: + FieldExtractor({ + "header": r"\*\*([^*]+)\*\*", + "items": {"pattern": r"^\s*-\s*(.+?)$", "schema": list} + }) """ - self.field_patterns = { - name: re.compile(pattern, re.MULTILINE | re.DOTALL) - for name, pattern in field_patterns.items() - } - + self.field_configs = {} + + for name, config in field_patterns.items(): + if isinstance(config, str): + # Simple string pattern - default to string type + self.field_configs[name] = { + "pattern": re.compile(config, re.MULTILINE | re.DOTALL), + "schema": str + } + elif isinstance(config, dict): + # Dict with pattern and schema + pattern = config.get("pattern", "") + schema = config.get("schema", str) + self.field_configs[name] = { + "pattern": re.compile(pattern, re.MULTILINE | re.DOTALL), + "schema": schema + } + else: + raise ValueError(f"Invalid config for field '{name}': must be str or dict") + def extract(self, content: str, marker_id: str = "") -> ExtractedFields: """ Extract all configured fields from content. @@ -203,23 +223,68 @@ def extract(self, content: str, marker_id: str = "") -> ExtractedFields: ExtractedFields object with parsed data """ fields = {} - - for field_name, pattern in self.field_patterns.items(): - match = pattern.search(content) - if match: - # If pattern has named groups, use them - if match.groupdict(): - fields[field_name] = match.groupdict() - # Otherwise use the first group or full match - elif match.groups(): - fields[field_name] = match.group(1).strip() + print("EXTRACTING") + + for field_name, config in self.field_configs.items(): + pattern = config["pattern"] + schema = config["schema"] + + if schema is list: + # For list schema, find all matches + matches = pattern.findall(content) + if matches: + # Clean up the matches + fields[field_name] = [m.strip() if isinstance(m, str) else m for m in matches] else: - fields[field_name] = match.group(0).strip() + fields[field_name] = [] else: - fields[field_name] = None + # For non-list schemas, find first match + match = pattern.search(content) + if match: + # If pattern has named groups, use them + if match.groupdict(): + value = match.groupdict() + # Otherwise use the first group or full match + elif match.groups(): + value = match.group(1).strip() + else: + value = match.group(0).strip() + + # Apply schema conversion + fields[field_name] = self._convert_to_schema(value, schema) + else: + fields[field_name] = None return ExtractedFields(marker_id=marker_id, raw_content=content, fields=fields) - + + def _convert_to_schema(self, value: Any, schema: Type) -> Any: + """ + Convert extracted value to the specified schema type. + + Args: + value: The extracted value + schema: Target type (str, int, float, bool, etc.) + + Returns: + Converted value + """ + if schema is str or value is None: + return value + + try: + if schema is int: + return int(value) + elif schema is float: + return float(value) + elif schema is bool: + return value.lower() in ('true', '1', 'yes', 'on') + else: + # For custom types, attempt direct conversion + return schema(value) + except (ValueError, TypeError): + # If conversion fails, return original value + return value + def extract_list(self, content: str, field_name: str) -> List[str]: """ Extract a list of items (e.g., candidate_identifiers). From c7db8fbc310951f44200aafd68db11f7a6dd1de1 Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:24:01 +0100 Subject: [PATCH 023/138] fix(ui): update field extractor regex patterns in app --- codetide/agents/tide/ui/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 7bcba2d..b59e1d5 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -529,8 +529,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option target_step=reasoning_step, stream_mode="full", field_extractor=FieldExtractor({ - "header": r"\*\*([^*]+)\*\*(?=\s*\n\s*\*\*content\*\*)", - "content": r"\*\*content\*\*:\s*(.+?)(?=\s*\*\*candidate_identifiers\*\*|$)", + "header": r"\*\*Task\*\*:\s*(.+?)(?=\n\s*\*\*Rationale\*\*)", + "content": r"\*\*Rationale\*\*:\s*(.+?)(?=\s*\*\*Candidate Identifiers\*\*|$)", "candidate_identifiers": {"pattern": "^\s*-\s*(.+?)$", "schema": list} }) ), From cc4be396a55f6fdafec5a77b094c51e7ddbe9732 Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:12:32 +0100 Subject: [PATCH 024/138] chore(ui): add debug prints and update reasoning element state --- codetide/agents/tide/ui/app.py | 5 +++++ codetide/agents/tide/ui/stream_processor.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index b59e1d5..b9d392c 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -540,6 +540,9 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option end_marker="*** End Summary", target_step=reasoning_step, stream_mode="full" + ### TODO update marker_config so that default field_extractor returns marker_id: contents as string + ### or list or whatever is specified + ### format should be {markerd_id, no_regex + type if None set to str} ), MarkerConfig( marker_id="context_identifiers", @@ -583,6 +586,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option stream_processor.global_fallback_msg = msg stream_processor.buffer = "" stream_processor.accumulated_content = "" + reasoning_element.props["finished"] =True + await reasoning_element.update() continue elif chunk == ROUND_FINISHED: diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 1bc8f7e..1160b22 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import json from typing import Any, Dict, Literal, Optional, List, NamedTuple, Type, Union import chainlit as cl import re @@ -119,6 +120,7 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: if not isinstance(content, dict): return + ### TODO this is only working with reasoning steps as the received content is not a dict print(f"UPDATING STREAM TOKEN {type(content)=}") @@ -144,6 +146,7 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: # Merge fields into dict self._smart_update_props({marker_id: fields}) + print(json.dumps(self.props, indent=4)) self.element.props.update(self.props) await self.element.update() From e71074664381de8aeda528d8098cc26cc9c63980 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 17:44:32 +0100 Subject: [PATCH 025/138] refactor(ui): enhance stream processing with custom element support and flexible content handling --- codetide/agents/tide/ui/stream_processor.py | 75 ++++++++++++++------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/codetide/agents/tide/ui/stream_processor.py b/codetide/agents/tide/ui/stream_processor.py index 1160b22..19609e4 100644 --- a/codetide/agents/tide/ui/stream_processor.py +++ b/codetide/agents/tide/ui/stream_processor.py @@ -1,6 +1,6 @@ +from typing import Any, Dict, Literal, Optional, List, Type, Union +from pydantic import BaseModel, ConfigDict from dataclasses import dataclass -import json -from typing import Any, Dict, Literal, Optional, List, NamedTuple, Type, Union import chainlit as cl import re @@ -120,13 +120,10 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: if not isinstance(content, dict): return - ### TODO this is only working with reasoning steps as the received content is not a dict - - print(f"UPDATING STREAM TOKEN {type(content)=}") - # content should have 'marker_id' and 'fields' marker_id = content.get("marker_id") - fields = content.get("fields", {}) + fields = content.get("fields") + raw_content = content.get("raw_content") if not marker_id or marker_id not in self.props: print(f"{marker_id=} not in {self.props.keys()=}") @@ -135,18 +132,24 @@ async def stream_token(self, content: Union[str, Dict[str, any]]) -> None: # Update prop based on its type prop_type = self.props_schema.get(marker_id) - if prop_type is list: - # Append fields dict to list - self._smart_update_props({marker_id: fields}) - elif prop_type is str: - # Concatenate string representation of fields - formatted = self._format_fields_as_string(fields) - self._smart_update_props({marker_id: formatted}) - elif prop_type is dict: - # Merge fields into dict - self._smart_update_props({marker_id: fields}) - - print(json.dumps(self.props, indent=4)) + if fields is not None: + if prop_type is list: + # Append fields dict to list + self._smart_update_props({marker_id: fields}) + elif prop_type is str: + # Concatenate string representation of fields + formatted = self._format_fields_as_string(fields) + self._smart_update_props({marker_id: formatted}) + elif prop_type is dict: + # Merge fields into dict + self._smart_update_props({marker_id: fields}) + + elif raw_content is not None: + if raw_content := raw_content.strip(): + if prop_type is list: + raw_content = [stripped for entry in raw_content.split("\n") if (stripped := entry.strip())] + self._smart_update_props({marker_id: raw_content}) + self.element.props.update(self.props) await self.element.update() @@ -167,12 +170,13 @@ class ExtractedFields: """Container for extracted field data from a marker block.""" marker_id: str raw_content: str - fields: Dict[str, any] + fields: Optional[Dict[str, any]]=None def to_dict(self) -> Dict[str, any]: """Convert to dict for streaming to CustomElementStep.""" return { "marker_id": self.marker_id, + "raw_content": self.raw_content, "fields": self.fields } @@ -226,7 +230,6 @@ def extract(self, content: str, marker_id: str = "") -> ExtractedFields: ExtractedFields object with parsed data """ fields = {} - print("EXTRACTING") for field_name, config in self.field_configs.items(): pattern = config["pattern"] @@ -307,17 +310,22 @@ def extract_list(self, content: str, field_name: str) -> List[str]: return [m.strip() if isinstance(m, str) else m[0].strip() for m in matches if m] -class MarkerConfig(NamedTuple): +class MarkerConfig(BaseModel): """Configuration for a single marker pair.""" begin_marker: str end_marker: str marker_id: str = "" start_wrapper: str = "" end_wrapper: str = "" - target_step: Optional[Union[cl.Step, CustomElementStep]] = None + target_step: Optional[Union[cl.Message, cl.Step, CustomElementStep]] = None fallback_msg: Optional[cl.Message] = None stream_mode: Literal["chunk", "full"] = "chunk" field_extractor: Optional[FieldExtractor] = None + _is_custom_element: Optional[bool] = None + + model_config = ConfigDict( + arbitrary_types_allowed=True + ) def process_content(self, content: str) -> Union[str, ExtractedFields]: """ @@ -331,7 +339,25 @@ def process_content(self, content: str) -> Union[str, ExtractedFields]: """ if self.field_extractor: return self.field_extractor.extract(content, self.marker_id) + + elif self.is_custom_element: + return ExtractedFields( + marker_id=self.marker_id, + raw_content=content + ) + return content + + @property + def is_custom_element(self)->bool: + if self._is_custom_element is not None: + ... + elif isinstance(self.target_step, CustomElementStep): + self._is_custom_element = True + else: + self._is_custom_element = False + + return self._is_custom_element class StreamProcessor: """ @@ -483,8 +509,7 @@ async def _process_inside_block(self) -> bool: idx = self.buffer.find(self.current_config.end_marker) # Check if we're in full mode with field extractor - is_full_mode = (self.current_config.stream_mode == "full" and - self.current_config.field_extractor is not None) + is_full_mode = self.current_config.stream_mode == "full" if idx == -1: # No end marker found From 873ce0f4206f89bc59810de4c31c56ce5729ce71 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 22:27:39 +0100 Subject: [PATCH 026/138] refactor(tide): improve history expansion logic and clean debug prints in agent; update candidate gathering and final selection prompts for clarity and formatting --- codetide/agents/tide/agent.py | 12 +- codetide/agents/tide/prompts.py | 273 +++++++++----------------------- codetide/agents/tide/ui/app.py | 2 +- 3 files changed, 81 insertions(+), 206 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 4f1a3b5..85f3dbe 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -161,7 +161,8 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti repo_tree = None expand_paths = ["./"] enough_identifiers = False - expanded_history = list(self.history)[-3:] # Track expanded history + history_memory = 3 + expanded_history = list(self.history)[-history_memory:] # Track expanded history while not enough_identifiers and iteration_count < max_iterations: iteration_count += 1 @@ -191,7 +192,7 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti stream=True ) - print(f"Phase 1 Iteration {iteration_count}: {phase1_response}") + # print(f"Phase 1 Iteration {iteration_count}: {phase1_response}") # Parse Phase 1 response reasoning_blocks = parse_blocks(phase1_response, block_word="Reasoning", multiple=True) @@ -218,10 +219,11 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti enough_identifiers = True # Check if we need more history - if "ENOUGH_HISTORY: FALSE" in phase1_response.upper() and iteration_count == 1: + if "ENOUGH_HISTORY: FALSE" in phase1_response.upper() and iteration_count <= 2: # Load more history for next iteration # TODO this should be imcremental i.e starting += 2 each time! - expanded_history = self.history[-5:] if len(self.history) > 1 else self.history + history_memory += 2 + expanded_history = self.history[-history_memory:] if len(self.history) > 1 else self.history # Parse expansion paths for next iteration if expand_paths_block and not enough_identifiers: @@ -250,7 +252,7 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti stream=True ) - print(f"Phase 2 Final Selection: {phase2_response}") + # print(f"Phase 2 Final Selection: {phase2_response}") # Parse Phase 2 response summary = parse_blocks(phase2_response, block_word="Summary", multiple=False) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index ac495fb..c28b8c3 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -567,238 +567,117 @@ GATHER_CANDIDATES_PROMPT = """ You are Agent **Tide**, operating in **Candidate Gathering Mode** on **{DATE}**. -**SUPPORTED_LANGUAGES** are: {SUPPORTED_LANGUAGES} +**SUPPORTED_LANGUAGES**: {SUPPORTED_LANGUAGES} -**ABSOLUTE PROHIBITION - NEVER UNDER ANY CIRCUMSTANCE:** -- Answer or address the user request directly or indirectly -- Provide solutions, suggestions, or advice about the user's problem -- View or analyze file contents -- Check implementation details inside files -- Verify inter-file dependencies -- Write solutions or code modifications -- Access actual identifier definitions -- Use markdown formatting, bold text, italics, headers, code blocks, or any special formatting whatsoever - -**YOUR SOLE PURPOSE:** Explore repository structure to identify ALL potential candidate identifiers through iterative tree expansion. - -**PHASE 1 MISSION:** Gather comprehensive candidate identifiers by expanding the repository tree strategically. +**ABSOLUTE PROHIBITIONS:** +- Do NOT answer user requests directly +- Do NOT provide solutions or suggestions +- Do NOT view/analyze file contents or check implementations +- Do NOT use any markdown formatting -**Current State:** -- **Repository tree**: {TREE_STATE} -- **User request**: Analyze to determine relevant areas -- **Previous iteration context**: {ACCUMULATED_CONTEXT} -- **Iteration number**: {ITERATION_COUNT} +**SOLE PURPOSE:** Identify potential candidate identifiers by expanding repository structure. -**EXPLORATION STRATEGY:** +**CURRENT STATE:** +- Repository tree: {TREE_STATE} +- Accumulated context: {ACCUMULATED_CONTEXT} +- Iteration: {ITERATION_COUNT} -1. **Analyze User Request**: Identify key functional areas, components, and concepts mentioned -2. **Scan Current Tree**: Look for directories and files that match these areas -3. **Identify Gaps**: Determine what's collapsed or hidden that could contain relevant code -4. **Strategic Expansion**: Request expansion of promising directories to reveal file structure +**STRATEGY:** Analyze user request for functional areas → scan tree for matches → expand collapsed directories → list relevant identifiers. -**REASONING OUTPUT FORMAT:** +**OUTPUT FORMAT - Concise Reasoning Block:** -For each relevant area or task you identify, output: - -``` *** Begin Reasoning -**Task**: [Brief task description based on user request] -**Rationale**: [Why this area is relevant, which files/directories look promising based on naming] -**Candidate Identifiers**: +**Task**: [Brief task from request] +**Rationale**: [Why this area matters] +**Candidate Identifiers**: [MAX 3 ONLY] - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] + - [third.identifier.or.path] *** End Reasoning -``` -**IDENTIFIER SELECTION RULES:** +**HARD LIMIT:** Each reasoning block must have AT MOST 3 candidate identifiers. Do not exceed this limit under any circumstances. -1. **Language-Based Selection:** - - For files in **SUPPORTED_LANGUAGES**: Return code identifiers (functions, classes, methods) using dot notation - - For files NOT in SUPPORTED_LANGUAGES: Return file paths only - - Do NOT include package names, imports, or external dependencies - -2. **Candidate Types:** - - Include identifiers for functions, classes, methods, variables, or attributes you believe exist in the codebase - - Include file paths for non-code files that might be relevant - - Make educated guesses based on file/directory naming patterns - -3. **Accumulation Approach:** - - Add NEW candidates not yet in {ACCUMULATED_CONTEXT} - - Build comprehensive coverage across iterations - - Don't worry about over-inclusion - Phase 2 will filter +**IDENTIFIER RULES:** +- For SUPPORTED_LANGUAGES files: Use dot notation (functions, classes, methods) +- For other files: Use file paths only +- No package names, imports, or external dependencies +- Make educated guesses based on naming patterns +- Select only the most relevant candidates; prioritize quality over quantity **EXPANSION DECISION:** -After outputting reasoning blocks, decide on expansion: - -``` *** Begin Expand Paths [path/to/directory/] [another/path/] *** End Expand Paths -``` - -**Expand When:** -- Core directories related to user request are collapsed -- Cannot see file names in obviously relevant areas -- Need to explore subdirectories to find specific components -- File structure is insufficient to identify candidates - -**SUFFICIENCY ASSESSMENT:** -Evaluate if you have gathered enough candidates: +Expand when core directories are collapsed or file names aren't visible. -**SUFFICIENT (TRUE) when:** -- All major functional areas from user request have been explored -- Relevant directories are expanded enough to see file organization -- Can identify files/identifiers for all key tasks implied by request -- Further expansion unlikely to reveal significantly new relevant areas +**ASSESSMENTS:** -**INSUFFICIENT (FALSE) when:** -- Core directories mentioned in request are still collapsed -- Cannot identify candidates for obvious tasks from user request -- Major functional areas are hidden or unexplored -- Expansion would likely reveal important relevant files - -Output: -``` ENOUGH_IDENTIFIERS: [TRUE|FALSE] -``` - -**HISTORY ASSESSMENT:** - -Determine if more conversation history is needed: - -**NEED HISTORY (FALSE) when:** -- Current user message is self-contained -- Request is clear without additional context -- No references to previous discussion -- First iteration only - -**NEED HISTORY (TRUE) when:** -- User references "it", "that", "the earlier discussion" -- Request builds on previous conversation -- Context from earlier messages would clarify intent -- Ambiguous request that prior messages might resolve +- TRUE: All major areas explored, file organization clear, key tasks identified +- FALSE: Core directories collapsed, core tasks not covered -Output: -``` -ENOUGH_HISTORY: [TRUE|FALSE] -``` - -**COMPLETE OUTPUT STRUCTURE:** - -[Plain text reasoning paragraph explaining your analysis approach] - -*** Begin Reasoning -**Task**: [task name] -**Rationale**: [explanation] -**Candidate Identifiers**: - - [identifier or path] -*** End Reasoning - -[Additional reasoning blocks as needed] - -*** Begin Expand Paths -[paths to expand, one per line, or empty] -*** End Expand Paths - -ENOUGH_IDENTIFIERS: [TRUE|FALSE] ENOUGH_HISTORY: [TRUE|FALSE] - -**GUIDELINES:** -- Be thorough in gathering candidates - Phase 2 will refine -- Trust file naming patterns to identify likely relevant code -- Expand strategically to maximize information per iteration -- Aim to complete gathering in 2-3 iterations maximum -- Make decisive sufficiency assessments +- TRUE: Request references prior context +- FALSE: Request is self-contained """ -# Phase 2: Final Selection and Classification Prompt FINALIZE_IDENTIFIERS_PROMPT = """ You are Agent **Tide**, operating in **Final Selection Mode** on **{DATE}**. -**SUPPORTED_LANGUAGES** are: {SUPPORTED_LANGUAGES} - -**ABSOLUTE PROHIBITION - NEVER UNDER ANY CIRCUMSTANCE:** -- Answer or address the user request directly or indirectly -- Provide solutions, suggestions, or advice -- View or analyze file contents -- Write code or modifications -- Use markdown formatting, bold text, italics, headers, code blocks, or any special formatting - -**YOUR SOLE PURPOSE:** Review all gathered candidates and make final classification decisions. +**SUPPORTED_LANGUAGES**: {SUPPORTED_LANGUAGES} -**PHASE 2 MISSION:** -1. Synthesize reasoning from Phase 1 -2. Classify candidates into Context vs Modify identifiers -3. Determine operation mode for downstream processing +**ABSOLUTE PROHIBITIONS:** +- Do NOT answer requests or provide solutions +- Do NOT view/analyze file contents +- Do NOT use any markdown formatting -**INPUT ANALYSIS:** +**SOLE PURPOSE:** Classify gathered candidates and determine operation mode. -You are receiving: -- **User request**: {USER_REQUEST} -- **All Phase 1 reasoning**: Complete exploration and candidate gathering -- **Candidate pool**: {ALL_CANDIDATES} +**PHASE 2 MISSION:** +1. Review all Phase 1 candidates +2. Classify into Context vs Modify +3. Determine operation mode -**CLASSIFICATION RULES:** +**CURRENT STATE:** +- User request: {USER_REQUEST} +- Candidate pool: {ALL_CANDIDATES} -**Context Identifiers:** -- Functions, classes, methods, variables that provide understanding -- Supporting code that won't be directly modified -- Base classes, utilities, configuration files -- Dependencies needed to understand modification scope -- Code that defines interfaces or contracts +**HARD LIMIT - FINAL RESPONSE:** Maximum 5 identifiers total across Context and Modify combined. -**Modify Identifiers:** -- Functions, classes, methods, variables requiring direct changes -- Code that implements the specific behavior to be altered -- Files where new code will be added -- Entities that must be updated to satisfy user request +**CLASSIFICATION:** -**CRITICAL CONSTRAINTS:** -- Only include actual code elements (functions, classes, methods, variables, attributes) -- NEVER include package names, import statements, or external dependencies -- NEVER include identifiers that represent only imports or modules without concrete definitions -- Even if present in candidate pool, exclude non-code-element identifiers - -**OPERATION MODE DETERMINATION:** - -Analyze the user request and determine appropriate processing mode(s): - -**STANDARD:** -- General questions, explanations, documentation -- Conceptual discussions about code -- No code modification or planning required -- Information retrieval tasks +**Context Identifiers** (understanding/reference, NOT direct dependencies): +- Supporting utilities, base classes, configuration +- Related functionality providing broader scope understanding +- Interfaces and contracts that inform approach +- NOTE: Direct dependencies of Modify identifiers are handled by framework; focus on peripheral context -**PLAN_STEPS:** -- Multi-step implementation required -- Complex feature additions -- Refactoring across multiple files -- Architecture changes -- Requires sequential task planning +**Modify Identifiers** (direct changes): +- Code requiring direct updates +- New code additions +- Entities to be altered +- NOTE: Do NOT include direct dependencies; framework coverage handles these -**PATCH_CODE:** -- Direct code modifications needed -- Bug fixes, updates, improvements -- Concrete implementation tasks -- Writing or editing specific code +**CRITICAL:** Only actual code elements (functions, classes, methods, variables). No packages, imports, or bare modules. -**Mode can be combined**: `STANDARD+PLAN_STEPS`, `PLAN_STEPS+PATCH_CODE`, etc. +**OPERATION MODES:** +- STANDARD: Explanations, info retrieval +- PLAN_STEPS: Multi-step implementation, complex changes +- PATCH_CODE: Direct fixes/updates +- Mix modes as needed (e.g., PLAN_STEPS+PATCH_CODE) **OUTPUT FORMAT:** -[Plain text summary paragraph synthesizing Phase 1 exploration and your final decisions] - *** Begin Summary -[Comprehensive summary of the reasoning from Phase 1, highlighting key areas identified, exploration path taken, and rationale for final identifier selection] +[4-5 lines max: Phase 1 exploration summary, key areas found, final classification rationale] *** End Summary *** Begin Context Identifiers [identifier.one] [identifier.two] -[path/to/file.ext] *** End Context Identifiers *** Begin Modify Identifiers @@ -806,26 +685,20 @@ [another.identifier] *** End Modify Identifiers -OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE|combinations] - -**DECISION GUIDELINES:** - -**For Context vs Modify:** -- When uncertain, prefer Context (safer for providing understanding) -- If request says "use X to do Y", X is Context, Y-related code is Modify -- If request is about understanding/explaining, most/all are Context -- If request is about changing behavior, focus on Modify +OPERATION_MODE: [MODE] -**For Operation Mode:** -- Default to STANDARD for questions and explanations -- Use PLAN_STEPS when request implies "add feature", "implement", "refactor" -- Use PATCH_CODE when request is "fix", "update", "change this specific thing" -- Combine when both planning AND implementation are clearly needed +**DECISION RULES:** +- Uncertain: prefer Context +- "Use X to do Y": X is Context, Y is Modify +- "Understand/explain": mostly Context +- "Change behavior": focus on Modify +- Match mode to request intent (STANDARD for questions, PLAN_STEPS for features, PATCH_CODE for fixes) +- Strict selection: Keep only 5 total identifiers; eliminate redundant or framework-covered dependencies **QUALITY CHECKS:** - Verify all identifiers are actual code elements, not imports/packages -- Ensure Modify identifiers align with what will actually change -- Confirm Context identifiers provide necessary understanding +- Ensure Modify identifiers are primary targets (not their dependencies) +- Confirm Context identifiers provide necessary peripheral understanding - Check Operation Mode matches request intent -- Keep identifier lists focused and relevant +- **CRITICAL: Enforce 5 identifier maximum total** """ diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index b9d392c..0b46c97 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -531,7 +531,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option field_extractor=FieldExtractor({ "header": r"\*\*Task\*\*:\s*(.+?)(?=\n\s*\*\*Rationale\*\*)", "content": r"\*\*Rationale\*\*:\s*(.+?)(?=\s*\*\*Candidate Identifiers\*\*|$)", - "candidate_identifiers": {"pattern": "^\s*-\s*(.+?)$", "schema": list} + "candidate_identifiers": {"pattern": r"^\s*-\s*(.+?)$", "schema": list} }) ), MarkerConfig( From eb694a5d89ee2bc28637dd872c4d454d85c1bc46 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 22:35:46 +0100 Subject: [PATCH 027/138] refactor(ui): improve agent loop message handling and regex parsing in app.py --- codetide/agents/tide/ui/app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 0b46c97..4ac97f8 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -478,7 +478,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "modify_identifiers": [], "finished": False }) - context_msg = await cl.Message(content="", author="AgentTide", elements=[reasoning_element]).send() + _ = await cl.Message(content="", author="AgentTide", elements=[reasoning_element]).send() ### TODO this needs to receive the message as well to call update reasoning_step = CustomElementStep( element=reasoning_element, @@ -529,8 +529,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option target_step=reasoning_step, stream_mode="full", field_extractor=FieldExtractor({ - "header": r"\*\*Task\*\*:\s*(.+?)(?=\n\s*\*\*Rationale\*\*)", - "content": r"\*\*Rationale\*\*:\s*(.+?)(?=\s*\*\*Candidate Identifiers\*\*|$)", + "header": r"\*{0,2}Task\*{0,2}:\s*(.+?)(?=\n\s*\*{0,2}Rationale\*{0,2})", + "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}Candidate Identifiers\*{0,2}|$)", "candidate_identifiers": {"pattern": r"^\s*-\s*(.+?)$", "schema": list} }) ), @@ -562,8 +562,6 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option global_fallback_msg=msg ) - st = time.time() - is_reasonig_sent = False loop = run_concurrent_tasks(agent_tide_ui, codeIdentifiers) async for chunk in loop: ### TODO update this to check FROM AGENT TIDE if reasoning is being ran and if so we need From ad3aad63c53d706330311db714d4ae50d25104b7 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 22:37:17 +0100 Subject: [PATCH 028/138] feat(ui): enhance ReasoningExplorer with animated wave, refined styling, and deeper analysis toggle --- .../ui/public/elements/ReasoningExplorer.jsx | 193 +++++++++++------- 1 file changed, 116 insertions(+), 77 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 03d2bfb..9b5ef2e 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -1,12 +1,21 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { ChevronDown, ChevronRight, Loader2, Brain, Layers } from "lucide-react"; -import { useState } from "react"; +import { ChevronDown, ChevronRight, Layers, Brain } from "lucide-react"; +import { useState, useEffect } from "react"; export default function ReasoningStepsCard() { const [expandedSteps, setExpandedSteps] = useState({}); - const [expandedAll, setExpandedAll] = useState(!props.finished); + const [expandedAll, setExpandedAll] = useState(true); const [expandedIdentifiers, setExpandedIdentifiers] = useState(false); + const [waveOffset, setWaveOffset] = useState(0); + + // Animate wave effect + useEffect(() => { + const interval = setInterval(() => { + setWaveOffset((prev) => (prev + 1) % 360); + }, 50); + return () => clearInterval(interval); + }, []); const toggleStep = (index) => { setExpandedSteps((prev) => ({ @@ -28,66 +37,76 @@ export default function ReasoningStepsCard() { props.context_identifiers.length === 0 && props.modify_identifiers.length === 0; - if (hasNoData) { + if (hasNoData && !props.finished) { return ( - - -
- - Analyzing... -
+ + + + + + + + + + Analyzing... ); } return ( - - {/* Header */} - -
-
- - {props.finished ? "Reasoning Completed" : "Reasoning ..."} - - {!props.finished && ( - + + {/* Header - Summary Only */} + +
+
+ {props.summary && ( +

+ {props.summary} +

)}
- {props.finished && props.summary && ( -
- {props.summary} -
- )}
{/* Timeline */} {expandedAll && ( - + {/* Reasoning Steps */} {props.reasoning_steps.map((step, index) => ( -
+
{/* Timeline Icon + Connector */} -
-
- +
+
+
{index < props.reasoning_steps.length - 1 && ( -
+
)}
@@ -95,37 +114,37 @@ export default function ReasoningStepsCard() {
-

+

{step.header}

-

+

{step.content}

{/* Expanded candidate identifiers */} {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( -
-

- Candidate Identifiers: +

+

+ Context Identifiers

-
+
{step.candidate_identifiers.map((id, idIndex) => ( {id} @@ -137,43 +156,36 @@ export default function ReasoningStepsCard() {
))} - {/* Unified Context + Modify Identifiers */} + {/* Deep Analysis Section */} {(props.context_identifiers.length > 0 || props.modify_identifiers.length > 0) && ( -
-
-
- -

- Additional Identifiers -

-
- -
+
+ {expandedIdentifiers && ( -
+
{/* Context Identifiers */} {props.context_identifiers.length > 0 && (
-

- Context Identifiers: +

+ Context Identifiers

-
+
{props.context_identifiers.map((id, index) => ( {id} @@ -182,18 +194,23 @@ export default function ReasoningStepsCard() {
)} + {/* Divider */} + {props.context_identifiers.length > 0 && props.modify_identifiers.length > 0 && ( +
+ )} + {/* Modify Identifiers */} {props.modify_identifiers.length > 0 && (
-

- Modify Identifiers: +

+ Modify Identifiers

-
+
{props.modify_identifiers.map((id, index) => ( {id} @@ -205,8 +222,30 @@ export default function ReasoningStepsCard() { )}
)} + + {/* Loading indicator at bottom */} + {!props.finished && ( +
+ + + + + + + + + + +
+ )} )} ); -} +} \ No newline at end of file From d79752f5139ccf07eeaec8ef04ef52fad0016a2c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 22:42:52 +0100 Subject: [PATCH 029/138] feat(ui): add loading wave animation and improve reasoning explorer layout --- .../ui/public/elements/ReasoningExplorer.jsx | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 9b5ef2e..0d05918 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -37,6 +37,8 @@ export default function ReasoningStepsCard() { props.context_identifiers.length === 0 && props.modify_identifiers.length === 0; + const isLoadingState = !props.finished && !props.summary; + if (hasNoData && !props.finished) { return ( @@ -71,14 +73,35 @@ export default function ReasoningStepsCard() { return ( - {/* Header - Summary Only */} + {/* Header - Summary or Loading Wave */}
- {props.summary && ( -

- {props.summary} -

+ {isLoadingState ? ( +
+ + + + + + + + + + +
+ ) : ( + props.summary && ( +

+ {props.summary} +

+ ) )}
{expandedIdentifiers && ( -
+
{/* Context Identifiers */} {props.context_identifiers.length > 0 && (
-

+

Context Identifiers

@@ -202,7 +225,7 @@ export default function ReasoningStepsCard() { {/* Modify Identifiers */} {props.modify_identifiers.length > 0 && (
-

+

Modify Identifiers

@@ -222,30 +245,8 @@ export default function ReasoningStepsCard() { )}
)} - - {/* Loading indicator at bottom */} - {!props.finished && ( -
- - - - - - - - - - -
- )} )} ); -} \ No newline at end of file +} From bfa26b68501586f2339120ebc80599bb7b66b311 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 22:51:31 +0100 Subject: [PATCH 030/138] fix(agent): add missing reasoning finished log call in tide agent --- codetide/agents/tide/agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 85f3dbe..3ff1bc6 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -322,6 +322,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self._context_identifier_window.pop(0) expanded_history = self.history[-5:] operation_mode = "STANDARD, PATH_CODE" + await self.llm.logger_fn(REASONING_FINISHED) ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history else: await self.llm.logger_fn(REASONING_STARTED) From a01c395d4b24b59bdf51927fead7da96d19d9fac Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 23:09:56 +0100 Subject: [PATCH 031/138] refactor(prompts): enhance candidate deduplication and reasoning clarity in tide agent prompts --- codetide/agents/tide/prompts.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index c28b8c3..c391335 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -582,27 +582,34 @@ - Accumulated context: {ACCUMULATED_CONTEXT} - Iteration: {ITERATION_COUNT} -**STRATEGY:** Analyze user request for functional areas → scan tree for matches → expand collapsed directories → list relevant identifiers. +**CRITICAL DEDUPLICATION REQUIREMENT:** +Each reasoning block MUST contribute NEW candidate identifiers. DO NOT repeat any identifier from other Reasoning Blocks. Verify each candidate is novel before including it. This ensures cumulative exploration, not repetition. + +**STRATEGY:** Analyze user request for functional areas → scan tree for matches → expand collapsed directories → identify NEW identifiers NOT in accumulated pool. **OUTPUT FORMAT - Concise Reasoning Block:** *** Begin Reasoning **Task**: [Brief task from request] -**Rationale**: [Why this area matters] -**Candidate Identifiers**: [MAX 3 ONLY] +**Rationale**: [Why this new area matters] +**NEW Candidate Identifiers**: [MAX 3 ONLY - MUST BE NOVEL] - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] - [third.identifier.or.path] *** End Reasoning -**HARD LIMIT:** Each reasoning block must have AT MOST 3 candidate identifiers. Do not exceed this limit under any circumstances. +**HARD LIMITS:** +- Each reasoning block: AT MOST 3 candidate identifiers +- ALL identifiers MUST be NEW (not from other Reasoning Blocks) +- Focus on unexplored areas and new functional domains +- Do NOT include duplicates under any circumstances **IDENTIFIER RULES:** - For SUPPORTED_LANGUAGES files: Use dot notation (functions, classes, methods) - For other files: Use file paths only - No package names, imports, or external dependencies - Make educated guesses based on naming patterns -- Select only the most relevant candidates; prioritize quality over quantity +- Cross-check against other Reasoning Blocks before inclusion **EXPANSION DECISION:** @@ -611,13 +618,17 @@ [another/path/] *** End Expand Paths -Expand when core directories are collapsed or file names aren't visible. +Expand when: +- Core directories are collapsed +- File names aren't visible +- New functional areas haven't been explored +- Previous reasoning didn't cover this directory **ASSESSMENTS:** ENOUGH_IDENTIFIERS: [TRUE|FALSE] -- TRUE: All major areas explored, file organization clear, key tasks identified -- FALSE: Core directories collapsed, core tasks not covered +- TRUE: All major areas explored, file organization clear, key tasks identified, no new areas to expand +- FALSE: Core directories collapsed, core tasks not covered, unexplored areas remain ENOUGH_HISTORY: [TRUE|FALSE] - TRUE: Request references prior context From b3605d4d20f645db5bb41a5eb044b2e558784547 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 23:20:51 +0100 Subject: [PATCH 032/138] feat(ui): enhance ReasoningExplorer with dynamic loading states, thinking timer, and improved layout This commit adds animated loading text cycling through multiple states and a thinking time counter to ReasoningExplorer.jsx. It refines the UI by adjusting spacing, typography, and button accessibility. The timeline and step content layout are improved for clarity and responsiveness. Expanded candidate identifiers and deeper analysis sections now have better styling and interaction cues. --- .../ui/public/elements/ReasoningExplorer.jsx | 131 +++++++++++------- 1 file changed, 81 insertions(+), 50 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 0d05918..952238f 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -8,15 +8,40 @@ export default function ReasoningStepsCard() { const [expandedAll, setExpandedAll] = useState(true); const [expandedIdentifiers, setExpandedIdentifiers] = useState(false); const [waveOffset, setWaveOffset] = useState(0); + const [loadingText, setLoadingText] = useState("Analyzing"); + const [thinkingTime, setThinkingTime] = useState(0); - // Animate wave effect + const loadingStates = ["Analyzing", "Thinking", "Updating", "Processing"]; + + // Animate wave effect and loading text useEffect(() => { - const interval = setInterval(() => { + const waveInterval = setInterval(() => { setWaveOffset((prev) => (prev + 1) % 360); }, 50); - return () => clearInterval(interval); + + const textInterval = setInterval(() => { + setLoadingText((prev) => { + const idx = loadingStates.indexOf(prev); + return loadingStates[(idx + 1) % loadingStates.length]; + }); + }, 1000); + + return () => { + clearInterval(waveInterval); + clearInterval(textInterval); + }; }, []); + // Track thinking time + useEffect(() => { + if (!props.finished) { + const timer = setInterval(() => { + setThinkingTime((prev) => prev + 1); + }, 1000); + return () => clearInterval(timer); + } + }, [props.finished]); + const toggleStep = (index) => { setExpandedSteps((prev) => ({ ...prev, @@ -41,8 +66,8 @@ export default function ReasoningStepsCard() { if (hasNoData && !props.finished) { return ( - - + + - Analyzing... + {loadingText}... ); } return ( - - {/* Header - Summary or Loading Wave */} - -
-
+ + {/* Header */} + +
+
{isLoadingState ? ( -
- +
+ + {loadingText}...
) : ( - props.summary && ( -

+

+

Thought for {thinkingTime}s

+

{props.summary}

- ) +
)}
- {/* Timeline */} + {/* Content */} {expandedAll && ( - + {/* Reasoning Steps */} {props.reasoning_steps.map((step, index) => ( -
- {/* Timeline Icon + Connector */} -
-
- +
+ {/* Timeline */} +
+
+
{index < props.reasoning_steps.length - 1 && ( -
+
)}
- {/* Step Content */} + {/* Content */}
-
-
-

+
+
+

{step.header}

{step.content}

- + {step.candidate_identifiers?.length > 0 && ( + + )}
- {/* Expanded candidate identifiers */} + {/* Identifiers */} {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( -
-

+

+

Context Identifiers

@@ -179,28 +210,28 @@ export default function ReasoningStepsCard() {
))} - {/* Deep Analysis Section */} + {/* Deep Analysis */} {(props.context_identifiers.length > 0 || props.modify_identifiers.length > 0) && ( -
+
{expandedIdentifiers && ( -
+
{/* Context Identifiers */} {props.context_identifiers.length > 0 && (
-

+

Context Identifiers

@@ -225,7 +256,7 @@ export default function ReasoningStepsCard() { {/* Modify Identifiers */} {props.modify_identifiers.length > 0 && (
-

+

Modify Identifiers

From ee3f71b130e7694664ce278b532bedfe8b254d22 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 12 Oct 2025 23:33:36 +0100 Subject: [PATCH 033/138] feat(ui): enhance ReasoningExplorer with toggleable identifiers and improved layout --- .../ui/public/elements/ReasoningExplorer.jsx | 112 ++++++++++-------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 952238f..28ce7c0 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -147,73 +147,81 @@ export default function ReasoningStepsCard() { {/* Content */} {expandedAll && ( - + {/* Reasoning Steps */} {props.reasoning_steps.map((step, index) => ( -
- {/* Timeline */} -
-
- +
+
+ {/* Timeline */} +
+
+ +
- {index < props.reasoning_steps.length - 1 && ( -
- )} -
- {/* Content */} -
-
-
-

- {step.header} -

-

- {step.content} -

+ {/* Content */} +
+
+
+

+ {step.header} +

+

+ {step.content} +

+
+ {step.candidate_identifiers?.length > 0 && ( + + )}
- {step.candidate_identifiers?.length > 0 && ( - + + {/* Identifiers */} + {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( +
+

+ Context Identifiers +

+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))} +
+
)}
+
- {/* Identifiers */} - {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( -
-

- Context Identifiers -

-
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} -
+ {/* Connector line - only between steps */} + {index < props.reasoning_steps.length - 1 && ( +
+
+
- )} -
+
+ )}
))} {/* Deep Analysis */} {(props.context_identifiers.length > 0 || props.modify_identifiers.length > 0) && ( -
+
-
+
+ - {/* Content */} + {/* Content - Expanded view */} {expandedAll && ( - + {/* Reasoning Steps */} {props.reasoning_steps.map((step, index) => (
From 75832995cf384be74d531002fd2cdb1ac7c43256 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 22:15:49 +0100 Subject: [PATCH 036/138] style(ui): improve text styling and truncation in ReasoningExplorer component --- codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index af6bce3..8be9b24 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -112,7 +112,7 @@ export default function ReasoningStepsCard() { {expandedAll && props.finished && (

Thought for {thinkingTime}s

)} -

+

{previewText}

From f62d4347147f467399c9e678be6d85e5c931e90b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 22:53:33 +0100 Subject: [PATCH 037/138] refactor(ui): simplify ReasoningExplorer loading state and preview rendering --- .../ui/public/elements/ReasoningExplorer.jsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 8be9b24..a03983e 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -57,12 +57,7 @@ export default function ReasoningStepsCard() { setExpandedIdentifiers(!expandedIdentifiers); }; - const hasNoData = - props.reasoning_steps.length === 0 && - props.context_identifiers.length === 0 && - props.modify_identifiers.length === 0; - - const isLoadingState = !props.finished && !props.summary; + const isLoadingState = !props.finished; // Get preview text for collapsed state const getPreviewText = () => { @@ -86,9 +81,9 @@ export default function ReasoningStepsCard() { onClick={toggleAll} className="w-full flex items-center justify-between gap-4 hover:opacity-80 transition text-left group" > -
- {isLoadingState ? ( -
+
+
+ {isLoadingState && ( - {loadingText}... -
- ) : ( + )}
{expandedAll && props.finished && (

Thought for {thinkingTime}s

)} -

+

{previewText}

- )} +
{expandedAll ? ( From e1328fea6cdd39b2ba57babe23c33baf2d6605b2 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 22:59:17 +0100 Subject: [PATCH 038/138] refactor(ui): improve ReasoningExplorer layout and loading animation handling --- .../ui/public/elements/ReasoningExplorer.jsx | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index a03983e..1764adf 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -82,35 +82,51 @@ export default function ReasoningStepsCard() { className="w-full flex items-center justify-between gap-4 hover:opacity-80 transition text-left group" >
-
- {isLoadingState && ( - - - - - - - - - - - )} -
- {expandedAll && props.finished && ( -

Thought for {thinkingTime}s

- )} -

- {previewText} -

-
-
+
+ {isLoadingState && ( + + + + + + + + + + + )} + + {/* 💥 Key change: ensure min-w-0 and overflow-hidden on the flex item */} +
+ {expandedAll && props.finished && ( +

+ Thought for {thinkingTime}s +

+ )} + {/* Text always trimmed to width */} +

+ {previewText} +

+
+
+
{expandedAll ? ( From 650fa8daaa8c7a03b1077718b8cbe4855cd17436 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 23:02:14 +0100 Subject: [PATCH 039/138] feat(ui): update ReasoningExplorer to show 'Finished' status when done --- codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 1764adf..233094c 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -68,6 +68,9 @@ export default function ReasoningStepsCard() { const lastStep = props.reasoning_steps[props.reasoning_steps.length - 1]; return lastStep.content.split('\n')[0]; } + if (props.finished) { + return `Finished` + } return `${loadingText}...`; }; From 67a16a62d42067ca59ea5205c62db35b8cac5383 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 23:08:36 +0100 Subject: [PATCH 040/138] style(ui): improve spacing and border radius in ReasoningExplorer identifiers sections --- .../tide/ui/public/elements/ReasoningExplorer.jsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 233094c..34fbfa4 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -182,8 +182,8 @@ export default function ReasoningStepsCard() { {/* Identifiers */} {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( -
-

+

+

Context Identifiers

@@ -228,13 +228,11 @@ export default function ReasoningStepsCard() { - {expandedIdentifiers && (
- {/* Context Identifiers */} {props.context_identifiers.length > 0 && (
-

+

Context Identifiers

@@ -251,15 +249,13 @@ export default function ReasoningStepsCard() {
)} - {/* Divider */} {props.context_identifiers.length > 0 && props.modify_identifiers.length > 0 && (
)} - {/* Modify Identifiers */} {props.modify_identifiers.length > 0 && (
-

+

Modify Identifiers

From e0f706d5da3f0d30d24c1ab677137db523439fd5 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 23:22:22 +0100 Subject: [PATCH 041/138] refactor(ui): improve ReasoningExplorer component with better state handling and animations --- .../ui/public/elements/ReasoningExplorer.jsx | 325 ++++++++++-------- 1 file changed, 184 insertions(+), 141 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 34fbfa4..d6cf5fb 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -1,6 +1,6 @@ -import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { ChevronDown, ChevronRight, Layers, Brain } from "lucide-react"; +import { ChevronDown, ChevronRight, Brain } from "lucide-react"; import { useState, useEffect } from "react"; export default function ReasoningStepsCard() { @@ -62,14 +62,14 @@ export default function ReasoningStepsCard() { // Get preview text for collapsed state const getPreviewText = () => { if (props.summary) { - return props.summary.split('\n')[0]; + return props.summary.split("\n")[0]; } - if (props.reasoning_steps.length > 0) { + if (props.reasoning_steps && props.reasoning_steps.length > 0) { const lastStep = props.reasoning_steps[props.reasoning_steps.length - 1]; - return lastStep.content.split('\n')[0]; + return lastStep.content.split("\n")[0]; } if (props.finished) { - return `Finished` + return `Finished`; } return `${loadingText}...`; }; @@ -79,56 +79,78 @@ export default function ReasoningStepsCard() { return ( {/* Header - Always visible */} - + + )}
- {step.candidate_identifiers?.length > 0 && ( - - )} -
- {/* Identifiers */} - {expandedSteps[index] && step.candidate_identifiers?.length > 0 && ( -
+ {/* Identifiers - always in DOM, animate visibility/height */} +

Context Identifiers

- {step.candidate_identifiers.map((id, idIndex) => ( + {step.candidate_identifiers?.map((id, idIndex) => (
- )} +
-
- {/* Connector line - only between steps */} - {index < props.reasoning_steps.length - 1 && ( -
-
-
+ {/* Connector line - only between steps */} + {index < props.reasoning_steps.length - 1 && ( +
+
+
+
-
- )} -
- ))} + )} +
+ ); + })} {/* Deep Analysis */} - {(props.context_identifiers.length > 0 || - props.modify_identifiers.length > 0) && ( + {(props.context_identifiers?.length > 0 || + props.modify_identifiers?.length > 0) && (
- {expandedIdentifiers && ( -
- {props.context_identifiers.length > 0 && ( -
-

- Context Identifiers -

-
- {props.context_identifiers.map((id, index) => ( - - {id} - - ))} -
+ + {/* Deeper analysis block: always in DOM, animated visibility */} +
+ {props.context_identifiers?.length > 0 && ( +
+

+ Context Identifiers +

+
+ {props.context_identifiers.map((id, index) => ( + + {id} + + ))}
- )} +
+ )} - {props.context_identifiers.length > 0 && props.modify_identifiers.length > 0 && ( + {props.context_identifiers?.length > 0 && + props.modify_identifiers?.length > 0 && (
)} - {props.modify_identifiers.length > 0 && ( -
-

- Modify Identifiers -

-
- {props.modify_identifiers.map((id, index) => ( - - {id} - - ))} -
+ {props.modify_identifiers?.length > 0 && ( +
+

+ Modify Identifiers +

+
+ {props.modify_identifiers.map((id, index) => ( + + {id} + + ))}
- )} -
- )} +
+ )} +
)} From 258c28024204b01e3223712018afc17ca36ca828 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 13 Oct 2025 23:29:19 +0100 Subject: [PATCH 042/138] fix(agent): add missing reasoning finished log and fix spacing in agent.py --- codetide/agents/tide/agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 85669f1..ca98edd 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -306,6 +306,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): codeContext = None if self._skip_context_retrieval: expanded_history = self.history[-1] + await self.llm.logger_fn(REASONING_FINISHED) else: autocomplete = AutoComplete(self.tide.cached_ids) print(f"{autocomplete=}") @@ -325,7 +326,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): operation_mode = "STANDARD, PATH_CODE" await self.llm.logger_fn(REASONING_FINISHED) ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history - else: + else: await self.llm.logger_fn(REASONING_STARTED) reasoning_output = await self.get_identifiers_two_phase(autocomplete, codeIdentifiers, TODAY) await self.llm.logger_fn(REASONING_FINISHED) From a5fbdd506d2617048c2d82d7e93f624a62352629 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 19:32:57 +0100 Subject: [PATCH 043/138] refactor(ui): simplify ReasoningExplorer component and improve readability --- .../ui/public/elements/ReasoningExplorer.jsx | 246 ++++++------------ 1 file changed, 80 insertions(+), 166 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index d6cf5fb..f961ce6 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -4,16 +4,13 @@ import { ChevronDown, ChevronRight, Brain } from "lucide-react"; import { useState, useEffect } from "react"; export default function ReasoningStepsCard() { - const [expandedSteps, setExpandedSteps] = useState({}); const [expandedAll, setExpandedAll] = useState(false); - const [expandedIdentifiers, setExpandedIdentifiers] = useState(false); const [waveOffset, setWaveOffset] = useState(0); const [loadingText, setLoadingText] = useState("Analyzing"); const [thinkingTime, setThinkingTime] = useState(0); const loadingStates = ["Analyzing", "Thinking", "Updating", "Processing"]; - // Animate wave effect and loading text useEffect(() => { const waveInterval = setInterval(() => { setWaveOffset((prev) => (prev + 1) % 360); @@ -32,7 +29,6 @@ export default function ReasoningStepsCard() { }; }, []); - // Track thinking time useEffect(() => { if (!props.finished) { const timer = setInterval(() => { @@ -42,35 +38,15 @@ export default function ReasoningStepsCard() { } }, [props.finished]); - const toggleStep = (index) => { - setExpandedSteps((prev) => ({ - ...prev, - [index]: !prev[index], - })); - }; - - const toggleAll = () => { - setExpandedAll(!expandedAll); - }; - - const toggleIdentifiers = () => { - setExpandedIdentifiers(!expandedIdentifiers); - }; + const toggleAll = () => setExpandedAll(!expandedAll); const isLoadingState = !props.finished; - // Get preview text for collapsed state const getPreviewText = () => { - if (props.summary) { - return props.summary.split("\n")[0]; - } - if (props.reasoning_steps && props.reasoning_steps.length > 0) { - const lastStep = props.reasoning_steps[props.reasoning_steps.length - 1]; - return lastStep.content.split("\n")[0]; - } - if (props.finished) { - return `Finished`; - } + if (props.summary) return props.summary.split("\n")[0]; + if (props.reasoning_steps?.length > 0) + return props.reasoning_steps.at(-1).content.split("\n")[0]; + if (props.finished) return "Finished"; return `${loadingText}...`; }; @@ -78,7 +54,7 @@ export default function ReasoningStepsCard() { return ( - {/* Header - Always visible */} + {/* Header */} - + )} - {/* Text always trimmed to width */}

-
{expandedAll ? ( - + ) : ( - + )}
- {/* Content - Expanded view */} + {/* Expanded Content */} {expandedAll && ( {/* Reasoning Steps */} - {props.reasoning_steps?.map((step, index) => { - const isOpen = !!expandedSteps[index]; - return ( -
-
- {/* Timeline */} -
-
- -
+ {props.reasoning_steps?.map((step, index) => ( +
+
+ {/* Timeline */} +
+
+
+
- {/* Content */} -
-
-
-

- {step.header} -

-

- {step.content} -

-
- {step.candidate_identifiers?.length > 0 && ( - - )} -
+ {/* Step Content */} +
+

+ {step.header} +

+

+ {step.content} +

- {/* Identifiers - always in DOM, animate visibility/height */} -
+ {/* Identifiers — always visible */} + {step.candidate_identifiers?.length > 0 && ( +

Context Identifiers

- {step.candidate_identifiers?.map((id, idIndex) => ( + {step.candidate_identifiers.map((id, idIndex) => (
-
+ )}
+
- {/* Connector line - only between steps */} - {index < props.reasoning_steps.length - 1 && ( -
-
-
-
+ {/* Connector line */} + {index < props.reasoning_steps.length - 1 && ( +
+
+
- )} -
- ); - })} +
+ )} +
+ ))} - {/* Deep Analysis */} + {/* Context + Modify Identifiers — always visible */} {(props.context_identifiers?.length > 0 || props.modify_identifiers?.length > 0) && ( -
- - - {/* Deeper analysis block: always in DOM, animated visibility */} -
- {props.context_identifiers?.length > 0 && ( -
-

- Context Identifiers -

-
- {props.context_identifiers.map((id, index) => ( - - {id} - - ))} -
+
+ {props.context_identifiers?.length > 0 && ( +
+

+ Context Identifiers +

+
+ {props.context_identifiers.map((id, index) => ( + + {id} + + ))}
- )} - - {props.context_identifiers?.length > 0 && - props.modify_identifiers?.length > 0 && ( -
- )} +
+ )} - {props.modify_identifiers?.length > 0 && ( -
-

- Modify Identifiers -

-
- {props.modify_identifiers.map((id, index) => ( - - {id} - - ))} -
+ {props.modify_identifiers?.length > 0 && ( +
+

+ Modify Identifiers +

+
+ {props.modify_identifiers.map((id, index) => ( + + {id} + + ))}
- )} -
+
+ )}
)} From 106be70423ea0cb667b2cf657a9a1e68ba9d0d9e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 19:43:50 +0100 Subject: [PATCH 044/138] refactor(ui): simplify ReasoningExplorer component and improve styling for reasoning steps and identifiers --- .../ui/public/elements/ReasoningExplorer.jsx | 310 ++++++++---------- 1 file changed, 132 insertions(+), 178 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index f961ce6..beb64be 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -1,15 +1,15 @@ import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { ChevronDown, ChevronRight, Brain } from "lucide-react"; +import { Brain } from "lucide-react"; import { useState, useEffect } from "react"; export default function ReasoningStepsCard() { - const [expandedAll, setExpandedAll] = useState(false); const [waveOffset, setWaveOffset] = useState(0); const [loadingText, setLoadingText] = useState("Analyzing"); const [thinkingTime, setThinkingTime] = useState(0); const loadingStates = ["Analyzing", "Thinking", "Updating", "Processing"]; + const isLoadingState = !props?.finished; useEffect(() => { const waveInterval = setInterval(() => { @@ -30,23 +30,19 @@ export default function ReasoningStepsCard() { }, []); useEffect(() => { - if (!props.finished) { + if (!props?.finished) { const timer = setInterval(() => { setThinkingTime((prev) => prev + 1); }, 1000); return () => clearInterval(timer); } - }, [props.finished]); - - const toggleAll = () => setExpandedAll(!expandedAll); - - const isLoadingState = !props.finished; + }, [props?.finished]); const getPreviewText = () => { - if (props.summary) return props.summary.split("\n")[0]; - if (props.reasoning_steps?.length > 0) + if (props?.summary) return props.summary.split("\n")[0]; + if (props?.reasoning_steps?.length > 0) return props.reasoning_steps.at(-1).content.split("\n")[0]; - if (props.finished) return "Finished"; + if (props?.finished) return "Finished"; return `${loadingText}...`; }; @@ -55,185 +51,143 @@ export default function ReasoningStepsCard() { return ( {/* Header */} - - +
- {/* Expanded Content */} - {expandedAll && ( - - {/* Reasoning Steps */} - {props.reasoning_steps?.map((step, index) => ( -
-
- {/* Timeline */} -
-
- + {/* Always visible content */} + + {/* Reasoning Steps */} + {props?.reasoning_steps?.length > 0 && ( +
+ {props.reasoning_steps.map((step, index) => ( +
+
+ {/* Timeline */} +
+
+ +
-
- {/* Step Content */} -
-

- {step.header} -

-

- {step.content} -

- - {/* Identifiers — always visible */} - {step.candidate_identifiers?.length > 0 && ( -
-

- Context Identifiers -

-
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} + {/* Step Content */} +
+

+ {step.header} +

+

+ {step.content} +

+ + {/* Candidate Identifiers — inline badges, left vertical line */} + {step.candidate_identifiers?.length > 0 && ( +
+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))} +
-
- )} -
-
- - {/* Connector line */} - {index < props.reasoning_steps.length - 1 && ( -
-
-
+ )}
- )} -
- ))} - - {/* Context + Modify Identifiers — always visible */} - {(props.context_identifiers?.length > 0 || - props.modify_identifiers?.length > 0) && ( -
- {props.context_identifiers?.length > 0 && ( -
-

- Context Identifiers -

-
- {props.context_identifiers.map((id, index) => ( - - {id} - - ))} + + {/* Connector line between steps */} + {index < props.reasoning_steps.length - 1 && ( +
+
+
+
+ )} +
+ ))} +
+ )} + + {/* Context + Modify Identifiers */} + {(props?.context_identifiers?.length > 0 || + props?.modify_identifiers?.length > 0) && ( +
+ {props.context_identifiers?.length > 0 && ( +
+
+ {props.context_identifiers.map((id, index) => ( + + {id} + + ))}
- )} - - {props.modify_identifiers?.length > 0 && ( -
-

- Modify Identifiers -

-
- {props.modify_identifiers.map((id, index) => ( - - {id} - - ))} -
+
+ )} + + {props.modify_identifiers?.length > 0 && ( +
+
+ {props.modify_identifiers.map((id, index) => ( + + {id} + + ))}
- )} -
- )} - - )} +
+ )} +
+ )} + ); } From 236f08f462acc15c6a966b68bbf6624dd2370334 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 20:12:14 +0100 Subject: [PATCH 045/138] refactor(ui): update ReasoningExplorer step layout and timeline styling --- .../ui/public/elements/ReasoningExplorer.jsx | 82 +++++++++---------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index beb64be..7aa27e5 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -95,56 +95,52 @@ export default function ReasoningStepsCard() { {/* Reasoning Steps */} {props?.reasoning_steps?.length > 0 && ( -
+
{props.reasoning_steps.map((step, index) => ( -
-
- {/* Timeline */} -
-
+
+ {/* Timeline Column with SVG connector */} +
+ {/* Vertical connector line SVG */} + {index < props.reasoning_steps.length - 1 && ( + + + + )} + + {/* Brain Icon Circle */} +
+
- - {/* Step Content */} -
-

- {step.header} -

-

- {step.content} -

- - {/* Candidate Identifiers — inline badges, left vertical line */} - {step.candidate_identifiers?.length > 0 && ( -
-
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} -
-
- )} -
- {/* Connector line between steps */} - {index < props.reasoning_steps.length - 1 && ( -
-
-
+ {/* Step Content */} +
+

+ {step.header} +

+

+ {step.content} +

+ + {/* Candidate Identifiers — inline badges, left vertical line */} + {step.candidate_identifiers?.length > 0 && ( +
+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))} +
-
- )} + )} +
))}
From 5f4b28e85b7a8ed3a52a3bc905af96c59a693497 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 20:26:14 +0100 Subject: [PATCH 046/138] fix(ui): correct reasoning steps timeline connector rendering and height --- codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 7aa27e5..bf0b899 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -101,8 +101,8 @@ export default function ReasoningStepsCard() { {/* Timeline Column with SVG connector */}
{/* Vertical connector line SVG */} - {index < props.reasoning_steps.length - 1 && ( - + {index < props.reasoning_steps.length && ( + )} From b1e2b26866b710f0421304b92f600d147a25a342 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 20:55:16 +0100 Subject: [PATCH 047/138] feat(ui): enhance ReasoningExplorer step icon with glow effect and improved styling --- .../tide/ui/public/elements/ReasoningExplorer.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index bf0b899..a27e911 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -100,19 +100,19 @@ export default function ReasoningStepsCard() {
{/* Timeline Column with SVG connector */}
+
+ {/* */} +
+ + {/* */} + +
{/* Vertical connector line SVG */} {index < props.reasoning_steps.length && ( )} - - {/* Brain Icon Circle */} -
-
- -
-
{/* Step Content */} From fc124e0b7344b33bd1a97cd8de14cbdd6e0ecfe4 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 21:55:08 +0100 Subject: [PATCH 048/138] refactor(ui): enhance ReasoningExplorer layout, styling, and badge presentation --- .../ui/public/elements/ReasoningExplorer.jsx | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index a27e911..4f34831 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -51,11 +51,11 @@ export default function ReasoningStepsCard() { return ( {/* Header */} - -
+ +
{isLoadingState && ( @@ -80,64 +80,68 @@ export default function ReasoningStepsCard() {
{props?.finished && ( -

+

Thought for {thinkingTime}s

)} -

+

{previewText}

- {/* Always visible content */} - + {/* Main Content */} + {/* Reasoning Steps */} {props?.reasoning_steps?.length > 0 && ( -
+
{props.reasoning_steps.map((step, index) => ( -
- {/* Timeline Column with SVG connector */} -
-
- {/* */} -
- - {/* */} - +
+ {/* Timeline Column */} +
+
+
+
- {/* Vertical connector line SVG */} - {index < props.reasoning_steps.length && ( - - + + {/* Vertical connector line */} + {index < props.reasoning_steps.length - 1 && ( + + )}
{/* Step Content */} -
-

+
+

{step.header}

-

+

{step.content}

- {/* Candidate Identifiers — inline badges, left vertical line */} + {/* Candidate Identifiers */} {step.candidate_identifiers?.length > 0 && ( -
-
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} -
+
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))}
)}
@@ -149,15 +153,16 @@ export default function ReasoningStepsCard() { {/* Context + Modify Identifiers */} {(props?.context_identifiers?.length > 0 || props?.modify_identifiers?.length > 0) && ( -
+
{props.context_identifiers?.length > 0 && ( -
+
+

Context

{props.context_identifiers.map((id, index) => ( {id} @@ -167,13 +172,14 @@ export default function ReasoningStepsCard() { )} {props.modify_identifiers?.length > 0 && ( -
+
+

Modifications

{props.modify_identifiers.map((id, index) => ( {id} @@ -186,4 +192,4 @@ export default function ReasoningStepsCard() { ); -} +} \ No newline at end of file From f5c2a6ac76c05403c686a24a71544e2798328a3e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 22:19:31 +0100 Subject: [PATCH 049/138] style(ui): update ReasoningExplorer context and modification badges spacing and styling --- .../ui/public/elements/ReasoningExplorer.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 4f34831..adb3694 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -153,16 +153,17 @@ export default function ReasoningStepsCard() { {/* Context + Modify Identifiers */} {(props?.context_identifiers?.length > 0 || props?.modify_identifiers?.length > 0) && ( -
+
{props.context_identifiers?.length > 0 && (
-

Context

-
+

Context Identifiers

+
{props.context_identifiers.map((id, index) => ( {id} @@ -173,13 +174,14 @@ export default function ReasoningStepsCard() { {props.modify_identifiers?.length > 0 && (
-

Modifications

-
+

Modification Identifiers

+
{props.modify_identifiers.map((id, index) => ( {id} From 24fed2f88ab7fef4f1a0a96e4061685a060525f7 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 22:34:40 +0100 Subject: [PATCH 050/138] style(ui): fix spacing and margin inconsistencies in ReasoningExplorer badges --- .../agents/tide/ui/public/elements/ReasoningExplorer.jsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index adb3694..98f755b 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -157,13 +157,12 @@ export default function ReasoningStepsCard() { {props.context_identifiers?.length > 0 && (

Context Identifiers

-
+
{props.context_identifiers.map((id, index) => ( {id} @@ -175,13 +174,12 @@ export default function ReasoningStepsCard() { {props.modify_identifiers?.length > 0 && (

Modification Identifiers

-
+
{props.modify_identifiers.map((id, index) => ( {id} From 1f3c731bf9a955f3213f1962d667111d60841e58 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 23:37:29 +0100 Subject: [PATCH 051/138] feat(ui): add collapsible reasoning steps card with expanded view and timeline indicators --- .../ui/public/elements/ReasoningExplorer.jsx | 241 ++++++++++-------- 1 file changed, 132 insertions(+), 109 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 98f755b..7287d5c 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -1,15 +1,16 @@ import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Brain } from "lucide-react"; +import { ChevronDown, ChevronRight, Brain } from "lucide-react"; import { useState, useEffect } from "react"; export default function ReasoningStepsCard() { + const [expanded, setExpanded] = useState(false); const [waveOffset, setWaveOffset] = useState(0); const [loadingText, setLoadingText] = useState("Analyzing"); const [thinkingTime, setThinkingTime] = useState(0); const loadingStates = ["Analyzing", "Thinking", "Updating", "Processing"]; - const isLoadingState = !props?.finished; + const isLoadingState = props.finished; useEffect(() => { const waveInterval = setInterval(() => { @@ -30,19 +31,23 @@ export default function ReasoningStepsCard() { }, []); useEffect(() => { - if (!props?.finished) { + if (isLoadingState) { const timer = setInterval(() => { setThinkingTime((prev) => prev + 1); }, 1000); return () => clearInterval(timer); } - }, [props?.finished]); + }, [isLoadingState]); const getPreviewText = () => { - if (props?.summary) return props.summary.split("\n")[0]; - if (props?.reasoning_steps?.length > 0) - return props.reasoning_steps.at(-1).content.split("\n")[0]; - if (props?.finished) return "Finished"; + // Mock data for demo + const reasoning_steps = props.reasoning_steps; + const summary = props.summary; + + if (summary) return summary.split("\n")[0]; + if (reasoning_steps?.length > 0) + return reasoning_steps.at(-1).content.split("\n")[0]; + return `${loadingText}...`; }; @@ -50,9 +55,12 @@ export default function ReasoningStepsCard() { return ( - {/* Header */} + {/* Header - Collapsible */} -
+ - {/* Main Content */} - - {/* Reasoning Steps */} - {props?.reasoning_steps?.length > 0 && ( -
- {props.reasoning_steps.map((step, index) => ( -
- {/* Timeline Column */} -
-
-
- -
- - {/* Vertical connector line */} - {index < props.reasoning_steps.length - 1 && ( - + {/* Reasoning Steps */} + {props?.reasoning_steps?.length > 0 && ( +
+ {props.reasoning_steps.map((step, index) => ( +
+ {/* Timeline Column */} +
+
- - - )} -
- - {/* Step Content */} -
-

- {step.header} -

-

- {step.content} -

- - {/* Candidate Identifiers */} - {step.candidate_identifiers?.length > 0 && ( -
- {step.candidate_identifiers.map((id, idIndex) => ( - - {id} - - ))} +
+
- )} + + {/* Vertical connector line */} + {index < props.reasoning_steps.length - 1 && ( + + + + )} +
+ + {/* Step Content */} +
+

+ {step.header} +

+

+ {step.content} +

+ + {/* Candidate Identifiers */} + {step.candidate_identifiers?.length > 0 && ( +
+ {step.candidate_identifiers.map((id, idIndex) => ( + + {id} + + ))} +
+ )} +
-
- ))} -
- )} - - {/* Context + Modify Identifiers */} - {(props?.context_identifiers?.length > 0 || - props?.modify_identifiers?.length > 0) && ( -
- {props.context_identifiers?.length > 0 && ( -
-

Context Identifiers

-
- {props.context_identifiers.map((id, index) => ( - - {id} - - ))} + ))} +
+ )} + + {/* Context + Modify Identifiers */} + {(props?.context_identifiers?.length > 0 || + props?.modify_identifiers?.length > 0) && ( +
+ {props.context_identifiers?.length > 0 && ( +
+

Context Identifiers

+
+ {props.context_identifiers.map((id, index) => ( + + {id} + + ))} +
-
- )} + )} - {props.modify_identifiers?.length > 0 && ( -
-

Modification Identifiers

-
- {props.modify_identifiers.map((id, index) => ( - - {id} - - ))} + {props.modify_identifiers?.length > 0 && ( +
+

Modification Identifiers

+
+ {props.modify_identifiers.map((id, index) => ( + + {id} + + ))} +
-
- )} -
- )} - + )} +
+ )} + + )} ); } \ No newline at end of file From c47ef88ba8da20f5915cda5c3aea6026f6efe963 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 23:43:52 +0100 Subject: [PATCH 052/138] fix(ui): correct loading state logic and adjust reasoning step connector height --- codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 7287d5c..8f748ee 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -10,7 +10,7 @@ export default function ReasoningStepsCard() { const [thinkingTime, setThinkingTime] = useState(0); const loadingStates = ["Analyzing", "Thinking", "Updating", "Processing"]; - const isLoadingState = props.finished; + const isLoadingState = !props.finished; useEffect(() => { const waveInterval = setInterval(() => { @@ -134,7 +134,7 @@ export default function ReasoningStepsCard() { {index < props.reasoning_steps.length - 1 && ( From 2885aa304cb1c993333161a5738da23e7494f01e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 23:51:46 +0100 Subject: [PATCH 053/138] fix(ui): update ReasoningExplorer header to show 'Finished' when done --- codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 8f748ee..2d7277e 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -47,7 +47,7 @@ export default function ReasoningStepsCard() { if (summary) return summary.split("\n")[0]; if (reasoning_steps?.length > 0) return reasoning_steps.at(-1).content.split("\n")[0]; - + if (props?.finished) return "Finished"; return `${loadingText}...`; }; From 952721254e7b8bbeaf3008ef8796819c082b773f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 14 Oct 2025 23:52:26 +0100 Subject: [PATCH 054/138] refactor(prompts): enhance candidate classification and filtering logic in tide agent --- codetide/agents/tide/prompts.py | 96 +++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 33 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index b205df1..c09d6ae 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -650,42 +650,60 @@ **SOLE PURPOSE:** Classify gathered candidates and determine operation mode. **PHASE 2 MISSION:** -1. Review all Phase 1 candidates -2. Classify into Context vs Modify -3. Determine operation mode +1. Review all Phase 1 candidates with strict confidence filtering +2. Select ONLY high-confidence, directly relevant identifiers +3. Classify into Context vs Modify +4. Determine operation mode **CURRENT STATE:** -- User request: {USER_REQUEST} -- Candidate pool: {ALL_CANDIDATES} +- User request: +``` +{USER_REQUEST} +``` +- Candidate pool: +``` +{ALL_CANDIDATES} +``` **HARD LIMIT - FINAL RESPONSE:** Maximum 5 identifiers total across Context and Modify combined. +**MINIMUM CONFIDENCE:** Only include candidates with >80% relevance to the specific user request. -**CLASSIFICATION:** +**CLASSIFICATION RULES - APPLY STRICTLY:** **Context Identifiers** (understanding/reference, NOT direct dependencies): -- Supporting utilities, base classes, configuration -- Related functionality providing broader scope understanding -- Interfaces and contracts that inform approach -- NOTE: Direct dependencies of Modify identifiers are handled by framework; focus on peripheral context +- ONLY if they directly inform the approach to solving the request +- Supporting utilities, base classes, configuration that the Modify identifiers depend on +- Interfaces and contracts that explain constraints or requirements +- EXCLUDE: Generic utilities, tangential files, framework internals +- Test: "Would this identifier be essential to understand WHY the Modify changes are needed?" **Modify Identifiers** (direct changes): -- Code requiring direct updates -- New code additions -- Entities to be altered -- NOTE: Do NOT include direct dependencies; framework coverage handles these +- Code requiring direct updates to satisfy the user request +- New code additions that directly implement the request +- Entities that must be altered to complete the request +- EXCLUDE: Their direct dependencies (framework handles these) +- EXCLUDE: Utilities unless they are the actual target of modification +- Test: "Does this directly contribute to fulfilling the user request?" **CRITICAL:** Only actual code elements (functions, classes, methods, variables). No packages, imports, or bare modules. +**PRIORITY MATRIX (use to eliminate weak candidates):** +High Priority: Direct implementation targets, core logic changes +Medium Priority: Essential context for understanding approach +Low Priority: Nice-to-have references, peripherally related utilities +→ **ELIMINATE all Low Priority candidates first** +→ **Keep only High/Medium if they meet >80% relevance threshold** + **OPERATION MODES:** -- STANDARD: Explanations, info retrieval -- PLAN_STEPS: Multi-step implementation, complex changes -- PATCH_CODE: Direct fixes/updates -- Mix modes as needed (e.g., PLAN_STEPS+PATCH_CODE) +- STANDARD: Explanations, info retrieval, analysis +- PLAN_STEPS: Multi-step implementation, complex features, architectural changes +- PATCH_CODE: Direct fixes, bug resolution, targeted updates +- Mix modes as needed (e.g., PLAN_STEPS+PATCH_CODE for feature with fixes) **OUTPUT FORMAT:** *** Begin Summary -[4-5 lines max: Phase 1 exploration summary, key areas found, final classification rationale] +[3-4 lines: Phase 1 exploration summary, key areas identified, strict rationale for final selection. Be explicit: "Excluded X because..." for any candidates not selected] *** End Summary *** Begin Context Identifiers @@ -700,18 +718,30 @@ OPERATION_MODE: [MODE] -**DECISION RULES:** -- Uncertain: prefer Context -- "Use X to do Y": X is Context, Y is Modify -- "Understand/explain": mostly Context -- "Change behavior": focus on Modify -- Match mode to request intent (STANDARD for questions, PLAN_STEPS for features, PATCH_CODE for fixes) -- Strict selection: Keep only 5 total identifiers; eliminate redundant or framework-covered dependencies - -**QUALITY CHECKS:** -- Verify all identifiers are actual code elements, not imports/packages -- Ensure Modify identifiers are primary targets (not their dependencies) -- Confirm Context identifiers provide necessary peripheral understanding -- Check Operation Mode matches request intent -- **CRITICAL: Enforce 5 identifier maximum total** +**SELECTION DECISION TREE:** +1. Extract core intent from {USER_REQUEST} +2. For each candidate: Does it directly support this intent? (Yes/No/Maybe) +3. Eliminate all "Maybe" candidates +4. Eliminate all "Yes" candidates scoring <80% relevance +5. Bucket remaining into Context vs Modify +6. Apply Priority Matrix to Context (can afford to drop some for Context) +7. Finalize Modify first (these are non-negotiable), then Context +8. If total > 5, drop lowest-priority Context identifiers first +9. Verify final selection: Each identifier should pass the "Why is this essential?" test + +**QUALITY CHECKS - ENFORCE STRICTLY:** +✓ Every identifier is actual code (not imports/packages/modules) +✓ Every identifier directly supports the stated user request +✓ Modify identifiers are primary targets (dependencies excluded) +✓ Context identifiers provide essential understanding only +✓ Operation Mode clearly matches request intent +✓ Total identifier count ≤ 5 +✓ For any excluded candidates, the summary explains why + +**RED FLAGS - REJECT CANDIDATES IF:** +- Generic/framework-internal utilities with no direct request relevance +- Indirect dependencies that will be handled by the system +- Candidates added "just in case" or "for completeness" +- Multiple similar utilities when one would suffice +- High-level modules when specific functions are the actual targets """ From 06d2755027aa6d7938726fd7f7de50efbcb8d441 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 15 Oct 2025 22:06:24 +0100 Subject: [PATCH 055/138] feat(ui): enhance ReasoningExplorer loading states with varied animated messages --- .../tide/ui/public/elements/ReasoningExplorer.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 2d7277e..6b28e06 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -9,7 +9,17 @@ export default function ReasoningStepsCard() { const [loadingText, setLoadingText] = useState("Analyzing"); const [thinkingTime, setThinkingTime] = useState(0); - const loadingStates = ["Analyzing", "Thinking", "Updating", "Processing"]; + const loadingStates = [ + "Diving deep into the code", + "Charting uncharted waters", + "Debugging the tide", + "Navigating the current flow", + "Riding the wave of logic", + "Casting nets into the depths", + "Exploring the digital ocean", + "Following the stream of creation" + ].sort(() => Math.random() - 0.5); + const isLoadingState = !props.finished; useEffect(() => { @@ -22,7 +32,7 @@ export default function ReasoningStepsCard() { const idx = loadingStates.indexOf(prev); return loadingStates[(idx + 1) % loadingStates.length]; }); - }, 1000); + }, 2500); return () => { clearInterval(waveInterval); From c53e9a7dd73d83e187da502715efc06987c6e737 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 15 Oct 2025 22:43:27 +0100 Subject: [PATCH 056/138] refactor(core): enhance _build_tree_dict with slim mode and improved filtering logic --- codetide/core/models.py | 284 ++++++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 124 deletions(-) diff --git a/codetide/core/models.py b/codetide/core/models.py index 87602ef..43e817f 100644 --- a/codetide/core/models.py +++ b/codetide/core/models.py @@ -644,14 +644,23 @@ def get_tree_view(self, include_modules: bool = False, include_types: bool = Fal return "\n".join(lines) - def _build_tree_dict(self, filter_paths: list = None): + def _build_tree_dict(self, filter_paths: list = None, slim: bool = False): """Creates nested dictionary representing codebase directory structure with optional filtering. - When filtering is applied, includes: + Args: + filter_paths: List of file paths to filter on + slim: When True, returns only contents of subdirectories at filter_paths level. + When False (default), preserves current behavior with siblings and context. + + When filtering with slim=False: 1. Filtered files (with full content) 2. Sibling files in same directories as filtered files 3. Sibling directories at the same level as directories containing filtered files 4. Contents of sibling directories (files and subdirectories) + + When filtering with slim=True: + - Returns ONLY the directory structure at the level of filter_paths + - No siblings, no parent context, just the immediate subdirs/files """ tree = {} @@ -676,150 +685,177 @@ def _build_tree_dict(self, filter_paths: list = None): dir_path = "/".join(path_parts[:-1]) filter_directories.add(dir_path) - # Extract parent directory to find sibling directories - parent_parts = path_parts[:-2] # Remove filename and immediate directory - if parent_parts: - parent_dir = "/".join(parent_parts) - parent_directories.add(parent_dir) - else: - # The filtered file's directory is at root level - parent_directories.add("") + if not slim: + # Extract parent directory to find sibling directories (slim=False only) + parent_parts = path_parts[:-2] # Remove filename and immediate directory + if parent_parts: + parent_dir = "/".join(parent_parts) + parent_directories.add(parent_dir) + else: + # The filtered file's directory is at root level + parent_directories.add("") else: # File is at root level filter_directories.add("") - # Find all directories that are siblings to directories containing filtered files - # AND all their subdirectories (to peek below) - sibling_directories = set() - for code_file in self.root: - if not code_file.file_path: - continue - - normalized_file_path = code_file.file_path.replace("\\", "/") - file_parts = normalized_file_path.split("/") + if slim: + # SLIM MODE: Only include files in the filter directories + relevant_files = [] + sibling_files = [] - if len(file_parts) > 1: - file_dir = "/".join(file_parts[:-1]) + for code_file in self.root: + if not code_file.file_path: + continue - # Check if this file's directory is a sibling to any filter directory - file_dir_parts = file_dir.split("/") - if len(file_dir_parts) > 1: - file_parent_dir = "/".join(file_dir_parts[:-1]) - if file_parent_dir in parent_directories: - sibling_directories.add(file_dir) + normalized_file_path = code_file.file_path.replace("\\", "/") + + # Check if this is a filtered file + if normalized_file_path in normalized_filter_paths: + relevant_files.append(code_file) else: - # File's directory is at root level - if "" in parent_directories: - sibling_directories.add(file_dir) - - # Also check if this directory is a subdirectory of any sibling directory - # This allows peeking into subdirectories - for parent_dir in parent_directories: - if parent_dir == "": - # Root level parent - include all top-level directories and their subdirs - if len(file_dir_parts) >= 1: + # Check if this file is in any filter directory + file_parts = normalized_file_path.split("/") + if len(file_parts) > 1: + file_dir = "/".join(file_parts[:-1]) + else: + file_dir = "" + + if file_dir in filter_directories: + sibling_files.append(code_file) + else: + # STANDARD MODE: Original behavior with siblings and context + # Find all directories that are siblings to directories containing filtered files + # AND all their subdirectories (to peek below) + sibling_directories = set() + for code_file in self.root: + if not code_file.file_path: + continue + + normalized_file_path = code_file.file_path.replace("\\", "/") + file_parts = normalized_file_path.split("/") + + if len(file_parts) > 1: + file_dir = "/".join(file_parts[:-1]) + + # Check if this file's directory is a sibling to any filter directory + file_dir_parts = file_dir.split("/") + if len(file_dir_parts) > 1: + file_parent_dir = "/".join(file_dir_parts[:-1]) + if file_parent_dir in parent_directories: sibling_directories.add(file_dir) else: - # Check if file_dir starts with any parent directory path - if file_dir.startswith(parent_dir + "/") or file_dir == parent_dir: + # File's directory is at root level + if "" in parent_directories: sibling_directories.add(file_dir) - else: - # File is at root level, check if root is a parent directory - if "" in parent_directories: - sibling_directories.add("") - - # Also add subdirectories of filter directories themselves - subdirectories = set() - for code_file in self.root: - if not code_file.file_path: - continue + + # Also check if this directory is a subdirectory of any sibling directory + # This allows peeking into subdirectories + for parent_dir in parent_directories: + if parent_dir == "": + # Root level parent - include all top-level directories and their subdirs + if len(file_dir_parts) >= 1: + sibling_directories.add(file_dir) + else: + # Check if file_dir starts with any parent directory path + if file_dir.startswith(parent_dir + "/") or file_dir == parent_dir: + sibling_directories.add(file_dir) + else: + # File is at root level, check if root is a parent directory + if "" in parent_directories: + sibling_directories.add("") + + # Also add subdirectories of filter directories themselves + subdirectories = set() + for code_file in self.root: + if not code_file.file_path: + continue + + normalized_file_path = code_file.file_path.replace("\\", "/") + file_parts = normalized_file_path.split("/") - normalized_file_path = code_file.file_path.replace("\\", "/") - file_parts = normalized_file_path.split("/") + if len(file_parts) > 1: + file_dir = "/".join(file_parts[:-1]) + + # Check if this directory is a subdirectory of any filter directory + for filter_dir in filter_directories: + if filter_dir == "": + # Root level filter - include everything + subdirectories.add(file_dir) + elif file_dir.startswith(filter_dir + "/") or file_dir == filter_dir: + subdirectories.add(file_dir) + + # Combine all relevant directories + all_relevant_directories = filter_directories.union(sibling_directories).union(subdirectories) - if len(file_parts) > 1: - file_dir = "/".join(file_parts[:-1]) + # Find all files that should be included + relevant_files = [] # Files that should show full content (filtered files) + sibling_files = [] # Files that should show as context (siblings and directory contents) + + for code_file in self.root: + if not code_file.file_path: + continue + + normalized_file_path = code_file.file_path.replace("\\", "/") - # Check if this directory is a subdirectory of any filter directory - for filter_dir in filter_directories: - if filter_dir == "": - # Root level filter - include everything - subdirectories.add(file_dir) - elif file_dir.startswith(filter_dir + "/") or file_dir == filter_dir: - subdirectories.add(file_dir) - - # Combine all relevant directories - all_relevant_directories = filter_directories.union(sibling_directories).union(subdirectories) - - # Find all files that should be included - relevant_files = [] # Files that should show full content (filtered files) - sibling_files = [] # Files that should show as context (siblings and directory contents) + # Check if this is a filtered file (should show full content) + if normalized_file_path in normalized_filter_paths: + relevant_files.append(code_file) + continue + + # Check if this file is in any of the relevant directories + file_parts = normalized_file_path.split("/") + if len(file_parts) > 1: + file_dir = "/".join(file_parts[:-1]) + else: + file_dir = "" + + if file_dir in all_relevant_directories: + sibling_files.append(code_file) - for code_file in self.root: + # Build tree structure from relevant files (with full content) + for code_file in relevant_files: if not code_file.file_path: continue - normalized_file_path = code_file.file_path.replace("\\", "/") + # Split the file path into parts + path_parts = code_file.file_path.replace("\\", "/").split("/") - # Check if this is a filtered file (should show full content) - if normalized_file_path in normalized_filter_paths: - relevant_files.append(code_file) + # Navigate/create the nested dictionary structure + current_level = tree + for i, part in enumerate(path_parts): + if i == len(path_parts) - 1: # This is the file + current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} + else: # This is a directory + if part not in current_level: + current_level[part] = {"_type": "directory"} + current_level = current_level[part] + + # Add sibling files and directory contents (show content for all when filtering for broader context) + for code_file in sibling_files: + if not code_file.file_path: continue + + # Split the file path into parts + path_parts = code_file.file_path.replace("\\", "/").split("/") - # Check if this file is in any of the relevant directories - file_parts = normalized_file_path.split("/") - if len(file_parts) > 1: - file_dir = "/".join(file_parts[:-1]) - else: - file_dir = "" - - if file_dir in all_relevant_directories: - sibling_files.append(code_file) - - # Build tree structure from relevant files (with full content) - for code_file in relevant_files: - if not code_file.file_path: - continue - - # Split the file path into parts - path_parts = code_file.file_path.replace("\\", "/").split("/") + # Navigate/create the nested dictionary structure + current_level = tree + for i, part in enumerate(path_parts): + if i == len(path_parts) - 1: # This is the file + # Check if file already exists (might have been added as relevant_files) + if part not in current_level: + # Show content for all files to provide broader context + current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} + else: # This is a directory + if part not in current_level: + current_level[part] = {"_type": "directory"} + current_level = current_level[part] - # Navigate/create the nested dictionary structure - current_level = tree - for i, part in enumerate(path_parts): - if i == len(path_parts) - 1: # This is the file - current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} - else: # This is a directory - if part not in current_level: - current_level[part] = {"_type": "directory"} - current_level = current_level[part] - - # Add sibling files and directory contents (show content for all when filtering for broader context) - for code_file in sibling_files: - if not code_file.file_path: - continue - - # Split the file path into parts - path_parts = code_file.file_path.replace("\\", "/").split("/") + # Add placeholder for omitted content when filtering is applied and not in slim mode + if filter_paths is not None and not slim: + tree = self._add_omitted_placeholders(tree, filter_paths) - # Navigate/create the nested dictionary structure - current_level = tree - for i, part in enumerate(path_parts): - if i == len(path_parts) - 1: # This is the file - # Check if file already exists (might have been added as relevant_files) - if part not in current_level: - # Show content for all files to provide broader context - current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} - else: # This is a directory - if part not in current_level: - current_level[part] = {"_type": "directory"} - current_level = current_level[part] - - # Add placeholder for omitted content when filtering is applied - if filter_paths is not None: - tree = self._add_omitted_placeholders(tree, filter_paths) - - self._tree_dict = tree + self._tree_dict = tree def _add_omitted_placeholders(self, tree: dict, filter_paths: list) -> dict: """Adds '...' placeholders for directories that contain omitted files.""" From b0821ee79426e5a4ca91939991d641dddb36f9d7 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 15 Oct 2025 22:52:14 +0100 Subject: [PATCH 057/138] refactor(agent): improve reasoning parsing and candidate extraction in tide agent --- codetide/agents/tide/agent.py | 61 +++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index ca98edd..6c80380 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -1,4 +1,5 @@ import json +import re from codetide import CodeTide from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file, parse_patch_blocks from ...core.defaults import DEFAULT_STORAGE_PATH @@ -6,7 +7,7 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -198,21 +199,39 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti reasoning_blocks = parse_blocks(phase1_response, block_word="Reasoning", multiple=True) expand_paths_block = parse_blocks(phase1_response, block_word="Expand Paths", multiple=False) + ### TODO update to use Rationale and New Candidates Indeintifiers and extract based onr egex ffs # Extract and accumulate candidates from reasoning blocks + + patterns = { + "header": r"\*{0,2}Task\*{0,2}:\s*(.+?)(?=\n\s*\*{0,2}Rationale\*{0,2})", + "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}Candidate Identifiers\*{0,2}|$)", + "candidate_identifiers": r"^\s*-\s*(.+?)$" + } + for reasoning in reasoning_blocks: - all_reasoning.append(reasoning) - # Extract candidate identifiers from reasoning block - if "**Candidate Identifiers**:" in reasoning or "**candidate_identifiers**:" in reasoning.lower(): - lines = reasoning.split('\n') - capture = False - for line in lines: - if "candidate" in line.lower() and "identifier" in line.lower(): - capture = True - continue - if capture and line.strip().startswith('-'): - ident = line.strip().lstrip('-').strip() - if ident := self.get_valid_identifier(autocomplete, ident): - candidate_pool.add(ident) + # Extract header + header_match = re.search(patterns["header"], reasoning, re.DOTALL) + header = header_match.group(1).strip() if header_match else None + + # Extract content (rationale) + content_match = re.search(patterns["content"], reasoning, re.DOTALL | re.MULTILINE) + content = content_match.group(1).strip() if content_match else None + + # Append only header and content to all_reasoning + if header and content: + all_reasoning.append(REASONING_TEMPLTAE.format(**{ + "header": header, + "content": content + })) + + # Extract candidate identifiers using regex + candidate_pattern = patterns["candidate_identifiers"] + candidate_matches = re.findall(candidate_pattern, reasoning, re.MULTILINE) + + for match in candidate_matches: + ident = match.strip() + if ident := self.get_valid_identifier(autocomplete, ident): + candidate_pool.add(ident) # Check if we need to expand more if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper(): @@ -240,16 +259,24 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti # Prepare Phase 2 input all_reasoning_text = "\n\n".join(all_reasoning) all_candidates_text = "\n".join(sorted(candidate_pool)) + + candidates_to_filter_tree = self.tide._as_file_paths(list(candidate_pool)) + self.tide.codebase._build_tree_dict(candidates_to_filter_tree, slim=True) + sub_tree = self.tide.codebase.get_tree_view() + + # print(sub_tree) + + # print(f"{all_candidates_text=}") phase2_response = await self.llm.acomplete( expanded_history, system_prompt=[FINALIZE_IDENTIFIERS_PROMPT.format( DATE=TODAY, SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, - USER_REQUEST=last_message, - ALL_CANDIDATES=all_candidates_text + EXPLORATION_STEPS=all_reasoning_text, + ALL_CANDIDATES=all_candidates_text, )], - prefix_prompt=f"Phase 1 Exploration Results:\n\n{all_reasoning_text}", + prefix_prompt=sub_tree, stream=True ) From 2876941f32986dda4837212d5fc2d9eac8fa50cc Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 15 Oct 2025 22:52:48 +0100 Subject: [PATCH 058/138] refactor(prompts): simplify and clarify final selection prompt formatting and instructions --- codetide/agents/tide/prompts.py | 152 +++++++++++++------------------- 1 file changed, 62 insertions(+), 90 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index c09d6ae..92daec7 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -640,70 +640,50 @@ FINALIZE_IDENTIFIERS_PROMPT = """ You are Agent **Tide**, operating in **Final Selection Mode** on **{DATE}**. -**SUPPORTED_LANGUAGES**: {SUPPORTED_LANGUAGES} - -**ABSOLUTE PROHIBITIONS:** -- Do NOT answer requests or provide solutions -- Do NOT view/analyze file contents -- Do NOT use any markdown formatting - -**SOLE PURPOSE:** Classify gathered candidates and determine operation mode. - -**PHASE 2 MISSION:** -1. Review all Phase 1 candidates with strict confidence filtering -2. Select ONLY high-confidence, directly relevant identifiers -3. Classify into Context vs Modify -4. Determine operation mode - -**CURRENT STATE:** -- User request: -``` -{USER_REQUEST} -``` -- Candidate pool: -``` -{ALL_CANDIDATES} -``` - -**HARD LIMIT - FINAL RESPONSE:** Maximum 5 identifiers total across Context and Modify combined. -**MINIMUM CONFIDENCE:** Only include candidates with >80% relevance to the specific user request. - -**CLASSIFICATION RULES - APPLY STRICTLY:** - -**Context Identifiers** (understanding/reference, NOT direct dependencies): -- ONLY if they directly inform the approach to solving the request -- Supporting utilities, base classes, configuration that the Modify identifiers depend on -- Interfaces and contracts that explain constraints or requirements -- EXCLUDE: Generic utilities, tangential files, framework internals -- Test: "Would this identifier be essential to understand WHY the Modify changes are needed?" - -**Modify Identifiers** (direct changes): -- Code requiring direct updates to satisfy the user request -- New code additions that directly implement the request -- Entities that must be altered to complete the request -- EXCLUDE: Their direct dependencies (framework handles these) -- EXCLUDE: Utilities unless they are the actual target of modification -- Test: "Does this directly contribute to fulfilling the user request?" - -**CRITICAL:** Only actual code elements (functions, classes, methods, variables). No packages, imports, or bare modules. - -**PRIORITY MATRIX (use to eliminate weak candidates):** -High Priority: Direct implementation targets, core logic changes -Medium Priority: Essential context for understanding approach -Low Priority: Nice-to-have references, peripherally related utilities -→ **ELIMINATE all Low Priority candidates first** -→ **Keep only High/Medium if they meet >80% relevance threshold** - -**OPERATION MODES:** -- STANDARD: Explanations, info retrieval, analysis -- PLAN_STEPS: Multi-step implementation, complex features, architectural changes -- PATCH_CODE: Direct fixes, bug resolution, targeted updates -- Mix modes as needed (e.g., PLAN_STEPS+PATCH_CODE for feature with fixes) - -**OUTPUT FORMAT:** +**LANGUAGES**: {SUPPORTED_LANGUAGES} +**PROHIBITIONS**: No answers/solutions, no file analysis, no markdown + +**MISSION**: Filter candidate pool → classify Context vs Modify → determine operation mode +*Request provided in user message* + +**INPUT STATE**: +- Exploration Logic: {EXPLORATION_STEPS} +- Candidate Pool: {ALL_CANDIDATES} + +**CONSTRAINTS**: +- MAX 5 identifiers total +- MIN 80% relevance threshold +- Only actual code elements (functions, classes, methods, variables) + +**CLASSIFICATION**: + +**Context** (understanding, not dependencies): +- Direct informants for the approach +- Supporting code the Modify depends on +- Constraints/requirements interfaces +- Test: "Essential to understand WHY the changes?" + +**Modify** (direct changes): +- Code requiring updates +- New implementations +- Targets of alteration +- EXCLUDE dependencies (framework handles) +- Test: "Directly fulfills the request?" + +**PRIORITY ELIMINATION**: +1. Parse user intent from message +2. Score each candidate: Does it directly support intent? Yes/No/Maybe +3. Eliminate Maybe + all <80% relevance +4. Bucket into Context vs Modify +5. Apply Priority: High → Medium → Low (drop Low first) +6. Modify candidates are non-negotiable +7. If >5 total, drop lowest-priority Context +8. Final check: "Why is this essential?" + +**OUTPUT FORMAT**: *** Begin Summary -[3-4 lines: Phase 1 exploration summary, key areas identified, strict rationale for final selection. Be explicit: "Excluded X because..." for any candidates not selected] +[3-4 lines: Key findings, why candidates selected/excluded] *** End Summary *** Begin Context Identifiers @@ -716,32 +696,24 @@ [another.identifier] *** End Modify Identifiers -OPERATION_MODE: [MODE] - -**SELECTION DECISION TREE:** -1. Extract core intent from {USER_REQUEST} -2. For each candidate: Does it directly support this intent? (Yes/No/Maybe) -3. Eliminate all "Maybe" candidates -4. Eliminate all "Yes" candidates scoring <80% relevance -5. Bucket remaining into Context vs Modify -6. Apply Priority Matrix to Context (can afford to drop some for Context) -7. Finalize Modify first (these are non-negotiable), then Context -8. If total > 5, drop lowest-priority Context identifiers first -9. Verify final selection: Each identifier should pass the "Why is this essential?" test - -**QUALITY CHECKS - ENFORCE STRICTLY:** -✓ Every identifier is actual code (not imports/packages/modules) -✓ Every identifier directly supports the stated user request -✓ Modify identifiers are primary targets (dependencies excluded) -✓ Context identifiers provide essential understanding only -✓ Operation Mode clearly matches request intent -✓ Total identifier count ≤ 5 -✓ For any excluded candidates, the summary explains why - -**RED FLAGS - REJECT CANDIDATES IF:** -- Generic/framework-internal utilities with no direct request relevance -- Indirect dependencies that will be handled by the system -- Candidates added "just in case" or "for completeness" -- Multiple similar utilities when one would suffice -- High-level modules when specific functions are the actual targets +OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE|MODE_COMBINATION] + +**QUALITY GATES**: +✓ Each identifier is actual code +✓ Each directly supports request +✓ Modify = primary targets only +✓ Context = essential understanding only +✓ Total ≤ 5 +✓ Summary explains all exclusions + +**RED FLAGS - REJECT IF**: +- Generic framework utilities +- Indirect dependencies +- "Just in case" additions +- Redundant similar utilities +""" + +REASONING_TEMPLTAE = """ +**Task**: {header} +**Rationale**: {content} """ From 273b95d8d2f730ee6573ff332cdc7a0bdf8aabbb Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 15 Oct 2025 23:02:20 +0100 Subject: [PATCH 059/138] refactor(prompts): update candidate gathering prompt for clarity and constraints --- codetide/agents/tide/prompts.py | 71 +++++++++++++-------------------- 1 file changed, 27 insertions(+), 44 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 92daec7..29baa1e 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -565,76 +565,59 @@ """ GATHER_CANDIDATES_PROMPT = """ -You are Agent **Tide**, operating in **Candidate Gathering Mode** on **{DATE}**. +You are Agent Tide in Candidate Gathering Mode | {DATE} +Languages: {SUPPORTED_LANGUAGES} -**SUPPORTED_LANGUAGES**: {SUPPORTED_LANGUAGES} +**CRITICAL RULES:** +- ONLY identify candidates, never solve or suggest +- NO markdown formatting +- NO file analysis or implementations +- DEDUPLICATE: Every candidate must be novel (verify against {ACCUMULATED_CONTEXT}) -**ABSOLUTE PROHIBITIONS:** -- Do NOT answer user requests directly -- Do NOT provide solutions or suggestions -- Do NOT view/analyze file contents or check implementations -- Do NOT use any markdown formatting +**STATE:** +Tree: {TREE_STATE} | Accumulated: {ACCUMULATED_CONTEXT} | Iteration: {ITERATION_COUNT} -**SOLE PURPOSE:** Identify potential candidate identifiers by expanding repository structure. - -**CURRENT STATE:** -- Repository tree: {TREE_STATE} -- Accumulated context: {ACCUMULATED_CONTEXT} -- Iteration: {ITERATION_COUNT} - -**CRITICAL DEDUPLICATION REQUIREMENT:** -Each reasoning block MUST contribute NEW candidate identifiers. DO NOT repeat any identifier from other Reasoning Blocks. Verify each candidate is novel before including it. This ensures cumulative exploration, not repetition. - -**STRATEGY:** Analyze user request for functional areas → scan tree for matches → expand collapsed directories → identify NEW identifiers NOT in accumulated pool. +--- -**OUTPUT FORMAT - Concise Reasoning Block:** +**REASONING FORMAT** (First-Person): *** Begin Reasoning **Task**: [Brief task from request] -**Rationale**: [Why this new area matters] +**Rationale**: [Why this new area matters - in first person: I focus on this because...] **NEW Candidate Identifiers**: [MAX 3 ONLY - MUST BE NOVEL] - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] - [third.identifier.or.path] *** End Reasoning -**HARD LIMITS:** -- Each reasoning block: AT MOST 3 candidate identifiers -- ALL identifiers MUST be NEW (not from other Reasoning Blocks) -- Focus on unexplored areas and new functional domains -- Do NOT include duplicates under any circumstances +**CONSTRAINTS:** +- Max 3 identifiers per reasoning block +- All must be NEW (cross-check {ACCUMULATED_CONTEXT}) +- Dot notation for {SUPPORTED_LANGUAGES} files (functions/classes/methods) +- File paths only for other formats +- Zero speculation—only traceable to {TREE_STATE} -**IDENTIFIER RULES - VALIDATION-FIRST APPROACH:** -- For SUPPORTED_LANGUAGES files: Use dot notation (functions, classes, methods) -- For other files: Use file paths only -- No package names, imports, or external dependencies -- ONLY suggest identifiers traceable to {TREE_STATE} or inferable from visible file/directory patterns -- Cross-reference each identifier against {TREE_STATE} before inclusion -- If unsure whether identifier exists in tree: DO NOT include it -- Never speculate, only include identifiers you are sure are valid, to maximize validation success +--- -**EXPANSION DECISION:** +**EXPANSION:** *** Begin Expand Paths [path/to/directory/] [another/path/] *** End Expand Paths -Expand when: -- Core directories are collapsed -- File names aren't visible -- New functional areas haven't been explored -- Previous reasoning didn't cover this directory +Expand when: directories collapsed, files hidden, or new areas unexplored. -**ASSESSMENTS:** +--- +**ASSESSMENTS:** ENOUGH_IDENTIFIERS: [TRUE|FALSE] -- TRUE: All major areas explored, file organization clear, key tasks identified, no new areas to expand -- FALSE: Core directories collapsed, core tasks not covered, unexplored areas remain +- TRUE when: all major areas explored, structure clear, key tasks identified +- FALSE when: core directories collapsed or unexplored areas remain ENOUGH_HISTORY: [TRUE|FALSE] -- TRUE: Request references prior context -- FALSE: Request is self-contained +- TRUE when: request references prior context +- FALSE when: request is self-contained """ FINALIZE_IDENTIFIERS_PROMPT = """ From 513b4b2dbeb13dda510656e1f8545c9a46da5f85 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 15 Oct 2025 23:03:06 +0100 Subject: [PATCH 060/138] refactor(agent): improve context labeling and prompt formatting in tide agent --- codetide/agents/tide/agent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 6c80380..89eaa93 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -179,17 +179,18 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti # Prepare accumulated context accumulated_context = "\n".join(sorted(candidate_pool)) if candidate_pool else "None yet" + view_type = "`Current view`" if iteration_count == 1 else "`Expanded view`" # Phase 1 LLM call phase1_response = await self.llm.acomplete( expanded_history, system_prompt=[GATHER_CANDIDATES_PROMPT.format( DATE=TODAY, SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, - TREE_STATE="`Current view`" if iteration_count == 1 else "`Expanded view`", + TREE_STATE=view_type, ACCUMULATED_CONTEXT=accumulated_context, ITERATION_COUNT=iteration_count )], - prefix_prompt=repo_tree, + prefix_prompt=rf"# {view_type}\n\n{repo_tree}", stream=True ) From 8daf3b8f154155dd73f97eb5799ae14c79f37b0e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 16 Oct 2025 20:15:13 +0100 Subject: [PATCH 061/138] style(ui): add top margin to modification identifiers label in ReasoningExplorer --- codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 6b28e06..81adf51 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -205,7 +205,7 @@ export default function ReasoningStepsCard() { {props.modify_identifiers?.length > 0 && (
-

Modification Identifiers

+

Modification Identifiers

{props.modify_identifiers.map((id, index) => ( Date: Thu, 16 Oct 2025 22:06:59 +0100 Subject: [PATCH 062/138] refactor(ui): clear example summary and comment out chat update in app.py --- codetide/agents/tide/ui/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 4ac97f8..8eefc43 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -142,7 +142,7 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): ], "context_identifiers": ["user_context", "system_requirements", "api_documentation"], "modify_identifiers": ["configuration_settings", "user_preferences"], - "summary": "no summary yet", + "summary": "", "finished": False } @@ -268,9 +268,9 @@ async def start_chat(): card_element = cl.CustomElement(name="ReasoningExplorer", props=example1) await cl.Message(content="", elements=[card_element]).send() - await asyncio.sleep(2) - card_element.props.update(example2) - await card_element.update() + # await asyncio.sleep(2) + # card_element.props.update(example2) + # await card_element.update() @cl.set_starters async def set_starters(): From a0aeb9dd703755da702d04c8ea70299c3eb01a90 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 16 Oct 2025 22:10:25 +0100 Subject: [PATCH 063/138] refactor(agent): add placeholders for operation mode and system prompt logic in tide agent --- codetide/agents/tide/agent.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 89eaa93..a7c994e 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -338,6 +338,10 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): else: autocomplete = AutoComplete(self.tide.cached_ids) print(f"{autocomplete=}") + + ### TODO super quick prompt here for operation mode + ### needs more context based on cached identifiers or not + ### needs more history or not, default is last 5 iteratinos if self._direct_mode: self.contextIdentifiers = None # Only extract matches from the last message @@ -379,6 +383,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self._last_code_context = codeContext await delete_file(self.patch_path) + ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( expanded_history, system_prompt=[ From 7527efbc5903b1976165b8d66a651a7aee93fc46 Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:21:10 +0100 Subject: [PATCH 064/138] fix(agent,ui): move update checks and message send under context retrieval --- codetide/agents/tide/agent.py | 11 ++++++----- codetide/agents/tide/ui/app.py | 5 ++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index a7c994e..ce78f1f 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -322,10 +322,6 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): TODAY = date.today() - await self.tide.check_for_updates(serialize=True, include_cached_ids=True) - print("Finished check for updates") - self._clean_history() - print("Finished clean history") # Initialize the context identifier window if not present if self._context_identifier_window is None: @@ -335,7 +331,12 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): if self._skip_context_retrieval: expanded_history = self.history[-1] await self.llm.logger_fn(REASONING_FINISHED) - else: + else: + await self.tide.check_for_updates(serialize=True, include_cached_ids=True) + print("Finished check for updates") + self._clean_history() + print("Finished clean history") + autocomplete = AutoComplete(self.tide.cached_ids) print(f"{autocomplete=}") diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 8eefc43..1881455 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -478,7 +478,10 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "modify_identifiers": [], "finished": False }) - _ = await cl.Message(content="", author="AgentTide", elements=[reasoning_element]).send() + + if not agent_tide_ui.agent_tide._skip_context_retrieval: + reasoning_mg = cl.Message(content="", author="AgentTide", elements=[reasoning_element]) + _ = await reasoning_mg.send() ### TODO this needs to receive the message as well to call update reasoning_step = CustomElementStep( element=reasoning_element, From a1fd59b787fe349af521625ac86774d1e9e3d497 Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:40:22 +0100 Subject: [PATCH 065/138] fix(prompts): remove date references from patch and steps system prompts --- codetide/agents/tide/prompts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 29baa1e..e215cc1 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -70,7 +70,7 @@ """ WRITE_PATCH_SYSTEM_PROMPT = """ -You are Agent **Tide**, operating in Patch Generation Mode on {DATE}. +You are Agent **Tide**, operating in Patch Generation Mode. Your mission is to generate atomic, high-precision, diff-style patches that exactly satisfy the user’s request while adhering to the STRICT PATCH PROTOCOL. --- @@ -235,7 +235,7 @@ """ STEPS_SYSTEM_PROMPT = """ -You are Agent **Tide**, operating in a multi-step planning and execution mode. Today is **{DATE}**. +You are Agent **Tide**, operating in a multi-step planning and execution mode. Your job is to take a user request, analyze any provided code context (including repository structure / repo_tree identifiers), and decompose the work into the minimal set of concrete implementation steps needed to fully satisfy the request. If the requirement is simple, output a single step; if it’s complex, decompose it into multiple ordered steps. You must build upon, refine, or correct any existing code context rather than ignoring it. From d99b3123384f5f2f6e5a369ae0e565fda11fe303 Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:41:00 +0100 Subject: [PATCH 066/138] refactor(prompts): remove unused history checks and operation mode lines, add operation mode and history assessment prompts --- codetide/agents/tide/prompts.py | 91 ++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index e215cc1..16ca782 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -614,10 +614,6 @@ ENOUGH_IDENTIFIERS: [TRUE|FALSE] - TRUE when: all major areas explored, structure clear, key tasks identified - FALSE when: core directories collapsed or unexplored areas remain - -ENOUGH_HISTORY: [TRUE|FALSE] -- TRUE when: request references prior context -- FALSE when: request is self-contained """ FINALIZE_IDENTIFIERS_PROMPT = """ @@ -626,7 +622,7 @@ **LANGUAGES**: {SUPPORTED_LANGUAGES} **PROHIBITIONS**: No answers/solutions, no file analysis, no markdown -**MISSION**: Filter candidate pool → classify Context vs Modify → determine operation mode +**MISSION**: Filter candidate pool → classify Context vs Modify *Request provided in user message* **INPUT STATE**: @@ -679,8 +675,6 @@ [another.identifier] *** End Modify Identifiers -OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE|MODE_COMBINATION] - **QUALITY GATES**: ✓ Each identifier is actual code ✓ Each directly supports request @@ -696,6 +690,89 @@ - Redundant similar utilities """ +DETERMINE_OPERATION_MODE_PROMPT = """ +You are Agent **Tide**, operating in **Operation Mode Extraction**. + +**PROHIBITIONS**: No answers/solutions, no file analysis, no markdown + +**MISSION**: Analyze request and identifiers → determine optimal operation mode & context sufficiency +*Last interaction provided in user message* +*Context & Modify identifiers provided from identifier finalization* +*Current conversation has {INTERACTION_COUNT} interactions* + +**INPUT STATE**: +- Code Identifiers: {CODE_IDENTIFIERS} +- Conversation Depth: {INTERACTION_COUNT} interactions + +**OPERATION MODE OPTIONS**: + +**STANDARD**: +- Exploratory tasks involving code files +- Analysis, understanding, reading code +- NO code changes, modifications, or bug fixes +- Context-gathering activities only + +**PLAN_STEPS**: +- Complex changes requiring decomposition +- Multi-phase implementation needed +- Architectural decisions required +- Multiple interconnected Modify targets + +**PATCH_CODE**: +- Localized, isolated fixes +- Single concern modification +- Minimal ripple effects +- 1-2 Modify identifiers maximum + +**CONTEXT SUFFICIENCY CHECK**: +- **ASSESS**: Are current Code Identifiers sufficient for the request? +- **IF YES**: sufficient_context = TRUE, history_count = current interaction count +- **IF NO**: sufficient_context = FALSE, history_count = minimum interactions needed (backwards from current) + +**DECISION LOGIC**: +1. Analyze scope: How many distinct areas affected? +2. Check complexity: Requires planning or direct execution? +3. Assess interdependencies: Are modifications isolated or interconnected? +4. Evaluate Modify count: Single vs multiple targets? +5. Determine execution strategy: Linear (PATCH_CODE) vs phased (PLAN_STEPS)? +6. Check history: Does request depend on prior interactions? Count backward if yes. + +**OUTPUT FORMAT**: + +OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE] + +SUFFICIENT_CONTEXT: [TRUE|FALSE] + +HISTORY_COUNT: [integer] +""" + +ASSESS_HISTORY_RELEVANCE_PROMPT = """ +You are Agent **Tide**, operating in **History Relevance Assessment**. + +**PROHIBITIONS**: No answers/solutions, no file analysis, no markdown + +**MISSION**: Determine if current history window captures all relevant context for the request +*Messages from index {START_INDEX} to {END_INDEX} provided* +*Total conversation length: {TOTAL_INTERACTIONS} interactions* + +**INPUT STATE**: +- Current History Window: {CURRENT_WINDOW} +- Latest Request: {LATEST_REQUEST} + +**ASSESSMENT LOGIC**: +1. Does the latest request reference outcomes/decisions from messages OUTSIDE current window? +2. Are there dependencies on earlier exchanges not yet included? +3. Is there sufficient context to understand the request intent? + +**OUTPUT FORMAT**: + +HISTORY_SUFFICIENT: [TRUE|FALSE] + +REQUIRES_MORE_MESSAGES: [integer] +- 0: Current window is sufficient +- N (>0): Additional N messages needed from earlier in conversation +""" + REASONING_TEMPLTAE = """ **Task**: {header} **Rationale**: {content} From ea5750630a07a56f80b58d308d30f4b11973d18a Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:41:15 +0100 Subject: [PATCH 067/138] feat(agent): add operation mode extraction and dynamic history expansion to AgentTide --- codetide/agents/tide/agent.py | 191 +++++++++++++++++++++++++++++++--- 1 file changed, 175 insertions(+), 16 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index ce78f1f..e3d7bb4 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -7,7 +7,7 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -26,7 +26,7 @@ from pydantic import BaseModel, Field, ConfigDict, model_validator from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit import PromptSession -from typing import List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple from typing_extensions import Self from functools import partial from datetime import date @@ -66,6 +66,11 @@ class AgentTide(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) + OPERATIONS :Dict[str, str] = { + "PLAN_STEPS": STEPS_SYSTEM_PROMPT, + "PATCH_CODE": WRITE_PATCH_SYSTEM_PROMPT + } + @model_validator(mode="after") def pass_custom_logger_fn(self)->Self: self.llm.logger_fn = partial(custom_logger_fn, session_id=self.session_id, filepath=self.patch_path) @@ -319,6 +324,147 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti "all_reasoning": all_reasoning_text, "iteration_count": iteration_count } + + async def expand_history_if_needed( + self, + sufficient_context: bool, + history_count: int, + ) -> int: + """ + Iteratively expand history window if initial assessment indicates more context is needed. + + Args: + sufficient_context: Boolean indicating if context is sufficient + history_count: Initial history count from operation mode extraction + + Returns: + Final history count to use for processing + + Raises: + ValueError: If extraction fails at any iteration + """ + current_history_count = history_count + max_iterations = 10 # Prevent infinite loops + iteration = 0 + + # If context is already sufficient, return early + if sufficient_context: + return current_history_count + + # Expand history iteratively + while iteration < max_iterations and current_history_count < len(self.history): + iteration += 1 + + # Calculate window indices + start_index = max(0, len(self.history) - current_history_count) + end_index = len(self.history) + current_window = self.history[start_index:end_index] + latest_request = self.history[-1] # Last interaction is the current request + + # Assess if current window has enough history + response = await self.llm.acomplete( + current_window, + system_prompt=ASSESS_HISTORY_RELEVANCE_PROMPT.format( + START_INDEX=start_index, + END_INDEX=end_index, + TOTAL_INTERACTIONS=len(self.history), + CURRENT_WINDOW=str(current_window), + LATEST_REQUEST=str(latest_request) + ) + ) + + # Extract HISTORY_SUFFICIENT + history_sufficient_match = re.search( + r'HISTORY_SUFFICIENT:\s*\[?(TRUE|FALSE)\]?', + response + ) + history_sufficient = ( + history_sufficient_match.group(1).lower() == 'true' + if history_sufficient_match else False + ) + + # Extract REQUIRES_MORE_MESSAGES + requires_more_match = re.search( + r'REQUIRES_MORE_MESSAGES:\s*\[?(\d+)\]?', + response + ) + requires_more = int(requires_more_match.group(1)) if requires_more_match else 0 + + # Validate extraction + if history_sufficient_match is None or requires_more_match is None: + raise ValueError( + f"Failed to extract relevance assessment fields at iteration {iteration}:\n{response}" + ) + + # If history is sufficient, we're done + if history_sufficient: + return current_history_count + + # If more messages are needed, expand the count + if requires_more > 0: + new_count = current_history_count + requires_more + # Prevent exceeding total history + if new_count > len(self.history): + new_count = len(self.history) + + current_history_count = new_count + else: + # No more messages required but not sufficient - use full history + current_history_count = len(self.history) + + # Return final count (capped at total history length) + return min(current_history_count, len(self.history)) + + async def extract_operation_mode( + self, + cached_identifiers: str + ) -> Tuple[str, bool, list]: + """ + Extract operation mode, context sufficiency, and history count from LLM response. + + Args: + llm: Language model instance with acomplete method + history: Conversation history + cached_identifiers: Code identifiers string + system_prompt: System prompt template (DETERMINE_OPERATION_MODE_PROMPT) + + Returns: + Tuple of (operation_mode, sufficient_context, history_count) + - operation_mode: str [STANDARD|PLAN_STEPS|PATCH_CODE] + - sufficient_context: bool + - history_count: int + + Raises: + ValueError: If required fields cannot be extracted from response + """ + response = await self.llm.acomplete( + self.history[-3:], + system_prompt=DETERMINE_OPERATION_MODE_PROMPT.format( + INTERACTION_COUNT=len(self.history), + CODE_IDENTIFIERS=cached_identifiers + ) + ) + + # Extract OPERATION_MODE + operation_mode_match = re.search(r'OPERATION_MODE:\s*\[?(STANDARD|PLAN_STEPS|PATCH_CODE)\]?', response) + operation_mode = operation_mode_match.group(1) if operation_mode_match else None + + # Extract SUFFICIENT_CONTEXT + sufficient_context_match = re.search(r'SUFFICIENT_CONTEXT:\s*\[?(TRUE|FALSE)\]?', response) + sufficient_context = sufficient_context_match.group(1).lower() == 'true' if sufficient_context_match else None + + # Extract HISTORY_COUNT + history_count_match = re.search(r'HISTORY_COUNT:\s*\[?(\d+)\]?', response) + history_count = int(history_count_match.group(1)) if history_count_match else len(self.history) + + # Validate extraction + if operation_mode is None or sufficient_context is None: + raise ValueError(f"Failed to extract required fields from response:\n{response}") + + final_history_count = await self.expand_history_if_needed(sufficient_context, history_count) + expanded_history = self.history[-final_history_count:] + + return operation_mode, sufficient_context, expanded_history async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): TODAY = date.today() @@ -327,22 +473,37 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): if self._context_identifier_window is None: self._context_identifier_window = [] + operation_mode = None codeContext = None if self._skip_context_retrieval: expanded_history = self.history[-1] await self.llm.logger_fn(REASONING_FINISHED) - else: - await self.tide.check_for_updates(serialize=True, include_cached_ids=True) + else: + cached_identifiers = self._last_code_identifers print("Finished check for updates") self._clean_history() print("Finished clean history") + if codeIdentifiers: + for identifier in codeIdentifiers: + cached_identifiers.add(identifier) + + tasks = [ + self.extract_operation_mode(cached_identifiers), + self.tide.check_for_updates(serialize=True, include_cached_ids=True) + ] + operation_context_history_task, _ = await asyncio.gather(*tasks) + + operation_mode, sufficient_context, expanded_history = operation_context_history_task autocomplete = AutoComplete(self.tide.cached_ids) - print(f"{autocomplete=}") ### TODO super quick prompt here for operation mode ### needs more context based on cached identifiers or not ### needs more history or not, default is last 5 iteratinos + if sufficient_context: + codeIdentifiers = list(self._last_code_identifers) + await self.llm.logger_fn(REASONING_FINISHED) + if self._direct_mode: self.contextIdentifiers = None # Only extract matches from the last message @@ -355,8 +516,6 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self._context_identifier_window.append(set(exact_matches)) if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: self._context_identifier_window.pop(0) - expanded_history = self.history[-5:] - operation_mode = "STANDARD, PATH_CODE" await self.llm.logger_fn(REASONING_FINISHED) ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history else: @@ -367,8 +526,6 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): codeIdentifiers = reasoning_output.get("context_identifiers", []) + reasoning_output.get("modify_identifiers", []) matches = reasoning_output.get("matches") - operation_mode = reasoning_output.get("operation_mode") - expanded_history = reasoning_output.get("expanded_history") # --- End Unified Identifier Retrieval --- if codeIdentifiers: @@ -381,18 +538,20 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): readmeFile = self.tide.get(["README.md"] + (matches if 'matches' in locals() else []), as_string_list=True) if readmeFile: codeContext = "\n".join([codeContext, README_CONTEXT_PROMPT.format(README=readmeFile)]) - self._last_code_context = codeContext await delete_file(self.patch_path) + + system_prompt = [ + AGENT_TIDE_SYSTEM_PROMPT.format(DATE=TODAY), + CALMNESS_SYSTEM_PROMPT + ] + if operation_mode in self.OPERATIONS: + system_prompt.insert(1, self.OPERATIONS.get(operation_mode)) + ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( expanded_history, - system_prompt=[ - AGENT_TIDE_SYSTEM_PROMPT.format(DATE=TODAY), - STEPS_SYSTEM_PROMPT.format(DATE=TODAY), - WRITE_PATCH_SYSTEM_PROMPT.format(DATE=TODAY), - CALMNESS_SYSTEM_PROMPT - ], + system_prompt=system_prompt, prefix_prompt=codeContext ) From 5ca7d746949c8f4fe43ca34cfe0aba1eb5e1d1ba Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:44:04 +0100 Subject: [PATCH 068/138] refactor(agent): update get_identifiers_two_phase to accept expanded_history param and remove internal history expansion logic --- codetide/agents/tide/agent.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index e3d7bb4..4d2a8a1 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -138,7 +138,7 @@ def _clean_history(self): if isinstance(message, dict): self.history[i] = message.get("content" ,"") - async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdentifiers=None, TODAY :str=None): + async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_history :list, codeIdentifiers=None, TODAY :str=None): """ Two-phase identifier resolution: Phase 1: Gather candidates through iterative tree expansion @@ -168,7 +168,6 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti expand_paths = ["./"] enough_identifiers = False history_memory = 3 - expanded_history = list(self.history)[-history_memory:] # Track expanded history while not enough_identifiers and iteration_count < max_iterations: iteration_count += 1 @@ -243,13 +242,6 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper(): enough_identifiers = True - # Check if we need more history - if "ENOUGH_HISTORY: FALSE" in phase1_response.upper() and iteration_count <= 2: - # Load more history for next iteration - # TODO this should be imcremental i.e starting += 2 each time! - history_memory += 2 - expanded_history = self.history[-history_memory:] if len(self.history) > 1 else self.history - # Parse expansion paths for next iteration if expand_paths_block and not enough_identifiers: expand_paths = [ @@ -293,13 +285,6 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti context_identifiers = parse_blocks(phase2_response, block_word="Context Identifiers", multiple=False) modify_identifiers = parse_blocks(phase2_response, block_word="Modify Identifiers", multiple=False) - # Extract operation mode - operation_mode = "STANDARD" # default - if "OPERATION_MODE:" in phase2_response: - mode_line = [line for line in phase2_response.split('\n') if 'OPERATION_MODE:' in line] - if mode_line: - operation_mode = mode_line[0].split('OPERATION_MODE:')[1].strip() - # Process final identifiers final_context = set() final_modify = set() @@ -318,9 +303,7 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, codeIdenti "matches": matches, "context_identifiers": list(final_context), "modify_identifiers": self.tide._as_file_paths(list(final_modify)), - "operation_mode": operation_mode, "summary": summary, - "expanded_history": expanded_history, # Make available for downstream "all_reasoning": all_reasoning_text, "iteration_count": iteration_count } @@ -520,7 +503,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history else: await self.llm.logger_fn(REASONING_STARTED) - reasoning_output = await self.get_identifiers_two_phase(autocomplete, codeIdentifiers, TODAY) + reasoning_output = await self.get_identifiers_two_phase(autocomplete, expanded_history, codeIdentifiers, TODAY) await self.llm.logger_fn(REASONING_FINISHED) print(json.dumps(reasoning_output, indent=4)) From 18a9c80fca35e4fdb720b1e3bcf4d87a047a6e9b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 19 Oct 2025 22:19:49 +0100 Subject: [PATCH 069/138] refactor(agent): disable streaming in prompt calls for operation mode and history checks --- codetide/agents/tide/agent.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 4d2a8a1..d5764de 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -353,7 +353,8 @@ async def expand_history_if_needed( TOTAL_INTERACTIONS=len(self.history), CURRENT_WINDOW=str(current_window), LATEST_REQUEST=str(latest_request) - ) + ), + stream=False ) # Extract HISTORY_SUFFICIENT @@ -425,7 +426,8 @@ async def extract_operation_mode( system_prompt=DETERMINE_OPERATION_MODE_PROMPT.format( INTERACTION_COUNT=len(self.history), CODE_IDENTIFIERS=cached_identifiers - ) + ), + stream=False ) # Extract OPERATION_MODE From 00efeec769636c22aa36031cdbf5f97231210d91 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 19 Oct 2025 22:37:19 +0100 Subject: [PATCH 070/138] fix(agent): correct sufficient_context extraction and refine direct_mode logic --- codetide/agents/tide/agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index d5764de..0a12609 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -436,7 +436,7 @@ async def extract_operation_mode( # Extract SUFFICIENT_CONTEXT sufficient_context_match = re.search(r'SUFFICIENT_CONTEXT:\s*\[?(TRUE|FALSE)\]?', response) - sufficient_context = sufficient_context_match.group(1).lower() == 'true' if sufficient_context_match else None + sufficient_context = 'true' in sufficient_context_match.group(1).lower() if sufficient_context_match else None # Extract HISTORY_COUNT history_count_match = re.search(r'HISTORY_COUNT:\s*\[?(\d+)\]?', response) @@ -489,7 +489,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): codeIdentifiers = list(self._last_code_identifers) await self.llm.logger_fn(REASONING_FINISHED) - if self._direct_mode: + elif self._direct_mode: self.contextIdentifiers = None # Only extract matches from the last message last_message = self.history[-1] if self.history else "" @@ -517,7 +517,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self._last_code_identifers = set(codeIdentifiers) codeContext = self.tide.get(codeIdentifiers, as_string=True) - if not codeContext: + if not codeContext and not sufficient_context: codeContext = REPO_TREE_CONTEXT_PROMPT.format(REPO_TREE=self.tide.codebase.get_tree_view()) # Use matches from the last message for README context readmeFile = self.tide.get(["README.md"] + (matches if 'matches' in locals() else []), as_string_list=True) From 70ccac8224d7711054dc4b9cafa4d1f02f8f5c54 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 22:47:15 +0100 Subject: [PATCH 071/138] refactor(prompts): update operation mode extraction prompt with stricter rules and clearer logic --- codetide/agents/tide/prompts.py | 85 ++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 16ca782..603c8f9 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -693,57 +693,86 @@ DETERMINE_OPERATION_MODE_PROMPT = """ You are Agent **Tide**, operating in **Operation Mode Extraction**. -**PROHIBITIONS**: No answers/solutions, no file analysis, no markdown +**PROHIBITIONS**: +- No explanations +- No markdown +- No conversational or narrative output +- No code generation or analysis +- Only output in the required format + +**MISSION**: +Determine the optimal operation mode for the latest user request, based on intent and scope of modification, and assess if current context is sufficient. -**MISSION**: Analyze request and identifiers → determine optimal operation mode & context sufficiency *Last interaction provided in user message* *Context & Modify identifiers provided from identifier finalization* *Current conversation has {INTERACTION_COUNT} interactions* +--- + **INPUT STATE**: - Code Identifiers: {CODE_IDENTIFIERS} - Conversation Depth: {INTERACTION_COUNT} interactions -**OPERATION MODE OPTIONS**: +--- -**STANDARD**: -- Exploratory tasks involving code files -- Analysis, understanding, reading code -- NO code changes, modifications, or bug fixes -- Context-gathering activities only +**OPERATION MODE DEFINITIONS**: + +**STANDARD**: +- Tasks about reading, understanding, or explaining code +- Exploratory or analytical questions +- No intent to create, edit, delete, or modify code or files +- Purely observational or discussion-based **PLAN_STEPS**: -- Complex changes requiring decomposition -- Multi-phase implementation needed -- Architectural decisions required -- Multiple interconnected Modify targets +- Complex requests requiring decomposition into multiple steps +- Multi-component or architectural changes +- Large-scale or multi-file operations +- Requires structured planning before any patching +- Involves 3 or more Modify identifiers, or interdependent code areas **PATCH_CODE**: -- Localized, isolated fixes -- Single concern modification -- Minimal ripple effects -- 1-2 Modify identifiers maximum +- **MANDATORY** if the request includes any of the following verbs or intents: + - “change”, “edit”, “update”, “modify”, “fix”, “create”, “delete”, “remove”, “rename”, “add”, “implement”, “refactor”, “patch”, or synonyms thereof +- Used for localized or isolated code/file changes +- Focused on direct code modification +- Affects only 1–2 Modify identifiers +- No high-level architectural planning required + +--- **CONTEXT SUFFICIENCY CHECK**: -- **ASSESS**: Are current Code Identifiers sufficient for the request? -- **IF YES**: sufficient_context = TRUE, history_count = current interaction count -- **IF NO**: sufficient_context = FALSE, history_count = minimum interactions needed (backwards from current) +1. Determine if all relevant Code Identifiers are present to fulfill the request. +2. If all required identifiers are available → `SUFFICIENT_CONTEXT: TRUE` +3. If some dependencies are missing → `SUFFICIENT_CONTEXT: FALSE` +4. `HISTORY_COUNT`: + - If sufficient_context = TRUE → set to current interaction count + - If FALSE → minimum backward interactions needed for full context + +--- **DECISION LOGIC**: -1. Analyze scope: How many distinct areas affected? -2. Check complexity: Requires planning or direct execution? -3. Assess interdependencies: Are modifications isolated or interconnected? -4. Evaluate Modify count: Single vs multiple targets? -5. Determine execution strategy: Linear (PATCH_CODE) vs phased (PLAN_STEPS)? -6. Check history: Does request depend on prior interactions? Count backward if yes. +1. Detect action intent: + - If request includes modification verbs → **PATCH_CODE** + - Else, continue to complexity evaluation +2. Evaluate number of Modify identifiers: + - 3 or more distinct areas → **PLAN_STEPS** + - 1–2 localized changes → **PATCH_CODE** + - None (purely analytical) → **STANDARD** +3. Assess context sufficiency as per above rules. +4. Output result strictly in format below. -**OUTPUT FORMAT**: +--- -OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE] +**STRICT OUTPUT FORMAT ENFORCEMENT** -SUFFICIENT_CONTEXT: [TRUE|FALSE] +Respond **ONLY** in the following format: +OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE] +SUFFICIENT_CONTEXT: [TRUE|FALSE] HISTORY_COUNT: [integer] + +No additional text, explanations, or formatting allowed. +If your output includes anything else, it is invalid. """ ASSESS_HISTORY_RELEVANCE_PROMPT = """ From 1d32afcabfde761c52a74f31339403a093fb8694 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 22:47:25 +0100 Subject: [PATCH 072/138] refactor(prompts): update history relevance prompt with stricter format and clearer prohibitions --- codetide/agents/tide/prompts.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 603c8f9..d698588 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -778,9 +778,14 @@ ASSESS_HISTORY_RELEVANCE_PROMPT = """ You are Agent **Tide**, operating in **History Relevance Assessment**. -**PROHIBITIONS**: No answers/solutions, no file analysis, no markdown +**PROHIBITIONS**: +- No explanations +- No markdown +- No conversational language +- No reasoning or justification + +**MISSION**: Determine if the current history window captures all relevant context for the request. -**MISSION**: Determine if current history window captures all relevant context for the request *Messages from index {START_INDEX} to {END_INDEX} provided* *Total conversation length: {TOTAL_INTERACTIONS} interactions* @@ -793,13 +798,13 @@ 2. Are there dependencies on earlier exchanges not yet included? 3. Is there sufficient context to understand the request intent? -**OUTPUT FORMAT**: +**STRICT FORMAT ENFORCEMENT** +Respond ONLY in this format: HISTORY_SUFFICIENT: [TRUE|FALSE] - REQUIRES_MORE_MESSAGES: [integer] -- 0: Current window is sufficient -- N (>0): Additional N messages needed from earlier in conversation + +If your response includes anything else, it is invalid. """ REASONING_TEMPLTAE = """ From 2e761eed68c3ca1cef22dd13d872dc586f1ac16c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 22:55:08 +0100 Subject: [PATCH 073/138] refactor(tide): centralize special tokens and update chunk logger filtering --- codetide/agents/tide/agent.py | 6 +----- codetide/agents/tide/consts.py | 12 +++++++++++- codetide/agents/tide/streaming/chunk_logger.py | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 0a12609..ea4c13b 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -11,7 +11,7 @@ REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section -from .consts import AGENT_TIDE_ASCII_ART +from .consts import AGENT_TIDE_ASCII_ART, REASONING_FINISHED, REASONING_STARTED, ROUND_FINISHED try: from aicore.llm import Llm @@ -36,10 +36,6 @@ import pygit2 import os -ROUND_FINISHED = "" -REASONING_STARTED = "" -REASONING_FINISHED = "" - class AgentTide(BaseModel): llm :Llm tide :CodeTide diff --git a/codetide/agents/tide/consts.py b/codetide/agents/tide/consts.py index 14185cb..dea1f21 100644 --- a/codetide/agents/tide/consts.py +++ b/codetide/agents/tide/consts.py @@ -5,4 +5,14 @@ \033[1;38;5;45m██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██╔══╝\033[0m \033[1;38;5;51m██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██║ ██║██████╔╝███████╗\033[0m \033[1;38;5;255m╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝\033[0m -""" \ No newline at end of file +""" + +ROUND_FINISHED = "" +REASONING_STARTED = "" +REASONING_FINISHED = "" + +AGENT_TIDE_SPECIAL_TOKENS = [ + ROUND_FINISHED, + REASONING_STARTED, + REASONING_FINISHED +] \ No newline at end of file diff --git a/codetide/agents/tide/streaming/chunk_logger.py b/codetide/agents/tide/streaming/chunk_logger.py index 775a67b..d1051a2 100644 --- a/codetide/agents/tide/streaming/chunk_logger.py +++ b/codetide/agents/tide/streaming/chunk_logger.py @@ -1,3 +1,4 @@ +from ..consts import AGENT_TIDE_SPECIAL_TOKENS from ....core.defaults import DEFAULT_ENCODING from aicore.logger import SPECIAL_TOKENS @@ -8,6 +9,7 @@ import asyncio import time + class ChunkLogger: def __init__(self, buffer_size: int = 1024, flush_interval: float = 0.1): self.buffer_size = buffer_size @@ -21,7 +23,7 @@ def __init__(self, buffer_size: int = 1024, flush_interval: float = 0.1): async def log_chunk(self, message: str, session_id: str, filepath: str): """Optimized chunk logging with batched file writes and direct streaming""" - if message not in SPECIAL_TOKENS: + if message not in SPECIAL_TOKENS or message not in AGENT_TIDE_SPECIAL_TOKENS: # Add to file buffer for batched writing self._file_buffers[filepath].append(message) current_time = time.time() From 168aca59a21019b6c3ff07a45836b79825be918b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 23:00:51 +0100 Subject: [PATCH 074/138] feat(prompts): add prefix summary prompt for enhanced context integration --- codetide/agents/tide/prompts.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index d698588..e3e5cea 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -296,6 +296,17 @@ Never make assumptions or proceed with incomplete information. Your priority is to ensure that every action is based on clear, explicit, and sufficient instructions. """ +PREFIX_SUMMARY_PROMPT = """ +You will receive a brief summary before the code context. This summary provides additional context to guide your final answer. + +Use this summary to better understand the user's intent and the overall task scope. +Incorporate the information from the summary along with the code context to produce a precise, complete, and high-quality response. + +Always prioritize the summary as a high-level guide and use it to clarify ambiguous or incomplete code context: + +{summary} +""" + REPO_TREE_CONTEXT_PROMPT = """ Here is a **tree representation of current state of the codebase** - you can refer to if needed: From 77ef187362c906fc0b943aa68734711f35bead90 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 23:04:27 +0100 Subject: [PATCH 075/138] fix(streaming): correct token filtering logic in chunk_logger --- codetide/agents/tide/streaming/chunk_logger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/streaming/chunk_logger.py b/codetide/agents/tide/streaming/chunk_logger.py index d1051a2..6267d88 100644 --- a/codetide/agents/tide/streaming/chunk_logger.py +++ b/codetide/agents/tide/streaming/chunk_logger.py @@ -9,6 +9,7 @@ import asyncio import time +IGNORED_TOKENS = set(SPECIAL_TOKENS + AGENT_TIDE_SPECIAL_TOKENS) class ChunkLogger: def __init__(self, buffer_size: int = 1024, flush_interval: float = 0.1): @@ -23,7 +24,7 @@ def __init__(self, buffer_size: int = 1024, flush_interval: float = 0.1): async def log_chunk(self, message: str, session_id: str, filepath: str): """Optimized chunk logging with batched file writes and direct streaming""" - if message not in SPECIAL_TOKENS or message not in AGENT_TIDE_SPECIAL_TOKENS: + if message not in IGNORED_TOKENS: # Add to file buffer for batched writing self._file_buffers[filepath].append(message) current_time = time.time() From d9e180647fb827d7c23212c27e42d5ac994d3d54 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 23:10:28 +0100 Subject: [PATCH 076/138] refactor(agent): enhance code context handling with prefilled summary in tide agent and update prompts --- codetide/agents/tide/agent.py | 15 ++++++++++++--- codetide/agents/tide/prompts.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index ea4c13b..7225ada 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -7,7 +7,7 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -163,7 +163,6 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_h repo_tree = None expand_paths = ["./"] enough_identifiers = False - history_memory = 3 while not enough_identifiers and iteration_count < max_iterations: iteration_count += 1 @@ -456,6 +455,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): operation_mode = None codeContext = None + prefilled_summary = None if self._skip_context_retrieval: expanded_history = self.history[-1] await self.llm.logger_fn(REASONING_FINISHED) @@ -507,6 +507,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): codeIdentifiers = reasoning_output.get("context_identifiers", []) + reasoning_output.get("modify_identifiers", []) matches = reasoning_output.get("matches") + prefilled_summary = reasoning_output.get("summary") # --- End Unified Identifier Retrieval --- if codeIdentifiers: @@ -528,12 +529,20 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ] if operation_mode in self.OPERATIONS: system_prompt.insert(1, self.OPERATIONS.get(operation_mode)) + + if prefilled_summary is not None: + prefil_context = [ + PREFIX_SUMMARY_PROMPT.format(SUMMARY=prefilled_summary), + codeContext + ] + else: + prefil_context = [codeContext] ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( expanded_history, system_prompt=system_prompt, - prefix_prompt=codeContext + prefix_prompt=prefil_context ) await trim_to_patch_section(self.patch_path) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index e3e5cea..c7f09f7 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -304,7 +304,7 @@ Always prioritize the summary as a high-level guide and use it to clarify ambiguous or incomplete code context: -{summary} +{SUMMARY} """ REPO_TREE_CONTEXT_PROMPT = """ From d8f2c4e8471c077910fdda95c4cd82381edb5ec6 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 20 Oct 2025 23:12:25 +0100 Subject: [PATCH 077/138] fix(agent): correct variable assignment and context handling in agent.py --- codetide/agents/tide/agent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 7225ada..54413d1 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -456,6 +456,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): operation_mode = None codeContext = None prefilled_summary = None + prefil_context = None if self._skip_context_retrieval: expanded_history = self.history[-1] await self.llm.logger_fn(REASONING_FINISHED) @@ -535,8 +536,8 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): PREFIX_SUMMARY_PROMPT.format(SUMMARY=prefilled_summary), codeContext ] - else: - prefil_context = [codeContext] + elif codeContext: + codeContext = [codeContext] ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( From 8becc8126da78ea835c2a6f89b1ec1c80e0e749f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 21 Oct 2025 20:43:55 +0100 Subject: [PATCH 078/138] refactor(prompts): update candidate gathering and final selection prompts for clarity and rules enforcement --- codetide/agents/tide/prompts.py | 113 ++++++++++---------------------- 1 file changed, 36 insertions(+), 77 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index c7f09f7..29e9248 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -579,101 +579,73 @@ You are Agent Tide in Candidate Gathering Mode | {DATE} Languages: {SUPPORTED_LANGUAGES} -**CRITICAL RULES:** -- ONLY identify candidates, never solve or suggest -- NO markdown formatting -- NO file analysis or implementations -- DEDUPLICATE: Every candidate must be novel (verify against {ACCUMULATED_CONTEXT}) +**RULES** +- Identify new candidate identifiers only — never solve or explain +- DEDUPLICATE: each must be novel vs {ACCUMULATED_CONTEXT} +- No markdown, code inspection, or speculation -**STATE:** +**STATE** Tree: {TREE_STATE} | Accumulated: {ACCUMULATED_CONTEXT} | Iteration: {ITERATION_COUNT} --- -**REASONING FORMAT** (First-Person): - *** Begin Reasoning -**Task**: [Brief task from request] -**Rationale**: [Why this new area matters - in first person: I focus on this because...] -**NEW Candidate Identifiers**: [MAX 3 ONLY - MUST BE NOVEL] +**Task**: [Brief summary of user request] +**Rationale**: [Why I explore this area – first person: I focus on this because...] +**NEW Candidate Identifiers** (max 3, all novel): - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] - [third.identifier.or.path] *** End Reasoning -**CONSTRAINTS:** -- Max 3 identifiers per reasoning block -- All must be NEW (cross-check {ACCUMULATED_CONTEXT}) -- Dot notation for {SUPPORTED_LANGUAGES} files (functions/classes/methods) -- File paths only for other formats -- Zero speculation—only traceable to {TREE_STATE} - --- -**EXPANSION:** - *** Begin Expand Paths [path/to/directory/] [another/path/] *** End Expand Paths -Expand when: directories collapsed, files hidden, or new areas unexplored. +Expand when directories are collapsed, unexplored, or likely hold key logic or docs (e.g., README, manifest, config). --- -**ASSESSMENTS:** +*** Begin Assessments ENOUGH_IDENTIFIERS: [TRUE|FALSE] -- TRUE when: all major areas explored, structure clear, key tasks identified -- FALSE when: core directories collapsed or unexplored areas remain +- TRUE: major areas explored, core logic mapped +- FALSE: unexplored or hidden structures remain +*** End Assessments + """ FINALIZE_IDENTIFIERS_PROMPT = """ -You are Agent **Tide**, operating in **Final Selection Mode** on **{DATE}**. - -**LANGUAGES**: {SUPPORTED_LANGUAGES} -**PROHIBITIONS**: No answers/solutions, no file analysis, no markdown +You are Agent Tide in Final Selection Mode | {DATE} +Languages: {SUPPORTED_LANGUAGES} -**MISSION**: Filter candidate pool → classify Context vs Modify -*Request provided in user message* +**MISSION** +Filter all gathered identifiers → select up to 5 most relevant. +Classify into **Context** (understanding) vs **Modify** (direct changes). -**INPUT STATE**: -- Exploration Logic: {EXPLORATION_STEPS} +**INPUT** +- Exploration Steps: {EXPLORATION_STEPS} - Candidate Pool: {ALL_CANDIDATES} +- User Intent: from message + +**SELECTION LOGIC** +1. Parse intent → detect system scope (specific vs general) +2. Score each candidate (1-100): relevance to fulfilling or informing the intent +3. Discard <80 +4. Group: + - **Modify** → code directly fulfilling the request + - **Context** → elements explaining why or how (architecture, constraints, top-level docs) +5. Prioritize: Modify > Context +6. If >5 total → drop lowest Context first +7. If request is general/system-wide → retain one top-level doc (README/config) in Context +8. Output final set with short rationale -**CONSTRAINTS**: -- MAX 5 identifiers total -- MIN 80% relevance threshold -- Only actual code elements (functions, classes, methods, variables) - -**CLASSIFICATION**: - -**Context** (understanding, not dependencies): -- Direct informants for the approach -- Supporting code the Modify depends on -- Constraints/requirements interfaces -- Test: "Essential to understand WHY the changes?" - -**Modify** (direct changes): -- Code requiring updates -- New implementations -- Targets of alteration -- EXCLUDE dependencies (framework handles) -- Test: "Directly fulfills the request?" - -**PRIORITY ELIMINATION**: -1. Parse user intent from message -2. Score each candidate: Does it directly support intent? Yes/No/Maybe -3. Eliminate Maybe + all <80% relevance -4. Bucket into Context vs Modify -5. Apply Priority: High → Medium → Low (drop Low first) -6. Modify candidates are non-negotiable -7. If >5 total, drop lowest-priority Context -8. Final check: "Why is this essential?" - -**OUTPUT FORMAT**: +--- *** Begin Summary -[3-4 lines: Key findings, why candidates selected/excluded] +[3-4 lines: key findings, rationale for inclusion/exclusion] *** End Summary *** Begin Context Identifiers @@ -686,19 +658,6 @@ [another.identifier] *** End Modify Identifiers -**QUALITY GATES**: -✓ Each identifier is actual code -✓ Each directly supports request -✓ Modify = primary targets only -✓ Context = essential understanding only -✓ Total ≤ 5 -✓ Summary explains all exclusions - -**RED FLAGS - REJECT IF**: -- Generic framework utilities -- Indirect dependencies -- "Just in case" additions -- Redundant similar utilities """ DETERMINE_OPERATION_MODE_PROMPT = """ From bdcb9319a964355a287a4d5f13089f85b46a2567 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 21 Oct 2025 22:35:07 +0100 Subject: [PATCH 079/138] refactor(prompts): update operation mode extraction prompt for clarity and format consistency --- codetide/agents/tide/prompts.py | 101 ++++++++++++-------------------- 1 file changed, 39 insertions(+), 62 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 29e9248..6058836 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -661,88 +661,65 @@ """ DETERMINE_OPERATION_MODE_PROMPT = """ -You are Agent **Tide**, operating in **Operation Mode Extraction**. +You are Agent **Tide** in **Operation Mode Extraction** -**PROHIBITIONS**: -- No explanations -- No markdown -- No conversational or narrative output +**PROHIBITIONS** +- No explanations, markdown, or narrative - No code generation or analysis -- Only output in the required format - -**MISSION**: -Determine the optimal operation mode for the latest user request, based on intent and scope of modification, and assess if current context is sufficient. +- Output only in required format -*Last interaction provided in user message* -*Context & Modify identifiers provided from identifier finalization* -*Current conversation has {INTERACTION_COUNT} interactions* - ---- +**MISSION** +Select the correct operation mode based on user intent, scope, and target type. +Also decide if more context is required. -**INPUT STATE**: +**INPUT** - Code Identifiers: {CODE_IDENTIFIERS} -- Conversation Depth: {INTERACTION_COUNT} interactions +- Conversation Depth: {INTERACTION_COUNT} --- -**OPERATION MODE DEFINITIONS**: - -**STANDARD**: -- Tasks about reading, understanding, or explaining code -- Exploratory or analytical questions -- No intent to create, edit, delete, or modify code or files -- Purely observational or discussion-based - -**PLAN_STEPS**: -- Complex requests requiring decomposition into multiple steps -- Multi-component or architectural changes -- Large-scale or multi-file operations -- Requires structured planning before any patching -- Involves 3 or more Modify identifiers, or interdependent code areas - -**PATCH_CODE**: -- **MANDATORY** if the request includes any of the following verbs or intents: - - “change”, “edit”, “update”, “modify”, “fix”, “create”, “delete”, “remove”, “rename”, “add”, “implement”, “refactor”, “patch”, or synonyms thereof -- Used for localized or isolated code/file changes -- Focused on direct code modification -- Affects only 1–2 Modify identifiers -- No high-level architectural planning required +**OPERATION MODES** ---- +**STANDARD** +- For reading, explaining, or non-code tasks (posts, docs, summaries) +- Default when no edit intent or code target exists -**CONTEXT SUFFICIENCY CHECK**: -1. Determine if all relevant Code Identifiers are present to fulfill the request. -2. If all required identifiers are available → `SUFFICIENT_CONTEXT: TRUE` -3. If some dependencies are missing → `SUFFICIENT_CONTEXT: FALSE` -4. `HISTORY_COUNT`: - - If sufficient_context = TRUE → set to current interaction count - - If FALSE → minimum backward interactions needed for full context +**PLAN_STEPS** +- For complex, multi-file, or architectural changes +- When 3+ Modify identifiers or linked modules are involved + +**PATCH_CODE** +- For direct, small edits to code files only +- Triggered only if both: + 1. User intent includes verbs like change, update, add, fix, create + 2. Target matches known code in {CODE_IDENTIFIERS} +- Never used for non-code outputs --- -**DECISION LOGIC**: -1. Detect action intent: - - If request includes modification verbs → **PATCH_CODE** - - Else, continue to complexity evaluation -2. Evaluate number of Modify identifiers: - - 3 or more distinct areas → **PLAN_STEPS** - - 1–2 localized changes → **PATCH_CODE** - - None (purely analytical) → **STANDARD** -3. Assess context sufficiency as per above rules. -4. Output result strictly in format below. +**CONTEXT SUFFICIENCY** +1. If all mentioned elements (functions, classes, files) exist in {CODE_IDENTIFIERS}, set TRUE +2. If any are missing or ambiguous, set FALSE +3. When FALSE, HISTORY_COUNT = number of past interactions needed to recover missing info +4. When TRUE, HISTORY_COUNT = current {INTERACTION_COUNT} --- -**STRICT OUTPUT FORMAT ENFORCEMENT** +**DECISION LOGIC** +1. Evaluate context sufficiency +2. Detect if the task targets code or not +3. If non-code, mode = STANDARD +4. If code and complex (≥3 Modify identifiers), mode = PLAN_STEPS +5. If code and localized (≤2 Modify identifiers), mode = PATCH_CODE +6. Output mode, sufficiency, and history count -Respond **ONLY** in the following format: +--- -OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE] -SUFFICIENT_CONTEXT: [TRUE|FALSE] +**STRICT OUTPUT FORMAT** +OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE] +SUFFICIENT_CONTEXT: [TRUE|FALSE] HISTORY_COUNT: [integer] -No additional text, explanations, or formatting allowed. -If your output includes anything else, it is invalid. """ ASSESS_HISTORY_RELEVANCE_PROMPT = """ From d58dd1c8a5664b5d0628eba59f24baa6686e6673 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 25 Oct 2025 17:00:40 +0100 Subject: [PATCH 080/138] style(prompts): improve formatting and clarify insertion context rules --- codetide/agents/tide/prompts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 6058836..93c2103 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -111,8 +111,10 @@ * You may include **multiple `@@` hunks** inside the same patch block if multiple changes are needed in that file. * Always preserve context and formatting as returned by `getCodeContext()`. * When adding new content (such as inserting lines without replacing any existing ones), you **must** include relevant, unmodified -context lines inside the `@@` headers and surrounding the insertion. This context is essential for precisely locating where the new -content should be added. Never emit a patch hunk without real, verbatim context from the file. + context lines inside the `@@` headers and surrounding the insertion. This context is essential for precisely locating where the new + content should be added. Never emit a patch hunk without real, verbatim context from the file. +* Specifically, if the update consists solely of insertions without any deletions, you must include enough context lines above the insertion point + to uniquely and precisely locate the insertion inside the file. Failure to do so may cause the insertion to be placed incorrectly or arbitrarily. --- From 6b500ee81d2b302721aa8f295d78bb84ac8096d8 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 25 Oct 2025 17:01:21 +0100 Subject: [PATCH 081/138] refactor(prompts): update mandate sections and reasoning format in tide prompts --- codetide/agents/tide/prompts.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 93c2103..b83bc48 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -591,9 +591,15 @@ --- +**MANDATES** +Each section below is independent and must always appear, even for a single task. +If a section has no new content, leave it with an empty line. + +--- + *** Begin Reasoning -**Task**: [Brief summary of user request] -**Rationale**: [Why I explore this area – first person: I focus on this because...] +**Task**: [Brief summary of user request — always present, even if single] +**Rationale**: [Why ths area is being explored — in third person: exploring this because...] **NEW Candidate Identifiers** (max 3, all novel): - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] @@ -603,20 +609,18 @@ --- *** Begin Expand Paths -[path/to/directory/] -[another/path/] +[path/to/directory/] or leave as empty line *** End Expand Paths -Expand when directories are collapsed, unexplored, or likely hold key logic or docs (e.g., README, manifest, config). +Expand paths whenever unexplored or collapsed directories might hold related logic or key files (e.g., README, manifest, config). --- *** Begin Assessments ENOUGH_IDENTIFIERS: [TRUE|FALSE] -- TRUE: major areas explored, core logic mapped -- FALSE: unexplored or hidden structures remain +- TRUE: core logic and relevant areas covered +- FALSE: additional unexplored or hidden structures remain *** End Assessments - """ FINALIZE_IDENTIFIERS_PROMPT = """ From fe5cd6fab67511768d5854bc9358a778af78615a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 25 Oct 2025 17:01:47 +0100 Subject: [PATCH 082/138] refactor(prompts): update operation mode extraction prompt for clarity and precision --- codetide/agents/tide/prompts.py | 64 +++++++++++++-------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index b83bc48..77f2c77 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -667,67 +667,51 @@ """ DETERMINE_OPERATION_MODE_PROMPT = """ -You are Agent **Tide** in **Operation Mode Extraction** +You are Agent **Tide** performing **Operation Mode Extraction** -**PROHIBITIONS** -- No explanations, markdown, or narrative -- No code generation or analysis -- Output only in required format - -**MISSION** -Select the correct operation mode based on user intent, scope, and target type. -Also decide if more context is required. +**NO** +- Explanations, markdown, or code +- Extra text outside required output **INPUT** - Code Identifiers: {CODE_IDENTIFIERS} -- Conversation Depth: {INTERACTION_COUNT} +- Interaction Count: {INTERACTION_COUNT} --- -**OPERATION MODES** +**CORE PRINCIPLES** +Intent detection, context sufficiency, and history recovery are independent. -**STANDARD** -- For reading, explaining, or non-code tasks (posts, docs, summaries) -- Default when no edit intent or code target exists - -**PLAN_STEPS** -- For complex, multi-file, or architectural changes -- When 3+ Modify identifiers or linked modules are involved +--- -**PATCH_CODE** -- For direct, small edits to code files only -- Triggered only if both: - 1. User intent includes verbs like change, update, add, fix, create - 2. Target matches known code in {CODE_IDENTIFIERS} -- Never used for non-code outputs +**1. OPERATION MODE** +- Detect purely from user intent and target type. +- STANDARD → reading, explanation, or any non-code request +- PATCH_CODE → direct or localized code/file edits (≤2 targets, verbs like update, change, fix, insert, modify, add, create) +- PLAN_STEPS → multi-file, architectural, or ≥3 edit targets --- -**CONTEXT SUFFICIENCY** -1. If all mentioned elements (functions, classes, files) exist in {CODE_IDENTIFIERS}, set TRUE -2. If any are missing or ambiguous, set FALSE -3. When FALSE, HISTORY_COUNT = number of past interactions needed to recover missing info -4. When TRUE, HISTORY_COUNT = current {INTERACTION_COUNT} +**2. CONTEXT SUFFICIENCY** +- TRUE if all mentioned items (files, funcs, classes) exist in {CODE_IDENTIFIERS} +- FALSE if any are missing or unclear --- -**DECISION LOGIC** -1. Evaluate context sufficiency -2. Detect if the task targets code or not -3. If non-code, mode = STANDARD -4. If code and complex (≥3 Modify identifiers), mode = PLAN_STEPS -5. If code and localized (≤2 Modify identifiers), mode = PATCH_CODE -6. Output mode, sufficiency, and history count +**3. HISTORY COUNT** +- If SUFFICIENT_CONTEXT = TRUE → HISTORY_COUNT = {INTERACTION_COUNT} +- If FALSE → HISTORY_COUNT = number of previous turns required to restore missing info --- -**STRICT OUTPUT FORMAT** -OPERATION_MODE: [STANDARD|PLAN_STEPS|PATCH_CODE] -SUFFICIENT_CONTEXT: [TRUE|FALSE] +**OUTPUT (exact format)** +OPERATION_MODE: [STANDARD|PATCH_CODE|PLAN_STEPS] +SUFFICIENT_CONTEXT: [TRUE|FALSE] HISTORY_COUNT: [integer] - """ + + ASSESS_HISTORY_RELEVANCE_PROMPT = """ You are Agent **Tide**, operating in **History Relevance Assessment**. From 5ad5f546662b48e091ede9e36dc1e30bef08a706 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 25 Oct 2025 23:16:34 +0100 Subject: [PATCH 083/138] refactor(prompts): update candidate identification rules and state formatting --- codetide/agents/tide/prompts.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 77f2c77..8d50e87 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -582,12 +582,14 @@ Languages: {SUPPORTED_LANGUAGES} **RULES** -- Identify new candidate identifiers only — never solve or explain -- DEDUPLICATE: each must be novel vs {ACCUMULATED_CONTEXT} +- Identify new candidate identifiers only [up to three] — never solve or explain +- DEDUPLICATE: each must be novel vs Accumulated - No markdown, code inspection, or speculation **STATE** -Tree: {TREE_STATE} | Accumulated: {ACCUMULATED_CONTEXT} | Iteration: {ITERATION_COUNT} +Tree: {TREE_STATE} | Iteration: {ITERATION_COUNT} +Accumulated: +{ACCUMULATED_CONTEXT} --- @@ -600,7 +602,7 @@ *** Begin Reasoning **Task**: [Brief summary of user request — always present, even if single] **Rationale**: [Why ths area is being explored — in third person: exploring this because...] -**NEW Candidate Identifiers** (max 3, all novel): +**NEW Candidate Identifiers**: - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] - [third.identifier.or.path] From 070dc248508d578e979baa4a92e3fec2caa6ba05 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 25 Oct 2025 23:41:29 +0100 Subject: [PATCH 084/138] refactor(prompts): clarify selection logic and classification in identifier filtering --- codetide/agents/tide/prompts.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 8d50e87..241213e 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -631,29 +631,32 @@ **MISSION** Filter all gathered identifiers → select up to 5 most relevant. -Classify into **Context** (understanding) vs **Modify** (direct changes). +Classify into **Context** (supporting understanding) and **Modify** (code that must be changed to fulfill the request). **INPUT** - Exploration Steps: {EXPLORATION_STEPS} - Candidate Pool: {ALL_CANDIDATES} - User Intent: from message +--- + **SELECTION LOGIC** -1. Parse intent → detect system scope (specific vs general) -2. Score each candidate (1-100): relevance to fulfilling or informing the intent -3. Discard <80 +1. Analyze user intent to determine system scope (specific vs general) +2. Score each candidate (1-100) for relevance to achieving or informing the goal +3. Discard scores <80 4. Group: - - **Modify** → code directly fulfilling the request - - **Context** → elements explaining why or how (architecture, constraints, top-level docs) -5. Prioritize: Modify > Context -6. If >5 total → drop lowest Context first -7. If request is general/system-wide → retain one top-level doc (README/config) in Context -8. Output final set with short rationale + - **Modify** → code or assets that need to be changed or extended to achieve the request + (not code that already fulfills it) + - **Context** → elements providing understanding, structure, or constraints (architecture, docs, configs) +5. Prioritize Modify > Context +6. If >5 total → remove lowest Context first +7. If intent is general/system-wide → retain one high-level doc (README/config) in Context +8. Always output all three sections below --- *** Begin Summary -[3-4 lines: key findings, rationale for inclusion/exclusion] +[3-4 lines summarizing rationale and key selection logic] *** End Summary *** Begin Context Identifiers @@ -665,7 +668,6 @@ [identifier.to.modify] [another.identifier] *** End Modify Identifiers - """ DETERMINE_OPERATION_MODE_PROMPT = """ From f2c333e0d04abc1ef2caf62a0fce5814ffe31181 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 00:02:28 +0100 Subject: [PATCH 085/138] refactor(prompts): enhance context sufficiency output with conditional search query --- codetide/agents/tide/prompts.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 241213e..0428c03 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -708,14 +708,20 @@ --- +**4. SEARCH QUERY (conditional)** +- Only output when SUFFICIENT_CONTEXT = FALSE +- A short natural-language search query describing the missing context or target +- If SUFFICIENT_CONTEXT = TRUE → omit this line completely + +--- + **OUTPUT (exact format)** OPERATION_MODE: [STANDARD|PATCH_CODE|PLAN_STEPS] SUFFICIENT_CONTEXT: [TRUE|FALSE] HISTORY_COUNT: [integer] +[optional search query only if context insufficient] """ - - ASSESS_HISTORY_RELEVANCE_PROMPT = """ You are Agent **Tide**, operating in **History Relevance Assessment**. From 39eeaf4c323881718e96c3fc0faf63ada61245c8 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 18:12:20 +0000 Subject: [PATCH 086/138] refactor(prompts): expand context sufficiency and search query criteria --- codetide/agents/tide/prompts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 0428c03..81bd556 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -697,7 +697,7 @@ --- **2. CONTEXT SUFFICIENCY** -- TRUE if all mentioned items (files, funcs, classes) exist in {CODE_IDENTIFIERS} +- TRUE if all mentioned items (files, funcs, classes, objects, or patterns) exist in {CODE_IDENTIFIERS} - FALSE if any are missing or unclear --- @@ -710,7 +710,8 @@ **4. SEARCH QUERY (conditional)** - Only output when SUFFICIENT_CONTEXT = FALSE -- A short natural-language search query describing the missing context or target +- A short natural-language description of the missing **code patterns, files, classes, or objects** related to the user’s request +- Avoid action verbs or search-oriented phrasing - If SUFFICIENT_CONTEXT = TRUE → omit this line completely --- From 8c0768ecad9c5345870d5dd141fa732a7e8d5ce2 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 18:16:18 +0000 Subject: [PATCH 087/138] refactor(prompts): update candidate gathering prompts for clarity and structure --- codetide/agents/tide/prompts.py | 54 +++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 81bd556..6fbddb7 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -577,31 +577,31 @@ **REMEMBER**: This is rapid identifier selection based on educated guessing from file/directory structure. Your job is to quickly identify likely relevant files based on naming patterns and organization. Make reasonable assumptions and avoid perfectionist analysis. Speed and decisiveness over exhaustive exploration. """ -GATHER_CANDIDATES_PROMPT = """ +GATHER_CANDIDATES_SYSTEM = """ You are Agent Tide in Candidate Gathering Mode | {DATE} Languages: {SUPPORTED_LANGUAGES} +You will receive the following inputs from the prefix prompt: +- **Last Search Query**: the most recent query used to discover identifiers +- **Iteration Count**: current iterative pass number +- **Accumulated Context**: identifiers gathered from prior iterations +- **Direct Matches**: identifiers explicitly present in the user request +- **Search Candidates**: identifiers or entities found via the last search query + +Use these inputs to assess coverage, propose new candidate identifiers, and if needed, recommend a new search query to continue gathering relevant context. + **RULES** - Identify new candidate identifiers only [up to three] — never solve or explain - DEDUPLICATE: each must be novel vs Accumulated - No markdown, code inspection, or speculation -**STATE** -Tree: {TREE_STATE} | Iteration: {ITERATION_COUNT} -Accumulated: -{ACCUMULATED_CONTEXT} - ---- - **MANDATES** Each section below is independent and must always appear, even for a single task. If a section has no new content, leave it with an empty line. ---- - *** Begin Reasoning **Task**: [Brief summary of user request — always present, even if single] -**Rationale**: [Why ths area is being explored — in third person: exploring this because...] +**Rationale**: [Why this area is being explored — in third person: exploring this because...] **NEW Candidate Identifiers**: - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] @@ -610,19 +610,35 @@ --- -*** Begin Expand Paths -[path/to/directory/] or leave as empty line -*** End Expand Paths - -Expand paths whenever unexplored or collapsed directories might hold related logic or key files (e.g., README, manifest, config). - ---- - *** Begin Assessments ENOUGH_IDENTIFIERS: [TRUE|FALSE] - TRUE: core logic and relevant areas covered - FALSE: additional unexplored or hidden structures remain *** End Assessments + +--- + +*** Begin Search Query +[new query to gather more context identifiers if ENOUGH_IDENTIFIERS = FALSE, otherwise leave empty] +*** End Search Query + +""" + +GATHER_CANDIDATES_PREFIX = """ +**STATE** +Last Search Query: {LAST_SEARCH_QUERY} +Iteration: {ITERATION_COUNT} + +Accumulated Context: +{ACCUMULATED_CONTEXT} + +Direct Matches: +{DIRECT_MATCHES} + +Search Candidates: +{SEARCH_CANDIDATES} + +--- """ FINALIZE_IDENTIFIERS_PROMPT = """ From 4cb4ada83fefa4b6c8a149cffd20737002ac00a1 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 18:17:24 +0000 Subject: [PATCH 088/138] feat(agent): integrate smart code search for identifier gathering and update operation mode extraction refactor(agent): simplify identifier resolution with search-driven candidate gathering and clean response parsing chore(agent): initialize smart code search with codebase files during preparation loop --- codetide/agents/tide/agent.py | 146 ++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 69 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 54413d1..3b04d1e 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -2,12 +2,13 @@ import re from codetide import CodeTide from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file, parse_patch_blocks +from ...search.code_search import SmartCodeSearch from ...core.defaults import DEFAULT_STORAGE_PATH from ...parsers import SUPPORTED_LANGUAGES from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PROMPT, GET_CODE_IDENTIFIERS_UNIFIED_PROMPT, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -26,7 +27,7 @@ from pydantic import BaseModel, Field, ConfigDict, model_validator from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit import PromptSession -from typing import Dict, List, Optional, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from typing_extensions import Self from functools import partial from datetime import date @@ -36,6 +37,11 @@ import pygit2 import os +FILE_TEMPLATE = """{FILENAME} + +{CONTENT} +""" + class AgentTide(BaseModel): llm :Llm tide :CodeTide @@ -54,6 +60,7 @@ class AgentTide(BaseModel): _last_code_context :Optional[str] = None _has_patch :bool=False _direct_mode :bool=False + _smart_code_search :Optional[Any]=None # Number of previous interactions to remember for context identifiers CONTEXT_WINDOW_SIZE: int = 3 @@ -134,7 +141,7 @@ def _clean_history(self): if isinstance(message, dict): self.history[i] = message.get("content" ,"") - async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_history :list, codeIdentifiers=None, TODAY :str=None): + async def get_identifiers_two_phase(self, search_query :Optional[str], autocomplete :AutoComplete, expanded_history :list, codeIdentifiers=None, TODAY :str=None): """ Two-phase identifier resolution: Phase 1: Gather candidates through iterative tree expansion @@ -143,61 +150,47 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_h # Initialize tracking last_message = self.history[-1] if self.history else "" matches = autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] - - self._context_identifier_window.append(set(matches)) - if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: - self._context_identifier_window.pop(0) - - window_identifiers = set() - for s in self._context_identifier_window: - window_identifiers.update(s) - - initial_identifiers = set(codeIdentifiers) if codeIdentifiers else set() - initial_identifiers.update(window_identifiers) + print(f"{matches=}") + + ### TODO replace matches with search based on received search query + ### get identifiers + ### search for identifers and ask llm to return only related ones -> if not enough then generate search query and keep cycling instead of expanding paths + if search_query is None: + search_query = expanded_history[-1] # ===== PHASE 1: CANDIDATE GATHERING ===== candidate_pool = set() all_reasoning = [] iteration_count = 0 max_iterations = 3 - repo_tree = None - expand_paths = ["./"] enough_identifiers = False while not enough_identifiers and iteration_count < max_iterations: iteration_count += 1 + serch_results = await self._smart_code_search.search_smart(search_query, use_variations=False, top_k=15) + identifiers_from_search = {result[0] for result in serch_results} - # Get current repo tree state - if repo_tree is None or iteration_count > 1: - repo_tree = await self.get_repo_tree_from_user_prompt( - expanded_history, - include_modules=bool(iteration_count > 1), - expand_paths=expand_paths - ) - - # Prepare accumulated context - accumulated_context = "\n".join(sorted(candidate_pool)) if candidate_pool else "None yet" - - view_type = "`Current view`" if iteration_count == 1 else "`Expanded view`" # Phase 1 LLM call phase1_response = await self.llm.acomplete( expanded_history, - system_prompt=[GATHER_CANDIDATES_PROMPT.format( + system_prompt=[GATHER_CANDIDATES_SYSTEM.format( DATE=TODAY, - SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, - TREE_STATE=view_type, - ACCUMULATED_CONTEXT=accumulated_context, - ITERATION_COUNT=iteration_count + SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES )], - prefix_prompt=rf"# {view_type}\n\n{repo_tree}", - stream=True + prefix_prompt=GATHER_CANDIDATES_PREFIX.format( + LAST_SEARCH_QUERY=search_query, + ITERATION_COUNT=iteration_count, + ACCUMULATED_CONTEXT=set(self._context_identifier_window), + DIRECT_MATCHES=set(matches), + SEARCH_CANDIDATES=identifiers_from_search, + ), + stream=True, + action_id=f"phase_1.{iteration_count}" ) - # print(f"Phase 1 Iteration {iteration_count}: {phase1_response}") - # Parse Phase 1 response reasoning_blocks = parse_blocks(phase1_response, block_word="Reasoning", multiple=True) - expand_paths_block = parse_blocks(phase1_response, block_word="Expand Paths", multiple=False) + search_query = parse_blocks(phase1_response, block_word="Search Query", multiple=False) ### TODO update to use Rationale and New Candidates Indeintifiers and extract based onr egex ffs # Extract and accumulate candidates from reasoning blocks @@ -207,7 +200,6 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_h "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}Candidate Identifiers\*{0,2}|$)", "candidate_identifiers": r"^\s*-\s*(.+?)$" } - for reasoning in reasoning_blocks: # Extract header header_match = re.search(patterns["header"], reasoning, re.DOTALL) @@ -216,7 +208,7 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_h # Extract content (rationale) content_match = re.search(patterns["content"], reasoning, re.DOTALL | re.MULTILINE) content = content_match.group(1).strip() if content_match else None - + # Append only header and content to all_reasoning if header and content: all_reasoning.append(REASONING_TEMPLTAE.format(**{ @@ -236,17 +228,7 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_h # Check if we need to expand more if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper(): enough_identifiers = True - - # Parse expansion paths for next iteration - if expand_paths_block and not enough_identifiers: - expand_paths = [ - path.strip() for path in expand_paths_block.strip().split('\n') - if path.strip() and self.get_valid_identifier(autocomplete, path.strip()) - ] - else: - expand_paths = [] - ## TODO shoudla append previous reasoning as input / prefix - + # ===== PHASE 2: FINAL SELECTION AND CLASSIFICATION ===== # Prepare Phase 2 input @@ -270,7 +252,8 @@ async def get_identifiers_two_phase(self, autocomplete :AutoComplete, expanded_h ALL_CANDIDATES=all_candidates_text, )], prefix_prompt=sub_tree, - stream=True + stream=True, + action_id="phase2.finalize" ) # print(f"Phase 2 Final Selection: {phase2_response}") @@ -424,27 +407,51 @@ async def extract_operation_mode( ), stream=False ) - - # Extract OPERATION_MODE - operation_mode_match = re.search(r'OPERATION_MODE:\s*\[?(STANDARD|PLAN_STEPS|PATCH_CODE)\]?', response) + + response_text = response.strip() + # Extract and remove OPERATION_MODE + operation_mode_match = re.search(r'OPERATION_MODE:\s*\[?(STANDARD|PLAN_STEPS|PATCH_CODE)\]?', response_text) operation_mode = operation_mode_match.group(1) if operation_mode_match else None - - # Extract SUFFICIENT_CONTEXT - sufficient_context_match = re.search(r'SUFFICIENT_CONTEXT:\s*\[?(TRUE|FALSE)\]?', response) - sufficient_context = 'true' in sufficient_context_match.group(1).lower() if sufficient_context_match else None - - # Extract HISTORY_COUNT - history_count_match = re.search(r'HISTORY_COUNT:\s*\[?(\d+)\]?', response) + if operation_mode_match: + response_text = response_text.replace(operation_mode_match.group(0), '') + + # Extract and remove SUFFICIENT_CONTEXT + sufficient_context_match = re.search(r'SUFFICIENT_CONTEXT:\s*\[?(TRUE|FALSE)\]?', response_text) + sufficient_context = ( + sufficient_context_match.group(1).strip().upper() == "TRUE" + if sufficient_context_match else None + ) + if sufficient_context_match: + response_text = response_text.replace(sufficient_context_match.group(0), '') + + # Extract and remove HISTORY_COUNT + history_count_match = re.search(r'HISTORY_COUNT:\s*\[?(\d+)\]?', response_text) history_count = int(history_count_match.group(1)) if history_count_match else len(self.history) - + if history_count_match: + response_text = response_text.replace(history_count_match.group(0), '') + + # Whatever remains (if anything) is the search query + search_query = response_text.strip() or None + # Validate extraction if operation_mode is None or sufficient_context is None: raise ValueError(f"Failed to extract required fields from response:\n{response}") - + final_history_count = await self.expand_history_if_needed(sufficient_context, history_count) expanded_history = self.history[-final_history_count:] - - return operation_mode, sufficient_context, expanded_history + + return operation_mode, sufficient_context, expanded_history, search_query + + async def prepare_loop(self): + await self.tide.check_for_updates(serialize=True, include_cached_ids=True) + ### TODO this whole process neds to be integrated and updated from coetide directly for efficiency + self._smart_code_search = SmartCodeSearch( + documents={ + codefile.file_path: FILE_TEMPLATE.format(CONTENT=codefile.raw, FILENAME=codefile.file_path) + for codefile in self.tide.codebase.root + } + ) + await self._smart_code_search.initialize_async() async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): TODAY = date.today() @@ -471,11 +478,12 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): tasks = [ self.extract_operation_mode(cached_identifiers), - self.tide.check_for_updates(serialize=True, include_cached_ids=True) + self.prepare_loop() ] operation_context_history_task, _ = await asyncio.gather(*tasks) - operation_mode, sufficient_context, expanded_history = operation_context_history_task + operation_mode, sufficient_context, expanded_history, search_query = operation_context_history_task + print(f"{search_query=}") autocomplete = AutoComplete(self.tide.cached_ids) @@ -502,7 +510,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history else: await self.llm.logger_fn(REASONING_STARTED) - reasoning_output = await self.get_identifiers_two_phase(autocomplete, expanded_history, codeIdentifiers, TODAY) + reasoning_output = await self.get_identifiers_two_phase(search_query, autocomplete, expanded_history, codeIdentifiers, TODAY) await self.llm.logger_fn(REASONING_FINISHED) print(json.dumps(reasoning_output, indent=4)) From 3284cfb8fb9c5e05badabe4f2743c3779c81184f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 18:17:39 +0000 Subject: [PATCH 089/138] refactor(prompts): update candidate identifier search query instructions --- codetide/agents/tide/prompts.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 6fbddb7..b9fb3bd 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -588,7 +588,7 @@ - **Direct Matches**: identifiers explicitly present in the user request - **Search Candidates**: identifiers or entities found via the last search query -Use these inputs to assess coverage, propose new candidate identifiers, and if needed, recommend a new search query to continue gathering relevant context. +Use these inputs to assess coverage, propose new candidate identifiers, and if needed, recommend a new query to continue gathering relevant context. **RULES** - Identify new candidate identifiers only [up to three] — never solve or explain @@ -619,9 +619,11 @@ --- *** Begin Search Query -[new query to gather more context identifiers if ENOUGH_IDENTIFIERS = FALSE, otherwise leave empty] +- Only include when ENOUGH_IDENTIFIERS = FALSE +- Provide a short, natural-language description of the missing **code patterns, files, classes, or objects** related to the task +- Avoid action verbs or search-related phrasing +- Keep it concise and directly tied to the user’s current intent *** End Search Query - """ GATHER_CANDIDATES_PREFIX = """ From a9c7fd927a91f1d9e1c691fafdda515074086829 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 18:37:10 +0000 Subject: [PATCH 090/138] refactor(agent): optimize codebase tree building and include repo tree in phase 1 context --- codetide/agents/tide/agent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 3b04d1e..4a71e34 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -169,6 +169,10 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl iteration_count += 1 serch_results = await self._smart_code_search.search_smart(search_query, use_variations=False, top_k=15) identifiers_from_search = {result[0] for result in serch_results} + + candidates_to_filter_tree = self.tide._as_file_paths(list(identifiers_from_search)[:5]) + self.tide.codebase._build_tree_dict(candidates_to_filter_tree, slim=True) + sub_tree = self.tide.codebase.get_tree_view() # Phase 1 LLM call phase1_response = await self.llm.acomplete( @@ -183,6 +187,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl ACCUMULATED_CONTEXT=set(self._context_identifier_window), DIRECT_MATCHES=set(matches), SEARCH_CANDIDATES=identifiers_from_search, + REPO_TREE=sub_tree ), stream=True, action_id=f"phase_1.{iteration_count}" @@ -235,10 +240,6 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl all_reasoning_text = "\n\n".join(all_reasoning) all_candidates_text = "\n".join(sorted(candidate_pool)) - candidates_to_filter_tree = self.tide._as_file_paths(list(candidate_pool)) - self.tide.codebase._build_tree_dict(candidates_to_filter_tree, slim=True) - sub_tree = self.tide.codebase.get_tree_view() - # print(sub_tree) # print(f"{all_candidates_text=}") @@ -251,7 +252,6 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl EXPLORATION_STEPS=all_reasoning_text, ALL_CANDIDATES=all_candidates_text, )], - prefix_prompt=sub_tree, stream=True, action_id="phase2.finalize" ) From 418e96b6c267465553fdb611b6dc7dda842c3c14 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 18:37:23 +0000 Subject: [PATCH 091/138] feat(prompts): add repo tree context to identifier search prompt --- codetide/agents/tide/prompts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index b9fb3bd..21cde43 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -587,6 +587,7 @@ - **Accumulated Context**: identifiers gathered from prior iterations - **Direct Matches**: identifiers explicitly present in the user request - **Search Candidates**: identifiers or entities found via the last search query +- **Repo Tree**: tree representation of the repository to be used as context when generating a new Search Query Use these inputs to assess coverage, propose new candidate identifiers, and if needed, recommend a new query to continue gathering relevant context. @@ -640,7 +641,8 @@ Search Candidates: {SEARCH_CANDIDATES} ---- +Repo Tree: +{REPO_TREE} """ FINALIZE_IDENTIFIERS_PROMPT = """ From fcc7654370d1848b21eed17cb24f7e67bb7430e1 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 21:16:00 +0000 Subject: [PATCH 092/138] refactor(prompts): enhance candidate identification logic and reasoning rules --- codetide/agents/tide/prompts.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 21cde43..72ede59 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -589,11 +589,16 @@ - **Search Candidates**: identifiers or entities found via the last search query - **Repo Tree**: tree representation of the repository to be used as context when generating a new Search Query -Use these inputs to assess coverage, propose new candidate identifiers, and if needed, recommend a new query to continue gathering relevant context. +Your goal is to iteratively broaden context coverage by identifying **novel, meaningful, and previously unexplored code areas**. +Each new reasoning step must add distinct insight or targets. Redundant reasoning or repeated identifiers provides no value. **RULES** - Identify new candidate identifiers only [up to three] — never solve or explain -- DEDUPLICATE: each must be novel vs Accumulated +- DEDUPLICATE: each must be novel vs Accumulated and all prior reasoning steps +- Each reasoning step must be substantially different from the previous one: + - Distinct focus, rationale, or code region + - New identifiers not already found or implied by previous queries +- Do not repeat or restate earlier reasoning or candidate identifiers - No markdown, code inspection, or speculation **MANDATES** @@ -602,7 +607,7 @@ *** Begin Reasoning **Task**: [Brief summary of user request — always present, even if single] -**Rationale**: [Why this area is being explored — in third person: exploring this because...] +**Rationale**: [Why this new area is being explored — must differ in focus or logic from prior reasoning] **NEW Candidate Identifiers**: - [fully.qualified.identifier or path/to/file.ext] - [another.identifier.or.path] @@ -621,9 +626,11 @@ *** Begin Search Query - Only include when ENOUGH_IDENTIFIERS = FALSE -- Provide a short, natural-language description of the missing **code patterns, files, classes, or objects** related to the task +- Describe **new** unexplored **code patterns, files, classes, or objects** +- Must focus on areas not already represented by Accumulated Context or previous queries - Avoid action verbs or search-related phrasing -- Keep it concise and directly tied to the user’s current intent +- Keep it concise, technically descriptive, and focused on new areas of inspection +- One search query only! *** End Search Query """ From 596ab3ca33462f15ed0fdaa4f0d71372b2f8210e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:09:27 +0000 Subject: [PATCH 093/138] fix(codetide): correct file path handling for new files in codebase --- codetide/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/codetide/__init__.py b/codetide/__init__.py index 47bd818..dfa1277 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -600,8 +600,6 @@ def _as_file_paths(self, code_identifiers: Union[str, List[str]])->List[str]: as_file_paths.append(code_identifier) elif element := self.codebase.cached_elements.get(code_identifier): as_file_paths.append(element.file_path) - else: ### covers new files - as_file_paths.append(element) return as_file_paths From ba0f604621d2b0f59f154cf30c091dbc3dfd331e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:09:49 +0000 Subject: [PATCH 094/138] refactor(prompts): clarify classification criteria and update summary instructions --- codetide/agents/tide/prompts.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 72ede59..34fbe07 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -672,9 +672,8 @@ 2. Score each candidate (1-100) for relevance to achieving or informing the goal 3. Discard scores <80 4. Group: - - **Modify** → code or assets that need to be changed or extended to achieve the request - (not code that already fulfills it) - - **Context** → elements providing understanding, structure, or constraints (architecture, docs, configs) + - **Modify** → code or assets that must be altered or extended to realize the user’s request (not code that already fulfills it) + - **Context** → elements providing structure, constraints, or necessary understanding (architecture, utilities, configs, docs) 5. Prioritize Modify > Context 6. If >5 total → remove lowest Context first 7. If intent is general/system-wide → retain one high-level doc (README/config) in Context @@ -683,7 +682,10 @@ --- *** Begin Summary -[3-4 lines summarizing rationale and key selection logic] +[3 - 5 lines describing, in third person, how the selected identifiers will be used to accomplish the request. +This should act as a practical guide or blueprint for the next operation — e.g., which files or classes will be modified, +how Context items support those modifications, and what the overall direction of work will be. +Avoid summarizing past reasoning; focus on forward application of the chosen identifiers.] *** End Summary *** Begin Context Identifiers From e62ac9d54d8a524e639d5987aab407bbd52e4c6c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:12:16 +0000 Subject: [PATCH 095/138] refactor(agent): improve multi-iteration candidate gathering and parsing logic in tide agent --- codetide/agents/tide/agent.py | 60 +++++++++++++++++------------------ 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 4a71e34..2100465 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -150,7 +150,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl # Initialize tracking last_message = self.history[-1] if self.history else "" matches = autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] - print(f"{matches=}") + # print(f"{matches=}") ### TODO replace matches with search based on received search query ### get identifiers @@ -164,78 +164,76 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl iteration_count = 0 max_iterations = 3 enough_identifiers = False + previous_phase_1_response = None while not enough_identifiers and iteration_count < max_iterations: + # print(f"{iteration_count=}") iteration_count += 1 serch_results = await self._smart_code_search.search_smart(search_query, use_variations=False, top_k=15) identifiers_from_search = {result[0] for result in serch_results} + # print(f"{identifiers_from_search=}") - candidates_to_filter_tree = self.tide._as_file_paths(list(identifiers_from_search)[:5]) + candidates_to_filter_tree = self.tide._as_file_paths(list(identifiers_from_search)) + # print("got identifiers") self.tide.codebase._build_tree_dict(candidates_to_filter_tree, slim=True) + # print("got tree") sub_tree = self.tide.codebase.get_tree_view() - - # Phase 1 LLM call - phase1_response = await self.llm.acomplete( - expanded_history, - system_prompt=[GATHER_CANDIDATES_SYSTEM.format( - DATE=TODAY, - SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES - )], - prefix_prompt=GATHER_CANDIDATES_PREFIX.format( + # print(sub_tree) + prefix_prompt = [ + GATHER_CANDIDATES_PREFIX.format( LAST_SEARCH_QUERY=search_query, ITERATION_COUNT=iteration_count, ACCUMULATED_CONTEXT=set(self._context_identifier_window), DIRECT_MATCHES=set(matches), SEARCH_CANDIDATES=identifiers_from_search, REPO_TREE=sub_tree + ) + ] + if previous_phase_1_response is not None: + prefix_prompt.insert(0, previous_phase_1_response) + + # Phase 1 LLM call + phase1_response = await self.llm.acomplete( + expanded_history, + system_prompt=GATHER_CANDIDATES_SYSTEM.format( + DATE=TODAY, + SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES ), + prefix_prompt=prefix_prompt, stream=True, action_id=f"phase_1.{iteration_count}" ) + previous_phase_1_response = phase1_response # Parse Phase 1 response reasoning_blocks = parse_blocks(phase1_response, block_word="Reasoning", multiple=True) search_query = parse_blocks(phase1_response, block_word="Search Query", multiple=False) - - ### TODO update to use Rationale and New Candidates Indeintifiers and extract based onr egex ffs - # Extract and accumulate candidates from reasoning blocks patterns = { "header": r"\*{0,2}Task\*{0,2}:\s*(.+?)(?=\n\s*\*{0,2}Rationale\*{0,2})", - "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}Candidate Identifiers\*{0,2}|$)", + "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}NEW Candidate Identifiers\*{0,2}|$)", "candidate_identifiers": r"^\s*-\s*(.+?)$" } + all_reasoning.extend(reasoning_blocks) for reasoning in reasoning_blocks: - # Extract header - header_match = re.search(patterns["header"], reasoning, re.DOTALL) - header = header_match.group(1).strip() if header_match else None - - # Extract content (rationale) - content_match = re.search(patterns["content"], reasoning, re.DOTALL | re.MULTILINE) - content = content_match.group(1).strip() if content_match else None - - # Append only header and content to all_reasoning - if header and content: - all_reasoning.append(REASONING_TEMPLTAE.format(**{ - "header": header, - "content": content - })) - # Extract candidate identifiers using regex candidate_pattern = patterns["candidate_identifiers"] candidate_matches = re.findall(candidate_pattern, reasoning, re.MULTILINE) + # print(f"{candidate_matches}=") for match in candidate_matches: ident = match.strip() if ident := self.get_valid_identifier(autocomplete, ident): + # print(f"{ident=}") candidate_pool.add(ident) + # print("exit here") # Check if we need to expand more if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper(): enough_identifiers = True # ===== PHASE 2: FINAL SELECTION AND CLASSIFICATION ===== - + # print("Here 2") # Prepare Phase 2 input all_reasoning_text = "\n\n".join(all_reasoning) all_candidates_text = "\n".join(sorted(candidate_pool)) From 637d48bfeb21a0f8370dc9d90b1fbbd6dd421e3a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:20:58 +0000 Subject: [PATCH 096/138] refactor(prompts): enforce strict structural compliance and update candidate gathering rules --- codetide/agents/tide/prompts.py | 63 +++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 34fbe07..d3d2f78 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -581,6 +581,10 @@ You are Agent Tide in Candidate Gathering Mode | {DATE} Languages: {SUPPORTED_LANGUAGES} +You operate in **strict structural compliance mode**. +Your only responsibility is to gather and propose identifiers for potential context expansion. +You must **never** begin implementing, interpreting, or solving the user’s request in any way. + You will receive the following inputs from the prefix prompt: - **Last Search Query**: the most recent query used to discover identifiers - **Iteration Count**: current iterative pass number @@ -592,18 +596,41 @@ Your goal is to iteratively broaden context coverage by identifying **novel, meaningful, and previously unexplored code areas**. Each new reasoning step must add distinct insight or targets. Redundant reasoning or repeated identifiers provides no value. +--- + +**ABSOLUTE DIRECTIVES** +- **DO NOT** process, transform, or execute the user’s request in any way. +- **DO NOT** produce explanations, implementation plans, or solutions. +- **DO NOT** change the required output format. +- **DO NOT** include additional commentary or text outside the required structure. + +--- + +**STRICT IDENTIFIER SUGGESTION RULE** +- You must only suggest new candidate identifiers that you are certain exist in the codebase. +- Valid sources for suggestions include: + - Direct matches explicitly present in the user request + - Identifiers found in the last search query results + - Identifiers present in the accumulated prior context + - Identifiers inferred from the repository tree structure +- You must **never** hallucinate or invent identifiers that have no basis in these sources. + +--- + **RULES** -- Identify new candidate identifiers only [up to three] — never solve or explain -- DEDUPLICATE: each must be novel vs Accumulated and all prior reasoning steps +- Identify new candidate identifiers only [up to three] — never solve or explain. +- DEDUPLICATE: each must be novel vs Accumulated and all prior reasoning steps. - Each reasoning step must be substantially different from the previous one: - - Distinct focus, rationale, or code region - - New identifiers not already found or implied by previous queries -- Do not repeat or restate earlier reasoning or candidate identifiers -- No markdown, code inspection, or speculation + - Distinct focus, rationale, or code region. + - New identifiers not already found or implied by previous queries. +- Do not repeat or restate earlier reasoning or candidate identifiers. +- No markdown, code inspection, or speculation. + +--- -**MANDATES** -Each section below is independent and must always appear, even for a single task. -If a section has no new content, leave it with an empty line. +**MANDATED OUTPUT STRUCTURE** +The following sections are independent and **must always appear in this exact order and formatting**. +If a section has no new content, leave it **intentionally blank** (do not omit). *** Begin Reasoning **Task**: [Brief summary of user request — always present, even if single] @@ -625,13 +652,19 @@ --- *** Begin Search Query -- Only include when ENOUGH_IDENTIFIERS = FALSE -- Describe **new** unexplored **code patterns, files, classes, or objects** -- Must focus on areas not already represented by Accumulated Context or previous queries -- Avoid action verbs or search-related phrasing -- Keep it concise, technically descriptive, and focused on new areas of inspection -- One search query only! +- Only include when ENOUGH_IDENTIFIERS = FALSE. +- Describe **new** unexplored **code patterns, files, classes, or objects**. +- Must focus on areas not already represented by Accumulated Context or previous queries. +- Avoid action verbs or search-related phrasing. +- Keep it concise, technically descriptive, and focused on new areas of inspection. +- Produce exactly one query line. *** End Search Query + +--- + +**FINAL COMPLIANCE NOTE** +If any section, label, or delimiter is missing, malformed, or reordered, the output is invalid. +You must never introduce free-form text, commentary, or reasoning outside the defined structure. """ GATHER_CANDIDATES_PREFIX = """ From 298900d1bfe9c95a30eee9d28e85f539be0d3ed6 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:48:55 +0000 Subject: [PATCH 097/138] refactor(tide): separate operation mode system and prefix prompts and update agent logic --- codetide/agents/tide/agent.py | 5 +++-- codetide/agents/tide/prompts.py | 30 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 2100465..c1a5859 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -8,7 +8,7 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, DETERMINE_OPERATION_MODE_SYSTEM, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -399,7 +399,8 @@ async def extract_operation_mode( """ response = await self.llm.acomplete( self.history[-3:], - system_prompt=DETERMINE_OPERATION_MODE_PROMPT.format( + system_prompt=DETERMINE_OPERATION_MODE_SYSTEM, + prefix_prompt=DETERMINE_OPERATION_MODE_PROMPT.format( INTERACTION_COUNT=len(self.history), CODE_IDENTIFIERS=cached_identifiers ), diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index d3d2f78..72e49c1 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -732,22 +732,28 @@ *** End Modify Identifiers """ -DETERMINE_OPERATION_MODE_PROMPT = """ +DETERMINE_OPERATION_MODE_SYSTEM = """ You are Agent **Tide** performing **Operation Mode Extraction** +You will receive the following inputs from the prefix prompt: +- **Code Identifiers**: the current set of known identifiers, files, functions, classes, or patterns available in context +- **Interaction Count**: the number of prior exchanges or iterations in the conversation + +Your task is to determine the current **operation mode**, assess **context sufficiency**, and if context is insufficient, propose a short **search query** to gather missing information. + **NO** - Explanations, markdown, or code - Extra text outside required output -**INPUT** -- Code Identifiers: {CODE_IDENTIFIERS} -- Interaction Count: {INTERACTION_COUNT} - --- **CORE PRINCIPLES** Intent detection, context sufficiency, and history recovery are independent. +**IMPORTANT:** +In case of the slightest doubt or uncertainty about context sufficiency, you MUST default to assuming that more context is needed. +It is NOT acceptable to respond without enough context to properly reply. + --- **1. OPERATION MODE** @@ -759,13 +765,13 @@ --- **2. CONTEXT SUFFICIENCY** -- TRUE if all mentioned items (files, funcs, classes, objects, or patterns) exist in {CODE_IDENTIFIERS} -- FALSE if any are missing or unclear +- TRUE if all mentioned items (files, funcs, classes, objects, or patterns) exist in Code Identifiers +- FALSE if any are missing, unclear, or if there is any doubt about sufficiency --- **3. HISTORY COUNT** -- If SUFFICIENT_CONTEXT = TRUE → HISTORY_COUNT = {INTERACTION_COUNT} +- If SUFFICIENT_CONTEXT = TRUE → HISTORY_COUNT = Interaction Count - If FALSE → HISTORY_COUNT = number of previous turns required to restore missing info --- @@ -785,6 +791,14 @@ [optional search query only if context insufficient] """ +DETERMINE_OPERATION_MODE_PROMPT = """ +**INPUT** +Code Identifiers: +{CODE_IDENTIFIERS} + +Interaction Count: {INTERACTION_COUNT} +""" + ASSESS_HISTORY_RELEVANCE_PROMPT = """ You are Agent **Tide**, operating in **History Relevance Assessment**. From 96a82eef9d108820e44e76974508eb8b816d164a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:51:16 +0000 Subject: [PATCH 098/138] chore(prompts): enforce strict output format and strengthen identifier suggestion rules --- codetide/agents/tide/prompts.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 72e49c1..740516b 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -585,6 +585,9 @@ Your only responsibility is to gather and propose identifiers for potential context expansion. You must **never** begin implementing, interpreting, or solving the user’s request in any way. +You must **always, without exception, reply strictly in the mandated output format** regardless of the type or content of input received. +Under no circumstances should you deviate from this format or omit any required sections. + You will receive the following inputs from the prefix prompt: - **Last Search Query**: the most recent query used to discover identifiers - **Iteration Count**: current iterative pass number @@ -607,13 +610,13 @@ --- **STRICT IDENTIFIER SUGGESTION RULE** -- You must only suggest new candidate identifiers that you are certain exist in the codebase. +- You must only suggest new candidate identifiers that you are absolutely certain exist in the codebase. - Valid sources for suggestions include: - Direct matches explicitly present in the user request - Identifiers found in the last search query results - Identifiers present in the accumulated prior context - Identifiers inferred from the repository tree structure -- You must **never** hallucinate or invent identifiers that have no basis in these sources. +- You must **never** hallucinate, invent, or propose new candidate identifiers unless you are 100% certain they exist. --- From 73bc8607b1641d39d80d14a9a55124afbfd62b2f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 22:59:49 +0000 Subject: [PATCH 099/138] refactor(prompts): clarify search query output to use concise keywords only --- codetide/agents/tide/prompts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 740516b..5ac401c 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -781,8 +781,8 @@ **4. SEARCH QUERY (conditional)** - Only output when SUFFICIENT_CONTEXT = FALSE -- A short natural-language description of the missing **code patterns, files, classes, or objects** related to the user’s request -- Avoid action verbs or search-oriented phrasing +- Provide a concise, targeted keyword or single pattern describing the missing **code patterns, files, classes, or objects** to search for in the codebase +- Use only focused keywords or short phrases, not full sentences or verbose text - If SUFFICIENT_CONTEXT = TRUE → omit this line completely --- From 70e833700cfcaf3a170387b0727d70a3c06a280f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:02:52 +0000 Subject: [PATCH 100/138] chore(prompts): update strict output format enforcement and context rules --- codetide/agents/tide/prompts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 5ac401c..0872685 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -586,6 +586,7 @@ You must **never** begin implementing, interpreting, or solving the user’s request in any way. You must **always, without exception, reply strictly in the mandated output format** regardless of the type or content of input received. +This requirement applies absolutely to every input, no matter its nature or complexity. Under no circumstances should you deviate from this format or omit any required sections. You will receive the following inputs from the prefix prompt: @@ -598,6 +599,7 @@ Your goal is to iteratively broaden context coverage by identifying **novel, meaningful, and previously unexplored code areas**. Each new reasoning step must add distinct insight or targets. Redundant reasoning or repeated identifiers provides no value. +Previous messages in the conversation history are solely for context and must never influence or dictate your output format or structure. --- From bd2d9227187aa585143eaf234db1ef614a248c82 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:31:52 +0000 Subject: [PATCH 101/138] refactor(ui): add reasoning thinking time tracking and cleanup ReasoningExplorer component --- codetide/agents/tide/ui/app.py | 10 ++++--- .../ui/public/elements/ReasoningExplorer.jsx | 26 +------------------ 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 1881455..447e955 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -476,7 +476,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "summary": "", "context_identifiers": [], # amrker "modify_identifiers": [], - "finished": False + "finished": False, + "thinkingTime": 0 }) if not agent_tide_ui.agent_tide._skip_context_retrieval: @@ -565,6 +566,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option global_fallback_msg=msg ) + reasoning_start_time = time.time() loop = run_concurrent_tasks(agent_tide_ui, codeIdentifiers) async for chunk in loop: ### TODO update this to check FROM AGENT TIDE if reasoning is being ran and if so we need @@ -582,12 +584,14 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option stream_processor.buffer = "" stream_processor.accumulated_content = "" continue - elif chunk == REASONING_FINISHED: + reasoning_end_time = time.time() + thinking_time = int(reasoning_end_time - reasoning_start_time) stream_processor.global_fallback_msg = msg stream_processor.buffer = "" stream_processor.accumulated_content = "" - reasoning_element.props["finished"] =True + reasoning_element.props["finished"] = True + reasoning_element.props["thinkingTime"] = thinking_time await reasoning_element.update() continue diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 81adf51..716a31f 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -7,7 +7,6 @@ export default function ReasoningStepsCard() { const [expanded, setExpanded] = useState(false); const [waveOffset, setWaveOffset] = useState(0); const [loadingText, setLoadingText] = useState("Analyzing"); - const [thinkingTime, setThinkingTime] = useState(0); const loadingStates = [ "Diving deep into the code", @@ -40,17 +39,7 @@ export default function ReasoningStepsCard() { }; }, []); - useEffect(() => { - if (isLoadingState) { - const timer = setInterval(() => { - setThinkingTime((prev) => prev + 1); - }, 1000); - return () => clearInterval(timer); - } - }, [isLoadingState]); - const getPreviewText = () => { - // Mock data for demo const reasoning_steps = props.reasoning_steps; const summary = props.summary; @@ -65,7 +54,6 @@ export default function ReasoningStepsCard() { return ( - {/* Header - Collapsible */} - {/* Content - Expands when clicked */} {expanded && ( - {/* Reasoning Steps */} {props?.reasoning_steps?.length > 0 && (
{props.reasoning_steps.map((step, index) => (
- {/* Timeline Column */}
- - {/* Vertical connector line */} {index < props.reasoning_steps.length - 1 && ( )}
- - {/* Step Content */}

{step.header} @@ -161,8 +142,6 @@ export default function ReasoningStepsCard() {

{step.content}

- - {/* Candidate Identifiers */} {step.candidate_identifiers?.length > 0 && (
{step.candidate_identifiers.map((id, idIndex) => ( @@ -181,8 +160,6 @@ export default function ReasoningStepsCard() { ))}
)} - - {/* Context + Modify Identifiers */} {(props?.context_identifiers?.length > 0 || props?.modify_identifiers?.length > 0) && (
@@ -202,7 +179,6 @@ export default function ReasoningStepsCard() {

)} - {props.modify_identifiers?.length > 0 && (

Modification Identifiers

From afb93b701d209c133d7789599224cffad5069d62 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:32:30 +0000 Subject: [PATCH 102/138] refactor(agent): improve context handling and identifier matching in agent logic --- codetide/agents/tide/agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index c1a5859..c77a40e 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -149,7 +149,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl """ # Initialize tracking last_message = self.history[-1] if self.history else "" - matches = autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] + matches = set(autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"]) # print(f"{matches=}") ### TODO replace matches with search based on received search query @@ -184,7 +184,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl LAST_SEARCH_QUERY=search_query, ITERATION_COUNT=iteration_count, ACCUMULATED_CONTEXT=set(self._context_identifier_window), - DIRECT_MATCHES=set(matches), + DIRECT_MATCHES=matches, SEARCH_CANDIDATES=identifiers_from_search, REPO_TREE=sub_tree ) @@ -276,7 +276,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl final_modify.add(ident) return { - "matches": matches, + "matches": list(matches), "context_identifiers": list(final_context), "modify_identifiers": self.tide._as_file_paths(list(final_modify)), "summary": summary, @@ -436,7 +436,7 @@ async def extract_operation_mode( if operation_mode is None or sufficient_context is None: raise ValueError(f"Failed to extract required fields from response:\n{response}") - final_history_count = await self.expand_history_if_needed(sufficient_context, history_count) + final_history_count = await self.expand_history_if_needed(sufficient_context, min(history_count, int(history_count * 0.2)+1)) expanded_history = self.history[-final_history_count:] return operation_mode, sufficient_context, expanded_history, search_query From 0ca8008e04d32731d647dd9728ff1bec4792d678 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:34:47 +0000 Subject: [PATCH 103/138] fix(agent): improve identifier sufficiency check in agent logic --- codetide/agents/tide/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index c77a40e..9d29e51 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -229,7 +229,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl # print("exit here") # Check if we need to expand more - if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper(): + if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper() or matches.issubset(candidate_pool): enough_identifiers = True # ===== PHASE 2: FINAL SELECTION AND CLASSIFICATION ===== From a45da4ae6fc596e70bb3c2cff565b9047e66281b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:40:51 +0000 Subject: [PATCH 104/138] style(prompts): update reminder to enforce output structure and identifier rules --- codetide/agents/tide/prompts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 0872685..20f3788 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -688,6 +688,10 @@ Repo Tree: {REPO_TREE} + +--- + +Remember that you must at all costs respecte the **MANDATED OUTPUT STRUCTURE** and **STRICT IDENTIFIER SUGGESTION RULE**! """ FINALIZE_IDENTIFIERS_PROMPT = """ From fcc16b8063929f3e324173fac40015166803895b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:42:43 +0000 Subject: [PATCH 105/138] refactor(prompts): update prompt summary guidelines for clarity and precision --- codetide/agents/tide/prompts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 20f3788..c7e6d04 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -724,10 +724,10 @@ --- *** Begin Summary -[3 - 5 lines describing, in third person, how the selected identifiers will be used to accomplish the request. -This should act as a practical guide or blueprint for the next operation — e.g., which files or classes will be modified, -how Context items support those modifications, and what the overall direction of work will be. -Avoid summarizing past reasoning; focus on forward application of the chosen identifiers.] +[3-5 lines written in third person, describing how the **selected identifiers** — both Context and Modify — relate to each other in fulfilling the user’s intent. +Focus on how Context elements support or constrain the planned modifications, and how Modify elements will be adapted or extended. +Do **not** mention identifiers that were considered but not selected, and do **not** recap previous reasoning. +The summary should read as a concise forward plan linking motivation, relationships, and purpose of the chosen items.] *** End Summary *** Begin Context Identifiers From 89821d1830f66fb961a0fa96a791e3dc2457a497 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 26 Oct 2025 23:45:17 +0000 Subject: [PATCH 106/138] refactor(agent): improve candidate identifier extraction logic in tide agent --- codetide/agents/tide/agent.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 9d29e51..1d8ee1c 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -214,20 +214,18 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}NEW Candidate Identifiers\*{0,2}|$)", "candidate_identifiers": r"^\s*-\s*(.+?)$" } - all_reasoning.extend(reasoning_blocks) - for reasoning in reasoning_blocks: - # Extract candidate identifiers using regex - candidate_pattern = patterns["candidate_identifiers"] - candidate_matches = re.findall(candidate_pattern, reasoning, re.MULTILINE) - # print(f"{candidate_matches}=") - - for match in candidate_matches: - ident = match.strip() - if ident := self.get_valid_identifier(autocomplete, ident): - # print(f"{ident=}") - candidate_pool.add(ident) - # print("exit here") - + if reasoning_blocks is not None: + all_reasoning.extend(reasoning_blocks) + for reasoning in reasoning_blocks: + # Extract candidate identifiers using regex + candidate_pattern = patterns["candidate_identifiers"] + candidate_matches = re.findall(candidate_pattern, reasoning, re.MULTILINE) + # print(f"{candidate_matches}=") + + for match in candidate_matches: + ident = match.strip() + if ident := self.get_valid_identifier(autocomplete, ident): + candidate_pool.add(ident) # Check if we need to expand more if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper() or matches.issubset(candidate_pool): enough_identifiers = True From 6fe6635ff6a71ebbb21d1f58c56cc465db9949e0 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 21:40:10 +0000 Subject: [PATCH 107/138] refactor(core): remove emoji icons from codebase tree display and use trailing slash for directories --- codetide/core/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/core/models.py b/codetide/core/models.py index 43e817f..f2cb23f 100644 --- a/codetide/core/models.py +++ b/codetide/core/models.py @@ -947,9 +947,9 @@ def sort_key(x): display_name = name if include_types: if data.get("_type") == "file": - display_name = f"📄 {name}" + display_name = f"{name}" else: - display_name = f"📁 {name}" + display_name = f"{name}/" lines.append(f"{prefix}{current_prefix}{display_name}") From 455c3ff8013ef24e66e63f9fa367a8a4db9001de Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 21:40:59 +0000 Subject: [PATCH 108/138] refactor(ui): update history management to conditionally sync with agent based on input flag --- codetide/agents/tide/ui/agent_tide_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/agent_tide_ui.py b/codetide/agents/tide/ui/agent_tide_ui.py index 2b9945d..7ebf2b3 100644 --- a/codetide/agents/tide/ui/agent_tide_ui.py +++ b/codetide/agents/tide/ui/agent_tide_ui.py @@ -81,11 +81,11 @@ def increment_step(self)->bool: self.agent_tide.steps = None return True - async def add_to_history(self, message): + async def add_to_history(self, message, is_input :bool=False): self.history.append(message) if not self.agent_tide: await self.load() - else: + if is_input: self.agent_tide.history.append(message) def settings(self): From 331280aaa6f4b24f2fdfa5caa9a5bb5d9e0350af Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 21:41:41 +0000 Subject: [PATCH 109/138] refactor(ui): update llm validation logic and enhance history tracking with input flag --- codetide/agents/tide/ui/app.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 447e955..df23bd1 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -73,8 +73,11 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): exception = True while exception: try: - agent_tide_ui.agent_tide.llm.provider.validate_config(force_check_against_provider=True) - exception = None + if hasattr(agent_tide_ui.agent_tide.llm.provider.config, "access_token"): + exception = None + else: + agent_tide_ui.agent_tide.llm.provider.validate_config(force_check_against_provider=True) + exception = None except (AuthenticationError, ModelError) as e: exception = e @@ -469,7 +472,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option message.content = "\n\n---\n\n".join([command_prompt, message.content]) chat_history.append({"role": "user", "content": message.content}) - await agent_tide_ui.add_to_history(message.content) + await agent_tide_ui.add_to_history(message.content, is_input=True) reasoning_element = cl.CustomElement(name="ReasoningExplorer", props={ "reasoning_steps": [], From ef91647747d619164bc0d469325166f9c9c5d195 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 22:04:54 +0000 Subject: [PATCH 110/138] fix(agent): resolve conditional checks, initialize history count, and add action_id tracking --- codetide/agents/tide/agent.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 1d8ee1c..00a6ff2 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -189,7 +189,7 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl REPO_TREE=sub_tree ) ] - if previous_phase_1_response is not None: + if previous_phase_1_response: prefix_prompt.insert(0, previous_phase_1_response) # Phase 1 LLM call @@ -303,6 +303,9 @@ async def expand_history_if_needed( current_history_count = history_count max_iterations = 10 # Prevent infinite loops iteration = 0 + + if not current_history_count: + current_history_count += 1 # If context is already sufficient, return early if sufficient_context: @@ -328,7 +331,8 @@ async def expand_history_if_needed( CURRENT_WINDOW=str(current_window), LATEST_REQUEST=str(latest_request) ), - stream=False + stream=False, + action_id=f"expand_history.iteration_{iteration}" ) # Extract HISTORY_SUFFICIENT @@ -402,7 +406,8 @@ async def extract_operation_mode( INTERACTION_COUNT=len(self.history), CODE_IDENTIFIERS=cached_identifiers ), - stream=False + stream=False, + action_id="extract_operation_mode" ) response_text = response.strip() @@ -542,13 +547,14 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): codeContext ] elif codeContext: - codeContext = [codeContext] + prefil_context = [codeContext] ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( expanded_history, system_prompt=system_prompt, - prefix_prompt=prefil_context + prefix_prompt=prefil_context, + action_id="agent_loop.main" ) await trim_to_patch_section(self.patch_path) From 766e0253aaa8cd4ca353eed5c6b4b301f5facaf6 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 22:49:34 +0000 Subject: [PATCH 111/138] feat(autocomplete): add async word extraction and lazy sorting optimization This commit introduces async_extract_words_from_text method with non-blocking chunked processing, adds lazy sorting with sort/async_sort methods, and optimizes performance for large word lists --- codetide/autocomplete.py | 273 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 272 insertions(+), 1 deletion(-) diff --git a/codetide/autocomplete.py b/codetide/autocomplete.py index a695327..e1b6351 100644 --- a/codetide/autocomplete.py +++ b/codetide/autocomplete.py @@ -1,5 +1,6 @@ from typing import List import difflib +import asyncio import os import re @@ -7,8 +8,20 @@ class AutoComplete: def __init__(self, word_list: List[str]) -> None: """Initialize with a list of strings to search from""" self.words = word_list + self._sorted = False # Sort words for better organization (optional) - self.words.sort() + + def sort(self): + if not self._sorted: + self._sorted = True + self.words.sort() + + async def async_sort(self): + if not self._sorted: + self._sorted = True + loop = asyncio.get_running_loop() + # Offload sorting to a background thread + self.words = await loop.run_in_executor(None, sorted, self.words) def get_suggestions(self, prefix: str, max_suggestions: int = 10, case_sensitive: bool = False) -> List[str]: """ @@ -206,6 +219,8 @@ def extract_words_from_text( 'substring_matches': [], 'all_found_words': [] } + + self.sort() # Extract words from text - handle dotted identifiers if preserve_dotted_identifiers: @@ -452,3 +467,259 @@ def is_valid_substring(longer_str, shorter_str): 'all_found_words': sorted(list(all_found_words)) } + async def async_extract_words_from_text( + self, + text: str, + similarity_threshold: float = 0.6, + case_sensitive: bool = False, + max_matches_per_word: int = None, + preserve_dotted_identifiers: bool = True + ) -> dict: + """ + Async non-blocking version of extract_words_from_text. + Extract words from the word list that are present in the given text, including similar words (potential typos) + and substring/subpath matches. + Optionally limit the number of matches returned per word found in the text. + + Args: + text (str): The input text to analyze + similarity_threshold (float): Minimum similarity score for fuzzy matching (0.0 to 1.0) + case_sensitive (bool): Whether matching should be case sensitive + max_matches_per_word (int, optional): Maximum number of matches to return per word in the text. + If None, all matches are returned. If 1, only the top match per word is returned. + preserve_dotted_identifiers (bool): If True, treats dot-separated strings as single tokens + (e.g., "module.submodule.function" stays as one word) + + Returns: + dict: Dictionary containing: + - 'exact_matches': List of words found exactly in the text + - 'fuzzy_matches': List of tuples (word_from_list, similar_word_in_text, similarity_score) + - 'substring_matches': List of tuples (word_from_list, matched_text_word, match_type) + - 'all_found_words': Combined list of all matched words from the word list + """ + if not text: + return { + 'exact_matches': [], + 'fuzzy_matches': [], + 'substring_matches': [], + 'all_found_words': [] + } + + await self.async_sort() + + if preserve_dotted_identifiers: + text_words = re.findall(r'\b[\w./]+\b', text) + else: + text_words = re.findall(r'\b\w+\b', text) + + if not text_words: + return { + 'exact_matches': [], + 'fuzzy_matches': [], + 'substring_matches': [], + 'all_found_words': [] + } + + exact_matches = [] + fuzzy_candidates = [] + substring_matches = [] + all_found_words = set() + matched_text_words = set() + + if case_sensitive: + text_words_set = set(text_words) + text_words_search = text_words + else: + text_words_set = set(word.lower() for word in text_words) + text_words_search = [word.lower() for word in text_words] + + chunk_size = max(1, len(self.words) // 100) + for i in range(0, len(self.words), chunk_size): + chunk = self.words[i:i + chunk_size] + + for word_from_list in chunk: + if word_from_list in all_found_words: + continue + + search_word = word_from_list if case_sensitive else word_from_list.lower() + + if search_word in text_words_set: + exact_matches.append(word_from_list) + all_found_words.add(word_from_list) + for tw in text_words: + tw_search = tw if case_sensitive else tw.lower() + if tw_search == search_word: + matched_text_words.add(tw) + + await asyncio.sleep(0) + + remaining_words = [word for word in self.words if word not in all_found_words] + + def is_valid_path_substring(longer_path, shorter_path): + if not ('/' in longer_path and '/' in shorter_path): + return False + + if len(shorter_path) < 3: + return False + + longer_parts = longer_path.split('/') + shorter_parts = shorter_path.split('/') + + if any(len(part) <= 1 for part in shorter_parts): + return False + + if len(shorter_parts) > len(longer_parts): + return False + + for start_idx in range(len(longer_parts) - len(shorter_parts) + 1): + if longer_parts[start_idx:start_idx + len(shorter_parts)] == shorter_parts: + return True + return False + + def is_valid_substring(longer_str, shorter_str): + if len(shorter_str) < 4: + return False + if len(shorter_str) / len(longer_str) < 0.3: + return False + return shorter_str in longer_str + + substring_candidates = [] + + chunk_size = max(1, len(remaining_words) // 100) + for i in range(0, len(remaining_words), chunk_size): + chunk = remaining_words[i:i + chunk_size] + + for word_from_list in chunk: + search_word = word_from_list if case_sensitive else word_from_list.lower() + + for idx, text_word in enumerate(text_words_search): + original_text_word = text_words[idx] + + if original_text_word in matched_text_words: + continue + + if len(text_word) <= 2: + continue + + if text_word in search_word and text_word != search_word: + if '/' in search_word and '/' in text_word: + if is_valid_path_substring(search_word, text_word): + score = len(text_word) / len(search_word) + substring_candidates.append((word_from_list, original_text_word, 'subpath', score)) + elif is_valid_substring(search_word, text_word): + score = len(text_word) / len(search_word) + substring_candidates.append((word_from_list, original_text_word, 'substring', score)) + + elif search_word in text_word and search_word != text_word: + if '/' in search_word and '/' in text_word: + if is_valid_path_substring(text_word, search_word): + score = len(search_word) / len(text_word) + substring_candidates.append((word_from_list, original_text_word, 'reverse_subpath', score)) + elif is_valid_substring(text_word, search_word): + score = len(search_word) / len(text_word) + substring_candidates.append((word_from_list, original_text_word, 'reverse_substring', score)) + + await asyncio.sleep(0) + + substring_candidates.sort(key=lambda x: x[3], reverse=True) + + for word_from_list, original_text_word, match_type, score in substring_candidates: + if original_text_word not in matched_text_words and word_from_list not in all_found_words: + substring_matches.append((word_from_list, original_text_word, match_type)) + all_found_words.add(word_from_list) + matched_text_words.add(original_text_word) + + remaining_words = [word for word in self.words if word not in all_found_words] + + chunk_size = max(1, len(remaining_words) // 100) + for i in range(0, len(remaining_words), chunk_size): + chunk = remaining_words[i:i + chunk_size] + + for word_from_list in chunk: + search_word = word_from_list if case_sensitive else word_from_list.lower() + + for idx, text_word in enumerate(text_words_search): + original_text_word = text_words[idx] + + if original_text_word in matched_text_words: + continue + + similarity = difflib.SequenceMatcher(None, search_word, text_word).ratio() + if similarity >= similarity_threshold: + original_text_word = text_words[idx] if case_sensitive else next( + (orig for orig in text_words if orig.lower() == text_word), text_word + ) + fuzzy_candidates.append((word_from_list, original_text_word, similarity)) + + await asyncio.sleep(0) + + best_fuzzy_matches = {} + used_text_words = set() + + fuzzy_candidates.sort(key=lambda x: x[2], reverse=True) + + for word_from_list, text_word, score in fuzzy_candidates: + if (word_from_list not in best_fuzzy_matches and + text_word not in used_text_words and + text_word not in matched_text_words): + best_fuzzy_matches[word_from_list] = (word_from_list, text_word, score) + used_text_words.add(text_word) + + fuzzy_matches = list(best_fuzzy_matches.values()) + fuzzy_matches.sort(key=lambda x: x[2], reverse=True) + + for word_from_list, _, _ in fuzzy_matches: + all_found_words.add(word_from_list) + + if max_matches_per_word is not None: + final_exact_matches = [] + final_substring_matches = [] + final_fuzzy_matches = [] + final_all_found_words = set() + + all_matched_words = set(exact_matches) | set(word for word, _, _ in substring_matches) | set(word for word, _, _ in fuzzy_matches) + + for word_from_list in all_matched_words: + word_matches = [] + + if word_from_list in exact_matches: + word_matches.append((word_from_list, 'exact', 1.0, 0)) + + for w, text_word, match_type in substring_matches: + if w == word_from_list: + score = 0.9 if match_type in ['subpath', 'substring'] else 0.85 + word_matches.append((w, 'substring', score, 1, text_word, match_type)) + + for w, text_word, score in fuzzy_matches: + if w == word_from_list: + word_matches.append((w, 'fuzzy', score, 2, text_word)) + + word_matches.sort(key=lambda x: (x[3], -x[2])) + + top_word_matches = word_matches[:max_matches_per_word] + + for match in top_word_matches: + final_all_found_words.add(match[0]) + + if match[1] == 'exact': + final_exact_matches.append(match[0]) + elif match[1] == 'substring': + final_substring_matches.append((match[0], match[4], match[5])) + elif match[1] == 'fuzzy': + final_fuzzy_matches.append((match[0], match[4], match[2])) + + exact_matches = final_exact_matches + substring_matches = final_substring_matches + fuzzy_matches = final_fuzzy_matches + all_found_words = final_all_found_words + + exact_matches.sort() + substring_matches.sort(key=lambda x: x[0]) + fuzzy_matches.sort(key=lambda x: x[2], reverse=True) + + return { + 'exact_matches': exact_matches, + 'fuzzy_matches': fuzzy_matches, + 'substring_matches': substring_matches, + 'all_found_words': sorted(list(all_found_words)) + } From 461b380b60921bcaaa42a57e6215a865c680dae1 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 22:50:25 +0000 Subject: [PATCH 112/138] refactor(agents/tide): optimize identifier resolution with direct match validation and async autocomplete extraction --- codetide/agents/tide/agent.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 00a6ff2..0fd2d7e 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -141,16 +141,14 @@ def _clean_history(self): if isinstance(message, dict): self.history[i] = message.get("content" ,"") - async def get_identifiers_two_phase(self, search_query :Optional[str], autocomplete :AutoComplete, expanded_history :list, codeIdentifiers=None, TODAY :str=None): + async def get_identifiers_two_phase(self, search_query :Optional[str], direct_matches :List[str], autocomplete :AutoComplete, expanded_history :list, codeIdentifiers=None, TODAY :str=None): """ Two-phase identifier resolution: Phase 1: Gather candidates through iterative tree expansion Phase 2: Classify and finalize identifiers with operation mode """ # Initialize tracking - last_message = self.history[-1] if self.history else "" - matches = set(autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"]) - # print(f"{matches=}") + matches = set(direct_matches) ### TODO replace matches with search based on received search query ### get identifiers @@ -171,7 +169,11 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], autocompl iteration_count += 1 serch_results = await self._smart_code_search.search_smart(search_query, use_variations=False, top_k=15) identifiers_from_search = {result[0] for result in serch_results} - # print(f"{identifiers_from_search=}") + + if matches.issubset(identifiers_from_search): + candidate_pool = matches + print("All matches found in indeintiferis from search") + break candidates_to_filter_tree = self.tide._as_file_paths(list(identifiers_from_search)) # print("got identifiers") @@ -471,20 +473,20 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): await self.llm.logger_fn(REASONING_FINISHED) else: cached_identifiers = self._last_code_identifers - print("Finished check for updates") - self._clean_history() - print("Finished clean history") if codeIdentifiers: for identifier in codeIdentifiers: cached_identifiers.add(identifier) - + + autocomplete = AutoComplete(self.tide.cached_ids) tasks = [ self.extract_operation_mode(cached_identifiers), + autocomplete.async_extract_words_from_text(self.history[-1] if self.history else "", max_matches_per_word=1), self.prepare_loop() ] - operation_context_history_task, _ = await asyncio.gather(*tasks) + operation_context_history_task, autocomplete_matches, _ = await asyncio.gather(*tasks) operation_mode, sufficient_context, expanded_history, search_query = operation_context_history_task + direct_matches = autocomplete_matches["all_found_words"] print(f"{search_query=}") autocomplete = AutoComplete(self.tide.cached_ids) @@ -492,7 +494,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ### TODO super quick prompt here for operation mode ### needs more context based on cached identifiers or not ### needs more history or not, default is last 5 iteratinos - if sufficient_context: + if sufficient_context or set(direct_matches).issubset(cached_identifiers): codeIdentifiers = list(self._last_code_identifers) await self.llm.logger_fn(REASONING_FINISHED) @@ -512,7 +514,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history else: await self.llm.logger_fn(REASONING_STARTED) - reasoning_output = await self.get_identifiers_two_phase(search_query, autocomplete, expanded_history, codeIdentifiers, TODAY) + reasoning_output = await self.get_identifiers_two_phase(search_query, direct_matches, autocomplete, expanded_history, codeIdentifiers, TODAY) await self.llm.logger_fn(REASONING_FINISHED) print(json.dumps(reasoning_output, indent=4)) From 0843a4cd3a8610226839e796d104a4eead17d4a2 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 22:51:48 +0000 Subject: [PATCH 113/138] refactor(agent): remove unused AutoComplete instantiation in search query handling --- codetide/agents/tide/agent.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 0fd2d7e..50c2e88 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -489,8 +489,6 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): direct_matches = autocomplete_matches["all_found_words"] print(f"{search_query=}") - autocomplete = AutoComplete(self.tide.cached_ids) - ### TODO super quick prompt here for operation mode ### needs more context based on cached identifiers or not ### needs more history or not, default is last 5 iteratinos From 7b377af047989675299cbb9f7e44a7f0d254fd11 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Mon, 27 Oct 2025 22:59:57 +0000 Subject: [PATCH 114/138] fix(ui): prevent expansion interaction when reasoning steps are empty in ReasoningExplorer --- .../ui/public/elements/ReasoningExplorer.jsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 716a31f..410be63 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -7,6 +7,7 @@ export default function ReasoningStepsCard() { const [expanded, setExpanded] = useState(false); const [waveOffset, setWaveOffset] = useState(0); const [loadingText, setLoadingText] = useState("Analyzing"); + const canExpand = props.reasoning_steps?.length > 0; const loadingStates = [ "Diving deep into the code", @@ -56,8 +57,8 @@ export default function ReasoningStepsCard() {
From 93a13a40e69ce6e1dc3020260d273a53b9daf1a6 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 28 Oct 2025 22:45:10 +0000 Subject: [PATCH 115/138] style(agent,prompts): fix indentation and refactor system prompt descriptions --- codetide/agents/tide/agent.py | 4 ++-- codetide/agents/tide/prompts.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 50c2e88..7694e11 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -553,8 +553,8 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): response = await self.llm.acomplete( expanded_history, system_prompt=system_prompt, - prefix_prompt=prefil_context, - action_id="agent_loop.main" + prefix_prompt=prefil_context, + action_id="agent_loop.main" ) await trim_to_patch_section(self.patch_path) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index c7e6d04..45e134c 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -70,7 +70,7 @@ """ WRITE_PATCH_SYSTEM_PROMPT = """ -You are Agent **Tide**, operating in Patch Generation Mode. +You are operating in Patch Generation Mode. Your mission is to generate atomic, high-precision, diff-style patches that exactly satisfy the user’s request while adhering to the STRICT PATCH PROTOCOL. --- @@ -237,7 +237,7 @@ """ STEPS_SYSTEM_PROMPT = """ -You are Agent **Tide**, operating in a multi-step planning and execution mode. +You are operating in a multi-step planning and execution mode. Your job is to take a user request, analyze any provided code context (including repository structure / repo_tree identifiers), and decompose the work into the minimal set of concrete implementation steps needed to fully satisfy the request. If the requirement is simple, output a single step; if it’s complex, decompose it into multiple ordered steps. You must build upon, refine, or correct any existing code context rather than ignoring it. @@ -296,16 +296,22 @@ If you do not have all the information you need, or if any part of the request is unclear, you must pause and explicitly request the necessary context or clarification from the user before taking any action. Never make assumptions or proceed with incomplete information. Your priority is to ensure that every action is based on clear, explicit, and sufficient instructions. + +You must always produce a valid response, empty responses are not acceptable! """ PREFIX_SUMMARY_PROMPT = """ -You will receive a brief summary before the code context. This summary provides additional context to guide your final answer. +You will receive a brief quickstart summary before the code context. +This summary is only a high-level guide to help you quickly understand the user's intent and the overall task scope. -Use this summary to better understand the user's intent and the overall task scope. -Incorporate the information from the summary along with the code context to produce a precise, complete, and high-quality response. +Use the summary together with the code context to produce a precise, complete, and high-quality answer. -Always prioritize the summary as a high-level guide and use it to clarify ambiguous or incomplete code context: +Important: +- You must always provide a meaningful and complete response to the user's message. +- Empty, generic, or evasive responses are not valid. +- Treat the summary as orientation only; rely on the code context for specific details. +Quickstart Summary: {SUMMARY} """ From ac3fd08179ee4c111404c89194f53827ce247b10 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 28 Oct 2025 23:24:45 +0000 Subject: [PATCH 116/138] refactor(agent): restructure context handling in agent loop to separate prefill and code context --- codetide/agents/tide/agent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 7694e11..76bd319 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -541,17 +541,17 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): if operation_mode in self.OPERATIONS: system_prompt.insert(1, self.OPERATIONS.get(operation_mode)) + prefil_context = None if prefilled_summary is not None: prefil_context = [ - PREFIX_SUMMARY_PROMPT.format(SUMMARY=prefilled_summary), - codeContext + PREFIX_SUMMARY_PROMPT.format(SUMMARY=prefilled_summary) ] - elif codeContext: - prefil_context = [codeContext] + # elif codeContext: + # prefil_context = [codeContext] ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( - expanded_history, + expanded_history + [codeContext] if codeContext else expanded_history, system_prompt=system_prompt, prefix_prompt=prefil_context, action_id="agent_loop.main" From e131c719adca5e26d3a469a5a53e1a16316a1c77 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Wed, 29 Oct 2025 22:44:04 +0000 Subject: [PATCH 117/138] feat(autocomplete): add timeout parameter to async word extraction to prevent blocking on large word lists --- codetide/agents/tide/agent.py | 6 +++++- codetide/autocomplete.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 76bd319..495f590 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -480,7 +480,11 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): autocomplete = AutoComplete(self.tide.cached_ids) tasks = [ self.extract_operation_mode(cached_identifiers), - autocomplete.async_extract_words_from_text(self.history[-1] if self.history else "", max_matches_per_word=1), + autocomplete.async_extract_words_from_text( + self.history[-1] if self.history else "", + max_matches_per_word=1, + timeout=30 + ), self.prepare_loop() ] operation_context_history_task, autocomplete_matches, _ = await asyncio.gather(*tasks) diff --git a/codetide/autocomplete.py b/codetide/autocomplete.py index e1b6351..d332467 100644 --- a/codetide/autocomplete.py +++ b/codetide/autocomplete.py @@ -3,6 +3,7 @@ import asyncio import os import re +import time class AutoComplete: def __init__(self, word_list: List[str]) -> None: @@ -473,7 +474,8 @@ async def async_extract_words_from_text( similarity_threshold: float = 0.6, case_sensitive: bool = False, max_matches_per_word: int = None, - preserve_dotted_identifiers: bool = True + preserve_dotted_identifiers: bool = True, + timeout: float = None ) -> dict: """ Async non-blocking version of extract_words_from_text. @@ -489,6 +491,8 @@ async def async_extract_words_from_text( If None, all matches are returned. If 1, only the top match per word is returned. preserve_dotted_identifiers (bool): If True, treats dot-separated strings as single tokens (e.g., "module.submodule.function" stays as one word) + timeout (float, optional): Maximum time in seconds to spend searching for matches. + If None, no timeout is applied. If exceeded, returns matches found so far. Returns: dict: Dictionary containing: @@ -504,6 +508,8 @@ async def async_extract_words_from_text( 'substring_matches': [], 'all_found_words': [] } + + start_time = time.time() if timeout is not None else None await self.async_sort() @@ -535,6 +541,9 @@ async def async_extract_words_from_text( chunk_size = max(1, len(self.words) // 100) for i in range(0, len(self.words), chunk_size): + if start_time is not None and (time.time() - start_time) >= timeout: + break + chunk = self.words[i:i + chunk_size] for word_from_list in chunk: @@ -587,6 +596,12 @@ def is_valid_substring(longer_str, shorter_str): chunk_size = max(1, len(remaining_words) // 100) for i in range(0, len(remaining_words), chunk_size): + if start_time is not None and (time.time() - start_time) >= timeout: + break + + if start_time is not None and (time.time() - start_time) >= timeout: + break + chunk = remaining_words[i:i + chunk_size] for word_from_list in chunk: From 8a10231f90d47ec71e5b17605b8b10d8a6b385d3 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 6 Nov 2025 22:19:40 +0000 Subject: [PATCH 118/138] refactor(core): fix indentation in codebase tree building logic --- codetide/core/models.py | 82 ++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/codetide/core/models.py b/codetide/core/models.py index f2cb23f..b6e4f37 100644 --- a/codetide/core/models.py +++ b/codetide/core/models.py @@ -811,51 +811,51 @@ def _build_tree_dict(self, filter_paths: list = None, slim: bool = False): if file_dir in all_relevant_directories: sibling_files.append(code_file) - - # Build tree structure from relevant files (with full content) - for code_file in relevant_files: - if not code_file.file_path: - continue - - # Split the file path into parts - path_parts = code_file.file_path.replace("\\", "/").split("/") + + # Build tree structure from relevant files (with full content) + for code_file in relevant_files: + if not code_file.file_path: + continue - # Navigate/create the nested dictionary structure - current_level = tree - for i, part in enumerate(path_parts): - if i == len(path_parts) - 1: # This is the file - current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} - else: # This is a directory - if part not in current_level: - current_level[part] = {"_type": "directory"} - current_level = current_level[part] + # Split the file path into parts + path_parts = code_file.file_path.replace("\\", "/").split("/") - # Add sibling files and directory contents (show content for all when filtering for broader context) - for code_file in sibling_files: - if not code_file.file_path: - continue - - # Split the file path into parts - path_parts = code_file.file_path.replace("\\", "/").split("/") + # Navigate/create the nested dictionary structure + current_level = tree + for i, part in enumerate(path_parts): + if i == len(path_parts) - 1: # This is the file + current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} + else: # This is a directory + if part not in current_level: + current_level[part] = {"_type": "directory"} + current_level = current_level[part] + + # Add sibling files and directory contents (show content for all when filtering for broader context) + for code_file in sibling_files: + if not code_file.file_path: + continue - # Navigate/create the nested dictionary structure - current_level = tree - for i, part in enumerate(path_parts): - if i == len(path_parts) - 1: # This is the file - # Check if file already exists (might have been added as relevant_files) - if part not in current_level: - # Show content for all files to provide broader context - current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} - else: # This is a directory - if part not in current_level: - current_level[part] = {"_type": "directory"} - current_level = current_level[part] + # Split the file path into parts + path_parts = code_file.file_path.replace("\\", "/").split("/") - # Add placeholder for omitted content when filtering is applied and not in slim mode - if filter_paths is not None and not slim: - tree = self._add_omitted_placeholders(tree, filter_paths) - - self._tree_dict = tree + # Navigate/create the nested dictionary structure + current_level = tree + for i, part in enumerate(path_parts): + if i == len(path_parts) - 1: # This is the file + # Check if file already exists (might have been added as relevant_files) + if part not in current_level: + # Show content for all files to provide broader context + current_level[part] = {"_type": "file", "_data": code_file, "_show_content": True} + else: # This is a directory + if part not in current_level: + current_level[part] = {"_type": "directory"} + current_level = current_level[part] + + # Add placeholder for omitted content when filtering is applied and not in slim mode + if filter_paths is not None and not slim: + tree = self._add_omitted_placeholders(tree, filter_paths) + + self._tree_dict = tree def _add_omitted_placeholders(self, tree: dict, filter_paths: list) -> dict: """Adds '...' placeholders for directories that contain omitted files.""" From 1874affd4dfe9551dced56ccffc827f7d8f176b3 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 8 Nov 2025 23:37:34 +0000 Subject: [PATCH 119/138] fix(agent): enhance condition for context sufficiency check in reasoning logic --- codetide/agents/tide/agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 495f590..b81a57e 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -8,7 +8,7 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, DETERMINE_OPERATION_MODE_SYSTEM, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REASONING_TEMPLTAE, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, DETERMINE_OPERATION_MODE_SYSTEM, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section @@ -496,7 +496,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ### TODO super quick prompt here for operation mode ### needs more context based on cached identifiers or not ### needs more history or not, default is last 5 iteratinos - if sufficient_context or set(direct_matches).issubset(cached_identifiers): + if sufficient_context or (direct_matches and set(direct_matches).issubset(cached_identifiers)): codeIdentifiers = list(self._last_code_identifers) await self.llm.logger_fn(REASONING_FINISHED) From dd0f87b74a0c9094649206e0e5ff9fe21e2cbc78 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 9 Nov 2025 14:11:41 +0000 Subject: [PATCH 120/138] feat(codetide): add filenames_mapped property for filename-to-path mapping --- codetide/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/codetide/__init__.py b/codetide/__init__.py index dfa1277..e1f9f5a 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -98,6 +98,13 @@ def relative_filepaths(self)->List[str]: str(filepath.relative_to(self.rootpath)).replace("\\", "/") for filepath in self.files ] + @property + def filenames_mapped(self)->Dict[str, str]: + return { + filepath.name: str(filepath.relative_to(self.rootpath)).replace("\\", "/") + for filepath in self.files + } + @property def cached_ids(self)->List[str]: return self.codebase.non_import_unique_ids+self.relative_filepaths From ba2b504352d32f9e01bf0ddd7e4dee8363cafa52 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 9 Nov 2025 14:12:18 +0000 Subject: [PATCH 121/138] feat(autocomplete): add mapped words support for filename resolution This commit introduces mapped_words parameter to AutoComplete class enabling filename mapping during exact match extraction in both sync and async methods --- codetide/agents/tide/agent.py | 4 ++-- codetide/autocomplete.py | 26 ++++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index b81a57e..8c646a7 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -477,7 +477,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): for identifier in codeIdentifiers: cached_identifiers.add(identifier) - autocomplete = AutoComplete(self.tide.cached_ids) + autocomplete = AutoComplete(self.tide.cached_ids, mapped_words=self.tide.filenames_mapped) tasks = [ self.extract_operation_mode(cached_identifiers), autocomplete.async_extract_words_from_text( @@ -504,7 +504,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self.contextIdentifiers = None # Only extract matches from the last message last_message = self.history[-1] if self.history else "" - exact_matches = autocomplete.extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] + exact_matches = await autocomplete.async_extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] self.modifyIdentifiers = self.tide._as_file_paths(exact_matches) codeIdentifiers = self.modifyIdentifiers self._direct_mode = False diff --git a/codetide/autocomplete.py b/codetide/autocomplete.py index d332467..ee4aa3d 100644 --- a/codetide/autocomplete.py +++ b/codetide/autocomplete.py @@ -1,16 +1,16 @@ -from typing import List +from typing import Dict, List, Optional import difflib import asyncio +import time import os import re -import time class AutoComplete: - def __init__(self, word_list: List[str]) -> None: + def __init__(self, word_list: List[str], mapped_words: Optional[Dict[str, str]]=None) -> None: """Initialize with a list of strings to search from""" self.words = word_list self._sorted = False - # Sort words for better organization (optional) + self.mapped_words = mapped_words def sort(self): if not self._sorted: @@ -255,13 +255,18 @@ def extract_words_from_text( text_words_search = [word.lower() for word in text_words] # Find exact matches first - for word_from_list in self.words: + exact_matche_search_space = self.words + (list(self.mapped_words.keys()) or []) + print(f"{exact_matche_search_space=}") + for word_from_list in exact_matche_search_space: + print(f"{word_from_list=}") if word_from_list in all_found_words: continue search_word = word_from_list if case_sensitive else word_from_list.lower() if search_word in text_words_set: + if self.mapped_words is not None and word_from_list in self.mapped_words: + word_from_list = self.mapped_words.get(word_from_list) exact_matches.append(word_from_list) all_found_words.add(word_from_list) # Mark all instances of this text word as matched @@ -539,12 +544,14 @@ async def async_extract_words_from_text( text_words_set = set(word.lower() for word in text_words) text_words_search = [word.lower() for word in text_words] - chunk_size = max(1, len(self.words) // 100) - for i in range(0, len(self.words), chunk_size): + exact_matche_search_space = self.words + (list(self.mapped_words.keys()) or []) + + chunk_size = max(1, len(exact_matche_search_space) // 100) + for i in range(0, len(exact_matche_search_space), chunk_size): if start_time is not None and (time.time() - start_time) >= timeout: break - chunk = self.words[i:i + chunk_size] + chunk = exact_matche_search_space[i:i + chunk_size] for word_from_list in chunk: if word_from_list in all_found_words: @@ -553,6 +560,9 @@ async def async_extract_words_from_text( search_word = word_from_list if case_sensitive else word_from_list.lower() if search_word in text_words_set: + if self.mapped_words is not None and word_from_list in self.mapped_words: + word_from_list = self.mapped_words.get(word_from_list) + exact_matches.append(word_from_list) all_found_words.add(word_from_list) for tw in text_words: From 58417ff243dc144cfc80af46ed3485c783f9157d Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 9 Nov 2025 14:40:31 +0000 Subject: [PATCH 122/138] fix(patch_code): comment out isolated change normalization logic to test patch application without final check that breaks isolated additions --- codetide/mcp/tools/patch_code/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codetide/mcp/tools/patch_code/__init__.py b/codetide/mcp/tools/patch_code/__init__.py index f7e66a6..fdedf7d 100644 --- a/codetide/mcp/tools/patch_code/__init__.py +++ b/codetide/mcp/tools/patch_code/__init__.py @@ -110,8 +110,9 @@ def text_to_patch(text: str, orig: Dict[str, str], rootpath: Optional[pathlib.Pa elif (line.startswith("---") and len(line) == 3) or not line.startswith(("+", "-", " ")): lines[i] = f" {line}" - elif line.startswith(("+", "-")) and 1 < i + 1 < len(lines) and lines[i+1].startswith(" ") and not lines[i-1].startswith(("+", "-")) and lines[i+1].strip(): - lines[i] = f" {line}" + ### TODO test wuthout final check that breaks for isolated addtions + # elif line.startswith(("+", "-")) and 1 < i + 1 < len(lines) and lines[i+1].startswith(" ") and not lines[i-1].startswith(("+", "-")) and lines[i+1].strip(): + # lines[i] = f" {line}" # Debug output # writeFile("\n".join(lines), "lines_processed.txt") From 379d72579586059b086a35d8d0f8a72b47b03046 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 14 Nov 2025 22:27:44 +0000 Subject: [PATCH 123/138] feat(ui): add hidden prop and update default expanded state in ReasoningExplorer component --- .../agents/tide/ui/public/elements/ReasoningExplorer.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx index 410be63..e45da13 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningExplorer.jsx @@ -4,10 +4,14 @@ import { ChevronDown, ChevronRight, Brain } from "lucide-react"; import { useState, useEffect } from "react"; export default function ReasoningStepsCard() { - const [expanded, setExpanded] = useState(false); + const [expanded, setExpanded] = useState(props.expanded ?? false); const [waveOffset, setWaveOffset] = useState(0); const [loadingText, setLoadingText] = useState("Analyzing"); const canExpand = props.reasoning_steps?.length > 0; + + if (props.hidden) { + return
; + } const loadingStates = [ "Diving deep into the code", @@ -47,7 +51,7 @@ export default function ReasoningStepsCard() { if (summary) return summary.split("\n")[0]; if (reasoning_steps?.length > 0) return reasoning_steps.at(-1).content.split("\n")[0]; - if (props?.finished) return "Finished"; + if (props?.finished) return ""; return `${loadingText}...`; }; From f21bdacd13b061da12399ea4784b67e85f961e40 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 14 Nov 2025 22:28:08 +0000 Subject: [PATCH 124/138] refactor(ui): remove unused example data and ticket functionality from agent UI --- codetide/agents/tide/ui/app.py | 140 ++------------------------------- 1 file changed, 5 insertions(+), 135 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index df23bd1..c71b631 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -129,122 +129,6 @@ async def validate_llm_config(agent_tide_ui: AgentTideUi): except Exception as e: exception = e -# Example 1: Partial data with reasoning steps, context and modify identifiers, not finished -example1 = { - "reasoning_steps": [ - { - "header": "Initial Analysis", - "content": "I'm examining the problem statement to understand the requirements and constraints.", - "candidate_identifiers": ["problem_id", "requirements", "constraints"] - }, - { - "header": "Solution Approach", - "content": "Based on the analysis, I'll implement a solution using a divide-and-conquer strategy.", - "candidate_identifiers": ["algorithm", "divide_conquer", "implementation"] - } - ], - "context_identifiers": ["user_context", "system_requirements", "api_documentation"], - "modify_identifiers": ["configuration_settings", "user_preferences"], - "summary": "", - "finished": False -} - -""" -*** Begin Reasoning -**first task header** -**content**: brief summary of the logic behind this task and the files to look into and why -**candidate_identifiers**: - - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to use as context -*** End Reasoning -*** Begin Reasoning -**first task header** -**content**: brief summary of the logic behind this task and the files to look into and why -**candidate_identifiers**: - - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step might need to modify or update -*** End Reasoning -""" -### use current expansion logic here then move to the next one once all possible candidate_identifiers have been found -### decide here together with expand_paths if we need to expand history i.e load older messages - -""" -*** Begin Summary -summary of the reasoning steps so far -*** End Summary - -*** Begin Context Identifiers - -*** End Context Identifiers - -*** Begin Modify Identifiers - -*** End Modify Identifiers - -""" - -# Example 2: Complete data with all fields populated and finished as true -example2 = { - "reasoning_steps": [ - { - "header": "Problem Identification", - "content": "The issue appears to be related to memory management in the application.", - "candidate_identifiers": ["memory_leak", "heap_overflow", "garbage_collection"] - }, - { - "header": "Root Cause Analysis", - "content": "After thorough investigation, I've identified that objects are not being properly deallocated.", - "candidate_identifiers": ["object_lifecycle", "destructor", "reference_counting"] - }, - { - "header": "Solution Implementation", - "content": "I'll implement a custom memory pool to better manage object allocation and deallocation.", - "candidate_identifiers": ["memory_pool", "allocation_strategy", "deallocation"] - } - ], - "context_identifiers": ["application_logs", "performance_metrics", "system_architecture"], - "modify_identifiers": ["memory_management_module", "allocation_policies"], - "summary": "This is the final reasoning summary", - "finished": True -} - -# Example 3: Only reasoning steps with no other data -example3 = { - "reasoning_steps": [ - { - "header": "Data Collection", - "content": "Gathering relevant data from various sources to build our dataset.", - "candidate_identifiers": ["data_sources", "extraction_methods", "validation"] - }, - { - "header": "Data Processing", - "content": "Cleaning and transforming the raw data to make it suitable for analysis.", - "candidate_identifiers": ["data_cleaning", "transformation", "normalization"] - }, - { - "header": "Model Training", - "content": "Training the machine learning model using the processed dataset.", - "candidate_identifiers": ["ml_algorithm", "hyperparameters", "training_split"] - }, - { - "header": "Model Evaluation", - "content": "Evaluating the model's performance using various metrics.", - "candidate_identifiers": ["accuracy", "precision", "recall", "f1_score"] - } - ], - "context_identifiers": [], - "modify_identifiers": [], - "finished": False -} - -async def get_ticket(): - """Pretending to fetch data from linear""" - return { - "title": "Fix Authentication Bug", - "status": "in-progress", - "assignee": "Sarah Chen", - "deadline": "2025-01-15", - "tags": ["security", "high-priority", "backend"] - } - @cl.on_chat_start async def start_chat(): # TODO think of fast way to initialize and get settings @@ -256,25 +140,6 @@ async def start_chat(): await cl.context.emitter.set_commands(AgentTideUi.commands) cl.user_session.set("chat_history", []) - # props = await get_ticket() - - # ticket_element = cl.CustomElement(name="LinearTicket", props=props) - # # Store the element if we want to update it server side at a later stage. - # cl.user_session.set("ticket_el", ticket_element) - - # await cl.Message(content="Here is the ticket information!", elements=[ticket_element]).send() - - # await asyncio.sleep(2) - # ticket_element.props["title"] = "Could not Fix Authentication Bug" - # await ticket_element.update() - - card_element = cl.CustomElement(name="ReasoningExplorer", props=example1) - await cl.Message(content="", elements=[card_element]).send() - - # await asyncio.sleep(2) - # card_element.props.update(example2) - # await card_element.update() - @cl.set_starters async def set_starters(): return [cl.Starter(**kwargs) for kwargs in STARTERS] @@ -481,6 +346,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "modify_identifiers": [], "finished": False, "thinkingTime": 0 + # "expanded": False }) if not agent_tide_ui.agent_tide._skip_context_retrieval: @@ -586,6 +452,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option stream_processor.global_fallback_msg = None stream_processor.buffer = "" stream_processor.accumulated_content = "" + # reasoning_element.props["expanded"] = True + await reasoning_element.update() continue elif chunk == REASONING_FINISHED: reasoning_end_time = time.time() @@ -600,6 +468,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option elif chunk == ROUND_FINISHED: # Handle any remaining content + # reasoning_element.props["expanded"] = False + # await reasoning_element.update() await stream_processor.finalize() await asyncio.sleep(0.5) await cancel_gen(loop) From ad39b5d8a98b5e77d14c4e9cfe19b18152550e88 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 14 Nov 2025 22:50:30 +0000 Subject: [PATCH 125/138] refactor(agents/tide): restructure agent architecture with modular components and two-phase identifier resolution --- codetide/agents/tide/agent.py | 1353 ++++++++++++++++++++------------- 1 file changed, 842 insertions(+), 511 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 8c646a7..776f5fb 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -8,9 +8,14 @@ from ...autocomplete import AutoComplete from .models import Steps from .prompts import ( - AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, DETERMINE_OPERATION_MODE_SYSTEM, FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, + AGENT_TIDE_SYSTEM_PROMPT, ASSESS_HISTORY_RELEVANCE_PROMPT, CALMNESS_SYSTEM_PROMPT, + CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_TRIGGER_PLANNING_STEPS, + CMD_WRITE_TESTS_PROMPT, DETERMINE_OPERATION_MODE_PROMPT, DETERMINE_OPERATION_MODE_SYSTEM, + FINALIZE_IDENTIFIERS_PROMPT, GATHER_CANDIDATES_PREFIX, GATHER_CANDIDATES_SYSTEM, + PREFIX_SUMMARY_PROMPT, README_CONTEXT_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, REPO_TREE_CONTEXT_PROMPT, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) +from .defaults import DEFAULT_MAX_HISTORY_TOKENS from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section from .consts import AGENT_TIDE_ASCII_ART, REASONING_FINISHED, REASONING_STARTED, ROUND_FINISHED @@ -27,7 +32,7 @@ from pydantic import BaseModel, Field, ConfigDict, model_validator from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit import PromptSession -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple from typing_extensions import Self from functools import partial from datetime import date @@ -37,215 +42,296 @@ import pygit2 import os +# ============================================================================ +# Constants +# ============================================================================ + FILE_TEMPLATE = """{FILENAME} {CONTENT} """ -class AgentTide(BaseModel): - llm :Llm - tide :CodeTide - history :Optional[list]=None - steps :Optional[Steps]=None - session_id :str=Field(default_factory=ulid) - changed_paths :List[str]=Field(default_factory=list) - request_human_confirmation :bool=False - - contextIdentifiers :Optional[List[str]]=None - modifyIdentifiers :Optional[List[str]]=None - reasoning :Optional[str]=None - - _skip_context_retrieval :bool=False - _last_code_identifers :Optional[Set[str]]=set() - _last_code_context :Optional[str] = None - _has_patch :bool=False - _direct_mode :bool=False - _smart_code_search :Optional[Any]=None - - # Number of previous interactions to remember for context identifiers - CONTEXT_WINDOW_SIZE: int = 3 - # Rolling window of identifier sets from previous N interactions - _context_identifier_window: Optional[list] = None - - model_config = ConfigDict(arbitrary_types_allowed=True) - - OPERATIONS :Dict[str, str] = { - "PLAN_STEPS": STEPS_SYSTEM_PROMPT, - "PATCH_CODE": WRITE_PATCH_SYSTEM_PROMPT - } - - @model_validator(mode="after") - def pass_custom_logger_fn(self)->Self: - self.llm.logger_fn = partial(custom_logger_fn, session_id=self.session_id, filepath=self.patch_path) - return self - - async def get_repo_tree_from_user_prompt(self, history :list, include_modules :bool=False, expand_paths :Optional[List[str]]=None)->str: - - history_str = "\n\n".join(history) - for CMD_PROMPT in [CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT, CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT]: - history_str.replace(CMD_PROMPT, "") - - self.tide.codebase._build_tree_dict(expand_paths) - - tree = self.tide.codebase.get_tree_view( - include_modules=include_modules, - include_types=True +# Default configuration values +DEFAULT_CONTEXT_WINDOW_SIZE = 3 +DEFAULT_MAX_EXPANSION_ITERATIONS = 10 +DEFAULT_MAX_CANDIDATE_ITERATIONS = 3 +DEFAULT_SEARCH_TOP_K = 15 + +# Operation modes +OPERATION_MODE_STANDARD = "STANDARD" +OPERATION_MODE_PLAN_STEPS = "PLAN_STEPS" +OPERATION_MODE_PATCH_CODE = "PATCH_CODE" + +# Commands to filter from history +COMMAND_PROMPTS = [ + CMD_TRIGGER_PLANNING_STEPS, + CMD_WRITE_TESTS_PROMPT, + CMD_BRAINSTORM_PROMPT, + CMD_CODE_REVIEW_PROMPT +] + + +# ============================================================================ +# Data Classes for Identifier Resolution +# ============================================================================ + +class IdentifierResolutionResult(BaseModel): + """Result of the two-phase identifier resolution process.""" + matches: List[str] + context_identifiers: List[str] + modify_identifiers: List[str] + summary: Optional[str] + all_reasoning: str + iteration_count: int + + +class OperationModeResult(BaseModel): + """Result of operation mode extraction.""" + operation_mode: str + sufficient_context: bool + expanded_history: list + search_query: Optional[str] + + +# ============================================================================ +# Helper Classes +# ============================================================================ + +class GitOperations: + """Handles Git-related operations.""" + + def __init__(self, repo: pygit2.Repository, rootpath: Path): + self.repo = repo + self.rootpath = rootpath + + def has_staged_changes(self) -> bool: + """Check if there are staged changes in the repository.""" + status = self.repo.status() + result = any([ + file_status == pygit2.GIT_STATUS_INDEX_MODIFIED + for file_status in status.values() + ]) + _logger.logger.debug(f"has_staged_changes result={result}") + return result + + async def get_staged_diff(self) -> str: + """Get the diff of staged changes.""" + if not Path(self.rootpath).is_dir(): + raise FileNotFoundError(f"Directory not found: {self.rootpath}") + + process = await asyncio.create_subprocess_exec( + 'git', 'diff', '--staged', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=self.rootpath ) - return tree - - def approve(self): - self._has_patch = False - if os.path.exists(self.patch_path): - changed_paths = process_patch(self.patch_path, open_file, write_file, remove_file, file_exists, root_path=self.tide.rootpath) - self.changed_paths.extend(changed_paths) - - previous_response = self.history[-1] - diffPatches = parse_patch_blocks(previous_response, multiple=True) - if diffPatches: - for patch in diffPatches: - # TODO this deletes previouspatches from history to make sure changes are always focused on the latest version of the file - previous_response = previous_response.replace(f"*** Begin Patch\n{patch}*** End Patch", "") - self.history[-1] = previous_response - - def reject(self, feedback :str): - self._has_patch = False - self.history.append(REJECT_PATCH_FEEDBACK_TEMPLATE.format( - FEEDBACK=feedback - )) - - @property - def patch_path(self)->Path: - if not os.path.exists(self.tide.rootpath / DEFAULT_STORAGE_PATH): - os.makedirs(self.tide.rootpath / DEFAULT_STORAGE_PATH, exist_ok=True) - return self.tide.rootpath / DEFAULT_STORAGE_PATH / f"{self.session_id}.bash" + stdout, stderr = await process.communicate() + + if process.returncode != 0: + raise Exception(f"Git command failed: {stderr.decode().strip()}") + + return stdout.decode() + + async def stage_files(self, changed_paths: List[str]) -> str: + """Stage files and return the diff.""" + index = self.repo.index + + if not self.has_staged_changes(): + for path in changed_paths: + index.add(str(Path(path))) + index.write() + + staged_diff = await self.get_staged_diff() + staged_diff = staged_diff.strip() + + return staged_diff if staged_diff else ( + "No files were staged. Nothing to commit. " + "Tell the user to request some changes so there is something to commit" + ) + + def commit(self, message: str) -> pygit2.Commit: + """ + Commit all staged files with the given message. + + Args: + message: Commit message + + Returns: + The created commit object + + Raises: + ValueError: If no files are staged for commit + Exception: For other git-related errors + """ + try: + config = self.repo.config + author_name = config._get('user.name')[1].value or 'Unknown Author' + author_email = config._get('user.email')[1].value or 'unknown@example.com' + + author = pygit2.Signature(author_name, author_email) + committer = author + + tree = self.repo.index.write_tree() + parents = [self.repo.head.target] if self.repo.head else [] + + commit_oid = self.repo.create_commit( + 'HEAD', + author, + committer, + message, + tree, + parents + ) + + self.repo.index.write() + return self.repo[commit_oid] + + except pygit2.GitError as e: + raise Exception(f"Git error: {e}") + except KeyError as e: + raise Exception(f"Configuration error: {e}") - @staticmethod - def trim_messages(messages, tokenizer_fn, max_tokens :Optional[int]=None): - max_tokens = max_tokens or int(os.environ.get("MAX_HISTORY_TOKENS", 1028)) - while messages and sum(len(tokenizer_fn(str(msg))) for msg in messages) > max_tokens: - messages.pop(0) # Remove from the beginning +class IdentifierResolver: + """Handles the two-phase identifier resolution process.""" + + def __init__( + self, + llm: Llm, + tide: CodeTide, + smart_code_search: SmartCodeSearch, + autocomplete: AutoComplete + ): + self.llm = llm + self.tide = tide + self.smart_code_search = smart_code_search + self.autocomplete = autocomplete + @staticmethod - def get_valid_identifier(autocomplete :AutoComplete, identifier:str)->Optional[str]: - result = autocomplete.validate_code_identifier(identifier) + def extract_candidate_identifiers(reasoning: str) -> List[str]: + """Extract candidate identifiers from reasoning text using regex.""" + pattern = r"^\s*-\s*(.+?)$" + matches = re.findall(pattern, reasoning, re.MULTILINE) + return [match.strip() for match in matches] + + def validate_identifier(self, identifier: str) -> Optional[str]: + """Validate and potentially correct an identifier.""" + result = self.autocomplete.validate_code_identifier(identifier) if result.get("is_valid"): return identifier elif result.get("matching_identifiers"): return result.get("matching_identifiers")[0] return None - - def _clean_history(self): - for i in range(len(self.history)): - message = self.history[i] - if isinstance(message, dict): - self.history[i] = message.get("content" ,"") - async def get_identifiers_two_phase(self, search_query :Optional[str], direct_matches :List[str], autocomplete :AutoComplete, expanded_history :list, codeIdentifiers=None, TODAY :str=None): - """ - Two-phase identifier resolution: - Phase 1: Gather candidates through iterative tree expansion - Phase 2: Classify and finalize identifiers with operation mode + async def gather_candidates( + self, + search_query: str, + direct_matches: Set[str], + expanded_history: list, + context_window: Set[str], + today: str + ) -> Tuple[Set[str], List[str], Optional[str]]: """ - # Initialize tracking - matches = set(direct_matches) - - ### TODO replace matches with search based on received search query - ### get identifiers - ### search for identifers and ask llm to return only related ones -> if not enough then generate search query and keep cycling instead of expanding paths - if search_query is None: - search_query = expanded_history[-1] + Phase 1: Gather candidate identifiers through iterative search and expansion. - # ===== PHASE 1: CANDIDATE GATHERING ===== + Returns: + Tuple of (candidate_pool, all_reasoning, final_search_query) + """ candidate_pool = set() all_reasoning = [] iteration_count = 0 - max_iterations = 3 - enough_identifiers = False - previous_phase_1_response = None + previous_response = None - while not enough_identifiers and iteration_count < max_iterations: - # print(f"{iteration_count=}") + while iteration_count < DEFAULT_MAX_CANDIDATE_ITERATIONS: iteration_count += 1 - serch_results = await self._smart_code_search.search_smart(search_query, use_variations=False, top_k=15) - identifiers_from_search = {result[0] for result in serch_results} - - if matches.issubset(identifiers_from_search): - candidate_pool = matches - print("All matches found in indeintiferis from search") + + # Search for relevant identifiers + search_results = await self.smart_code_search.search_smart( + search_query, + use_variations=False, + top_k=DEFAULT_SEARCH_TOP_K + ) + identifiers_from_search = {result[0] for result in search_results} + + # Early exit if all direct matches found + if direct_matches.issubset(identifiers_from_search): + candidate_pool = direct_matches + print("All matches found in identifiers from search") break - - candidates_to_filter_tree = self.tide._as_file_paths(list(identifiers_from_search)) - # print("got identifiers") - self.tide.codebase._build_tree_dict(candidates_to_filter_tree, slim=True) - # print("got tree") + + # Build filtered tree view + candidates_to_filter = self.tide._as_file_paths(list(identifiers_from_search)) + self.tide.codebase._build_tree_dict(candidates_to_filter, slim=True) sub_tree = self.tide.codebase.get_tree_view() - # print(sub_tree) + + # Prepare prompts prefix_prompt = [ GATHER_CANDIDATES_PREFIX.format( LAST_SEARCH_QUERY=search_query, ITERATION_COUNT=iteration_count, - ACCUMULATED_CONTEXT=set(self._context_identifier_window), - DIRECT_MATCHES=matches, + ACCUMULATED_CONTEXT=context_window, + DIRECT_MATCHES=direct_matches, SEARCH_CANDIDATES=identifiers_from_search, REPO_TREE=sub_tree ) ] - if previous_phase_1_response: - prefix_prompt.insert(0, previous_phase_1_response) + if previous_response: + prefix_prompt.insert(0, previous_response) - # Phase 1 LLM call + # Get LLM response phase1_response = await self.llm.acomplete( expanded_history, system_prompt=GATHER_CANDIDATES_SYSTEM.format( - DATE=TODAY, + DATE=today, SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES ), prefix_prompt=prefix_prompt, stream=True, action_id=f"phase_1.{iteration_count}" ) - previous_phase_1_response = phase1_response + previous_response = phase1_response - # Parse Phase 1 response + # Parse response reasoning_blocks = parse_blocks(phase1_response, block_word="Reasoning", multiple=True) search_query = parse_blocks(phase1_response, block_word="Search Query", multiple=False) - - patterns = { - "header": r"\*{0,2}Task\*{0,2}:\s*(.+?)(?=\n\s*\*{0,2}Rationale\*{0,2})", - "content": r"\*{0,2}Rationale\*{0,2}:\s*(.+?)(?=\s*\*{0,2}NEW Candidate Identifiers\*{0,2}|$)", - "candidate_identifiers": r"^\s*-\s*(.+?)$" - } - if reasoning_blocks is not None: + + # Extract candidates from reasoning + if reasoning_blocks: all_reasoning.extend(reasoning_blocks) for reasoning in reasoning_blocks: - # Extract candidate identifiers using regex - candidate_pattern = patterns["candidate_identifiers"] - candidate_matches = re.findall(candidate_pattern, reasoning, re.MULTILINE) - # print(f"{candidate_matches}=") - + candidate_matches = self.extract_candidate_identifiers(reasoning) for match in candidate_matches: - ident = match.strip() - if ident := self.get_valid_identifier(autocomplete, ident): - candidate_pool.add(ident) - # Check if we need to expand more - if "ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper() or matches.issubset(candidate_pool): - enough_identifiers = True - - # ===== PHASE 2: FINAL SELECTION AND CLASSIFICATION ===== - # print("Here 2") - # Prepare Phase 2 input + if validated := self.validate_identifier(match): + candidate_pool.add(validated) + + # Check if we have enough identifiers + if ("ENOUGH_IDENTIFIERS: TRUE" in phase1_response.upper() or + direct_matches.issubset(candidate_pool)): + break + + return candidate_pool, all_reasoning, search_query + + async def finalize_identifiers( + self, + candidate_pool: Set[str], + all_reasoning: List[str], + expanded_history: list, + today: str + ) -> Tuple[Set[str], Set[str], Optional[str]]: + """ + Phase 2: Classify candidates into context and modify identifiers. + + Returns: + Tuple of (context_identifiers, modify_identifiers, summary) + """ all_reasoning_text = "\n\n".join(all_reasoning) all_candidates_text = "\n".join(sorted(candidate_pool)) - - # print(sub_tree) - - # print(f"{all_candidates_text=}") phase2_response = await self.llm.acomplete( expanded_history, system_prompt=[FINALIZE_IDENTIFIERS_PROMPT.format( - DATE=TODAY, + DATE=today, SUPPORTED_LANGUAGES=SUPPORTED_LANGUAGES, EXPLORATION_STEPS=all_reasoning_text, ALL_CANDIDATES=all_candidates_text, @@ -254,152 +340,314 @@ async def get_identifiers_two_phase(self, search_query :Optional[str], direct_ma action_id="phase2.finalize" ) - # print(f"Phase 2 Final Selection: {phase2_response}") - - # Parse Phase 2 response + # Parse results summary = parse_blocks(phase2_response, block_word="Summary", multiple=False) - context_identifiers = parse_blocks(phase2_response, block_word="Context Identifiers", multiple=False) - modify_identifiers = parse_blocks(phase2_response, block_word="Modify Identifiers", multiple=False) + context_identifiers = parse_blocks( + phase2_response, + block_word="Context Identifiers", + multiple=False + ) + modify_identifiers = parse_blocks( + phase2_response, + block_word="Modify Identifiers", + multiple=False + ) - # Process final identifiers + # Process and validate identifiers final_context = set() final_modify = set() if context_identifiers: for ident in context_identifiers.strip().split('\n'): - if ident := self.get_valid_identifier(autocomplete, ident.strip()): - final_context.add(ident) + if validated := self.validate_identifier(ident.strip()): + final_context.add(validated) if modify_identifiers: for ident in modify_identifiers.strip().split('\n'): - if ident := self.get_valid_identifier(autocomplete, ident.strip()): - final_modify.add(ident) - - return { - "matches": list(matches), - "context_identifiers": list(final_context), - "modify_identifiers": self.tide._as_file_paths(list(final_modify)), - "summary": summary, - "all_reasoning": all_reasoning_text, - "iteration_count": iteration_count - } + if validated := self.validate_identifier(ident.strip()): + final_modify.add(validated) + + return final_context, final_modify, summary + + async def resolve_identifiers( + self, + search_query: Optional[str], + direct_matches: List[str], + expanded_history: list, + context_window: Set[str], + today: str + ) -> IdentifierResolutionResult: + """ + Execute the full two-phase identifier resolution process. + + Args: + search_query: Initial search query (if None, uses last history item) + direct_matches: Identifiers directly matched from autocomplete + expanded_history: Conversation history to use + context_window: Set of identifiers from recent context + today: Current date string + + Returns: + IdentifierResolutionResult with all resolved identifiers + """ + if search_query is None: + search_query = expanded_history[-1] + + # Phase 1: Gather candidates + candidate_pool, all_reasoning, _ = await self.gather_candidates( + search_query, + set(direct_matches), + expanded_history, + context_window, + today + ) + + # Phase 2: Finalize classification + context_ids, modify_ids, summary = await self.finalize_identifiers( + candidate_pool, + all_reasoning, + expanded_history, + today + ) + + return IdentifierResolutionResult( + matches=direct_matches, + context_identifiers=list(context_ids), + modify_identifiers=self.tide._as_file_paths(list(modify_ids)), + summary=summary, + all_reasoning="\n\n".join(all_reasoning), + iteration_count=len(all_reasoning) + ) + + +class HistoryManager: + """Manages conversation history expansion and relevance assessment.""" + + def __init__(self, llm: Llm): + self.llm = llm + + @staticmethod + def trim_messages(messages: list, tokenizer_fn, max_tokens: Optional[int] = None): + """Trim messages to fit within token budget.""" + max_tokens = max_tokens or int( + os.environ.get("MAX_HISTORY_TOKENS", DEFAULT_MAX_HISTORY_TOKENS) + ) + while messages and sum(len(tokenizer_fn(str(msg))) for msg in messages) > max_tokens: + messages.pop(0) async def expand_history_if_needed( self, + history: list, sufficient_context: bool, - history_count: int, + initial_history_count: int, ) -> int: """ - Iteratively expand history window if initial assessment indicates more context is needed. + Iteratively expand history window if more context is needed. Args: - sufficient_context: Boolean indicating if context is sufficient - history_count: Initial history count from operation mode extraction + history: Full conversation history + sufficient_context: Whether initial context is sufficient + initial_history_count: Starting history count Returns: - Final history count to use for processing - - Raises: - ValueError: If extraction fails at any iteration + Final history count to use """ - current_history_count = history_count - max_iterations = 10 # Prevent infinite loops - iteration = 0 - - if not current_history_count: - current_history_count += 1 + current_count = max(initial_history_count, 1) - # If context is already sufficient, return early if sufficient_context: - return current_history_count + return current_count - # Expand history iteratively - while iteration < max_iterations and current_history_count < len(self.history): + iteration = 0 + while iteration < DEFAULT_MAX_EXPANSION_ITERATIONS and current_count < len(history): iteration += 1 - # Calculate window indices - start_index = max(0, len(self.history) - current_history_count) - end_index = len(self.history) - current_window = self.history[start_index:end_index] - latest_request = self.history[-1] # Last interaction is the current request + start_index = max(0, len(history) - current_count) + end_index = len(history) + current_window = history[start_index:end_index] + latest_request = history[-1] - # Assess if current window has enough history response = await self.llm.acomplete( current_window, system_prompt=ASSESS_HISTORY_RELEVANCE_PROMPT.format( START_INDEX=start_index, END_INDEX=end_index, - TOTAL_INTERACTIONS=len(self.history), + TOTAL_INTERACTIONS=len(history), CURRENT_WINDOW=str(current_window), LATEST_REQUEST=str(latest_request) ), - stream=False, - action_id=f"expand_history.iteration_{iteration}" - ) - - # Extract HISTORY_SUFFICIENT - history_sufficient_match = re.search( - r'HISTORY_SUFFICIENT:\s*\[?(TRUE|FALSE)\]?', - response - ) - history_sufficient = ( - history_sufficient_match.group(1).lower() == 'true' - if history_sufficient_match else False + stream=False, + action_id=f"expand_history.iteration_{iteration}" ) - # Extract REQUIRES_MORE_MESSAGES - requires_more_match = re.search( - r'REQUIRES_MORE_MESSAGES:\s*\[?(\d+)\]?', - response - ) - requires_more = int(requires_more_match.group(1)) if requires_more_match else 0 + # Extract assessment fields + history_sufficient = self._extract_boolean_field(response, "HISTORY_SUFFICIENT") + requires_more = self._extract_integer_field(response, "REQUIRES_MORE_MESSAGES") - # Validate extraction - if history_sufficient_match is None or requires_more_match is None: + if history_sufficient is None or requires_more is None: raise ValueError( - f"Failed to extract relevance assessment fields at iteration {iteration}:\n{response}" + f"Failed to extract relevance assessment at iteration {iteration}:\n{response}" ) - # If history is sufficient, we're done if history_sufficient: - return current_history_count + return current_count - # If more messages are needed, expand the count if requires_more > 0: - new_count = current_history_count + requires_more - # Prevent exceeding total history - if new_count > len(self.history): - new_count = len(self.history) - - current_history_count = new_count + current_count = min(current_count + requires_more, len(history)) else: - # No more messages required but not sufficient - use full history - current_history_count = len(self.history) + current_count = len(history) - # Return final count (capped at total history length) - return min(current_history_count, len(self.history)) + return min(current_count, len(history)) + + @staticmethod + def _extract_boolean_field(text: str, field_name: str) -> Optional[bool]: + """Extract a boolean field from response text.""" + match = re.search(rf'{field_name}:\s*\[?(TRUE|FALSE)\]?', text) + if match: + return match.group(1).upper() == "TRUE" + return None + + @staticmethod + def _extract_integer_field(text: str, field_name: str) -> Optional[int]: + """Extract an integer field from response text.""" + match = re.search(rf'{field_name}:\s*\[?(\d+)\]?', text) + if match: + return int(match.group(1)) + return None + + +# ============================================================================ +# Main Agent Class +# ============================================================================ + +class AgentTide(BaseModel): + """Main agent for autonomous code editing and task execution.""" + + llm: Llm + tide: CodeTide + history: Optional[list] = None + steps: Optional[Steps] = None + session_id: str = Field(default_factory=ulid) + changed_paths: List[str] = Field(default_factory=list) + request_human_confirmation: bool = False + + context_identifiers: Optional[List[str]] = None + modify_identifiers: Optional[List[str]] = None + reasoning: Optional[str] = None + + # Internal state + _skip_context_retrieval: bool = False + _last_code_identifiers: Optional[Set[str]] = set() + _last_code_context: Optional[str] = None + _has_patch: bool = False + _direct_mode: bool = False + _smart_code_search: Optional[SmartCodeSearch] = None + _context_identifier_window: Optional[list] = None + _git_operations: Optional[GitOperations] = None + _history_manager: Optional[HistoryManager] = None + + # Configuration + CONTEXT_WINDOW_SIZE: int = DEFAULT_CONTEXT_WINDOW_SIZE + + OPERATIONS: Dict[str, str] = { + OPERATION_MODE_PLAN_STEPS: STEPS_SYSTEM_PROMPT, + OPERATION_MODE_PATCH_CODE: WRITE_PATCH_SYSTEM_PROMPT + } + + model_config = ConfigDict(arbitrary_types_allowed=True) + + @model_validator(mode="after") + def initialize_components(self) -> Self: + """Initialize helper components and configure logging.""" + self.llm.logger_fn = partial( + custom_logger_fn, + session_id=self.session_id, + filepath=self.patch_path + ) + self._git_operations = GitOperations(self.tide.repo, self.tide.rootpath) + self._history_manager = HistoryManager(self.llm) + return self + + @property + def patch_path(self) -> Path: + """Get the path for storing patches.""" + storage_dir = self.tide.rootpath / DEFAULT_STORAGE_PATH + storage_dir.mkdir(exist_ok=True) + return storage_dir / f"{self.session_id}.bash" + + # ======================================================================== + # Patch Management + # ======================================================================== + + def approve(self): + """Approve and apply the current patch.""" + self._has_patch = False + if not os.path.exists(self.patch_path): + return + + changed_paths = process_patch( + self.patch_path, + open_file, + write_file, + remove_file, + file_exists, + root_path=self.tide.rootpath + ) + self.changed_paths.extend(changed_paths) + + # Clean up patch blocks from history + self._remove_patch_blocks_from_history() + + def reject(self, feedback: str): + """Reject the current patch with feedback.""" + self._has_patch = False + self.history.append(REJECT_PATCH_FEEDBACK_TEMPLATE.format(FEEDBACK=feedback)) + + def _remove_patch_blocks_from_history(self): + """Remove patch blocks from the last response in history.""" + if not self.history: + return + + previous_response = self.history[-1] + diff_patches = parse_patch_blocks(previous_response, multiple=True) + + if diff_patches: + for patch in diff_patches: + previous_response = previous_response.replace( + f"*** Begin Patch\n{patch}*** End Patch", + "" + ) + self.history[-1] = previous_response + + # ======================================================================== + # History Management + # ======================================================================== + + def _clean_history(self): + """Convert history messages to plain strings.""" + for i, message in enumerate(self.history): + if isinstance(message, dict): + self.history[i] = message.get("content", "") + + def _filter_command_prompts_from_history(self, history: list) -> str: + """Remove command prompts from history string.""" + history_str = "\n\n".join(history) + for cmd_prompt in COMMAND_PROMPTS: + history_str = history_str.replace(cmd_prompt, "") + return history_str + + # ======================================================================== + # Operation Mode and Context Extraction + # ======================================================================== async def extract_operation_mode( self, - cached_identifiers: str - ) -> Tuple[str, bool, list]: + cached_identifiers: Set[str] + ) -> OperationModeResult: """ - Extract operation mode, context sufficiency, and history count from LLM response. - - Args: - llm: Language model instance with acomplete method - history: Conversation history - cached_identifiers: Code identifiers string - system_prompt: System prompt template (DETERMINE_OPERATION_MODE_PROMPT) + Extract operation mode, context sufficiency, and relevant history. Returns: - Tuple of (operation_mode, sufficient_context, history_count) - - operation_mode: str [STANDARD|PLAN_STEPS|PATCH_CODE] - - sufficient_context: bool - - history_count: int - - Raises: - ValueError: If required fields cannot be extracted from response + OperationModeResult with all extracted information """ response = await self.llm.acomplete( self.history[-3:], @@ -408,343 +656,426 @@ async def extract_operation_mode( INTERACTION_COUNT=len(self.history), CODE_IDENTIFIERS=cached_identifiers ), - stream=False, - action_id="extract_operation_mode" - ) - - response_text = response.strip() - # Extract and remove OPERATION_MODE - operation_mode_match = re.search(r'OPERATION_MODE:\s*\[?(STANDARD|PLAN_STEPS|PATCH_CODE)\]?', response_text) - operation_mode = operation_mode_match.group(1) if operation_mode_match else None - if operation_mode_match: - response_text = response_text.replace(operation_mode_match.group(0), '') - - # Extract and remove SUFFICIENT_CONTEXT - sufficient_context_match = re.search(r'SUFFICIENT_CONTEXT:\s*\[?(TRUE|FALSE)\]?', response_text) - sufficient_context = ( - sufficient_context_match.group(1).strip().upper() == "TRUE" - if sufficient_context_match else None + stream=False, + action_id="extract_operation_mode" ) - if sufficient_context_match: - response_text = response_text.replace(sufficient_context_match.group(0), '') - - # Extract and remove HISTORY_COUNT - history_count_match = re.search(r'HISTORY_COUNT:\s*\[?(\d+)\]?', response_text) - history_count = int(history_count_match.group(1)) if history_count_match else len(self.history) - if history_count_match: - response_text = response_text.replace(history_count_match.group(0), '') - - # Whatever remains (if anything) is the search query - search_query = response_text.strip() or None - + + # Extract fields from response + operation_mode = self._extract_field(response, "OPERATION_MODE") + sufficient_context = self._extract_field(response, "SUFFICIENT_CONTEXT") + history_count = self._extract_field(response, "HISTORY_COUNT") + # Validate extraction if operation_mode is None or sufficient_context is None: raise ValueError(f"Failed to extract required fields from response:\n{response}") - - final_history_count = await self.expand_history_if_needed(sufficient_context, min(history_count, int(history_count * 0.2)+1)) + + # Parse values + operation_mode = operation_mode.strip() + sufficient_context = sufficient_context.strip().upper() == "TRUE" + history_count = int(history_count) if history_count else len(self.history) + + # Extract search query (remaining text after removing known fields) + search_query = self._extract_search_query(response) + + # Expand history if needed + final_history_count = await self._history_manager.expand_history_if_needed( + self.history, + sufficient_context, + min(history_count, int(history_count * 0.2) + 1) + ) expanded_history = self.history[-final_history_count:] - - return operation_mode, sufficient_context, expanded_history, search_query - - async def prepare_loop(self): + + return OperationModeResult( + operation_mode=operation_mode, + sufficient_context=sufficient_context, + expanded_history=expanded_history, + search_query=search_query + ) + + @staticmethod + def _extract_field(text: str, field_name: str) -> Optional[str]: + """Extract a field value from response text.""" + pattern = rf'{field_name}:\s*\[?([^\]]+?)\]?(?:\n|$)' + match = re.search(pattern, text) + return match.group(1) if match else None + + @staticmethod + def _extract_search_query(response: str) -> Optional[str]: + """Extract search query by removing known fields from response.""" + cleaned = response + for field in ["OPERATION_MODE", "SUFFICIENT_CONTEXT", "HISTORY_COUNT"]: + cleaned = re.sub(rf'{field}:\s*\[?[^\]]+?\]?', '', cleaned) + search_query = cleaned.strip() + return search_query if search_query else None + + # ======================================================================== + # Context Building + # ======================================================================== + + async def prepare_search_infrastructure(self): + """Initialize search components and update codebase.""" await self.tide.check_for_updates(serialize=True, include_cached_ids=True) - ### TODO this whole process neds to be integrated and updated from coetide directly for efficiency + self._smart_code_search = SmartCodeSearch( documents={ - codefile.file_path: FILE_TEMPLATE.format(CONTENT=codefile.raw, FILENAME=codefile.file_path) + codefile.file_path: FILE_TEMPLATE.format( + CONTENT=codefile.raw, + FILENAME=codefile.file_path + ) for codefile in self.tide.codebase.root } ) await self._smart_code_search.initialize_async() - - async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): - TODAY = date.today() - - # Initialize the context identifier window if not present + + async def get_repo_tree_from_user_prompt( + self, + history: list, + include_modules: bool = False, + expand_paths: Optional[List[str]] = None + ) -> str: + """Get a tree view of the repository based on user prompt context.""" + self._filter_command_prompts_from_history(history) + self.tide.codebase._build_tree_dict(expand_paths) + + return self.tide.codebase.get_tree_view( + include_modules=include_modules, + include_types=True + ) + + def _build_code_context( + self, + code_identifiers: Optional[List[str]], + matches: Optional[List[str]] = None + ) -> Optional[str]: + """Build code context from identifiers, falling back to tree view if needed.""" + if code_identifiers: + return self.tide.get(code_identifiers, as_string=True) + + # Fallback to tree view and README + tree_view = REPO_TREE_CONTEXT_PROMPT.format( + REPO_TREE=self.tide.codebase.get_tree_view() + ) + + readme_files = self.tide.get( + ["README.md"] + (matches or []), + as_string_list=True + ) + + if readme_files: + return "\n".join([ + tree_view, + README_CONTEXT_PROMPT.format(README=readme_files) + ]) + + return tree_view + + # ======================================================================== + # Identifier Resolution + # ======================================================================== + + async def resolve_identifiers_for_request( + self, + operation_result: OperationModeResult, + autocomplete: AutoComplete, + today: str + ) -> Tuple[Optional[List[str]], Optional[str], Optional[str]]: + """ + Resolve code identifiers based on operation mode and context. + + Returns: + Tuple of (code_identifiers, code_context, prefilled_summary) + """ + # Initialize context window if needed if self._context_identifier_window is None: self._context_identifier_window = [] - + + expanded_history = operation_result.expanded_history + sufficient_context = operation_result.sufficient_context + search_query = operation_result.search_query + + # Extract direct matches from last message + autocomplete_result = await autocomplete.async_extract_words_from_text( + self.history[-1] if self.history else "", + max_matches_per_word=1, + timeout=30 + ) + direct_matches = autocomplete_result["all_found_words"] + + print(f"operation_mode={operation_result.operation_mode}") + print(f"direct_matches={direct_matches}") + print(f"search_query={search_query}") + print(f"sufficient_context={sufficient_context}") + + # Case 1: Sufficient context with cached identifiers + if sufficient_context or ( + direct_matches and set(direct_matches).issubset(self._last_code_identifiers) + ): + await self.llm.logger_fn(REASONING_FINISHED) + return list(self._last_code_identifiers), None, None + + # Case 2: Direct mode - use only exact matches + if self._direct_mode: + self.context_identifiers = None + self.modify_identifiers = self.tide._as_file_paths(direct_matches) + self._update_context_window(direct_matches) + self._direct_mode = False + await self.llm.logger_fn(REASONING_FINISHED) + return self.modify_identifiers, None, None + + # Case 3: Full two-phase identifier resolution + print("Entering two-phase identifier resolution") + await self.llm.logger_fn(REASONING_STARTED) + + resolver = IdentifierResolver( + self.llm, + self.tide, + self._smart_code_search, + autocomplete + ) + + context_window = set() + if self._context_identifier_window: + context_window = set().union(*self._context_identifier_window) + + resolution_result = await resolver.resolve_identifiers( + search_query, + direct_matches, + expanded_history, + context_window, + today + ) + + await self.llm.logger_fn(REASONING_FINISHED) + print(json.dumps(resolution_result.dict(), indent=4)) + + code_identifiers = ( + resolution_result.context_identifiers + + resolution_result.modify_identifiers + ) + self._update_context_window(resolution_result.matches) + + return code_identifiers, None, resolution_result.summary + + def _update_context_window(self, new_identifiers: List[str]): + """Update the rolling window of context identifiers.""" + self._context_identifier_window.append(set(new_identifiers)) + if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: + self._context_identifier_window.pop(0) + + # ======================================================================== + # Main Agent Loop + # ======================================================================== + + async def agent_loop(self, code_identifiers: Optional[List[str]] = None): + """ + Main agent execution loop. + + Args: + code_identifiers: Optional list of code identifiers to use directly + """ + today = date.today() operation_mode = None - codeContext = None + code_context = None prefilled_summary = None - prefil_context = None + + # Skip context retrieval if flagged if self._skip_context_retrieval: - expanded_history = self.history[-1] + expanded_history = [self.history[-1]] await self.llm.logger_fn(REASONING_FINISHED) else: - cached_identifiers = self._last_code_identifers - if codeIdentifiers: - for identifier in codeIdentifiers: - cached_identifiers.add(identifier) + # Prepare autocomplete and search infrastructure + cached_identifiers = self._last_code_identifiers.copy() + if code_identifiers: + cached_identifiers.update(code_identifiers) - autocomplete = AutoComplete(self.tide.cached_ids, mapped_words=self.tide.filenames_mapped) - tasks = [ + autocomplete = AutoComplete( + self.tide.cached_ids, + mapped_words=self.tide.filenames_mapped + ) + + # Run preparation and mode extraction in parallel + operation_result, _ = await asyncio.gather( self.extract_operation_mode(cached_identifiers), - autocomplete.async_extract_words_from_text( - self.history[-1] if self.history else "", - max_matches_per_word=1, - timeout=30 - ), - self.prepare_loop() - ] - operation_context_history_task, autocomplete_matches, _ = await asyncio.gather(*tasks) - - operation_mode, sufficient_context, expanded_history, search_query = operation_context_history_task - direct_matches = autocomplete_matches["all_found_words"] - print(f"{search_query=}") - - ### TODO super quick prompt here for operation mode - ### needs more context based on cached identifiers or not - ### needs more history or not, default is last 5 iteratinos - if sufficient_context or (direct_matches and set(direct_matches).issubset(cached_identifiers)): - codeIdentifiers = list(self._last_code_identifers) - await self.llm.logger_fn(REASONING_FINISHED) - - elif self._direct_mode: - self.contextIdentifiers = None - # Only extract matches from the last message - last_message = self.history[-1] if self.history else "" - exact_matches = await autocomplete.async_extract_words_from_text(last_message, max_matches_per_word=1)["all_found_words"] - self.modifyIdentifiers = self.tide._as_file_paths(exact_matches) - codeIdentifiers = self.modifyIdentifiers - self._direct_mode = False - # Update the context identifier window - self._context_identifier_window.append(set(exact_matches)) - if len(self._context_identifier_window) > self.CONTEXT_WINDOW_SIZE: - self._context_identifier_window.pop(0) - await self.llm.logger_fn(REASONING_FINISHED) - ### TODO create lightweight version to skip tree expansion and infer operationan_mode and expanded_history - else: - await self.llm.logger_fn(REASONING_STARTED) - reasoning_output = await self.get_identifiers_two_phase(search_query, direct_matches, autocomplete, expanded_history, codeIdentifiers, TODAY) - await self.llm.logger_fn(REASONING_FINISHED) - print(json.dumps(reasoning_output, indent=4)) - - codeIdentifiers = reasoning_output.get("context_identifiers", []) + reasoning_output.get("modify_identifiers", []) - matches = reasoning_output.get("matches") - prefilled_summary = reasoning_output.get("summary") - - # --- End Unified Identifier Retrieval --- - if codeIdentifiers: - self._last_code_identifers = set(codeIdentifiers) - codeContext = self.tide.get(codeIdentifiers, as_string=True) - - if not codeContext and not sufficient_context: - codeContext = REPO_TREE_CONTEXT_PROMPT.format(REPO_TREE=self.tide.codebase.get_tree_view()) - # Use matches from the last message for README context - readmeFile = self.tide.get(["README.md"] + (matches if 'matches' in locals() else []), as_string_list=True) - if readmeFile: - codeContext = "\n".join([codeContext, README_CONTEXT_PROMPT.format(README=readmeFile)]) - self._last_code_context = codeContext + self.prepare_search_infrastructure() + ) + + operation_mode = operation_result.operation_mode + expanded_history = operation_result.expanded_history + + # Resolve identifiers and build context + code_identifiers, _, prefilled_summary = await self.resolve_identifiers_for_request( + operation_result, + autocomplete, + str(today) + ) + + # Build code context + if code_identifiers: + self._last_code_identifiers = set(code_identifiers) + code_context = self.tide.get(code_identifiers, as_string=True) + + if not code_context and not operation_result.sufficient_context: + code_context = self._build_code_context(code_identifiers) + + # Store context for potential reuse + self._last_code_context = code_context await delete_file(self.patch_path) - + + # Build system prompt system_prompt = [ - AGENT_TIDE_SYSTEM_PROMPT.format(DATE=TODAY), + AGENT_TIDE_SYSTEM_PROMPT.format(DATE=today), CALMNESS_SYSTEM_PROMPT ] if operation_mode in self.OPERATIONS: - system_prompt.insert(1, self.OPERATIONS.get(operation_mode)) + system_prompt.insert(1, self.OPERATIONS[operation_mode]) + + # Build prefix prompt + prefix_prompt = None + if prefilled_summary: + prefix_prompt = [PREFIX_SUMMARY_PROMPT.format(SUMMARY=prefilled_summary)] + + # Generate response + history_with_context = ( + expanded_history + [code_context] if code_context else expanded_history + ) - prefil_context = None - if prefilled_summary is not None: - prefil_context = [ - PREFIX_SUMMARY_PROMPT.format(SUMMARY=prefilled_summary) - ] - # elif codeContext: - # prefil_context = [codeContext] - - ### TODO get system prompt based on OEPRATION_MODE response = await self.llm.acomplete( - expanded_history + [codeContext] if codeContext else expanded_history, + history_with_context, system_prompt=system_prompt, - prefix_prompt=prefil_context, + prefix_prompt=prefix_prompt, action_id="agent_loop.main" ) - + + # Process response + await self._process_agent_response(response) + + self.history.append(response) + await self.llm.logger_fn(ROUND_FINISHED) + + async def _process_agent_response(self, response: str): + """Process the agent's response for patches, commits, and steps.""" await trim_to_patch_section(self.patch_path) + + # Handle patches if not self.request_human_confirmation: self.approve() - - commitMessage = parse_blocks(response, multiple=False, block_word="Commit") - if commitMessage: - self.commit(commitMessage) - + + # Handle commits + commit_message = parse_blocks(response, multiple=False, block_word="Commit") + if commit_message: + self.commit(commit_message) + + # Handle steps steps = parse_steps_markdown(response) if steps: self.steps = Steps.from_steps(steps) - - diffPatches = parse_patch_blocks(response, multiple=True) - if diffPatches: + + # Track patches for human confirmation + diff_patches = parse_patch_blocks(response, multiple=True) + if diff_patches: if self.request_human_confirmation: self._has_patch = True else: - for patch in diffPatches: - # TODO this deletes previouspatches from history to make sure changes are always focused on the latest version of the file - response = response.replace(f"*** Begin Patch\n{patch}*** End Patch", "") - - self.history.append(response) - await self.llm.logger_fn(ROUND_FINISHED) - - @staticmethod - async def get_git_diff_staged_simple(directory: str) -> str: - """ - Simple async function to get git diff --staged output - """ - # Validate directory exists - if not Path(directory).is_dir(): - raise FileNotFoundError(f"Directory not found: {directory}") - - process = await asyncio.create_subprocess_exec( - 'git', 'diff', '--staged', - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=directory - ) - - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise Exception(f"Git command failed: {stderr.decode().strip()}") - - return stdout.decode() - - def _has_staged(self)->bool: - status = self.tide.repo.status() - result = any([file_status == pygit2.GIT_STATUS_INDEX_MODIFIED for file_status in status.values()]) - _logger.logger.debug(f"_has_staged {result=}") - return result - - async def _stage(self)->str: - index = self.tide.repo.index - if not self._has_staged(): - for path in self.changed_paths: - index.add(str(Path(path))) - - index.write() - - staged_diff = await self.get_git_diff_staged_simple(self.tide.rootpath) - staged_diff = staged_diff.strip() - return staged_diff if staged_diff else "No files were staged. Nothing to commit. Tell the user to request some changes so there is something to commit" - - async def prepare_commit(self)->str: - staged_diff = await self._stage() + # Remove patch blocks from response to keep history clean + for patch in diff_patches: + response = response.replace( + f"*** Begin Patch\n{patch}*** End Patch", + "" + ) + + # ======================================================================== + # Git Operations + # ======================================================================== + + async def prepare_commit(self) -> str: + """Stage files and prepare commit context.""" + staged_diff = await self._git_operations.stage_files(self.changed_paths) self.changed_paths = [] self._skip_context_retrieval = True return STAGED_DIFFS_TEMPLATE.format(diffs=staged_diff) - - def commit(self, message :str): + + def commit(self, message: str): + """Commit staged changes with the given message.""" + try: + self._git_operations.commit(message) + finally: + self._skip_context_retrieval = False + + # ======================================================================== + # Command Handling + # ======================================================================== + + async def _handle_commands(self, command: str) -> str: """ - Commit all staged files in a git repository with the given message. + Handle special commands. Args: - repo_path (str): Path to the git repository - message (str): Commit message - author_name (str, optional): Author name. If None, uses repo config - author_email (str, optional): Author email. If None, uses repo config + command: Command to execute Returns: - pygit2.Commit: The created commit object, or None if no changes to commit - - Raises: - ValueError: If no files are staged for commit - Exception: For other git-related errors + Context string resulting from command execution """ - try: - # Open the repository - repo = self.tide.repo - - # Get author and committer information - config = repo.config - author_name = config._get('user.name')[1].value or 'Unknown Author' - author_email = config._get('user.email')[1].value or 'unknown@example.com' - - author = pygit2.Signature(author_name, author_email) - committer = author # Typically same as author - - # Get the current tree from the index - tree = repo.index.write_tree() - - # Get the parent commit (current HEAD) - parents = [repo.head.target] if repo.head else [] - - # Create the commit - commit_oid = repo.create_commit( - 'HEAD', # Reference to update - author, - committer, - message, - tree, - parents - ) - - # Clear the staging area after successful commit - repo.index.write() - - return repo[commit_oid] - - except pygit2.GitError as e: - raise Exception(f"Git error: {e}") - except KeyError as e: - raise Exception(f"Configuration error: {e}") - - finally: - self._skip_context_retrieval = False - + if command == "commit": + return await self.prepare_commit() + elif command == "direct_mode": + self._direct_mode = True + return "" + return "" + + # ======================================================================== + # Interactive Loop + # ======================================================================== + async def run(self, max_tokens: int = 48000): + """ + Run the interactive agent loop. + + Args: + max_tokens: Maximum tokens to keep in history + """ if self.history is None: self.history = [] - - # 1. Set up key bindings + + # Set up key bindings bindings = KeyBindings() - + @bindings.add('escape') - def _(event): - """When Esc is pressed, exit the application.""" + def exit_handler(event): + """Exit on Escape key.""" _logger.logger.warning("Escape key pressed — exiting...") event.app.exit() - - # 2. Create a prompt session with the custom key bindings + session = PromptSession(key_bindings=bindings) - + print(f"\n{AGENT_TIDE_ASCII_ART}\n") _logger.logger.info("Ready to surf. Press ESC to exit.") + try: while True: try: - # 3. Use the async prompt instead of input() message = await session.prompt_async("You: ") if message is None: break message = message.strip() - if not message: continue - + except (EOFError, KeyboardInterrupt): - # prompt_toolkit raises EOFError on Ctrl-D and KeyboardInterrupt on Ctrl-C _logger.logger.warning("Exiting...") break - + self.history.append(message) - self.trim_messages(self.history, self.llm.tokenizer, max_tokens) - + self._history_manager.trim_messages( + self.history, + self.llm.tokenizer, + max_tokens + ) + print("Agent: Thinking...") await self.agent_loop() - + except asyncio.CancelledError: - # This can happen if the event loop is shut down pass finally: _logger.logger.info("Exited by user. Goodbye!") - - async def _handle_commands(self, command :str) -> str: - # TODO add logic here to handlle git command, i.e stage files, write commit messages and checkout - # expand to support new branches - context = "" - if command == "commit": - context = await self.prepare_commit() - elif command == "direct_mode": - self._direct_mode = True - - return context From 9c992b47599e2f82b1c864bdd568b32e256a631e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 14 Nov 2025 22:51:10 +0000 Subject: [PATCH 126/138] fix(ui): correct typo in code identifier variable name across UI modules --- codetide/agents/tide/ui/app.py | 6 +++--- examples/hf_demo_space/app.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index c71b631..bafe2a3 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -259,11 +259,11 @@ async def on_inspect_context(action :cl.Action): elements= [ cl.Text( name="CodeTIde Retrieved Identifiers", - content=f"""```json\n{json.dumps(list(agent_tide_ui.agent_tide._last_code_identifers), indent=4)}\n```""" + content=f"""```json\n{json.dumps(list(agent_tide_ui.agent_tide._last_code_identifiers), indent=4)}\n```""" ) ] ) - agent_tide_ui.agent_tide._last_code_identifers = None + agent_tide_ui.agent_tide._last_code_identifiers = None if agent_tide_ui.agent_tide._last_code_context: inspect_msg.elements.append( @@ -494,7 +494,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option ) ] - if agent_tide_ui.agent_tide._last_code_identifers: + if agent_tide_ui.agent_tide._last_code_identifiers: msg.actions.append( cl.Action( name="inspect_code_context", diff --git a/examples/hf_demo_space/app.py b/examples/hf_demo_space/app.py index ac81462..fad7ea6 100644 --- a/examples/hf_demo_space/app.py +++ b/examples/hf_demo_space/app.py @@ -280,11 +280,11 @@ async def on_inspect_context(action :cl.Action): elements= [ cl.Text( name="CodeTIde Retrieved Identifiers", - content=f"""```json\n{json.dumps(list(agent_tide_ui.agent_tide._last_code_identifers), indent=4)}\n```""" + content=f"""```json\n{json.dumps(list(agent_tide_ui.agent_tide._last_code_identifiers), indent=4)}\n```""" ) ] ) - agent_tide_ui.agent_tide._last_code_identifers = None + agent_tide_ui.agent_tide._last_code_identifiers = None if agent_tide_ui.agent_tide._last_code_context: inspect_msg.elements.append( @@ -398,7 +398,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option ) ] - if agent_tide_ui.agent_tide._last_code_identifers: + if agent_tide_ui.agent_tide._last_code_identifiers: msg.actions.append( cl.Action( name="inspect_code_context", From 4900224d05702728d7f56e5df05ebbb3258c2b7a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 14 Nov 2025 23:44:06 +0000 Subject: [PATCH 127/138] feat(tide): add topic detection and enhance operation mode extraction with structured search query handling --- codetide/agents/tide/agent.py | 15 +++++++++---- codetide/agents/tide/prompts.py | 39 ++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 776f5fb..5cb49d4 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -91,6 +91,8 @@ class OperationModeResult(BaseModel): sufficient_context: bool expanded_history: list search_query: Optional[str] + is_new_topic: Optional[bool]=None + topic_title: Optional[str]=None # ============================================================================ @@ -664,6 +666,9 @@ async def extract_operation_mode( operation_mode = self._extract_field(response, "OPERATION_MODE") sufficient_context = self._extract_field(response, "SUFFICIENT_CONTEXT") history_count = self._extract_field(response, "HISTORY_COUNT") + is_new_topic = self._extract_field(response, "IS_NEW_TOPIC") + topic_title = self._extract_field(response, "TOPIC_TITLE") + search_query = self._extract_field(response, "SEARCH_QUERY") # Validate extraction if operation_mode is None or sufficient_context is None: @@ -673,9 +678,9 @@ async def extract_operation_mode( operation_mode = operation_mode.strip() sufficient_context = sufficient_context.strip().upper() == "TRUE" history_count = int(history_count) if history_count else len(self.history) - - # Extract search query (remaining text after removing known fields) - search_query = self._extract_search_query(response) + is_new_topic = is_new_topic.strip().upper() == "TRUE" if is_new_topic else False + topic_title = topic_title.strip() if topic_title and topic_title.strip().lower() != "null" else None + search_query = search_query.strip() if search_query and search_query.strip().upper() != "NO" else None # Expand history if needed final_history_count = await self._history_manager.expand_history_if_needed( @@ -689,7 +694,9 @@ async def extract_operation_mode( operation_mode=operation_mode, sufficient_context=sufficient_context, expanded_history=expanded_history, - search_query=search_query + search_query=search_query, + is_new_topic=is_new_topic, + topic_title=topic_title ) @staticmethod diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 45e134c..9459401 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -748,13 +748,13 @@ """ DETERMINE_OPERATION_MODE_SYSTEM = """ -You are Agent **Tide** performing **Operation Mode Extraction** +You are Agent **Tide** performing **Operation Mode Extraction**. You will receive the following inputs from the prefix prompt: -- **Code Identifiers**: the current set of known identifiers, files, functions, classes, or patterns available in context +- **Code Identifiers**: the current set of known identifiers, files, functions, classes, or patterns available in the codebase context - **Interaction Count**: the number of prior exchanges or iterations in the conversation -Your task is to determine the current **operation mode**, assess **context sufficiency**, and if context is insufficient, propose a short **search query** to gather missing information. +Your task is to determine the current **operation mode**, assess **context sufficiency**, detect **new conversation topics**, and if context is insufficient, propose a short **search query** to gather missing information from the codebase. **NO** - Explanations, markdown, or code @@ -763,7 +763,7 @@ --- **CORE PRINCIPLES** -Intent detection, context sufficiency, and history recovery are independent. +Intent detection, context sufficiency, history recovery, and topic detection are independent. **IMPORTANT:** In case of the slightest doubt or uncertainty about context sufficiency, you MUST default to assuming that more context is needed. @@ -773,29 +773,36 @@ **1. OPERATION MODE** - Detect purely from user intent and target type. -- STANDARD → reading, explanation, or any non-code request -- PATCH_CODE → direct or localized code/file edits (≤2 targets, verbs like update, change, fix, insert, modify, add, create) -- PLAN_STEPS → multi-file, architectural, or ≥3 edit targets +- STANDARD → reading, explanation, documentation, or any non-code request +- PATCH_CODE → direct or localized code/file edits (≤2 targets, verbs like update, change, fix, insert, modify, add, create, refactor) +- PLAN_STEPS → multi-file, architectural changes, feature additions, or ≥3 edit targets --- **2. CONTEXT SUFFICIENCY** -- TRUE if all mentioned items (files, funcs, classes, objects, or patterns) exist in Code Identifiers -- FALSE if any are missing, unclear, or if there is any doubt about sufficiency +- TRUE if all mentioned items (files, funcs, classes, objects, modules, or patterns) exist in Code Identifiers +- FALSE if any are missing, unclear, ambiguous, or if there is any doubt about sufficiency --- **3. HISTORY COUNT** - If SUFFICIENT_CONTEXT = TRUE → HISTORY_COUNT = Interaction Count -- If FALSE → HISTORY_COUNT = number of previous turns required to restore missing info +- If FALSE → HISTORY_COUNT = number of previous turns required to restore missing info from conversation history --- -**4. SEARCH QUERY (conditional)** -- Only output when SUFFICIENT_CONTEXT = FALSE -- Provide a concise, targeted keyword or single pattern describing the missing **code patterns, files, classes, or objects** to search for in the codebase +**4. NEW TOPIC DETECTION** +- IS_NEW_TOPIC → TRUE if message indicates a new conversation topic or task, FALSE otherwise +- TOPIC_TITLE → 2-3 word title capturing the new topic (only if IS_NEW_TOPIC = TRUE, otherwise null) + +--- + +**5. SEARCH QUERY** +- Default value is "NO" +- Only provide a search query when SUFFICIENT_CONTEXT = FALSE +- Provide a concise, targeted keyword or single pattern describing the missing **code patterns, files, classes, functions, or modules** to search for in the codebase - Use only focused keywords or short phrases, not full sentences or verbose text -- If SUFFICIENT_CONTEXT = TRUE → omit this line completely +- If SUFFICIENT_CONTEXT = TRUE → must output "NO" --- @@ -803,7 +810,9 @@ OPERATION_MODE: [STANDARD|PATCH_CODE|PLAN_STEPS] SUFFICIENT_CONTEXT: [TRUE|FALSE] HISTORY_COUNT: [integer] -[optional search query only if context insufficient] +IS_NEW_TOPIC: [TRUE|FALSE] +TOPIC_TITLE: [2-3 word title or null] +SEARCH_QUERY: [search query or NO] """ DETERMINE_OPERATION_MODE_PROMPT = """ From d7156a1f709c7b9f3f489644f066561c373707b1 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 14 Nov 2025 23:50:04 +0000 Subject: [PATCH 128/138] prompt(tide): refactor calmness system prompt to improve CLI response style and clarity --- codetide/agents/tide/prompts.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 9459401..0a5a587 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -291,13 +291,24 @@ """ CALMNESS_SYSTEM_PROMPT = """ -Remain calm and do not rush into execution if the user's request is ambiguous, lacks sufficient context, or is not explicit enough to proceed safely. - -If you do not have all the information you need, or if any part of the request is unclear, you must pause and explicitly request the necessary context or clarification from the user before taking any action. - -Never make assumptions or proceed with incomplete information. Your priority is to ensure that every action is based on clear, explicit, and sufficient instructions. - -You must always produce a valid response, empty responses are not acceptable! +You are operating in a command line interface. Be concise, direct, and to the point. + +**Response Style:** +- Answer directly without elaboration, explanation, or details +- Avoid introductions, conclusions, and preambles +- Never use phrases like "The answer is...", "Here is...", "Based on...", or "I will..." +- One word answers are best when possible +- Share file names and code snippets when relevant to the query + +**Context Requirements:** +- Remain calm and do not rush into execution if the request is ambiguous or lacks sufficient context +- If any part of the request is unclear, explicitly request the necessary context or clarification before taking action +- Never make assumptions or proceed with incomplete information +- Ensure every action is based on clear, explicit, and sufficient instructions + +**Critical:** +- You must always produce a valid response +- Empty responses are not acceptable """ PREFIX_SUMMARY_PROMPT = """ From cc24f8767aa6e00b4c0ce9f1472fdc26629db32d Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 15 Nov 2025 00:12:18 +0000 Subject: [PATCH 129/138] prompt(tide): remove file names and code snippets guidance from response style --- codetide/agents/tide/prompts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 0a5a587..0d6d000 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -298,7 +298,6 @@ - Avoid introductions, conclusions, and preambles - Never use phrases like "The answer is...", "Here is...", "Based on...", or "I will..." - One word answers are best when possible -- Share file names and code snippets when relevant to the query **Context Requirements:** - Remain calm and do not rush into execution if the request is ambiguous or lacks sufficient context From 78e0da2869ed43cb0f324e260f79f0b9cbebff4f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 15 Nov 2025 00:26:06 +0000 Subject: [PATCH 130/138] docs: fix typographic apostrophes in README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 420ad73..524b445 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ CodeTide provides the following tools for agents: 2. **`getRepoTree`**: Explore the repository structure. #### Example: Initializing an LLM with CodeTide -Here’s a snippet from `agent_tide.py` demonstrating how to initialize an LLM with CodeTide as an MCP server: +Here's a snippet from `agent_tide.py` demonstrating how to initialize an LLM with CodeTide as an MCP server: ```python from aicore.llm import Llm, LlmConfig @@ -176,7 +176,7 @@ def init_llm() -> Llm: return llm ``` -This setup allows the LLM to leverage CodeTide’s tools for codebase interactions. +This setup allows the LLM to leverage CodeTide's tools for codebase interactions. CodeTide can now be used as an MCP Server! This allows seamless integration with AI tools and workflows. Below are the tools available: The available tools are: @@ -517,7 +517,7 @@ if __name__ == "__main__": ## 🧠 Philosophy -CodeTide is about giving developers structure-aware tools that are **fast, predictable, and private**. Your code is parsed, navigated, and queried as a symbolic graph - not treated as a black box of tokens. Whether you’re building, refactoring, or feeding context into an LLM - **you stay in control**. +CodeTide is about giving developers structure-aware tools that are **fast, predictable, and private**. Your code is parsed, navigated, and queried as a symbolic graph - not treated as a black box of tokens. Whether you're building, refactoring, or feeding context into an LLM - **you stay in control**. > Like a tide, your codebase evolves - and CodeTide helps you move with it, intelligently. @@ -539,7 +539,7 @@ Instead, it uses: ## 🗺️ Roadmap -Here’s what’s next for CodeTide: +Here's what's next for CodeTide: - 🧩 **Support more languages** already integrated with [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) → **TypeScript** is the top priority. **Now available in Beta** @@ -558,7 +558,7 @@ Here’s what’s next for CodeTide: CodeTide now includes an `agents` module, featuring **AgentTide**—a precision-driven software engineering agent that connects directly to your codebase and executes your requests with full code context. -**AgentTide** leverages CodeTide’s symbolic code understanding to: +**AgentTide** leverages CodeTide's symbolic code understanding to: - Retrieve and reason about relevant code context for any request - Generate atomic, high-precision patches using strict protocols - Apply changes directly to your codebase, with robust validation @@ -568,7 +568,7 @@ CodeTide now includes an `agents` module, featuring **AgentTide**—a precision- ### What It Does AgentTide acts as an autonomous agent that: -- Connects to your codebase using CodeTide’s parsing and context tools +- Connects to your codebase using CodeTide's parsing and context tools - Interacts with users via a conversational interface - Identifies relevant files, classes, and functions for any request - Generates and applies diff-style patches, ensuring code quality and requirements fidelity From 3fe7452ea5c54186100cd2913fe50761f26681f3 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 15 Nov 2025 01:01:37 +0000 Subject: [PATCH 131/138] refactor(agent,prompts): update context handling and summary prompt --- codetide/agents/tide/agent.py | 4 +++- codetide/agents/tide/prompts.py | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 5cb49d4..f30ee9b 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -756,6 +756,8 @@ def _build_code_context( ) -> Optional[str]: """Build code context from identifiers, falling back to tree view if needed.""" if code_identifiers: + ### TODO prefix this into: + # As you answer the user's questions, you can use the following context: return self.tide.get(code_identifiers, as_string=True) # Fallback to tree view and README @@ -943,7 +945,7 @@ async def agent_loop(self, code_identifiers: Optional[List[str]] = None): # Generate response history_with_context = ( - expanded_history + [code_context] if code_context else expanded_history + expanded_history[:-1] + [code_context] + expanded_history[-1:] if code_context else expanded_history ) response = await self.llm.acomplete( diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 0d6d000..f1b9838 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -311,18 +311,22 @@ """ PREFIX_SUMMARY_PROMPT = """ -You will receive a brief quickstart summary before the code context. -This summary is only a high-level guide to help you quickly understand the user's intent and the overall task scope. +**Quickstart Summary:** +{SUMMARY} -Use the summary together with the code context to produce a precise, complete, and high-quality answer. +--- -Important: -- You must always provide a meaningful and complete response to the user's message. -- Empty, generic, or evasive responses are not valid. -- Treat the summary as orientation only; rely on the code context for specific details. +**Instructions:** +The above summary provides a high-level overview of the user's intent and task scope. The code context has already been provided to you. -Quickstart Summary: -{SUMMARY} +Use both the summary and the code context together to produce a precise, complete, and high-quality response. + +**Critical Requirements:** +- You must provide a meaningful and complete response to the user's message +- Empty, generic, or evasive responses are not acceptable +- Treat the summary as orientation; rely on the code context for specific implementation details +- Be concise and direct in your response (CLI environment) +- Answer the user's question now based on all provided context """ REPO_TREE_CONTEXT_PROMPT = """ From ac663ccf39bacb0a84d54b990560ad9a505e4681 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 15 Nov 2025 01:05:12 +0000 Subject: [PATCH 132/138] refactor(agent): comment out early exit in identifier resolver --- codetide/agents/tide/agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index f30ee9b..7e7c778 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -257,10 +257,10 @@ async def gather_candidates( identifiers_from_search = {result[0] for result in search_results} # Early exit if all direct matches found - if direct_matches.issubset(identifiers_from_search): - candidate_pool = direct_matches - print("All matches found in identifiers from search") - break + # if identifiers_from_search.issubset(direct_matches): + # candidate_pool = identifiers_from_search + # print("All matches found in identifiers from search") + # break # Build filtered tree view candidates_to_filter = self.tide._as_file_paths(list(identifiers_from_search)) From a5e4c4099daa6893ea84e0392b1b1ab78660d9c6 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 15 Nov 2025 01:07:23 +0000 Subject: [PATCH 133/138] docs(readme): update agenttide usage instructions and descriptions --- README.md | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 524b445..07b9d25 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ uvx --from codetide codetide-cli --help ``` ## AgentTide -AgentTide consists of a demo, showing how CodeTide can integrate with LLMs and augment code generation and condebase related workflows. If you ask Tide to describe himself, he will say something like this: I'm the next-generation, precision-driven software engineering agent built on top of CodeTide. You can use it via the command-line interface (CLI) or a beautiful interactive UI. +AgentTide is a next-generation, precision-driven software engineering agent built on top of CodeTide. It is ready to help you dig deep into your codebase, automate code changes, and provide intelligent, context-aware assistance. You can use it via the command-line interface (CLI) or a beautiful interactive UI. -> **Demo available:** Try AgentTide live on Hugging Face Spaces: [https://mclovinittt-agenttidedemo.hf.space/](https://mclovinittt-agenttidedemo.hf.space/) +> **Try AgentTide live:** [https://mclovinittt-agenttidedemo.hf.space/](https://mclovinittt-agenttidedemo.hf.space/) --- @@ -57,35 +57,33 @@ AgentTide consists of a demo, showing how CodeTide can integrate with LLMs and a **AgentTide CLI** -To use the AgentTide conversational CLI, you must install the `[agents]` extra and launch via: +To use the AgentTide conversational CLI, install the `[agents]` extra and launch via: ```sh uvx --from codetide[agents] agent-tide ``` -This will start an interactive terminal session with AgentTide. - -You can also pass the `--project_path` argument to start AgentTide on a specific path: +This starts an interactive terminal session with AgentTide. You can specify a project path: ```sh uvx --from codetide[agents] agent-tide --project_path /path/to/your/project ``` -If you do not provide the `--project_path` argument, AgentTide will start in the current directory by default. +If `--project_path` is not provided, AgentTide starts in the current directory. **AgentTide UI** -To use the AgentTide web UI, you must install the `[agents-ui]` extra and launch via: +To use the AgentTide web UI, install the `[agents-ui]` extra and launch: ```sh uvx --from codetide[agents-ui] agent-tide-ui ``` -This will start a web server for the AgentTide UI. Follow the on-screen instructions to interact with the agent in your browser at [http://localhost:9753](http://localhost:9753) (or the port you specified) +This starts a web server for the AgentTide UI. Interact with the agent in your browser at [http://localhost:9753](http://localhost:9753) (or your specified port). -### Why Try AgentTide? ([Full Guide & Tips Here](codetide/agents/tide/ui/chainlit.md)) +### Why Use AgentTide? ([Full Guide & Tips Here](codetide/agents/tide/ui/chainlit.md)) -**Local-First & Private:** All code analysis and patching is performed locally. Your code never leaves your machine. +- **Local-First & Private:** All code analysis and patching is performed locally. Your code never leaves your machine. - **Transparent & Stepwise:** See every plan and patch before it's applied. Edit, reorder, or approve steps—you're always in control. - **Context-Aware:** AgentTide loads only the relevant code identifiers and dependencies for your request, making it fast and precise. - **Human-in-the-Loop:** After each step, review the patch, provide feedback, or continue—no black-box agent behavior. @@ -93,10 +91,10 @@ This will start a web server for the AgentTide UI. Follow the on-screen instruct **Usage Tips:** - If you know the exact code context, specify identifiers directly in your request (e.g., `module.submodule.file_withoutextension.object`). -- You can use the `plan` command to generate a step-by-step implementation plan for your request, review and edit the plan, and then proceed step-by-step. -- The `commit` command allows you to review and finalize changes before they are applied. +- Use the `plan` command to generate a step-by-step implementation plan for your request, review and edit the plan, and then proceed step-by-step. +- Use the `commit` command to review and finalize changes before they are applied. - See the [chainlit.md](codetide/agents/tide/ui/chainlit.md) for full details and advanced workflows, including the latest specifications for these commands! +See [chainlit.md](codetide/agents/tide/ui/chainlit.md) for full details and advanced workflows, including the latest specifications for these commands! --- @@ -554,9 +552,9 @@ Here's what's next for CodeTide: ## 🤖 Agents Module: AgentTide -> **Demo available:** Try AgentTide live on Hugging Face Spaces: [https://mclovinittt-agenttidedemo.hf.space/](https://mclovinittt-agenttidedemo.hf.space/) +> **Try AgentTide live:** [https://mclovinittt-agenttidedemo.hf.space/](https://mclovinittt-agenttidedemo.hf.space/) -CodeTide now includes an `agents` module, featuring **AgentTide**—a precision-driven software engineering agent that connects directly to your codebase and executes your requests with full code context. +CodeTide now includes an `agents` module, featuring **AgentTide**—a production-ready, precision-driven software engineering agent that connects directly to your codebase and executes your requests with full code context. **AgentTide** leverages CodeTide's symbolic code understanding to: - Retrieve and reason about relevant code context for any request @@ -567,14 +565,15 @@ CodeTide now includes an `agents` module, featuring **AgentTide**—a precision- - Source: [`codetide/agents/tide/agent.py`](codetide/agents/tide/agent.py) ### What It Does -AgentTide acts as an autonomous agent that: +AgentTide is an autonomous, precision-driven software engineering agent that: - Connects to your codebase using CodeTide's parsing and context tools -- Interacts with users via a conversational interface -- Identifies relevant files, classes, and functions for any request -- Generates and applies diff-style patches, ensuring code quality and requirements fidelity +- Interacts with users via a conversational interface (CLI or UI) +- Identifies relevant files, classes, and functions for any request using advanced identifier resolution and code search +- Generates and applies atomic, diff-style patches using a strict protocol, ensuring code quality and requirements fidelity +- Supports stepwise planning, patch review, and human-in-the-loop approval for every change ### Example Usage -To use AgentTide, ensure you have the `aicore` package installed (`pip install codetide[agents]`), then instantiate and run the agent: +To use AgentTide programmatically, ensure you have the `aicore` package installed (`pip install codetide[agents]`), then instantiate and run the agent: ```python from codetide import CodeTide @@ -599,10 +598,10 @@ if __name__ == "__main__": asyncio.run(main()) ``` -AgentTide will prompt you for requests, retrieve the relevant code context, and generate precise patches to fulfill your requirements. +AgentTide will prompt you for requests, retrieve the relevant code context, and generate precise, atomic patches to fulfill your requirements. All changes are patch-based and require explicit approval before being applied. -**Disclaimer:** -AgentTide is designed for focused, context-aware code editing, not for generating entire applications from vague ideas. While CodeTide as a platform can support larger workflows, the current version of AgentTide is optimized for making precise, well-scoped changes. For best results, provide one clear request at a time. AgentTide does not yet have access to your terminal or the ability to execute commands, but support for test-based validation is planned in future updates. +**Note:** +AgentTide is designed for focused, context-aware code editing, not for generating entire applications from vague ideas. For best results, provide one clear request at a time. AgentTide does not execute code or shell commands, but support for test-based validation is planned in future updates. For more details, see the [agents module source code](codetide/agents/tide/agent.py). From 1db713a1c4f0dbf0ed05796acad4e14c6624557e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 16 Nov 2025 15:21:42 +0000 Subject: [PATCH 134/138] refactor(agent): add default values to field extraction --- codetide/agents/tide/agent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 7e7c778..feed1c7 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -663,9 +663,9 @@ async def extract_operation_mode( ) # Extract fields from response - operation_mode = self._extract_field(response, "OPERATION_MODE") - sufficient_context = self._extract_field(response, "SUFFICIENT_CONTEXT") - history_count = self._extract_field(response, "HISTORY_COUNT") + operation_mode = self._extract_field(response, "OPERATION_MODE", "STANDARD") + sufficient_context = self._extract_field(response, "SUFFICIENT_CONTEXT", "FALSE") + history_count = self._extract_field(response, "HISTORY_COUNT", "2") is_new_topic = self._extract_field(response, "IS_NEW_TOPIC") topic_title = self._extract_field(response, "TOPIC_TITLE") search_query = self._extract_field(response, "SEARCH_QUERY") @@ -700,11 +700,11 @@ async def extract_operation_mode( ) @staticmethod - def _extract_field(text: str, field_name: str) -> Optional[str]: + def _extract_field(text: str, field_name: str, default :Optional[str]=None) -> Optional[str]: """Extract a field value from response text.""" pattern = rf'{field_name}:\s*\[?([^\]]+?)\]?(?:\n|$)' match = re.search(pattern, text) - return match.group(1) if match else None + return match.group(1) if match else default @staticmethod def _extract_search_query(response: str) -> Optional[str]: From 95a30e74827d566d2a00f20ca5311b2d5395ccc1 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 23 Nov 2025 22:16:43 +0000 Subject: [PATCH 135/138] feat(codetide): add module identifier handling and precheck functionality --- codetide/__init__.py | 95 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/codetide/__init__.py b/codetide/__init__.py index e1f9f5a..b791a32 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -14,6 +14,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator from typing import Optional, List, Tuple, Union, Dict from datetime import datetime, timezone +from collections import defaultdict from pathlib import Path import traceback import asyncio @@ -419,6 +420,7 @@ def _get_changed_files(self) -> Tuple[List[Path], bool]: """ file_deletion_detected = False files = self._find_code_files() # Dict[Path, datetime] + print("found code files") changed_files = [] @@ -535,6 +537,97 @@ def _is_file_content_valid(filepath :Path)->bool: return True + @staticmethod + def _is_subdirectory(identifier: str) -> bool: + """ + Check if an identifier represents a module/subdirectory. + + Args: + identifier: A string or Path object to check + + Returns: + True if the identifier ends with '/' (indicating a module), False otherwise + """ + if isinstance(identifier, Path): + return False + elif identifier.endswith("/"): + return True + else: + return False + + def get_module_identifiers(self, module_ids: List[str]) -> Dict[str, List[str]]: + """ + Get all file identifiers that belong to specified modules. + + Args: + module_ids: List of module identifier strings (directories) + + Returns: + Dictionary mapping module names to lists of relative file paths within each module + """ + module_paths = { + self.rootpath / module_id + for module_id in module_ids + } + modules_identifiers = defaultdict(list) + for filepath in self.files: + for module_path in module_paths: + if filepath.is_relative_to(module_path): + modules_identifiers[module_path.name].append( + str(filepath.relative_to(self.rootpath)) + ) + break + + # Log the results + logger.info(f"Found {len(modules_identifiers)} modules") + for module_name, identifiers in modules_identifiers.items(): + logger.info(f"Module '{module_name}' contains {len(identifiers)} identifiers") + + return modules_identifiers + + def inject_identifiers_from_modules(self, unique_ids: List[str]) -> List[str]: + """ + Expand module identifiers into their constituent file identifiers. + + Takes a list of identifiers that may include module directories, finds all files + within those modules, and replaces the module identifiers with individual file paths. + + Args: + unique_ids: List of identifiers, may include both files and modules (ending with '/') + + Returns: + Expanded list with module identifiers replaced by their constituent file identifiers + """ + modules_identifiers = [ + unique_id for unique_id in unique_ids if self._is_subdirectory(unique_id) + ] + identifiers_per_module = self.get_module_identifiers(module_ids=modules_identifiers) + + unique_ids = [ + unique_id for unique_id in unique_ids + if unique_id not in modules_identifiers + ] + for identifiers in identifiers_per_module.values(): + unique_ids.extend(identifiers) + + return unique_ids + + def precheck(self, unique_ids: List[str]) -> Dict[Path, str]: + """ + Preprocess and validate identifiers before further operations. + + Expands any module identifiers into their constituent files and validates + that all identifiers correspond to actual files. + + Args: + unique_ids: List of file or module identifiers to precheck + + Returns: + Dictionary mapping validated file paths to their identifier strings + """ + unique_ids = self.inject_identifiers_from_modules(unique_ids) + return self._precheck_id_is_file(unique_ids) + def _precheck_id_is_file(self, unique_ids : List[str])->Dict[Path, str]: """ Preload file contents for the given IDs if they correspond to known files. @@ -587,7 +680,7 @@ def get( f"Formats: string={as_string}, list={as_string_list}" ) - requested_files = self._precheck_id_is_file(code_identifiers) + requested_files = self.precheck(code_identifiers) return self.codebase.get( unique_id=code_identifiers, degree=context_depth, From 4506911a78be066f0e0b39af82703b2d7f3ca85a Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 23 Nov 2025 22:21:06 +0000 Subject: [PATCH 136/138] feat(codetide): add relative_directories property and update cached_ids --- codetide/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/codetide/__init__.py b/codetide/__init__.py index b791a32..20a86a9 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -98,7 +98,17 @@ def relative_filepaths(self)->List[str]: return [ str(filepath.relative_to(self.rootpath)).replace("\\", "/") for filepath in self.files ] - + + @property + def relative_directories(self) -> List[str]: + dirs = set() + for filepath in self.files: + p = filepath.resolve().parent + while p != self.rootpath: + dirs.add(p.relative_to(self.rootpath).as_posix()) + p = p.parent + return sorted(dirs) + @property def filenames_mapped(self)->Dict[str, str]: return { @@ -108,7 +118,7 @@ def filenames_mapped(self)->Dict[str, str]: @property def cached_ids(self)->List[str]: - return self.codebase.non_import_unique_ids+self.relative_filepaths + return self.codebase.non_import_unique_ids + self.relative_filepaths + self.relative_directories @property def repo(self)->Optional[pygit2.Repository]: From 1ba2354bedf60bddcb1148157d0428d01ce2c129 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Tue, 9 Dec 2025 21:56:40 +0000 Subject: [PATCH 137/138] build(deps): update dependencies for agents and agents-ui --- pyproject.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03fddab..566d65b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,10 @@ dependencies = [ [project.optional-dependencies] agents = [ + "aiofiles==23.2.1", "core-for-ai>=0.1.98", "prompt_toolkit==3.0.50", + "portalocker==3.2.0" # Required for the agent-tide CLI entry point ] visualization = [ @@ -47,12 +49,15 @@ visualization = [ "plotly==5.24.1", ] agents-ui = [ + "aiofiles==23.2.1", "core-for-ai>=0.1.98", "prompt_toolkit==3.0.50", + "portalocker==3.2.0", # Required for the agent-tide CLI entry point "chainlit==2.6.3", - "aiosqlite==0.21.0", - "SQLAlchemy==2.0.36" + "SQLAlchemy==2.0.36", + "asyncpg==0.30.0", + "docker==7.1.0" ] [project.scripts] From 20882632190d07f0843b5b732d41ae52b408b87c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Thu, 18 Dec 2025 22:31:53 +0000 Subject: [PATCH 138/138] fix(codetide): normalize path handling to preserve trailing slashes --- codetide/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/codetide/__init__.py b/codetide/__init__.py index 20a86a9..1a53f73 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -723,8 +723,11 @@ def get_unique_paths(path_list): unique_paths = [] for path in path_list: - # Normalize the path to use OS-appropriate separators - normalized = os.path.normpath(path) + if isinstance(path, str) and path.endswith("/"): + normalized = path + else: + # Normalize the path to use OS-appropriate separators + normalized = os.path.normpath(path) # Only add if we haven't seen this normalized path before if normalized not in seen: