Skip to content

Commit 94b82eb

Browse files
authored
Merge pull request #438 from dwash96/v0.97.0
V0.97.0
2 parents 0de366e + 4d66b24 commit 94b82eb

24 files changed

Lines changed: 845 additions & 854 deletions

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ The current priorities are to improve core capabilities and user experience of t
145145

146146
4. **Context Delivery** - [Discussion](https://github.com/dwash96/cecli/issues/47)
147147
* [ ] Use workflow for internal discovery to better target file snippets needed for specific tasks
148-
* [ ] Add support for partial files and code snippets in model completion messages
148+
* [x] Add support for partial files and code snippets in model completion messages
149149
* [x] Update message request structure for optimal caching
150150

151151
5. **TUI Experience** - [Discussion](https://github.com/dwash96/cecli/issues/48)
@@ -162,6 +162,38 @@ The current priorities are to improve core capabilities and user experience of t
162162
* [x] Add a plugin-like system for allowing agent mode to use user-defined tools in simple python files
163163
* [x] Add a dynamic tool discovery tool to allow the system to have only the tools it needs in context
164164

165+
7. **Sub Agents**
166+
* [ ] Add `/fork` and `/rejoin` commands to manually manage parts of the conversation history
167+
* [ ] Add an instance-able view of the conversation system so sub agents get their own context and workspaces
168+
* [ ] Modify coder classes to have discrete identifiers for themselves/management utilities for them to have their own slices of the world
169+
* [ ] Refactor global files like todo lists to live inside instance folders to avoid state conflicts
170+
* [ ] Add a `spawn` tool that launches a sub agent as a background command that the parent model waits for to finish
171+
* [ ] Add visibility into active sub agent calls in TUI
172+
173+
8. **Hooks**
174+
* [ ] Add hooks base class for user defined python hooks with an execute method with type and priority settings
175+
* [ ] Add hook manager that can accept user defined files and command line commands
176+
* [ ] Integrate hook manager with coder classes with hooks for `start`, `on_message`, `end_message`, `pre_tool`, and `post_tool`
177+
178+
9. **Efficient File Editing**
179+
* [ ] Explore use of hashline file representation for more targeted file editing
180+
* [ ] Assuming viability, update SEARCH part of SEARCH/REPLACE with hashline identification
181+
* [ ] Update agent mode edit tools to work with hashline identification
182+
* [ ] Update internal file diff representation to support hashline propagation
183+
184+
10. **Dynamic Context Management**
185+
* [ ] Update compaction to use observational memory sub agent calls to generate decision records that are used as the compaction basis
186+
* [ ] Persist decision records to disk for sessions with some settings for managing lifetimes of such persistence
187+
* [ ] Integrate RLM to extract information from decision records on disk and other definable notes
188+
* [ ] Add a "describe" tool that launches a sub agent workflow that populates an RLM call's context with:
189+
* Current Conversation History
190+
* Past Decision Records
191+
* Repo Map Found Files
192+
193+
11. **Quality of Life**
194+
* [ ] Add hot keys support for running repeatable commands like switching between preferred models
195+
* [ ] Unified error message logging inside of `.cecli` directory
196+
165197
### All Contributors (Both Cecli and Aider main)
166198

167199
<table>

cecli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from packaging import version
22

3-
__version__ = "0.96.10.dev"
3+
__version__ = "0.97.0.dev"
44
safe_version = __version__
55

66
try:

cecli/coders/agent_coder.py

Lines changed: 83 additions & 515 deletions
Large diffs are not rendered by default.

cecli/coders/base_coder.py

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import httpx
3333
from litellm import experimental_mcp_client
34-
from litellm.types.utils import ModelResponse
34+
from litellm.types.utils import ChatCompletionMessageToolCall, Function, ModelResponse
3535
from prompt_toolkit.patch_stdout import patch_stdout
3636
from rich.console import Console
3737

@@ -64,7 +64,7 @@
6464
from cecli.sessions import SessionManager
6565
from cecli.tools.utils.output import print_tool_response
6666
from cecli.tools.utils.registry import ToolRegistry
67-
from cecli.utils import format_tokens, is_image_file
67+
from cecli.utils import copy_tool_call, format_tokens, is_image_file
6868

6969
from ..dump import dump # noqa: F401
7070
from ..prompts.utils.registry import PromptObject, PromptRegistry
@@ -2357,23 +2357,19 @@ async def send_message(self, inp):
23572357
return
23582358

23592359
async def process_tool_calls(self, tool_call_response):
2360-
if tool_call_response is None:
2361-
return False
2362-
2363-
# Handle different response structures
2364-
try:
2365-
# Try to get tool calls from the standard OpenAI response format
2366-
if hasattr(tool_call_response, "choices") and tool_call_response.choices:
2367-
message = tool_call_response.choices[0].message
2368-
if hasattr(message, "tool_calls") and message.tool_calls:
2369-
original_tool_calls = message.tool_calls
2370-
else:
2371-
return False
2372-
else:
2373-
# Handle other response formats
2374-
return False
2375-
except (AttributeError, IndexError):
2376-
return False
2360+
# Use partial_response_tool_calls if available (populated by consolidate_chunks)
2361+
# otherwise try to extract from tool_call_response
2362+
original_tool_calls = []
2363+
if self.partial_response_tool_calls:
2364+
original_tool_calls = self.partial_response_tool_calls
2365+
elif tool_call_response is not None:
2366+
try:
2367+
if hasattr(tool_call_response, "choices") and tool_call_response.choices:
2368+
message = tool_call_response.choices[0].message
2369+
if hasattr(message, "tool_calls") and message.tool_calls:
2370+
original_tool_calls = message.tool_calls
2371+
except (AttributeError, IndexError):
2372+
pass
23772373

23782374
if not original_tool_calls:
23792375
return False
@@ -2404,10 +2400,13 @@ async def process_tool_calls(self, tool_call_response):
24042400
continue
24052401

24062402
# Create a new tool call for each JSON chunk, with a unique ID.
2407-
new_function = tool_call.function.model_copy(update={"arguments": chunk})
2408-
new_tool_call = tool_call.model_copy(
2409-
update={"id": f"{tool_call.id}-{i}", "function": new_function}
2410-
)
2403+
new_tool_call = copy_tool_call(tool_call)
2404+
if hasattr(new_tool_call, "model_copy"):
2405+
new_tool_call.function.arguments = chunk
2406+
new_tool_call.id = f"{tool_call.id}-{i}"
2407+
else:
2408+
new_tool_call.function.arguments = chunk
2409+
new_tool_call.id = f"{getattr(tool_call, 'id', 'call')}-{i}"
24112410
expanded_tool_calls.append(new_tool_call)
24122411

24132412
# Collect all tool calls grouped by server
@@ -2551,7 +2550,7 @@ async def _exec_server_tools(server, tool_calls_list):
25512550

25522551
all_results_content = []
25532552
for args in parsed_args_list:
2554-
new_tool_call = tool_call.model_copy(deep=True)
2553+
new_tool_call = copy_tool_call(tool_call)
25552554
new_tool_call.function.arguments = json.dumps(args)
25562555

25572556
call_result = await experimental_mcp_client.call_openai_tool(
@@ -2806,6 +2805,7 @@ def add_assistant_reply_to_cur_messages(self):
28062805
ConversationManager.add_message(
28072806
message_dict=msg,
28082807
tag=MessageTag.CUR,
2808+
hash_key=("assistant_message", str(msg), str(time.monotonic_ns())),
28092809
)
28102810

28112811
def get_file_mentions(self, content, ignore_current=False):
@@ -3202,22 +3202,9 @@ def consolidate_chunks(self):
32023202
# Add provider-specific fields directly to the tool call object
32033203
tool_call.provider_specific_fields = provider_specific_fields_by_index[i]
32043204

3205-
# Create dictionary version with provider-specific fields
3206-
tool_call_dict = tool_call.model_dump()
3207-
3208-
# Add provider-specific fields to the dictionary too (in case model_dump() doesn't include them)
3209-
if tool_id in provider_specific_fields_by_id:
3210-
tool_call_dict["provider_specific_fields"] = provider_specific_fields_by_id[
3211-
tool_id
3212-
]
3213-
elif i in provider_specific_fields_by_index:
3214-
tool_call_dict["provider_specific_fields"] = (
3215-
provider_specific_fields_by_index[i]
3216-
)
3217-
32183205
# Only append to partial_response_tool_calls if it's empty
32193206
if len(self.partial_response_tool_calls) == 0:
3220-
self.partial_response_tool_calls.append(tool_call_dict)
3207+
self.partial_response_tool_calls.append(tool_call)
32213208

32223209
self.partial_response_function_call = (
32233210
response.choices[0].message.tool_calls[0].function
@@ -3253,6 +3240,70 @@ def consolidate_chunks(self):
32533240
except AttributeError as e:
32543241
content_err = e
32553242

3243+
# If no native tool calls, check if the content contains JSON tool calls
3244+
# This handles models that write JSON in text instead of using native calling
3245+
if not self.partial_response_tool_calls and self.partial_response_content:
3246+
try:
3247+
# Simple extraction of JSON-like structures that look like tool calls
3248+
# Only look for tool calls if it looks like JSON
3249+
if "{" in self.partial_response_content or "[" in self.partial_response_content:
3250+
json_chunks = utils.split_concatenated_json(self.partial_response_content)
3251+
extracted_calls = []
3252+
chunk_index = 0
3253+
3254+
for chunk in json_chunks:
3255+
chunk_index += 1
3256+
try:
3257+
json_obj = json.loads(chunk)
3258+
if (
3259+
isinstance(json_obj, dict)
3260+
and "name" in json_obj
3261+
and "arguments" in json_obj
3262+
):
3263+
# Create a Pydantic model for the tool call
3264+
function_obj = Function(
3265+
name=json_obj["name"],
3266+
arguments=(
3267+
json.dumps(json_obj["arguments"])
3268+
if isinstance(json_obj["arguments"], (dict, list))
3269+
else str(json_obj["arguments"])
3270+
),
3271+
)
3272+
tool_call_obj = ChatCompletionMessageToolCall(
3273+
type="function",
3274+
function=function_obj,
3275+
id=f"call_{len(extracted_calls)}_{int(time.time())}_{chunk_index}",
3276+
)
3277+
extracted_calls.append(tool_call_obj)
3278+
elif isinstance(json_obj, list):
3279+
for item in json_obj:
3280+
if (
3281+
isinstance(item, dict)
3282+
and "name" in item
3283+
and "arguments" in item
3284+
):
3285+
function_obj = Function(
3286+
name=item["name"],
3287+
arguments=(
3288+
json.dumps(item["arguments"])
3289+
if isinstance(item["arguments"], (dict, list))
3290+
else str(item["arguments"])
3291+
),
3292+
)
3293+
tool_call_obj = ChatCompletionMessageToolCall(
3294+
type="function",
3295+
function=function_obj,
3296+
id=f"call_{len(extracted_calls)}_{int(time.time())}_{chunk_index}",
3297+
)
3298+
extracted_calls.append(tool_call_obj)
3299+
except json.JSONDecodeError:
3300+
continue
3301+
3302+
if extracted_calls:
3303+
self.partial_response_tool_calls = extracted_calls
3304+
except Exception:
3305+
pass
3306+
32563307
return response, func_err, content_err
32573308

32583309
def stream_wrapper(self, content, final):
@@ -3298,13 +3349,19 @@ def preprocess_response(self):
32983349
tool_list = []
32993350
tool_id_set = set()
33003351

3301-
for tool_call_dict in self.partial_response_tool_calls:
3352+
for tool_call in self.partial_response_tool_calls:
3353+
# Handle both dictionary and object tool calls
3354+
if isinstance(tool_call, dict):
3355+
tool_id = tool_call.get("id")
3356+
else:
3357+
tool_id = getattr(tool_call, "id", None)
3358+
33023359
# LLM APIs sometimes return duplicates and that's annoying part 2
3303-
if tool_call_dict.get("id") in tool_id_set:
3360+
if tool_id in tool_id_set:
33043361
continue
33053362

3306-
tool_id_set.add(tool_call_dict.get("id"))
3307-
tool_list.append(tool_call_dict)
3363+
tool_id_set.add(tool_id)
3364+
tool_list.append(tool_call)
33083365

33093366
self.partial_response_tool_calls = tool_list
33103367

0 commit comments

Comments
 (0)