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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
## Why `cecli`?

`cecli` (probably pronounced like "Cecily", aka `aider-ce`) is a community-driven fork of the [Aider](https://cecli.dev/) AI pair programming tool.
Aider is a fantastic piece of software with a wonderful community but it has been painfully slow in receiving updates in the quickly evolving AI tooling space.
`cecli` (probably pronounced like "Cecily") is yet another cli agent crafted for extensibility and customization. Originally a fork of the [Aider](https://cecli.dev/) AI pair programming tool, we aim to make agentic coding as maximally effective as it can be based on the growing capabilities of large language models.

We aim to foster an open, collaborative ecosystem where new features, experiments, and improvements can be developed and shared rapidly. We believe in genuine FOSS principles and actively welcome contributors of all skill levels.
We aim to foster an open, collaborative ecosystem where new features, experiments, and improvements can be developed and shared rapidly. We believe in the principles of FOSS and actively welcome contributors of all skill levels.

If you are looking for bleeding-edge features or want to get your hands dirty with the internals of an AI coding agent, here's your sign.

LLMs are a part of our lives from here on out so join us in learning about and crafting the future.

### Links
Expand Down
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.97.3.dev"
__version__ = "0.97.4.dev"
safe_version = __version__

try:
Expand Down
6 changes: 6 additions & 0 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ def get_parser(default_config_files, git_root):
default=3,
help="Maximum number of retries a model gets on malformed outputs (default: 3)",
)
group.add_argument(
"--cost-limit",
type=float,
default=None,
help="Cost limit per session, exceeding this forces prompt confirmation (default: None)",
)
group.add_argument(
"--file-diffs",
action=argparse.BooleanOptionalAction,
Expand Down
4 changes: 4 additions & 0 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ def __init__(self, *args, **kwargs):
self.skip_cli_confirmations = False
self.agent_finished = False
self.agent_config = self._get_agent_config()
self._setup_agent()
ToolRegistry.build_registry(agent_config=self.agent_config)
super().__init__(*args, **kwargs)

def _setup_agent(self):
os.makedirs(".cecli/workspace", exist_ok=True)

def _get_agent_config(self):
"""
Parse and return agent configuration from args.agent_config.
Expand Down
10 changes: 4 additions & 6 deletions cecli/coders/architect_coder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio

from ..commands import SwitchCoderSignal
from ..helpers.conversation import ConversationManager, MessageTag
from ..helpers.conversation import ConversationManager
from .ask_coder import AskCoder
from .base_coder import Coder

Expand Down Expand Up @@ -61,9 +61,7 @@ async def reply_completed(self):
editor_coder = await Coder.create(**new_kwargs)

# Re-initialize ConversationManager with editor coder
ConversationManager.initialize(
editor_coder, reset=True, reformat=True, preserve_tags=[MessageTag.DONE, MessageTag.CUR]
)
ConversationManager.initialize(editor_coder, reset=True, reformat=True, preserve_tags=True)

if self.verbose:
editor_coder.show_announcements()
Expand All @@ -84,7 +82,7 @@ async def reply_completed(self):
original_coder or self,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

self.total_cost = editor_coder.total_cost
Expand All @@ -96,7 +94,7 @@ async def reply_completed(self):
original_coder or self,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

raise SwitchCoderSignal(main_model=self.main_model, edit_format="architect")
129 changes: 28 additions & 101 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@

import httpx
from litellm import experimental_mcp_client
from litellm.types.utils import ChatCompletionMessageToolCall, Function, ModelResponse
from litellm.types.utils import ModelResponse
from prompt_toolkit.patch_stdout import patch_stdout
from rich.console import Console

import cecli.prompts.utils.system as prompts
from cecli import __version__, models, urls, utils
from cecli.commands import Commands, SwitchCoderSignal
from cecli.exceptions import LiteLLMExceptions
from cecli.helpers import command_parser, coroutines, nested
from cecli.helpers import command_parser, coroutines, nested, responses
from cecli.helpers.conversation import (
ConversationChunks,
ConversationManager,
Expand Down Expand Up @@ -157,6 +157,7 @@ class Coder:
last_user_message = ""
uuid = ""
model_kwargs = {}
cost_multiplier = 1

# Task coordination state variables
input_running = False
Expand Down Expand Up @@ -211,34 +212,14 @@ async def create(

use_kwargs = dict(from_coder.original_kwargs) # copy orig kwargs

# If the edit format changes, we can't leave old ASSISTANT
# messages in the chat history. The old edit format will
# confused the new LLM. It may try and imitate it, disobeying
# the system prompt.
# Get DONE messages from ConversationManager
done_messages = ConversationManager.get_messages_dict(MessageTag.DONE)
if edit_format != from_coder.edit_format and done_messages and summarize_from_coder:
try:
io.tool_warning("Summarizing messages, please wait...")
done_messages = await from_coder.summarizer.summarize_all(done_messages)
except (KeyboardInterrupt, ValueError):
# If summarization fails, keep the original messages and warn the user
io.tool_warning(
"Chat history summarization failed, continuing with full history"
)

# Bring along context from the old Coder
# Get CUR messages from ConversationManager
cur_messages = ConversationManager.get_messages_dict(MessageTag.CUR)

update = dict(
fnames=list(from_coder.abs_fnames),
read_only_fnames=list(from_coder.abs_read_only_fnames), # Copy read-only files
read_only_stubs_fnames=list(
from_coder.abs_read_only_stubs_fnames
), # Copy read-only stubs
done_messages=done_messages,
cur_messages=cur_messages,
done_messages=[],
cur_messages=[],
coder_commit_hashes=from_coder.coder_commit_hashes,
commands=from_coder.commands.clone(),
total_cost=from_coder.total_cost,
Expand Down Expand Up @@ -415,22 +396,6 @@ def __init__(
self.add_gitignore_files = add_gitignore_files
self.abs_read_only_stubs_fnames = set()

# Always use ConversationManager as the source of truth
# Add any provided messages to ConversationManager
if done_messages:
for msg in done_messages:
ConversationManager.add_message(
message_dict=msg,
tag=MessageTag.DONE,
)

if cur_messages:
for msg in cur_messages:
ConversationManager.add_message(
message_dict=msg,
tag=MessageTag.CUR,
)

self.io = io
self.io.coder = weakref.ref(self)

Expand Down Expand Up @@ -1616,6 +1581,18 @@ async def run_one(self, user_message, preproc):
self.reflected_message = None
self.tool_reflection = False

if float(self.total_cost) > self.cost_multiplier * (
nested.getter(self.args, "cost_limit", float("inf")) or float("inf")
):
if await self.io.confirm_ask(
"You have reached your configured cost limit. Continue?",
group_response="Cost Limit",
explicit_yes_required=True,
):
Coder.cost_multiplier += 1
else:
return

async for _ in self.send_message(message):
pass

Expand Down Expand Up @@ -2405,7 +2382,7 @@ async def send_message(self, inp):
force=True, # Force update existing message
)

if edited and self.auto_test:
if edited and self.auto_test and self.test_cmd:
test_errors = await self.commands.execute("test", self.test_cmd)
self.test_outcome = not test_errors
if test_errors:
Expand Down Expand Up @@ -3334,66 +3311,16 @@ def consolidate_chunks(self):
# If no native tool calls, check if the content contains JSON tool calls
# This handles models that write JSON in text instead of using native calling
if not self.partial_response_tool_calls and self.partial_response_content:
try:
# Simple extraction of JSON-like structures that look like tool calls
# Only look for tool calls if it looks like JSON
if "{" in self.partial_response_content or "[" in self.partial_response_content:
json_chunks = utils.split_concatenated_json(self.partial_response_content)
extracted_calls = []
chunk_index = 0

for chunk in json_chunks:
chunk_index += 1
try:
json_obj = json.loads(chunk)
if (
isinstance(json_obj, dict)
and "name" in json_obj
and "arguments" in json_obj
):
# Create a Pydantic model for the tool call
function_obj = Function(
name=json_obj["name"],
arguments=(
json.dumps(json_obj["arguments"])
if isinstance(json_obj["arguments"], (dict, list))
else str(json_obj["arguments"])
),
)
tool_call_obj = ChatCompletionMessageToolCall(
type="function",
function=function_obj,
id=f"call_{len(extracted_calls)}_{int(time.time())}_{chunk_index}",
)
extracted_calls.append(tool_call_obj)
elif isinstance(json_obj, list):
for item in json_obj:
if (
isinstance(item, dict)
and "name" in item
and "arguments" in item
):
function_obj = Function(
name=item["name"],
arguments=(
json.dumps(item["arguments"])
if isinstance(item["arguments"], (dict, list))
else str(item["arguments"])
),
)
tool_call_obj = ChatCompletionMessageToolCall(
type="function",
function=function_obj,
id=f"call_{len(extracted_calls)}_{int(time.time())}_{chunk_index}",
)
extracted_calls.append(tool_call_obj)
except json.JSONDecodeError:
continue

if extracted_calls:
self.partial_response_tool_calls = extracted_calls
except Exception:
pass
extracted_calls = responses.extract_tools_from_content_json(
self.partial_response_content
)
if not extracted_calls:
extracted_calls = responses.extract_tools_from_content_xml(
self.partial_response_content
)

if extracted_calls:
self.partial_response_tool_calls = extracted_calls

return response, func_err, content_err

Expand Down
6 changes: 3 additions & 3 deletions cecli/commands/agent_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import cecli.models as models
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers.conversation import ConversationManager, MessageTag
from cecli.helpers.conversation import ConversationManager


class AgentModelCommand(BaseCommand):
Expand Down Expand Up @@ -69,7 +69,7 @@ async def execute(cls, io, coder, args, **kwargs):
temp_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

verbose = kwargs.get("verbose", False)
Expand All @@ -86,7 +86,7 @@ async def execute(cls, io, coder, args, **kwargs):
original_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

# Restore the original model configuration
Expand Down
6 changes: 3 additions & 3 deletions cecli/commands/editor_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import cecli.models as models
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers.conversation import ConversationManager, MessageTag
from cecli.helpers.conversation import ConversationManager


class EditorModelCommand(BaseCommand):
Expand Down Expand Up @@ -69,7 +69,7 @@ async def execute(cls, io, coder, args, **kwargs):
temp_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

verbose = kwargs.get("verbose", False)
Expand All @@ -86,7 +86,7 @@ async def execute(cls, io, coder, args, **kwargs):
original_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

# Restore the original model configuration
Expand Down
6 changes: 3 additions & 3 deletions cecli/commands/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import cecli.models as models
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers.conversation import ConversationManager, MessageTag
from cecli.helpers.conversation import ConversationManager


class ModelCommand(BaseCommand):
Expand Down Expand Up @@ -73,7 +73,7 @@ async def execute(cls, io, coder, args, **kwargs):
temp_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

verbose = kwargs.get("verbose", False)
Expand All @@ -90,7 +90,7 @@ async def execute(cls, io, coder, args, **kwargs):
original_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

# Restore the original model configuration
Expand Down
1 change: 1 addition & 0 deletions cecli/commands/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ async def execute(cls, io, coder, args, **kwargs):
tokens_done = 0
tokens_cur = 0
tokens_diffs = 0
tokens_file_contexts = 0

if msgs_done:
tokens_done = coder.main_model.token_count(msgs_done)
Expand Down
8 changes: 3 additions & 5 deletions cecli/commands/utils/base_command.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABC, ABCMeta, abstractmethod
from typing import List

from cecli.helpers.conversation import ConversationManager, MessageTag
from cecli.helpers.conversation import ConversationManager


class CommandMeta(ABCMeta):
Expand Down Expand Up @@ -152,9 +152,7 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N
new_coder = await Coder.create(**kwargs)

# Re-initialize ConversationManager with new coder
ConversationManager.initialize(
new_coder, reset=True, reformat=True, preserve_tags=[MessageTag.DONE, MessageTag.CUR]
)
ConversationManager.initialize(new_coder, reset=True, reformat=True, preserve_tags=True)

await new_coder.generate(user_message=user_msg, preproc=False)
coder.coder_commit_hashes = new_coder.coder_commit_hashes
Expand All @@ -164,7 +162,7 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N
original_coder,
reset=True,
reformat=True,
preserve_tags=[MessageTag.DONE, MessageTag.CUR],
preserve_tags=True,
)

from cecli.commands import SwitchCoderSignal
Expand Down
Loading
Loading