"""Compatibility-aware call boundary for plugin slash-command handlers.""" from __future__ import annotations import inspect from typing import Any, Callable _KEYWORD_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY) def invoke_plugin_command(handler: Callable, raw_args: str, **context: Any) -> Any: """Call *handler* once with *raw_args* plus the *context* keywords it declares. Every surface passes the same context (``source``, ``session_id``, ``session_key``, ``platform``); a handler opts in by naming any of them as keyword-capable parameters, or takes them all with ``**kwargs``. Legacy ``fn(raw_args)`` handlers and opaque signatures keep the one-argument call. Handler exceptions propagate; they are never retried as a legacy call. """ try: parameters = list(inspect.signature(handler).parameters.values()) except (TypeError, ValueError): return handler(raw_args) # The first positional parameter receives raw_args whatever its name (``def h(source, **kw)`` # must not get ``source`` twice). if parameters and parameters[0].kind is not inspect.Parameter.KEYWORD_ONLY: if parameters[0].kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: context = {k: v for k, v in context.items() if k != parameters[0].name} parameters = parameters[1:] if any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters): return handler(raw_args, **context) accepted = {p.name for p in parameters if p.kind in _KEYWORD_KINDS} return handler(raw_args, **{k: v for k, v in context.items() if k in accepted})