From b4ac4c6fe23513c67341643c32b564aa26af1f58 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Apr 2026 21:33:22 -0400 Subject: [PATCH 1/5] Clear invocation cache every 3 unique tools --- cecli/tools/utils/base_tool.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cecli/tools/utils/base_tool.py b/cecli/tools/utils/base_tool.py index ce981bbfd25..f35f2ebf3ef 100644 --- a/cecli/tools/utils/base_tool.py +++ b/cecli/tools/utils/base_tool.py @@ -14,6 +14,7 @@ class BaseTool(ABC): # Invocation tracking for detecting repeated tool calls _invocations = {} # Dict to store last 3 invocations per tool + _invocation_summary = set() # Set to track distinct tool names TRACK_INVOCATIONS = True # Default to True, subclasses can override @classmethod @@ -86,6 +87,18 @@ def process_response(cls, coder, params): else: tool_name = cls.__name__ + # Check if adding this tool would reach 3 distinct tools + # If so, clear all tracking and start fresh without counting this tool + if ( + len(cls._invocation_summary) + (0 if tool_name in cls._invocation_summary else 1) + >= 3 + ): + cls._invocations.clear() + cls._invocation_summary.clear() + else: + # Only add to summary if we didn't just clear everything + cls._invocation_summary.add(tool_name) + # Initialize invocation tracking for this tool if not exists if tool_name not in cls._invocations: cls._invocations[tool_name] = [] @@ -127,3 +140,4 @@ def on_duplicate_request(cls, coder, **kwargs): @classmethod def clear_invocation_cache(cls): cls._invocations.clear() + cls._invocation_summary.clear() From 035f3fb87d4c96866ff73a6e45f850b1271aaa62 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 12:01:18 -0400 Subject: [PATCH 2/5] Update cymbal version for directory change fix --- requirements.txt | 2 +- requirements/common-constraints.txt | 2 +- requirements/requirements.in | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 954d9ec1a6e..da3062df4b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -274,7 +274,7 @@ ptyprocess==0.7.0 # via # -c requirements/common-constraints.txt # pexpect -py-cymbal==0.1.5 +py-cymbal==0.1.6 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in diff --git a/requirements/common-constraints.txt b/requirements/common-constraints.txt index 6e526a7c092..aa5ecdc63fe 100644 --- a/requirements/common-constraints.txt +++ b/requirements/common-constraints.txt @@ -345,7 +345,7 @@ psutil==7.1.3 # via -r requirements/requirements.in ptyprocess==0.7.0 # via pexpect -py-cymbal==0.1.5 +py-cymbal==0.1.6 # via -r requirements/requirements.in pycodestyle==2.14.0 # via flake8 diff --git a/requirements/requirements.in b/requirements/requirements.in index edd319f89b5..46ff0b74403 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -32,7 +32,7 @@ textual>=6.0.0 tomlkit>=0.14.0 truststore xxhash>=3.6.0 -py-cymbal>=0.1.5 +py-cymbal>=0.1.6 # Replaced networkx with rustworkx for better performance in repomap rustworkx>=0.15.0 From d8b8c0a1bcfc7f6fb8745f3486341ede488d6c1c Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 12:01:34 -0400 Subject: [PATCH 3/5] Let ls tool clear invocation cache --- cecli/tools/ls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cecli/tools/ls.py b/cecli/tools/ls.py index c6d19761fe3..9a9ed276340 100644 --- a/cecli/tools/ls.py +++ b/cecli/tools/ls.py @@ -7,6 +7,7 @@ class Tool(BaseTool): NORM_NAME = "ls" + TRACK_INVOCATIONS = False SCHEMA = { "type": "function", "function": { From 06855eebeae8d50631615af34f1a466901515d2f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 12:02:57 -0400 Subject: [PATCH 4/5] Updates to message ordering for cache efficiency and relying on other updates to handle repetition prevention --- cecli/coders/agent_coder.py | 12 ++++++------ cecli/coders/base_coder.py | 8 ++++---- cecli/helpers/conversation/files.py | 4 ++-- cecli/helpers/conversation/integration.py | 4 ++-- cecli/prompts/agent.yml | 13 +++++++------ 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index 37cb3ed920e..a3cf86b629e 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -1016,25 +1016,25 @@ def _generate_tool_context(self, repetitive_tools): if not self.model_kwargs: self.model_kwargs = { - "temperature": default_temp + 0.1, - "frequency_penalty": default_fp + 0.2, + "temperature": default_temp + 2**-5, + "frequency_penalty": default_fp + 2**-5, # "presence_penalty": 0.1, } else: temperature = nested.getter(self.model_kwargs, "temperature", default_temp) freq_penalty = nested.getter(self.model_kwargs, "frequency_penalty", default_fp) - self.model_kwargs["temperature"] = temperature + 0.1 - self.model_kwargs["frequency_penalty"] = freq_penalty + 0.1 + self.model_kwargs["temperature"] = temperature + 2**-5 + self.model_kwargs["frequency_penalty"] = freq_penalty + 2**-5 if random.random() < 0.2: self.model_kwargs["temperature"] = max( default_temp, - temperature - 0.15, + temperature - 2**-4, ) self.model_kwargs["frequency_penalty"] = max( default_fp, - freq_penalty - 0.15, + freq_penalty - 2**-4, ) self.model_kwargs["temperature"] = max( diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 1c221547ee9..0dd6203e298 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -2737,8 +2737,8 @@ async def process_tool_calls(self, tool_call_response): message_dict=tool_response, tag=MessageTag.CUR, hash_key=(tool_response["tool_call_id"], str(time.monotonic_ns())), - promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE, - mark_for_demotion=1, + # promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE, + # mark_for_demotion=1, ) return bool(tool_responses) @@ -2925,8 +2925,8 @@ async def add_assistant_reply_to_cur_messages(self): message_dict=msg, tag=MessageTag.CUR, hash_key=("assistant_message", str(msg), str(time.monotonic_ns())), - promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE, - mark_for_demotion=1, + # promotion=ConversationService.get_manager(self).DEFAULT_TAG_PROMOTION_VALUE, + # mark_for_demotion=1, ) def get_file_mentions(self, content, ignore_current=False): diff --git a/cecli/helpers/conversation/files.py b/cecli/helpers/conversation/files.py index f0cc62ed77c..13b69641c5a 100644 --- a/cecli/helpers/conversation/files.py +++ b/cecli/helpers/conversation/files.py @@ -261,8 +261,8 @@ def update_file_diff(self, fname: str) -> Optional[str]: ConversationService.get_manager(coder).add_message( message_dict=diff_message, tag=MessageTag.DIFFS, - promotion=ConversationService.get_manager(coder).DEFAULT_TAG_PROMOTION_VALUE, - mark_for_demotion=1, + # promotion=ConversationService.get_manager(coder).DEFAULT_TAG_PROMOTION_VALUE, + # mark_for_demotion=1, ) return diff diff --git a/cecli/helpers/conversation/integration.py b/cecli/helpers/conversation/integration.py index 40b247a2a2e..5519cb1deec 100644 --- a/cecli/helpers/conversation/integration.py +++ b/cecli/helpers/conversation/integration.py @@ -8,7 +8,7 @@ from cecli.utils import is_image_file from .service import ConversationService -from .tags import MessageTag +from .tags import DEFAULT_TAG_PRIORITY, MessageTag class ConversationChunks: @@ -897,7 +897,7 @@ def add_post_message_context_blocks(self) -> None: ConversationService.get_manager(coder).add_message( message_dict={"role": "user", "content": block_content}, tag=MessageTag.STATIC, # Use STATIC tag but with different priority - priority=250, # Between CUR (200) and REMINDER (300) + priority=DEFAULT_TAG_PRIORITY[MessageTag.REMINDER] + 25, # After REMINDER (300) mark_for_delete=0, hash_key=("post_message", block_type), force=True, diff --git a/cecli/prompts/agent.yml b/cecli/prompts/agent.yml index dc15634561b..c97c7402382 100644 --- a/cecli/prompts/agent.yml +++ b/cecli/prompts/agent.yml @@ -63,12 +63,13 @@ main_system: | system_reminder: | - ## Reminders - **Strict Scope**: Stay on task. Do not alter functionality and syntax that is out of scope or pursue unrequested refactors. Do not attempt to modify large files in one shot. Work step by step. - **Context Hygiene**: Remove files and loaded skills from context using `ContextManager` and/or `RemoveSkill` once they are no longer needed to save tokens and prevent confusion. - **Turn Management**: Tool calls trigger the next turn. Do not include tool calls in your final summary to the user. You must use `ShowContext` to view the relevant hashline range before each edit. - **Sandbox**: Use `.cecli/temp` for all verification and temporary logic. - **Novelty**: Do not repeat phrases in your responses to the user. You do not need to declare you understand the task. Simply proceed. Only give status updates when you have new information. + ## Operational Rules + - **Scope**: No unrequested refactors. Edit files incrementally; avoid full-file rewrites. + - **Hygiene**: Use `ContextManager`/`RemoveSkill` to evict unneeded files/skills immediately after use. + - **Verification**: You MUST use `ShowContext` to verify hashline ranges before every edit. + - **Outputs**: Tool calls trigger turns. Never include tool syntax in final user summaries. + - **Sandbox**: Perform all verification and temp logic in `.cecli/temp`. + - **Vibe**: Zero conversational filler. Do not confirm instructions or state "I understand." Provide status updates only when you have new information. {lazy_prompt} {shell_cmd_reminder} From dc8728212b0fe5fdb17319560f19eb5c73f99973 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 12:42:33 -0400 Subject: [PATCH 5/5] Allow, mcp, skills, and hooks tool to work with multiple arguments --- cecli/commands/load_hook.py | 40 +++++++++++++++++++--------------- cecli/commands/load_mcp.py | 35 ++++++++++++++--------------- cecli/commands/load_skill.py | 21 +++++++++++------- cecli/commands/remove_hook.py | 38 +++++++++++++++++--------------- cecli/commands/remove_mcp.py | 24 ++++++++++---------- cecli/commands/remove_skill.py | 21 +++++++++++------- 6 files changed, 97 insertions(+), 82 deletions(-) diff --git a/cecli/commands/load_hook.py b/cecli/commands/load_hook.py index e3370e4fe1b..f8149f69682 100644 --- a/cecli/commands/load_hook.py +++ b/cecli/commands/load_hook.py @@ -15,24 +15,28 @@ async def execute(cls, io, coder, args, **kwargs): if not args.strip(): io.tool_error("Usage: /load-hook ") return 1 - - hook_name = args.strip() - - # Check if hook exists + hook_names = args.strip().split() hook_manager = HookManager() - if not hook_manager.hook_exists(hook_name): - io.tool_error(f"Error: Hook '{hook_name}' not found") - return 1 + results = [] + errors = 0 - # Enable the hook - success = hook_manager.enable_hook(hook_name) + for hook_name in hook_names: + if not hook_manager.hook_exists(hook_name): + io.tool_error(f"Error: Hook '{hook_name}' not found") + results.append(f"Hook '{hook_name}' not found") + errors += 1 + continue - if success: - io.tool_output(f"Hook '{hook_name}' enabled successfully") - return 0 - else: - io.tool_error(f"Error: Failed to enable hook '{hook_name}'") - return 1 + success = hook_manager.enable_hook(hook_name) + if success: + io.tool_output(f"Hook '{hook_name}' enabled successfully") + results.append(f"Hook '{hook_name}' enabled") + else: + io.tool_error(f"Error: Failed to enable hook '{hook_name}'") + results.append(f"Failed to enable hook '{hook_name}'") + errors += 1 + + return 0 if errors == 0 else 1 @classmethod def get_completions(cls, io, coder, args) -> List[str]: @@ -58,10 +62,10 @@ def get_help(cls) -> str: """Get help text for the load-hook command.""" help_text = super().get_help() help_text += "\nUsage:\n" - help_text += " /load-hook # Enable a specific hook\n" + help_text += " /load-hook ... # Enable one or more hooks\n" help_text += "\nExamples:\n" help_text += " /load-hook my_start_hook\n" - help_text += " /load-hook check_commands\n" - help_text += "\nThis command enables a hook that was previously disabled.\n" + help_text += " /load-hook check_commands my_start_hook\n" + help_text += "\nThis command enables one or more hooks that were previously disabled.\n" help_text += "Use /hooks to see all available hooks and their current state.\n" return help_text diff --git a/cecli/commands/load_mcp.py b/cecli/commands/load_mcp.py index ad19ebc0b62..110a55dce5c 100644 --- a/cecli/commands/load_mcp.py +++ b/cecli/commands/load_mcp.py @@ -19,25 +19,22 @@ async def execute(cls, io, coder, args, **kwargs): io, cls.NORM_NAME, "No MCP servers found, nothing to load." ) - server_name = args.strip() - server = coder.mcp_manager.get_server(server_name) - if server is None: - return format_command_result( - io, cls.NORM_NAME, "", f"MCP server {server_name} does not exist." - ) - - did_connect = await coder.mcp_manager.connect_server(server.name) - - if not did_connect: - return format_command_result(io, cls.NORM_NAME, f"Unable to load server: {server_name}") + server_names = args.strip().split() + results = [] + for server_name in server_names: + server = coder.mcp_manager.get_server(server_name) + if server is None: + results.append(f"MCP server {server_name} does not exist.") + continue - try: + did_connect = await coder.mcp_manager.connect_server(server.name) if did_connect: - return format_command_result(io, cls.NORM_NAME, f"Loaded server: {server_name}") + results.append(f"Loaded server: {server_name}") else: - return format_command_result( - io, cls.NORM_NAME, "", f"Unable to Load server: {server_name}" - ) + results.append(f"Unable to load server: {server_name}") + + try: + return format_command_result(io, cls.NORM_NAME, "\n".join(results)) finally: from . import SwitchCoderSignal @@ -69,9 +66,9 @@ def get_help(cls) -> str: """Get help text for the load-mcp command.""" help_text = super().get_help() help_text += "\nUsage:\n" - help_text += " /load-mcp # Load a mcp by name\n" + help_text += " /load-mcp ... # Load one or more mcps by name\n" help_text += "\nExamples:\n" help_text += " /load-mcp context7 # Load the context7 mcp\n" - help_text += " /load-mcp github # Load the github mcp\n" - help_text += "\nThis command loads a MCP server by name.\n" + help_text += " /load-mcp github context7 # Load both github and context7 mcps\n" + help_text += "\nThis command loads one or more MCP servers by name.\n" return help_text diff --git a/cecli/commands/load_skill.py b/cecli/commands/load_skill.py index 8fabc6833b7..ccb90ef8353 100644 --- a/cecli/commands/load_skill.py +++ b/cecli/commands/load_skill.py @@ -15,7 +15,7 @@ async def execute(cls, io, coder, args, **kwargs): io.tool_output("Usage: /load-skill ") return format_command_result(io, "load-skill", "Usage: /load-skill ") - skill_name = args.strip() + skill_names = args.strip().split() # Check if we're in agent mode if not hasattr(coder, "edit_format") or coder.edit_format != "agent": @@ -35,10 +35,14 @@ async def execute(cls, io, coder, args, **kwargs): ) return format_command_result(io, "load-skill", "Skills manager is not initialized") - # Use the instance method on skills_manager - result = coder.skills_manager.load_skill(skill_name) - io.tool_output(result) - return format_command_result(io, "load-skill", f"Loaded skill: {skill_name}") + results = [] + for skill_name in skill_names: + # Use the instance method on skills_manager + result = coder.skills_manager.load_skill(skill_name) + io.tool_output(result) + results.append(result) + + return format_command_result(io, "load-skill", "\n".join(results)) @classmethod def get_completions(cls, io, coder, args) -> List[str]: @@ -57,12 +61,13 @@ def get_help(cls) -> str: """Get help text for the load-skill command.""" help_text = super().get_help() help_text += "\nUsage:\n" - help_text += " /load-skill # Load a skill by name\n" + help_text += " /load-skill ... # Load one or more skills by name\n" help_text += "\nExamples:\n" help_text += " /load-skill pdf # Load the PDF skill\n" - help_text += " /load-skill web # Load the web skill\n" + help_text += " /load-skill web pdf # Load both web and PDF skills\n" help_text += ( - "\nThis command loads a skill by name. Skills are only available in agent mode.\n" + "\nThis command loads one or more skills by name. Skills are only available in agent" + " mode.\n" ) help_text += "Skills provide additional functionality and tools to the agent.\n" return help_text diff --git a/cecli/commands/remove_hook.py b/cecli/commands/remove_hook.py index 378c5528326..2090a05aade 100644 --- a/cecli/commands/remove_hook.py +++ b/cecli/commands/remove_hook.py @@ -15,24 +15,28 @@ async def execute(cls, io, coder, args, **kwargs): if not args.strip(): io.tool_error("Usage: /remove-hook ") return 1 - - hook_name = args.strip() - - # Check if hook exists + hook_names = args.strip().split() hook_manager = HookManager() - if not hook_manager.hook_exists(hook_name): - io.tool_error(f"Error: Hook '{hook_name}' not found") - return 1 + results = [] + errors = 0 - # Disable the hook - success = hook_manager.disable_hook(hook_name) + for hook_name in hook_names: + if not hook_manager.hook_exists(hook_name): + io.tool_error(f"Error: Hook '{hook_name}' not found") + results.append(f"Hook '{hook_name}' not found") + errors += 1 + continue - if success: - io.tool_output(f"Hook '{hook_name}' disabled successfully") - return 0 - else: - io.tool_error(f"Error: Failed to disable hook '{hook_name}'") - return 1 + success = hook_manager.disable_hook(hook_name) + if success: + io.tool_output(f"Hook '{hook_name}' disabled successfully") + results.append(f"Hook '{hook_name}' disabled") + else: + io.tool_error(f"Error: Failed to disable hook '{hook_name}'") + results.append(f"Failed to disable hook '{hook_name}'") + errors += 1 + + return 0 if errors == 0 else 1 @classmethod def get_completions(cls, io, coder, args) -> List[str]: @@ -58,10 +62,10 @@ def get_help(cls) -> str: """Get help text for the remove-hook command.""" help_text = super().get_help() help_text += "\nUsage:\n" - help_text += " /remove-hook # Disable a specific hook\n" + help_text += " /remove-hook ... # Disable one or more hooks\n" help_text += "\nExamples:\n" help_text += " /remove-hook my_start_hook\n" - help_text += " /remove-hook check_commands\n" + help_text += " /remove-hook check_commands my_start_hook\n" help_text += "\nThis command disables a hook without removing it from the registry.\n" help_text += "Use /load-hook to re-enable it later.\n" help_text += "Use /hooks to see all available hooks and their current state.\n" diff --git a/cecli/commands/remove_mcp.py b/cecli/commands/remove_mcp.py index 9350a9670d8..f12caaf7b25 100644 --- a/cecli/commands/remove_mcp.py +++ b/cecli/commands/remove_mcp.py @@ -19,16 +19,17 @@ async def execute(cls, io, coder, args, **kwargs): io, cls.NORM_NAME, "No MCP servers connected, nothing to remove." ) - server_name = args.strip() - was_disconnected = await coder.mcp_manager.disconnect_server(server_name) - - try: + server_names = args.strip().split() + results = [] + for server_name in server_names: + was_disconnected = await coder.mcp_manager.disconnect_server(server_name) if was_disconnected: - return format_command_result(io, cls.NORM_NAME, f"Removed server: {server_name}") + results.append(f"Removed server: {server_name}") else: - return format_command_result( - io, cls.NORM_NAME, "", f"Unable to remove server: {server_name}" - ) + results.append(f"Unable to remove server: {server_name}") + + try: + return format_command_result(io, cls.NORM_NAME, "\n".join(results)) finally: from . import SwitchCoderSignal @@ -57,9 +58,8 @@ def get_help(cls) -> str: """Get help text for the remove-mcp command.""" help_text = super().get_help() help_text += "\nUsage:\n" - help_text += " /remove-mcp # Remove a mcp by name\n" + help_text += " /remove-mcp ... # Remove one or more mcps by name\n" help_text += "\nExamples:\n" help_text += " /remove-mcp context7 # Remove the context7 mcp\n" - help_text += " /remove-mcp github # Remove the github mcp\n" - help_text += "\nThis command removes a MCP server by name.\n" - return help_text + help_text += " /remove-mcp github context7 # Remove both github and context7 mcps\n" + help_text += "\nThis command removes one or more MCP servers by name.\n" diff --git a/cecli/commands/remove_skill.py b/cecli/commands/remove_skill.py index 0fe7a992317..4d665453a73 100644 --- a/cecli/commands/remove_skill.py +++ b/cecli/commands/remove_skill.py @@ -15,7 +15,7 @@ async def execute(cls, io, coder, args, **kwargs): io.tool_output("Usage: /remove-skill ") return format_command_result(io, "remove-skill", "Usage: /remove-skill ") - skill_name = args.strip() + skill_names = args.strip().split() # Check if we're in agent mode if not hasattr(coder, "edit_format") or coder.edit_format != "agent": @@ -35,10 +35,14 @@ async def execute(cls, io, coder, args, **kwargs): ) return format_command_result(io, "remove-skill", "Skills manager is not initialized") - # Use the instance method on skills_manager - result = coder.skills_manager.remove_skill(skill_name) - io.tool_output(result) - return format_command_result(io, "remove-skill", f"Removed skill: {skill_name}") + results = [] + for skill_name in skill_names: + # Use the instance method on skills_manager + result = coder.skills_manager.remove_skill(skill_name) + io.tool_output(result) + results.append(result) + + return format_command_result(io, "remove-skill", "\n".join(results)) @classmethod def get_completions(cls, io, coder, args) -> List[str]: @@ -57,12 +61,13 @@ def get_help(cls) -> str: """Get help text for the remove-skill command.""" help_text = super().get_help() help_text += "\nUsage:\n" - help_text += " /remove-skill # Remove a skill by name\n" + help_text += " /remove-skill ... # Remove one or more skills by name\n" help_text += "\nExamples:\n" help_text += " /remove-skill pdf # Remove the PDF skill\n" - help_text += " /remove-skill web # Remove the web skill\n" + help_text += " /remove-skill web pdf # Remove both web and PDF skills\n" help_text += ( - "\nThis command removes a skill by name. Skills are only available in agent mode.\n" + "\nThis command removes one or more skills by name. Skills are only available in agent" + " mode.\n" ) help_text += "Skills provide additional functionality and tools to the agent.\n" return help_text