Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 15 additions & 20 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll site to Pages
# Workflow for building and deploying the docmd site to GitHub Pages
name: Deploy site to Pages

on:
push:
Expand Down Expand Up @@ -40,21 +35,21 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Ruby
uses: ruby/setup-ruby@v1
- name: Setup Node
uses: actions/setup-node@v4
with:
ruby-version: '3.3' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
cache-version: 0 # Increment this number if you need to re-download cached gems
working-directory: '${{ github.workspace }}/cecli/website'
node-version: '20'
cache: npm
cache-dependency-path: 'cecli/website/package-lock.json'
- name: Setup Pages
id: pages
uses: actions/configure-pages@v3
- name: Build with Jekyll
# Outputs to the './_site' directory by default
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Install dependencies
run: npm ci
working-directory: cecli/website
- name: Build site with docmd
run: bash ../../scripts/docmd_build.sh
working-directory: cecli/website
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
Expand All @@ -71,7 +66,7 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
Expand All @@ -84,4 +79,4 @@ jobs:

- name: Run linkchecker
run: |
linkchecker --ignore-url='.+\.(mp4|mov|avi)' https://cecli.dev
linkchecker --ignore-url='.+\\.(mp4|mov|avi)' https://cecli.dev
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ env/
__pycache__/

# Ignore Folders
cecli/website/_site/*
cecli/website/_site/*
cecli/website/.sass-cache/*
cecli/website/.docmd-*/*
cecli/website/node_modules/*
34 changes: 20 additions & 14 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ensure that your contributions can be integrated smoothly.

```bash
# Clone the repository
git clone https://github.com/dwash96/cecli.git
git clone https://github.com/cecli-dev/cecli.git
cd cecli

# Make a venv
Expand Down Expand Up @@ -68,20 +68,20 @@ In order for your PR to be accepted it must:
2. Comply with project coding standards (including running the pre-commit formatting hooks)
3. Include test coverage
4. Update relevant user-facing documentation:
- Primary documentation will live in `aider/website/docs/config/`
- Primary documentation will live in `cecli/website/docs/config/`
- Check new cli arguments with the output of `/help` and `--help`

### Python Compatibility

cecli supports Python versions 3.10, 3.11, 3.12, 3.13, and 3.14. When contributing code, ensure compatibility with these supported Python versions.
Cecli supports Python versions 3.10, 3.11, 3.12, 3.13, and 3.14. When contributing code, ensure compatibility with these supported Python versions.

### Code Style

The project follows the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code, with a maximum line length of 100 characters. Additionally, the project uses [isort](https://pycqa.github.io/isort/) and [Black](https://black.readthedocs.io/en/stable/) for sorting imports and code formatting, respectively. Please install the pre-commit hooks to automatically format your code before committing changes.

### Testing

The project uses [pytest](https://docs.pytest.org/en/latest/) for running unit tests. The test files are located in the `aider/tests` directory and follow the naming convention `test_*.py`.
The project uses [pytest](https://docs.pytest.org/en/latest/) for running unit tests. The test files are located in the `tests` directory and follow the naming convention `test_*.py`.

#### Running Tests

Expand All @@ -105,17 +105,17 @@ The project uses GitHub Actions for continuous integration. The testing workflow
- `.github/workflows/ubuntu-tests.yml`: Runs tests on Ubuntu for Python versions 3.9 through 3.12.
- `.github/workflows/windows-tests.yml`: Runs that on Windows

These workflows are triggered on push and pull request events to the `main` branch, ignoring changes to the `aider/website/**` and `README.md` files.
These workflows are triggered on push and pull request events to the `main` branch, ignoring changes to the `cecli/website/**` and `README.md` files.

#### Docker Build and Test

The `.github/workflows/docker-build-test.yml` workflow is used to build a Docker image for the project on every push or pull request event to the `main` branch. It checks out the code, sets up Docker, logs in to DockerHub, and then builds the Docker image without pushing it to the registry.

#### Writing Tests

When contributing new features or making changes to existing code, ensure that you write appropriate tests to maintain code coverage. Follow the existing patterns and naming conventions used in the `aider/tests` directory.
When contributing new features or making changes to existing code, ensure that you write appropriate tests to maintain code coverage. Follow the existing patterns and naming conventions used in the `tests` directory.

If you need to mock or create test data, consider adding it to the test files or creating separate fixtures or utility functions within the `aider/tests` directory.
If you need to mock or create test data, consider adding it to the test files or creating separate fixtures or utility functions within the `tests` directory.

#### Test Requirements

Expand All @@ -142,21 +142,27 @@ You can also pass one argument to `pip-compile.sh`, which will flow through to `

### Building the Documentation

The project's documentation is built using Jekyll and hosted on GitHub Pages. To build the documentation locally, follow these steps:
The project's documentation is built with [docmd](https://docmd.io) and hosted on GitHub Pages. To build the documentation locally, follow these steps:

1. Install Ruby and Bundler (if not already installed).
2. Navigate to the `aider/website` directory.
3. Install the required gems:
1. Install Node.js 18 or newer (the build uses `npx`).
2. Navigate to the `cecli/website` directory.
3. Install the required npm dependencies:
```
bundle install
npm install
```
4. Build the documentation:
```
bundle exec jekyll build
npm run build
```
Or run the full site build (homepage + docs + assets) with:
```
bash ../../scripts/docmd_build.sh
```
5. Preview the website while editing (optional):
```
bundle exec jekyll serve
npm run dev
```

Built documentation is written to `cecli/website/_site` by `scripts/docmd_build.sh` (docs land in `_site/docs`, the homepage in `_site/index.html`).

The built documentation will be available in the `cecli/website/_site` directory.
14 changes: 9 additions & 5 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from datetime import datetime
from pathlib import Path

import xxhash

from cecli.args import AGENT_CONFIG_LIST_FIELDS
from cecli.change_tracker import ChangeTracker
from cecli.helpers import nested, responses
Expand Down Expand Up @@ -631,6 +633,8 @@ def format_chat_chunks(self):
ConversationService.get_chunks(self).add_readonly_files_messages()
ConversationService.get_chunks(self).add_chat_files_messages()

ConversationService.get_manager(self).flush_queue()

# Add post-message context blocks (priority 250 - between CUR and REMINDER)
ConversationService.get_chunks(self).add_post_message_context_blocks()

Expand Down Expand Up @@ -920,20 +924,20 @@ async def gather_and_await():
"# Fix the linting errors below, and then continue with your task.",
1,
)
lint_hash = xxhash.xxh3_128_hexdigest(lint_errors.encode("utf-8", errors="replace"))
ConversationService.get_manager(self).add_message(
message_dict=dict(role="user", content=lint_errors),
tag=MessageTag.LINT,
hash_key=("lint_errors", "agent", lint_errors),
hash_key=("lint_errors", "agent", lint_hash),
)
ConversationService.get_manager(self).add_message(
ConversationService.get_manager(self).queue_message(
message_dict=dict(
role="user", content="Please address the latest linting errors."
),
tag=MessageTag.LINT,
hash_key=("lint_errors", "agent", lint_errors, "cta"),
hash_key=("lint_errors", "agent", lint_hash, "cta"),
promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE,
mark_for_demotion=1,
mark_for_delete=0,
)
else:
if has_errors:
Expand Down Expand Up @@ -1351,7 +1355,7 @@ def _generate_tool_context(self, repetitive_tools):
)

if repetition_warning:
ConversationService.get_manager(self).add_message(
ConversationService.get_manager(self).queue_message(
message_dict=dict(role="user", content=repetition_warning),
tag=MessageTag.CUR,
hash_key=("repetition", "agent"),
Expand Down
48 changes: 43 additions & 5 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from collections import defaultdict
from datetime import date, datetime

import xxhash

# Optional dependency: used to convert locale codes (eg ``en_US``)
# into human-readable language names (eg ``English``).
try:
Expand All @@ -40,6 +42,7 @@
from cecli.helpers.conversation import ConversationService, MessageTag
from cecli.helpers.file_system import FileSystemService
from cecli.helpers.io_proxy import IOProxy
from cecli.helpers.memory_control import trim_memory
from cecli.helpers.observations.service import ObservationService
from cecli.helpers.profiler import TokenProfiler
from cecli.helpers.threading import ThreadSafeEvent
Expand Down Expand Up @@ -317,6 +320,7 @@ async def create(
mcp_manager=from_coder.mcp_manager,
registered_tools=copy.deepcopy(from_coder.registered_tools),
registered_servers=copy.deepcopy(from_coder.registered_servers),
auto_memory=from_coder.auto_memory,
uuid=from_coder.uuid,
parent_uuid=from_coder.parent_uuid,
repo=from_coder.repo,
Expand Down Expand Up @@ -1797,6 +1801,8 @@ async def generate(self, user_message, preproc):
self.run_one_completed = True
self.compact_context_completed = True
self.io.stop_spinner()
# Trim memory in the background so it doesn't stall the event loop
coroutines.fire_and_forget(asyncio.to_thread(trim_memory))

def copy_context(self):
if self.auto_copy_context:
Expand Down Expand Up @@ -1880,6 +1886,8 @@ async def run_one(self, user_message, preproc):
ConversationService.get_chunks(self).flush_removals()
self.last_user_message = user_message
self.error_code = None
# Trim memory in the background so it doesn't delay the response
coroutines.fire_and_forget(asyncio.to_thread(trim_memory))
# Fire memorizer after each user request
# if self.auto_memory and self.edit_format not in ["subagent"]:
# from cecli.helpers.memory.utils import invoke_memorizer
Expand Down Expand Up @@ -2439,6 +2447,8 @@ def format_chat_chunks(self):
# Add chat and edit file messages
ConversationService.get_chunks(self).add_chat_files_messages()

ConversationService.get_manager(self).flush_queue()

# Return formatted messages for LLM
return ConversationService.get_manager(self).get_messages_dict()

Expand Down Expand Up @@ -2567,12 +2577,14 @@ async def send_message(self, inp):
self.format_chat_chunks()

# Always add user message to conversation manager
ConversationService.get_manager(self).add_message(
ConversationService.get_manager(self).queue_message(
message_dict=dict(role="user", content=inp),
tag=MessageTag.CUR,
hash_key=("user_message", inp, str(time.monotonic_ns())),
promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE,
mark_for_demotion=1,
hash_key=(
"user_message",
xxhash.xxh3_128_hexdigest(inp.encode("utf-8", errors="replace")),
str(time.monotonic_ns()),
),
)

ConversationService.get_manager(self).decrement_message_markers()
Expand Down Expand Up @@ -2765,6 +2777,7 @@ async def format_in_executor():

await self.show_exhausted_error()
self.num_exhausted_context_windows += 1
self._release_response_buffers()
return
if self.partial_response_function_call:
args = self.parse_partial_args()
Expand Down Expand Up @@ -2798,6 +2811,9 @@ async def format_in_executor():
mark_for_demotion=1,
)

# The reply was interrupted mid-stream; drop the partial chunk buffers
# rather than holding them until the next send().
self._release_response_buffers()
return

edited = await self.apply_updates()
Expand Down Expand Up @@ -2883,6 +2899,11 @@ async def format_in_executor():
self.reflected_message = test_errors
return

# Turn complete: drop the per-turn LLM stream buffers. They are reset at
# the start of the next send(), so holding on to them while idle only
# wastes memory (chunks can be large for long streaming responses).
self._release_response_buffers()

def _extract_and_prepare_tool_calls(self, tool_call_response):
"""
Unified extraction and preparation of tool calls.
Expand Down Expand Up @@ -3401,6 +3422,16 @@ def __del__(self):
"""Cleanup when the Coder object is destroyed."""
self.ok_to_warm_cache = False

def _release_response_buffers(self):
"""Drop per-turn LLM stream data now that the turn has completed.

`partial_response_content` is intentionally kept: subclasses and callers
(get_edits, reply_completed, run_stream, ...) read it after the turn.
"""
self.partial_response_chunks = []
self.partial_response_consolidated = None
self.partial_response_reasoning_content = ""

async def add_assistant_reply_to_cur_messages(self):
"""
Add the assistant's reply to `cur_messages`.
Expand Down Expand Up @@ -3447,10 +3478,17 @@ async def add_assistant_reply_to_cur_messages(self):
self.io.tool_warning("Execution stopped by end message hook")
return

if self.edit_format in ("agent", "subagent"):
msg.pop("function_call", None)

ConversationService.get_manager(self).add_message(
message_dict=msg,
tag=MessageTag.CUR,
hash_key=("assistant_message", str(msg), str(time.monotonic_ns())),
hash_key=(
"assistant_message",
xxhash.xxh3_128_hexdigest(str(msg).encode("utf-8", errors="replace")),
str(time.monotonic_ns()),
),
# promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE,
# mark_for_demotion=1,
)
Expand Down
Loading
Loading