Skip to content

Commit 226003e

Browse files
author
Your Name
committed
Add hooks sub system
1 parent ada5e16 commit 226003e

25 files changed

Lines changed: 2109 additions & 8 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ LLMs are a part of our lives from here on out so join us in learning about and c
2121
* [TUI Configuration](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/tui.md)
2222
* [Skills](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/skills.md)
2323
* [Session Management](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/sessions.md)
24+
* [Hooks](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/hooks.md)
2425
* [Custom Commands](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/custom-commands.md)
2526
* [Custom System Prompts](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/custom-system-prompts.md)
2627
* [Custom Tools](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/agent-mode.md#creating-custom-tools)

cecli/args.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ def get_parser(default_config_files, git_root):
310310
help="Specify Agent Mode configuration as a JSON string",
311311
default=None,
312312
)
313+
group.add_argument(
314+
"--hooks",
315+
metavar="HOOKS_CONFIG_JSON",
316+
help="Specify hooks configuration as a JSON string",
317+
default=None,
318+
)
313319
group.add_argument(
314320
"--agent-model",
315321
metavar="AGENT_MODEL",

cecli/coders/agent_coder.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
normalize_vector,
2727
)
2828
from cecli.helpers.skills import SkillsManager
29+
from cecli.hooks import HookIntegration
2930
from cecli.llm import litellm
3031
from cecli.mcp import LocalServer, McpServerManager
3132
from cecli.tools.utils.registry import ToolRegistry
@@ -241,6 +242,17 @@ async def _execute_local_tool_calls(self, tool_calls_list):
241242
try:
242243
args_string = tool_call.function.arguments.strip()
243244
parsed_args_list = []
245+
246+
if not await HookIntegration.call_pre_tool_hooks(self, tool_name, args_string):
247+
tool_responses.append(
248+
{
249+
"role": "tool",
250+
"tool_call_id": tool_call.id,
251+
"content": "Tool Request Aborted.",
252+
}
253+
)
254+
continue
255+
244256
if args_string:
245257
json_chunks = utils.split_concatenated_json(args_string)
246258
for chunk in json_chunks:
@@ -293,6 +305,19 @@ async def _execute_local_tool_calls(self, tool_calls_list):
293305
if tasks:
294306
task_results = await asyncio.gather(*tasks)
295307
all_results_content.extend(str(res) for res in task_results)
308+
309+
if not await HookIntegration.call_post_tool_hooks(
310+
self, tool_name, args_string, "\n\n".join(all_results_content)
311+
):
312+
tool_responses.append(
313+
{
314+
"role": "tool",
315+
"tool_call_id": tool_call.id,
316+
"content": "Tool Response Redacted.",
317+
}
318+
)
319+
continue
320+
296321
result_message = "\n\n".join(all_results_content)
297322
except Exception as e:
298323
result_message = f"Error executing {tool_name}: {e}"

cecli/coders/base_coder.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
)
4949
from cecli.helpers.profiler import TokenProfiler
5050
from cecli.history import ChatSummary
51+
from cecli.hooks import HookIntegration
5152
from cecli.io import ConfirmGroup, InputOutput
5253
from cecli.linter import Linter
5354
from cecli.llm import litellm
@@ -1590,6 +1591,10 @@ async def preproc_user_input(self, inp):
15901591
async def run_one(self, user_message, preproc):
15911592
self.init_before_message()
15921593

1594+
if not await HookIntegration.call_start_hooks(self):
1595+
self.io.tool_warning("Execution stopped by start hook")
1596+
return
1597+
15931598
if preproc:
15941599
message = await self.preproc_user_input(user_message)
15951600
else:
@@ -1637,6 +1642,10 @@ async def run_one(self, user_message, preproc):
16371642

16381643
await self.auto_save_session(force=True)
16391644

1645+
if not await HookIntegration.call_end_hooks(self):
1646+
self.io.tool_warning("Execution stopped by end hook")
1647+
return
1648+
16401649
def _is_url_allowed(self, url):
16411650
allowed_domains = self.security_config.get("allowed-domains")
16421651
if not allowed_domains:
@@ -2274,7 +2283,7 @@ async def send_message(self, inp):
22742283

22752284
self.io.tool_output()
22762285
self.show_usage_report()
2277-
self.add_assistant_reply_to_cur_messages()
2286+
await self.add_assistant_reply_to_cur_messages()
22782287

22792288
if exhausted:
22802289
cur_messages = ConversationManager.get_messages_dict(MessageTag.CUR)
@@ -2596,6 +2605,13 @@ async def _exec_server_tools(server, tool_calls_list):
25962605
new_tool_call = copy_tool_call(tool_call)
25972606
new_tool_call.function.arguments = json.dumps(args)
25982607

2608+
if not await HookIntegration.call_pre_tool_hooks(
2609+
self, new_tool_call.function.name, args
2610+
):
2611+
self.io.tool_warning("Tool call skipped by pre-tool call hook")
2612+
all_results_content.append("Tool Request Aborted.")
2613+
continue
2614+
25992615
call_result = await experimental_mcp_client.call_openai_tool(
26002616
session=session,
26012617
openai_tool=new_tool_call,
@@ -2628,6 +2644,16 @@ async def _exec_server_tools(server, tool_calls_list):
26282644
content_parts.append(item.text)
26292645

26302646
result_text = "".join(content_parts)
2647+
2648+
if not await HookIntegration.call_post_tool_hooks(
2649+
self, new_tool_call.function.name, args, result_text
2650+
):
2651+
self.io.tool_warning(
2652+
"Tool call output skipped by post-tool call hook"
2653+
)
2654+
all_results_content.append("Tool Response Redacted.")
2655+
continue
2656+
26312657
all_results_content.append(result_text)
26322658

26332659
tool_responses.append(
@@ -2803,7 +2829,7 @@ def __del__(self):
28032829
"""Cleanup when the Coder object is destroyed."""
28042830
self.ok_to_warm_cache = False
28052831

2806-
def add_assistant_reply_to_cur_messages(self):
2832+
async def add_assistant_reply_to_cur_messages(self):
28072833
"""
28082834
Add the assistant's reply to `cur_messages`.
28092835
Handles model-specific quirks, like Deepseek which requires `content`
@@ -2845,6 +2871,10 @@ def add_assistant_reply_to_cur_messages(self):
28452871
or msg.get("tool_calls", None)
28462872
or msg.get("function_call", None)
28472873
):
2874+
if not await HookIntegration.call_end_message_hooks(self, str(msg)):
2875+
self.io.tool_warning("Execution stopped by end message hook")
2876+
return
2877+
28482878
ConversationManager.add_message(
28492879
message_dict=msg,
28502880
tag=MessageTag.CUR,
@@ -2972,7 +3002,7 @@ async def send(self, messages, model=None, functions=None, tools=None):
29723002
async for chunk in self.show_send_output_stream(completion):
29733003
yield chunk
29743004
else:
2975-
self.show_send_output(completion)
3005+
await self.show_send_output(completion)
29763006

29773007
response, func_err, content_err = self.consolidate_chunks()
29783008

@@ -3002,7 +3032,7 @@ async def send(self, messages, model=None, functions=None, tools=None):
30023032
if args:
30033033
self.io.ai_output(json.dumps(args, indent=4))
30043034

3005-
def show_send_output(self, completion):
3035+
async def show_send_output(self, completion):
30063036
if self.verbose:
30073037
print(completion)
30083038

@@ -3018,6 +3048,10 @@ def show_send_output(self, completion):
30183048

30193049
response, func_err, content_err = self.consolidate_chunks()
30203050

3051+
if not await HookIntegration.call_on_message_hooks(self, self.partial_response_content):
3052+
self.io.tool_warning("Execution stopped by on message hook")
3053+
return
3054+
30213055
resp_hash = dict(
30223056
function_call=str(self.partial_response_function_call),
30233057
content=self.partial_response_content,
@@ -3183,6 +3217,10 @@ async def show_send_output_stream(self, completion):
31833217
# The Part Doing the Heavy Lifting Now
31843218
self.consolidate_chunks()
31853219

3220+
if not await HookIntegration.call_on_message_hooks(self, self.partial_response_content):
3221+
self.io.tool_warning("Execution stopped by on message hook")
3222+
return
3223+
31863224
if not received_content and len(self.partial_response_tool_calls) == 0:
31873225
self.io.tool_warning("Empty response received from LLM. Check your provider account?")
31883226

cecli/coders/copypaste_coder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async def send(self, messages, model=None, functions=None, tools=None):
146146
try:
147147
hash_object, completion = self.copy_paste_completion(messages, model)
148148
self.chat_completion_call_hashes.append(hash_object.hexdigest())
149-
self.show_send_output(completion)
149+
await self.show_send_output(completion)
150150
self.calculate_and_show_tokens_and_cost(messages, completion)
151151
finally:
152152
self.preprocess_response()

cecli/coders/single_wholefile_func_coder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class SingleWholeFileFunctionCoder(Coder):
3838
def __init__(self, *args, **kwargs):
3939
super().__init__(*args, **kwargs)
4040

41-
def add_assistant_reply_to_cur_messages(self, edited):
41+
async def add_assistant_reply_to_cur_messages(self, edited):
4242
if edited:
4343
# Always add to conversation manager
4444
ConversationManager.add_message(

cecli/coders/wholefile_func_coder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __init__(self, *args, **kwargs):
4949

5050
super().__init__(*args, **kwargs)
5151

52-
def add_assistant_reply_to_cur_messages(self, edited):
52+
async def add_assistant_reply_to_cur_messages(self, edited):
5353
if edited:
5454
# Always add to conversation manager
5555
ConversationManager.add_message(

cecli/commands/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
from .hashline import HashlineCommand
3131
from .help import HelpCommand
3232
from .history_search import HistorySearchCommand
33+
from .hooks import HooksCommand
3334
from .lint import LintCommand
3435
from .list_sessions import ListSessionsCommand
3536
from .load import LoadCommand
37+
from .load_hook import LoadHookCommand
3638
from .load_mcp import LoadMcpCommand
3739
from .load_session import LoadSessionCommand
3840
from .load_skill import LoadSkillCommand
@@ -47,6 +49,7 @@
4749
from .read_only import ReadOnlyCommand
4850
from .read_only_stub import ReadOnlyStubCommand
4951
from .reasoning_effort import ReasoningEffortCommand
52+
from .remove_hook import RemoveHookCommand
5053
from .remove_mcp import RemoveMcpCommand
5154
from .remove_skill import RemoveSkillCommand
5255
from .report import ReportCommand
@@ -102,9 +105,11 @@
102105
CommandRegistry.register(HashlineCommand)
103106
CommandRegistry.register(HelpCommand)
104107
CommandRegistry.register(HistorySearchCommand)
108+
CommandRegistry.register(HooksCommand)
105109
CommandRegistry.register(LintCommand)
106110
CommandRegistry.register(ListSessionsCommand)
107111
CommandRegistry.register(LoadCommand)
112+
CommandRegistry.register(LoadHookCommand)
108113
CommandRegistry.register(LoadMcpCommand)
109114
CommandRegistry.register(LoadSessionCommand)
110115
CommandRegistry.register(LoadSkillCommand)
@@ -119,6 +124,7 @@
119124
CommandRegistry.register(ReadOnlyCommand)
120125
CommandRegistry.register(ReadOnlyStubCommand)
121126
CommandRegistry.register(ReasoningEffortCommand)
127+
CommandRegistry.register(RemoveHookCommand)
122128
CommandRegistry.register(RemoveMcpCommand)
123129
CommandRegistry.register(RemoveSkillCommand)
124130
CommandRegistry.register(ReportCommand)
@@ -171,9 +177,11 @@
171177
"HashlineCommand",
172178
"HelpCommand",
173179
"HistorySearchCommand",
180+
"HookCommand",
174181
"LintCommand",
175182
"ListSessionsCommand",
176183
"LoadCommand",
184+
"LoadHookCommand",
177185
"LoadMcpCommand",
178186
"LoadSessionCommand",
179187
"LoadSkillCommand",
@@ -190,6 +198,7 @@
190198
"ReadOnlyCommand",
191199
"ReadOnlyStubCommand",
192200
"ReasoningEffortCommand",
201+
"RemoveHookCommand",
193202
"RemoveMcpCommand",
194203
"RemoveSkillCommand",
195204
"ReportCommand",

0 commit comments

Comments
 (0)