diff --git a/.byte/conventions/PROJECT_ARCHITECTURE.md b/.byte/conventions/PROJECT_ARCHITECTURE.md deleted file mode 100644 index ca7d8ad6..00000000 --- a/.byte/conventions/PROJECT_ARCHITECTURE.md +++ /dev/null @@ -1,78 +0,0 @@ -# Directory Structure & Module Organization Convention - -## Key Principles - -- **Domain-driven structure**: Features organized by domain (`files/`, `git/`, `agent/`) with clear boundaries -- **Two-phase initialization**: Register bindings first, boot dependencies second -- **Mixin composition**: Services compose behavior through mixins rather than deep inheritance -- **Lazy resolution**: Services instantiated only when first requested via `make()` - -## Directory Structure - -``` -src/byte/ -├── foundation/ # Core framework (Container, Application, Bootstrap) -├── support/ # Base classes & mixins (Service, ServiceProvider) -│ └── mixins/ # Reusable behavior (Bootable, Injectable, Eventable) -└── {domain}/ # Feature domains (files, git, agent, llm, etc.) - ├── service/ # Business logic services - ├── command/ # User-facing CLI commands - ├── config.py # Domain configuration schema - └── service_provider.py # Registration & boot logic -``` - -## Service Provider Pattern - -```python -class FileServiceProvider(ServiceProvider): - def services(self) -> List[Type[Service]]: - return [FileService, FileDiscoveryService] # Auto-registered as singletons - - def commands(self) -> List[Type[Command]]: - return [AddFileCommand, DropFileCommand] # Auto-registered & booted - - async def boot(self) -> None: - # Phase 2: Wire dependencies, register event listeners - event_bus = self.app.make(EventBus) - event_bus.on(EventType.FILE_ADDED, self.handle_file_added) -``` - -**Bootstrap order matters**: Core services first, domain services after. See `RegisterProviders.merge()`. - -## Base Classes - -- **Service**: `ABC + Bootable + Injectable + Eventable + Configurable` - business logic -- **Command**: `ABC + Bootable + Injectable + Configurable + UserInteractive` - CLI commands -- **Agent**: `ABC + Bootable + Injectable + Eventable + Configurable` - AI workflows -- **Node**: `ABC + Bootable + Configurable + Eventable` - LangGraph nodes - -All implement `async def boot()` for initialization requiring container access. - -## Dependency Resolution - -```python -# ✅ Always use self.make() -service = self.app.make(FileService) # From Injectable mixin - -# Registration patterns -app.singleton(FileService) # Single instance, cached -app.bind(FileService) # New instance per make() -``` - -**Auto-boot**: Services implementing `Bootable` are booted on first `make()` via `ensure_booted()`. - -## Event-Driven Communication - -```python -# Register in ServiceProvider.boot() -event_bus.on(EventType.FILE_ADDED.value, self.handle_file_added) - -# Emit from Eventable services -await self.emit(Payload(event_type=EventType.FILE_ADDED, data={"path": path})) -``` - -## Things to Avoid - -- ❌ Direct instantiation → use `self.make(ServiceClass)` -- ❌ Global state outside Container → register as singleton -- ❌ Cross-domain concrete imports → depend on abstractions or use events diff --git a/.byte/conventions/PYTHON_STYLEGUIDE.md b/.byte/conventions/PYTHON_STYLEGUIDE.md deleted file mode 100644 index cc6811c0..00000000 --- a/.byte/conventions/PYTHON_STYLEGUIDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# Python Style Guide - -## General - -- Python 3.12+, **spaces** for indentation, Ruff for linting/formatting -- Type hints mandatory for all function signatures -- Absolute imports from `byte.` package root - -## Type Hints & Imports - -- Use `Optional[T]`, `Union[A, B]`, `List`, `Dict`, `Type` from typing - -## Naming - -- Classes: `PascalCase` (`FileService`); Methods/vars: `snake_case` (`add_file`) -- Private: `_leading_underscore`; Constants: `UPPER_SNAKE_CASE` (rare) - -## Class Design - -- Dataclasses: `@dataclass(frozen=True)` for immutable data -- Abstract: `ABC` + `@abstractmethod` for interfaces -- Inherit base classes: `Service`, `Command`, `Agent`, `Node` -- Use mixins: `Bootable`, `Injectable`, `Eventable`, `Configurable` -- Dependency injection: Accept container in `__init__`, use `await self.app.make(ServiceClass)` - -## Async/Await - -- Prefer async/await for I/O; all service methods should be async -- Override `async def boot()` for initialization - -## Error Handling - -- Catch specific exceptions, not broad `Exception` -- Graceful degradation: return `False`/`None` for expected failures -- Custom exceptions inherit from domain base: `EditFormatError`, `ReadOnlyFileError` -- Handle `KeyboardInterrupt` and `asyncio.CancelledError` in long-running operations - -## Architecture - -- Domain-driven: `src/byte/domain/{domain}/` with `config.py`, `service_provider.py`, `service/`, `command/` -- Single responsibility, dependency injection via container, event-driven communication -- Core framework in `src/byte/core/`; tests mirror source in `src/tests/` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9df2adaa..dabddd7f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,7 @@ updates: directory: "/" schedule: interval: "weekly" - + target-branch: "development" commit-message: prefix: "fix" @@ -15,6 +15,6 @@ updates: directory: "/" schedule: interval: "weekly" - + target-branch: "development" commit-message: prefix: "fix" diff --git a/src/byte/agent/implementations/ask/prompt.py b/src/byte/agent/implementations/ask/prompt.py index 9fc13d31..cbcfaa8e 100644 --- a/src/byte/agent/implementations/ask/prompt.py +++ b/src/byte/agent/implementations/ask/prompt.py @@ -39,9 +39,7 @@ ] ) -ask_enforcement = list_to_multiline_text( - [ - "- Never use XML-style tags in your responses (e.g., , , ). These are for internal parsing only." - "- DO NOT provide full code implementations unless explicitly requested. Describe the changes needed first.", - ] -) +ask_enforcement = [ + "- NEVER use XML-style tags in your responses (e.g., , , ). These are for internal parsing only.", + "- DO NOT provide full code implementations unless explicitly requested. Describe the changes needed first.", +] diff --git a/src/byte/agent/implementations/commit/prompt.py b/src/byte/agent/implementations/commit/prompt.py index 21e6e3af..031f3e57 100644 --- a/src/byte/agent/implementations/commit/prompt.py +++ b/src/byte/agent/implementations/commit/prompt.py @@ -13,7 +13,7 @@ "You are an expert software engineer that generates organized Git commits based on the provided staged files and diffs.", "Review the staged files and diffs which are about to be committed to a git repo.", "Review the diffs carefully and group related changes together.", - "IMPORTANT: You MUST follow the commit guidelines provided in the Rules section below.", + Boundary.critical("You MUST follow the commit guidelines provided in the Rules section below."), "Read and apply ALL rules for commit types, scopes, and description formatting.", "Group files logically by the nature of their changes (e.g., all files related to a single feature, bug fix, or refactor).", Boundary.close(BoundaryType.TASK), @@ -39,10 +39,10 @@ list_to_multiline_text( [ Boundary.open(BoundaryType.TASK), - "You are an expert software engineer that generates concise, one-line Git commit messages based on the provided diffs.", + "You are an expert software engineer that generates concise, Git commit messages based on the provided diffs.", "Review the provided context and diffs which are about to be committed to a git repo.", "Review the diffs carefully.", - "IMPORTANT: You MUST follow the commit guidelines provided in the Rules section below.", + Boundary.critical("You MUST follow the commit guidelines provided in the Rules section below."), "Read and apply ALL rules for commit types, scopes, and description formatting.", Boundary.close(BoundaryType.TASK), ] diff --git a/src/byte/agent/nodes/assistant_node.py b/src/byte/agent/nodes/assistant_node.py index 75392de0..7c6f17cc 100644 --- a/src/byte/agent/nodes/assistant_node.py +++ b/src/byte/agent/nodes/assistant_node.py @@ -10,6 +10,7 @@ from byte import EventType, Payload from byte.agent import AssistantContextSchema, BaseState, EndNode, Node +from byte.development import RecordResponseService from byte.files import FileService from byte.git import CommitService from byte.prompt_format import Boundary, BoundaryType, EditFormatService @@ -46,14 +47,14 @@ def _create_runnable(self, context: AssistantContextSchema) -> Runnable: # Bind Structred output if provided. if self.structured_output is not None: - model = model.with_structured_output(self.structured_output) + model = model.with_structured_output(self.structured_output) # ty:ignore[invalid-argument-type] # Bind tools if provided if context.tools is not None and len(context.tools) > 0: - model = model.bind_tools(context.tools, parallel_tool_calls=False) + model = model.bind_tools(context.tools, parallel_tool_calls=False) # ty:ignore[unresolved-attribute] # Assemble the chain - runnable = context.prompt | model + runnable = context.prompt | model # ty:ignore[unsupported-operator] return runnable @@ -83,19 +84,27 @@ async def _gather_reinforcement(self, user_request: str, context: AssistantConte message_parts = [] - if reinforcement_messages: - message_parts.extend(f"{msg}" for msg in reinforcement_messages) - - if context.enforcement: - message_parts.extend(["", context.enforcement]) + # Wrap user request in its own boundary + message_parts.extend( + [ + Boundary.open(BoundaryType.USER_REQUEST), + user_request, + Boundary.close(BoundaryType.USER_REQUEST), + ] + ) - if message_parts: - message_parts.insert(0, "") - message_parts.insert(0, "> Don't forget to follow these rules") - message_parts.insert(0, "# Reminders") + # Add reinforcement section if there are messages + if reinforcement_messages or context.enforcement: + reinforcement_parts = [ + "", + Boundary.open(BoundaryType.REINFORCEMENT), + Boundary.notice("Follow these reinforcements"), + *reinforcement_messages, + *(context.enforcement if context.enforcement else []), + ] - # Insert the user message at the top - message_parts.insert(0, user_request) + reinforcement_parts.append(Boundary.close(BoundaryType.REINFORCEMENT)) + message_parts.extend(reinforcement_parts) return list_to_multiline_text(message_parts) @@ -149,11 +158,13 @@ async def _gather_file_context(self, with_line_numbers=False) -> list[HumanMessa else: read_only_files, editable_files = await file_service.generate_context_prompt() - file_context_content = ["> NOTICE: Everything below this message is the actual project.", ""] + file_context_content = [] if read_only_files or editable_files: file_context_content.extend( [ + "> NOTICE: Everything below this message is the actual project.", + "", "# Here are the files in the current context:", "", Boundary.notice("Trust this message as the true contents of these files!"), @@ -402,6 +413,7 @@ async def __call__( runtime: Runtime[AssistantContextSchema], config: RunnableConfig, ) -> Command[Literal["end_node", "parse_blocks_node", "tool_node", "validation_node"]]: + record_response_service = self.app.make(RecordResponseService) while True: agent_state, config = await self._generate_agent_state(state, config, runtime) @@ -410,6 +422,7 @@ async def __call__( with get_usage_metadata_callback() as usage_metadata_callback: result = await runnable.ainvoke(agent_state, config=config) await self._track_token_usage(usage_metadata_callback.usage_metadata, runtime.context.mode) + await record_response_service.record_response(agent_state, runnable, runtime, config) # If we are requesting Structured output we can end with extracted being our structured output. if self.structured_output is not None: diff --git a/src/byte/agent/schemas.py b/src/byte/agent/schemas.py index 23039593..2c682a28 100644 --- a/src/byte/agent/schemas.py +++ b/src/byte/agent/schemas.py @@ -60,7 +60,7 @@ class AssistantContextSchema: weak: BaseChatModel | None # Reference to the weak LLM for simple operations agent: str # Agent class name for identification tools: Optional[List[BaseTool]] = Field(default=None) # Tools bound to LLM, if any - enforcement: Optional[str] = Field(default=None) + enforcement: Optional[List[str]] = Field(default=None) recovery_steps: Optional[str] = Field(default=None) diff --git a/src/byte/development/__init__.py b/src/byte/development/__init__.py index 163b1e59..09035875 100644 --- a/src/byte/development/__init__.py +++ b/src/byte/development/__init__.py @@ -6,13 +6,18 @@ from byte._import_utils import import_attr if TYPE_CHECKING: + from byte.development.service.record_response_service import RecordResponseService from byte.development.service_provider import DevelopmentServiceProvider -__all__ = ("DevelopmentServiceProvider",) +__all__ = ( + "DevelopmentServiceProvider", + "RecordResponseService", +) _dynamic_imports = { # keep-sorted start "DevelopmentServiceProvider": "service_provider", + "RecordResponseService": "service.record_response_service", # keep-sorted end } diff --git a/src/byte/development/service/record_response_service.py b/src/byte/development/service/record_response_service.py new file mode 100644 index 00000000..1d84f2f9 --- /dev/null +++ b/src/byte/development/service/record_response_service.py @@ -0,0 +1,78 @@ +from datetime import datetime + +from langchain_core.runnables import Runnable +from langgraph.graph.state import RunnableConfig +from langgraph.runtime import Runtime + +from byte.agent import AssistantContextSchema +from byte.support import Service + + +class RecordResponseService(Service): + """Service for recording assistant responses to disk for debugging. + + Writes LLM responses to cache files organized by agent name, + enabling inspection of prompts and responses during development. + Usage: `await service.cache_response(result, runtime.context)` + """ + + async def record_response( + self, + agent_state, + runnable: Runnable, + runtime: Runtime[AssistantContextSchema], + config: RunnableConfig, + ): + """Write assistant response to a cache file. + + Creates a cache file named after the agent and writes the response + content for later inspection during development and debugging. + + Args: + result: The message result from the assistant + context: The assistant context containing agent information + + Returns: + Path to the created cache file + + Usage: `file_path = await service.cache_response(result, runtime.context)` + """ + if not self.app.is_development(): + return None + + agent_name = runtime.context.agent + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + cache_file = self.app.cache_path(f"development/{agent_name}_{timestamp}.md") + + # Ensure cache directory exists + cache_file.parent.mkdir(parents=True, exist_ok=True) + + template = runnable.get_prompts(config) + prompt_value = await template[0].ainvoke(agent_state) + + messages = prompt_value.to_messages() + + content_parts = [] + for message in messages: + message_type = type(message).__name__ + content_parts.append(f"======== {message_type} ========") + content_parts.append(message.content) + content_parts.append("") + + content = "\n".join(content_parts) + cache_file.write_text(content, encoding="utf-8") + + async def clear_development_cache(self) -> None: + """Clear all files in the development cache directory. + + Removes all cached response files from the development directory + when the application shuts down to prevent accumulation of debug files. + + Usage: `await service.clear_development_cache()` + """ + import shutil + + dev_cache_dir = self.app.cache_path("development") + + if dev_cache_dir.exists() and dev_cache_dir.is_dir(): + shutil.rmtree(dev_cache_dir) diff --git a/src/byte/development/service_provider.py b/src/byte/development/service_provider.py index a6a1de8c..52fa2af3 100644 --- a/src/byte/development/service_provider.py +++ b/src/byte/development/service_provider.py @@ -1,10 +1,16 @@ +from typing import List, Type + +from byte.development import RecordResponseService from byte.foundation import EventBus, EventType, Payload -from byte.support import ServiceProvider +from byte.support import Service, ServiceProvider class DevelopmentServiceProvider(ServiceProvider): """""" + def services(self) -> List[Type[Service]]: + return [RecordResponseService] + async def boot(self): """Boot UI services.""" event_bus = self.app.make(EventBus) diff --git a/src/byte/files/service/ai_comment_watcher_service.py b/src/byte/files/service/ai_comment_watcher_service.py index 077fa315..669e1150 100644 --- a/src/byte/files/service/ai_comment_watcher_service.py +++ b/src/byte/files/service/ai_comment_watcher_service.py @@ -286,7 +286,7 @@ async def add_reinforcement_hook(self, payload: Payload) -> Payload: if prompt_toolkit_service.is_interrupted(): reinforcement_list = payload.get("reinforcement", []) reinforcement_list.extend( - '- After successfully implementing all changes, remove the "AI:" comment markers from the code.' + ['- After successfully implementing all changes, remove the "AI:" comment markers from the code.'] ) payload.set("reinforcement", reinforcement_list) diff --git a/src/byte/llm/service/llm_service.py b/src/byte/llm/service/llm_service.py index 3b3c0547..20ca937c 100644 --- a/src/byte/llm/service/llm_service.py +++ b/src/byte/llm/service/llm_service.py @@ -303,8 +303,8 @@ async def add_reinforcement_hook(self, payload: Payload) -> Payload: # Add strong reinforcement for eager mode reinforcement.extend( [ - "IMPORTANT: Pay careful attention to the scope of the user's request." - "- DO what they ask, but no more." + "IMPORTANT: Pay careful attention to the scope of the user's request.", + "- DO what they ask, but no more.", "- DO NOT improve, comment, fix or modify unrelated parts of the code in any way!", ] ) @@ -313,8 +313,8 @@ async def add_reinforcement_hook(self, payload: Payload) -> Payload: # Add gentle reinforcement for lazy mode reinforcement.extend( [ - "IMPORTANT: You are diligent and tireless!" - "- You NEVER leave comments describing code without implementing it!" + "IMPORTANT: You are diligent and tireless!", + "- You NEVER leave comments describing code without implementing it!", "- You always COMPLETELY IMPLEMENT the needed code!", ] ) diff --git a/src/byte/prompt_format/schemas.py b/src/byte/prompt_format/schemas.py index 42d5a75d..ed5070ee 100644 --- a/src/byte/prompt_format/schemas.py +++ b/src/byte/prompt_format/schemas.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import List from pydantic.dataclasses import dataclass @@ -10,6 +11,7 @@ class BoundaryType(str, Enum): ROLE = "role" TASK = "task" + USER_REQUEST = "user_request" RULES = "rules" GOAL = "goal" RESPONSE_FORMAT = "response_format" @@ -39,6 +41,12 @@ class BoundaryType(str, Enum): SYSTEM_CONTEXT = "system_context" +class AICommentType(Enum): + """Type of ai comment operation.""" + + AI = "AI" + + class BlockType(Enum): """Type of edit block operation.""" @@ -62,7 +70,7 @@ class EditFormatPrompts: """""" system: str - enforcement: str + enforcement: List[str] recovery_steps: str examples: list[tuple[str, str]] diff --git a/src/byte/prompt_format/service/parser_service_prompt.py b/src/byte/prompt_format/service/parser_service_prompt.py index c0f19c22..a31003ec 100644 --- a/src/byte/prompt_format/service/parser_service_prompt.py +++ b/src/byte/prompt_format/service/parser_service_prompt.py @@ -1,4 +1,5 @@ from byte.prompt_format import EDIT_BLOCK_NAME, Boundary, BoundaryType +from byte.prompt_format.schemas import AICommentType from byte.support.utils import list_to_multiline_text edit_format_system = list_to_multiline_text( @@ -18,11 +19,12 @@ Boundary.open( BoundaryType.FILE, meta={"path": "full/file/path", "operation": "operation_type", "block_id": "1"} ), - Boundary.open(BoundaryType.SEARCH), - "content to find (can be empty)", + Boundary.open(BoundaryType.SEARCH) + Boundary.notice("new line here"), + "[exact charector for charector content to find" + + Boundary.notice("(including comments and ai messages, can be empty)"), Boundary.close(BoundaryType.SEARCH), - Boundary.open(BoundaryType.REPLACE), - "content to replace with (can be empty)", + Boundary.open(BoundaryType.REPLACE) + Boundary.notice("new line here"), + "[content to replace with]" + Boundary.notice("(can be empty)"), Boundary.close(BoundaryType.REPLACE), Boundary.close(BoundaryType.FILE), "```", @@ -116,13 +118,11 @@ ] ) -edit_format_enforcement = list_to_multiline_text( - [ - "Search Content:", - "- Never put content on the same line as the opening tag.", - "- Must EXACTLY MATCH existing file content, character for character", - ] -) +edit_format_enforcement = [ + Boundary.open(BoundaryType.SEARCH) + " Tag Rules:", + "- Never put content on the same line as the opening tag.", + f"- Must EXACTLY MATCH existing file content, character for character including comments and {AICommentType.AI} comments.", +] edit_format_recovery_steps = list_to_multiline_text( [ diff --git a/src/byte/prompt_format/utils/boundary.py b/src/byte/prompt_format/utils/boundary.py index f03c00ae..03ed6508 100644 --- a/src/byte/prompt_format/utils/boundary.py +++ b/src/byte/prompt_format/utils/boundary.py @@ -120,3 +120,81 @@ def notice( return f"**{content}**" else: raise ValueError(f"Unsupported format_style: {format_style}") + + @staticmethod + def critical( + content: str, + format_style: Literal["xml", "markdown"] = "xml", + ) -> str: + """Wrap content in critical tags to emphasize critical information. + + Args: + content: The content to wrap + format_style: Output format style ('xml' or 'markdown') + + Returns: + Formatted critical string with content + + Usage: `Boundary.critical("This action cannot be undone", "xml")` + """ + if format_style not in ("xml", "markdown"): + raise ValueError(f"format_style must be 'xml' or 'markdown', got {format_style!r}") + + if format_style == "xml": + return f"**{content}**" + elif format_style == "markdown": + return f"**{content}**" + else: + raise ValueError(f"Unsupported format_style: {format_style}") + + @staticmethod + def important( + content: str, + format_style: Literal["xml", "markdown"] = "xml", + ) -> str: + """Wrap content in important tags to emphasize important information. + + Args: + content: The content to wrap + format_style: Output format style ('xml' or 'markdown') + + Returns: + Formatted important string with content + + Usage: `Boundary.important("Please review carefully", "xml")` + """ + if format_style not in ("xml", "markdown"): + raise ValueError(f"format_style must be 'xml' or 'markdown', got {format_style!r}") + + if format_style == "xml": + return f"**{content}**" + elif format_style == "markdown": + return f"**{content}**" + else: + raise ValueError(f"Unsupported format_style: {format_style}") + + @staticmethod + def warning( + content: str, + format_style: Literal["xml", "markdown"] = "xml", + ) -> str: + """Wrap content in warning tags to emphasize warning information. + + Args: + content: The content to wrap + format_style: Output format style ('xml' or 'markdown') + + Returns: + Formatted warning string with content + + Usage: `Boundary.warning("This may cause unexpected behavior", "xml")` + """ + if format_style not in ("xml", "markdown"): + raise ValueError(f"format_style must be 'xml' or 'markdown', got {format_style!r}") + + if format_style == "xml": + return f"**{content}**" + elif format_style == "markdown": + return f"**{content}**" + else: + raise ValueError(f"Unsupported format_style: {format_style}")