@@ -102,6 +102,7 @@ def _terminal_supports_unicode() -> bool:
102102from subagents import AgentProfile , SubAgentManager
103103from capability_tokens import issue_capability_token
104104from dynamic_tools import SAFE_BUILTINS , validate_tool_source
105+ from plugin_runtime import get_plugin_runtime
105106from tools import (AVAILABLE_TOOLS , set_workspace_root as _set_tools_workspace_root ,
106107 resolve_capabilities , tool_capability )
107108from providers import (
@@ -1875,6 +1876,23 @@ def _system_prompt(tools_list: str) -> str:
18751876_learning_notices : list [str ] = []
18761877
18771878
1879+ def _record_plugin_event (event_type : str , payload : dict [str , Any ]) -> None :
1880+ """Persist plugin runtime diagnostics without allowing them to break work."""
1881+ try :
1882+ memory_bank .store .append_event (
1883+ event_type , payload , user_id = memory_bank .user_id ,
1884+ workspace_id = memory_bank .workspace_id , session_id = memory_bank .session_id ,
1885+ )
1886+ except Exception :
1887+ pass
1888+
1889+
1890+ def _plugin_runtime_for_surface (surface : str | None = None ):
1891+ return get_plugin_runtime (
1892+ surface or _EXECUTION_SURFACE , event_recorder = _record_plugin_event ,
1893+ )
1894+
1895+
18781896def _subagent_action_arguments (action : str , args : Any ) -> Any :
18791897 """Normalise common structured action arguments without widening access."""
18801898 if not isinstance (args , dict ):
@@ -1914,12 +1932,17 @@ def _run_subagent_tool(profile: AgentProfile, action: str, args: Any, tools: set
19141932 canonical = TOOL_ALIASES .get (str (action ).strip (), str (action ).strip ())
19151933 receipt_id = f"subagent_receipt_{ uuid .uuid4 ().hex } "
19161934 if canonical not in tools :
1935+ _notify_tool_execute (
1936+ canonical , args ,
1937+ f"Error: tool '{ canonical } ' is not authorized for the '{ profile .name } ' profile." ,
1938+ )
19171939 return {
19181940 "receipt_id" : receipt_id , "action" : canonical , "args" : str (args )[:500 ],
19191941 "result" : f"Error: tool '{ canonical } ' is not authorized for the '{ profile .name } ' profile." ,
19201942 "success" : False , "authorized" : False ,
19211943 }
19221944 if not isinstance (args , (str , dict )):
1945+ _notify_tool_execute (canonical , args , "Error: Action args must be a plain string or supported object." )
19231946 return {
19241947 "receipt_id" : receipt_id , "action" : canonical , "args" : str (args )[:500 ],
19251948 "result" : "Error: Action args must be a plain string or supported object." ,
@@ -1978,6 +2001,10 @@ def _run_subagent_llm(profile: AgentProfile, task: str, context: list[dict[str,
19782001 if unknown or malformed :
19792002 executed_steps += 1
19802003 rejected_action = unknown or "malformed"
2004+ _notify_tool_execute (
2005+ str (rejected_action ), "" ,
2006+ "Error: malformed or unknown Action rejected; no tool was executed." ,
2007+ )
19812008 tool_records .append ({
19822009 "receipt_id" : f"subagent_receipt_{ uuid .uuid4 ().hex } " ,
19832010 "action" : str (rejected_action ), "args" : "" ,
@@ -2561,6 +2588,22 @@ def _add(data: dict) -> None:
25612588 return calls
25622589
25632590
2591+ def _notify_tool_execute (action : str , args : Any , result : Any ) -> None :
2592+ """Emit exactly one isolated plugin hook for every attempted tool call."""
2593+ result_text = str (result )
2594+ try :
2595+ _plugin_runtime_for_surface ().tool_execute (
2596+ action = action , args = args , result = result_text ,
2597+ success = not _is_tool_error (result_text ),
2598+ error = None if not _is_tool_error (result_text ) else result_text ,
2599+ user_id = memory_bank .user_id , workspace_id = memory_bank .workspace_id ,
2600+ session_id = memory_bank .session_id ,
2601+ )
2602+ except Exception :
2603+ # A plugin is an observer and cannot change tool semantics.
2604+ pass
2605+
2606+
25642607def _run_tool (action : str , args : str ) -> str :
25652608 # Map aliases
25662609 action = TOOL_ALIASES .get (action , action )
@@ -2593,15 +2636,21 @@ def _run_tool(action: str, args: str) -> str:
25932636 pass
25942637 fn = AVAILABLE_TOOLS .get (action )
25952638 if not fn :
2596- return f"Error: unknown tool '{ action } '"
2639+ result = f"Error: unknown tool '{ action } '"
2640+ _notify_tool_execute (action , args , result )
2641+ return result
25972642 required_capability = tool_capability (action )
25982643 if not _execution_capability_token .allows (required_capability ):
2599- return f"Error: tool '{ action } ' requires capability '{ required_capability } '"
2644+ result = f"Error: tool '{ action } ' requires capability '{ required_capability } '"
2645+ _notify_tool_execute (action , args , result )
2646+ return result
26002647 if not _confirm_tool_action (action , str (args )):
2601- return (
2648+ result = (
26022649 f"Error: { action } requires confirmation. "
26032650 "Approve it interactively or set KYROZEN_APPROVAL_MODE=never for an explicitly automated CLI."
26042651 )
2652+ _notify_tool_execute (action , args , result )
2653+ return result
26052654 start = time .time ()
26062655 try :
26072656 result = str (fn (args ))
@@ -2611,6 +2660,7 @@ def _run_tool(action: str, args: str) -> str:
26112660 result = f"Error: { e } "
26122661 elapsed = time .time () - start
26132662 _track_tool_performance (action , result , elapsed )
2663+ _notify_tool_execute (action , args , result )
26142664 return result
26152665
26162666
@@ -3115,8 +3165,8 @@ def _classify_complexity(user_input: str) -> str:
31153165 return "medium"
31163166
31173167
3118- def _chat_turn (user_input : str , clear_tasks : bool = False , profile : str | None = None ,
3119- memory_context : dict [str , Any ] | None = None ) -> str :
3168+ def _chat_turn_impl (user_input : str , clear_tasks : bool = False , profile : str | None = None ,
3169+ memory_context : dict [str , Any ] | None = None ) -> str :
31203170 """One user turn: build context, get LLM reply, execute tool calls
31213171 with automatic retries and failure memory."""
31223172
@@ -3189,6 +3239,9 @@ def _chat_turn(user_input: str, clear_tasks: bool = False, profile: str | None =
31893239 if not unknown_action :
31903240 break
31913241 _unknown_retries += 1
3242+ _notify_tool_execute (
3243+ unknown_action , "" , f"Error: unknown tool '{ unknown_action } '" ,
3244+ )
31923245 msg = (
31933246 f"System: Action '{ unknown_action } ' is not recognized. "
31943247 "You must use one of the following actions: "
@@ -3315,6 +3368,7 @@ def _chat_turn(user_input: str, clear_tasks: bool = False, profile: str | None =
33153368 "Example: `\" args\" : \" python3 process_logs.py\" `\n "
33163369 "Do NOT use `\" args\" : {\" cmd\" : ...}`.\n "
33173370 )
3371+ _notify_tool_execute (action , args , result )
33183372 else :
33193373 if not isinstance (args , str ):
33203374 args = str (args )
@@ -3581,6 +3635,7 @@ def _chat_turn(user_input: str, clear_tasks: bool = False, profile: str | None =
35813635 "Example: `\" args\" : \" python3 process_logs.py\" `\n "
35823636 "Do NOT use `\" args\" : {\" cmd\" : ...}`.\n "
35833637 )
3638+ _notify_tool_execute (action , args , result2 )
35843639 else :
35853640 if not isinstance (args , str ):
35863641 args = str (args )
@@ -3697,6 +3752,30 @@ def _chat_turn(user_input: str, clear_tasks: bool = False, profile: str | None =
36973752 )
36983753
36993754
3755+ def _chat_turn (user_input : str , clear_tasks : bool = False , profile : str | None = None ,
3756+ memory_context : dict [str , Any ] | None = None ) -> str :
3757+ """Run one chat turn with failure-isolated plugin lifecycle hooks."""
3758+ runtime = _plugin_runtime_for_surface ()
3759+ context = {
3760+ "user_id" : memory_bank .user_id ,
3761+ "workspace_id" : memory_bank .workspace_id ,
3762+ "session_id" : memory_bank .session_id ,
3763+ "profile" : profile or _agent_profile_mode ,
3764+ }
3765+ runtime .turn_start (user_input = user_input , ** context )
3766+ try :
3767+ reply = _chat_turn_impl (
3768+ user_input , clear_tasks = clear_tasks , profile = profile ,
3769+ memory_context = memory_context ,
3770+ )
3771+ except Exception as exc :
3772+ runtime .turn_end (reply = "" , success = False ,
3773+ error = f"{ type (exc ).__name__ } : { exc } " , ** context )
3774+ raise
3775+ runtime .turn_end (reply = reply , success = True , ** context )
3776+ return reply
3777+
3778+
37003779def _split_reply (text : str ) -> tuple [str , str ]:
37013780 text = text .strip ()
37023781 thought_match = re .search (r"^Thought:\s*(.*?)(?=\n(?:Action:|(?:\n|$)))" , text , re .DOTALL | re .MULTILINE )
@@ -3965,6 +4044,7 @@ def main() -> None:
39654044 sys .exit (1 )
39664045 # Set workspace root for sandbox
39674046 _set_workspace_root (os .getcwd ())
4047+ _plugin_runtime_for_surface ().load_once ()
39684048 task_results = _run_recovered_tasks ()
39694049 for item in task_results :
39704050 console .print (f"[{ _SUCCESS } ]Durable task { item ['task' ]['id' ]} : { item ['status' ]} [/{ _SUCCESS } ]" )
0 commit comments