1111 to the tool description, the one field a model always sees.
1212- **Validation + instructive errors** — a missing/blank required argument returns
1313 an error that names the argument and its example, and logs a WARNING.
14- - **Logging** — WARNING on a rejected call (args truncated, secret-looking values
15- redacted), INFO on success, under the plugin's own logger namespace.
14+ - **Logging** — DEBUG on invocation, WARNING on rejected/failed calls with
15+ tracebacks for exceptions, and INFO on success with elapsed time. Arguments
16+ are truncated and secret-looking values are recursively redacted.
1617- **Envelope + safety** — a handler returns a plain ``dict`` (or raises); the kit
1718 encodes the JSON string, wraps exceptions, and always returns ``str`` from an
1819 ``(args, **kwargs)`` signature, exactly as the registry requires.
@@ -48,6 +49,7 @@ def register(ctx):
4849import logging
4950import re
5051import sys
52+ import time
5153from typing import Any , Callable
5254
5355__all__ = [
@@ -213,11 +215,23 @@ def build_schema(name: str, description: str, params: dict | None) -> dict:
213215# Logging helpers
214216# ---------------------------------------------------------------------------
215217
218+ def _redacted_value (value : Any ) -> Any :
219+ if isinstance (value , dict ):
220+ return {
221+ key : (
222+ "***"
223+ if any (hint in str (key ).lower () for hint in _REDACT_HINTS )
224+ else _redacted_value (item )
225+ )
226+ for key , item in value .items ()
227+ }
228+ if isinstance (value , (list , tuple )):
229+ return [_redacted_value (item ) for item in value ]
230+ return value
231+
232+
216233def _redacted_args (args : dict ) -> dict :
217- out : dict [str , Any ] = {}
218- for key , value in (args or {}).items ():
219- out [key ] = "***" if any (hint in key .lower () for hint in _REDACT_HINTS ) else value
220- return out
234+ return _redacted_value (args or {})
221235
222236
223237def _truncate (value : Any ) -> str :
@@ -264,6 +278,19 @@ def decorate(fn: Callable) -> Callable:
264278 @functools .wraps (fn )
265279 def wrapper (args : dict , ** kwargs : Any ) -> str :
266280 args = args or {}
281+ started = time .perf_counter ()
282+ safe_args = _truncate (_redacted_args (args ))
283+ context = {
284+ key : kwargs [key ]
285+ for key in ("session_id" , "task_id" )
286+ if kwargs .get (key ) is not None
287+ }
288+ log .debug (
289+ "%s: invoked; args=%s; context=%s" ,
290+ tool_name ,
291+ safe_args ,
292+ _truncate (context ),
293+ )
267294 for key in required :
268295 value = args .get (key )
269296 if value is None or (isinstance (value , str ) and not value .strip ()):
@@ -272,23 +299,39 @@ def wrapper(args: dict, **kwargs: Any) -> str:
272299 f" (e.g. { example !r} )" if example is not None else ""
273300 )
274301 log .warning (
275- "%s: rejected call, missing %s; args=%s" ,
302+ "%s: rejected call, missing %s; elapsed_ms=%.2f; args=%s" ,
276303 tool_name ,
277304 key ,
278- _truncate (_redacted_args (args )),
305+ (time .perf_counter () - started ) * 1000 ,
306+ safe_args ,
279307 )
280308 return json .dumps ({"success" : False , "error" : message }, ensure_ascii = False )
281309 try :
282310 result = fn (args , ** kwargs )
283311 except Exception as exc : # noqa: BLE001 — tool errors stay in-band
284- log .warning ("%s: handler raised: %s" , tool_name , exc )
312+ log .exception (
313+ "%s: handler raised; elapsed_ms=%.2f; error=%s" ,
314+ tool_name ,
315+ (time .perf_counter () - started ) * 1000 ,
316+ exc ,
317+ )
285318 return json .dumps (
286319 {"success" : False , "error" : f"{ tool_name } failed: { exc } " },
287320 ensure_ascii = False ,
288321 )
289322 if isinstance (result , str ):
323+ log .info (
324+ "%s: ok; elapsed_ms=%.2f; result=encoded_string" ,
325+ tool_name ,
326+ (time .perf_counter () - started ) * 1000 ,
327+ )
290328 return result
291- log .info ("%s: ok" , tool_name )
329+ log .info (
330+ "%s: ok; elapsed_ms=%.2f; result=%s" ,
331+ tool_name ,
332+ (time .perf_counter () - started ) * 1000 ,
333+ type (result ).__name__ ,
334+ )
292335 return json .dumps ({"success" : True , "data" : result }, ensure_ascii = False )
293336
294337 setattr (
@@ -319,6 +362,7 @@ def register_all(ctx: Any, module: Any) -> int:
319362 """
320363 if isinstance (module , str ):
321364 module = sys .modules [module ]
365+ log = logging .getLogger (getattr (module , "__name__" , "hermes_plugin_kit" ))
322366 count = 0
323367 seen : set [str ] = set ()
324368 for _ , obj in inspect .getmembers (module ):
@@ -336,4 +380,9 @@ def register_all(ctx: Any, module: Any) -> int:
336380 emoji = spec ["emoji" ],
337381 )
338382 count += 1
383+ log .info (
384+ "hermes_plugin_kit: registered %d tool(s); names=%s" ,
385+ count ,
386+ "," .join (sorted (seen )) or "<none>" ,
387+ )
339388 return count
0 commit comments