mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
Merge pull request #29724 from bbednarski9/bbednarski/nmf-41B-nemoflow-plugin
feat(middleware): add adaptive middleware to hermes-agent, consumed by NeMo-Relay
This commit is contained in:
commit
d4a7bfd3aa
14 changed files with 2170 additions and 151 deletions
|
|
@ -1620,13 +1620,37 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
|
|||
|
||||
def invoke_tool(agent, function_name: str, function_args: dict, effective_task_id: str,
|
||||
tool_call_id: Optional[str] = None, messages: list = None,
|
||||
pre_tool_block_checked: bool = False) -> str:
|
||||
pre_tool_block_checked: bool = False,
|
||||
skip_tool_request_middleware: bool = False,
|
||||
tool_request_middleware_trace: Optional[List[Dict[str, Any]]] = None) -> str:
|
||||
"""Invoke a single tool and return the result string. No display logic.
|
||||
|
||||
Handles both agent-level tools (todo, memory, etc.) and registry-dispatched
|
||||
tools. Used by the concurrent execution path; the sequential path retains
|
||||
its own inline invocation for backward-compatible display handling.
|
||||
"""
|
||||
if not isinstance(function_args, dict):
|
||||
function_args = {}
|
||||
|
||||
_tool_middleware_trace = list(tool_request_middleware_trace or [])
|
||||
try:
|
||||
from hermes_cli.middleware import apply_tool_request_middleware
|
||||
|
||||
if not skip_tool_request_middleware:
|
||||
_tool_request_mw = apply_tool_request_middleware(
|
||||
function_name,
|
||||
function_args,
|
||||
task_id=effective_task_id or "",
|
||||
session_id=getattr(agent, "session_id", "") or "",
|
||||
tool_call_id=tool_call_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
)
|
||||
function_args = _tool_request_mw.payload
|
||||
_tool_middleware_trace = _tool_request_mw.trace
|
||||
except Exception as _mw_err:
|
||||
logger.debug("tool_request middleware error: %s", _mw_err)
|
||||
|
||||
# Check plugin hooks for a block directive before executing anything.
|
||||
block_message: Optional[str] = None
|
||||
if not pre_tool_block_checked:
|
||||
|
|
@ -1640,6 +1664,7 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
|
|||
tool_call_id=tool_call_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
middleware_trace=list(_tool_middleware_trace),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -1659,6 +1684,7 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
|
|||
status="blocked",
|
||||
error_type="plugin_block",
|
||||
error_message=block_message,
|
||||
middleware_trace=list(_tool_middleware_trace),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -1666,12 +1692,13 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
|
|||
|
||||
tool_start_time = time.monotonic()
|
||||
|
||||
def _finish_agent_tool(result: Any) -> Any:
|
||||
def _finish_agent_tool(result: Any, observed_args: Optional[dict] = None) -> Any:
|
||||
hook_args = observed_args if isinstance(observed_args, dict) else function_args
|
||||
try:
|
||||
from model_tools import _emit_post_tool_call_hook
|
||||
_emit_post_tool_call_hook(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
function_args=hook_args,
|
||||
result=result,
|
||||
task_id=effective_task_id or "",
|
||||
session_id=getattr(agent, "session_id", "") or "",
|
||||
|
|
@ -1679,89 +1706,116 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
|
|||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
duration_ms=int((time.monotonic() - tool_start_time) * 1000),
|
||||
middleware_trace=list(_tool_middleware_trace),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
if function_name == "todo":
|
||||
from tools.todo_tool import todo_tool as _todo_tool
|
||||
return _finish_agent_tool(
|
||||
_todo_tool(
|
||||
todos=function_args.get("todos"),
|
||||
merge=function_args.get("merge", False),
|
||||
store=agent._todo_store,
|
||||
def _execute(next_args: dict) -> Any:
|
||||
from tools.todo_tool import todo_tool as _todo_tool
|
||||
return _finish_agent_tool(
|
||||
_todo_tool(
|
||||
todos=next_args.get("todos"),
|
||||
merge=next_args.get("merge", False),
|
||||
store=agent._todo_store,
|
||||
),
|
||||
next_args,
|
||||
)
|
||||
)
|
||||
elif function_name == "session_search":
|
||||
session_db = agent._get_session_db_for_recall()
|
||||
if not session_db:
|
||||
from hermes_state import format_session_db_unavailable
|
||||
return _finish_agent_tool(json.dumps({"success": False, "error": format_session_db_unavailable()}))
|
||||
from tools.session_search_tool import session_search as _session_search
|
||||
return _finish_agent_tool(
|
||||
_session_search(
|
||||
query=function_args.get("query", ""),
|
||||
role_filter=function_args.get("role_filter"),
|
||||
limit=function_args.get("limit", 3),
|
||||
session_id=function_args.get("session_id"),
|
||||
around_message_id=function_args.get("around_message_id"),
|
||||
window=function_args.get("window", 5),
|
||||
sort=function_args.get("sort"),
|
||||
db=session_db,
|
||||
current_session_id=agent.session_id,
|
||||
def _execute(next_args: dict) -> Any:
|
||||
session_db = agent._get_session_db_for_recall()
|
||||
if not session_db:
|
||||
from hermes_state import format_session_db_unavailable
|
||||
return _finish_agent_tool(json.dumps({"success": False, "error": format_session_db_unavailable()}), next_args)
|
||||
from tools.session_search_tool import session_search as _session_search
|
||||
return _finish_agent_tool(
|
||||
_session_search(
|
||||
query=next_args.get("query", ""),
|
||||
role_filter=next_args.get("role_filter"),
|
||||
limit=next_args.get("limit", 3),
|
||||
session_id=next_args.get("session_id"),
|
||||
around_message_id=next_args.get("around_message_id"),
|
||||
window=next_args.get("window", 5),
|
||||
sort=next_args.get("sort"),
|
||||
db=session_db,
|
||||
current_session_id=agent.session_id,
|
||||
),
|
||||
next_args,
|
||||
)
|
||||
)
|
||||
elif function_name == "memory":
|
||||
target = function_args.get("target", "memory")
|
||||
from tools.memory_tool import memory_tool as _memory_tool
|
||||
result = _memory_tool(
|
||||
action=function_args.get("action"),
|
||||
target=target,
|
||||
content=function_args.get("content"),
|
||||
old_text=function_args.get("old_text"),
|
||||
store=agent._memory_store,
|
||||
)
|
||||
# Bridge: notify external memory provider of built-in memory writes
|
||||
if agent._memory_manager and function_args.get("action") in {"add", "replace"}:
|
||||
try:
|
||||
agent._memory_manager.on_memory_write(
|
||||
function_args.get("action", ""),
|
||||
target,
|
||||
function_args.get("content", ""),
|
||||
metadata=agent._build_memory_write_metadata(
|
||||
task_id=effective_task_id,
|
||||
tool_call_id=tool_call_id,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return _finish_agent_tool(result)
|
||||
elif agent._memory_manager and agent._memory_manager.has_tool(function_name):
|
||||
return _finish_agent_tool(agent._memory_manager.handle_tool_call(function_name, function_args))
|
||||
elif function_name == "clarify":
|
||||
from tools.clarify_tool import clarify_tool as _clarify_tool
|
||||
return _finish_agent_tool(
|
||||
_clarify_tool(
|
||||
question=function_args.get("question", ""),
|
||||
choices=function_args.get("choices"),
|
||||
callback=agent.clarify_callback,
|
||||
def _execute(next_args: dict) -> Any:
|
||||
target = next_args.get("target", "memory")
|
||||
from tools.memory_tool import memory_tool as _memory_tool
|
||||
result = _memory_tool(
|
||||
action=next_args.get("action"),
|
||||
target=target,
|
||||
content=next_args.get("content"),
|
||||
old_text=next_args.get("old_text"),
|
||||
store=agent._memory_store,
|
||||
)
|
||||
# Bridge: notify external memory provider of built-in memory writes
|
||||
if agent._memory_manager and next_args.get("action") in {"add", "replace"}:
|
||||
try:
|
||||
agent._memory_manager.on_memory_write(
|
||||
next_args.get("action", ""),
|
||||
target,
|
||||
next_args.get("content", ""),
|
||||
metadata=agent._build_memory_write_metadata(
|
||||
task_id=effective_task_id,
|
||||
tool_call_id=tool_call_id,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return _finish_agent_tool(result, next_args)
|
||||
elif agent._memory_manager and agent._memory_manager.has_tool(function_name):
|
||||
def _execute(next_args: dict) -> Any:
|
||||
return _finish_agent_tool(agent._memory_manager.handle_tool_call(function_name, next_args), next_args)
|
||||
elif function_name == "clarify":
|
||||
def _execute(next_args: dict) -> Any:
|
||||
from tools.clarify_tool import clarify_tool as _clarify_tool
|
||||
return _finish_agent_tool(
|
||||
_clarify_tool(
|
||||
question=next_args.get("question", ""),
|
||||
choices=next_args.get("choices"),
|
||||
callback=agent.clarify_callback,
|
||||
),
|
||||
next_args,
|
||||
)
|
||||
)
|
||||
elif function_name == "delegate_task":
|
||||
return _finish_agent_tool(agent._dispatch_delegate_task(function_args))
|
||||
def _execute(next_args: dict) -> Any:
|
||||
return _finish_agent_tool(agent._dispatch_delegate_task(next_args), next_args)
|
||||
else:
|
||||
return _ra().handle_function_call(
|
||||
function_name, function_args, effective_task_id,
|
||||
tool_call_id=tool_call_id,
|
||||
session_id=agent.session_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
enabled_tools=list(agent.valid_tool_names) if agent.valid_tool_names else None,
|
||||
skip_pre_tool_call_hook=True,
|
||||
enabled_toolsets=getattr(agent, "enabled_toolsets", None),
|
||||
disabled_toolsets=getattr(agent, "disabled_toolsets", None),
|
||||
)
|
||||
def _execute(next_args: dict) -> Any:
|
||||
return _ra().handle_function_call(
|
||||
function_name, next_args, effective_task_id,
|
||||
tool_call_id=tool_call_id,
|
||||
session_id=agent.session_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
enabled_tools=list(agent.valid_tool_names) if agent.valid_tool_names else None,
|
||||
skip_pre_tool_call_hook=True,
|
||||
skip_tool_request_middleware=True,
|
||||
enabled_toolsets=getattr(agent, "enabled_toolsets", None),
|
||||
disabled_toolsets=getattr(agent, "disabled_toolsets", None),
|
||||
tool_request_middleware_trace=list(_tool_middleware_trace),
|
||||
)
|
||||
|
||||
from hermes_cli.middleware import run_tool_execution_middleware
|
||||
|
||||
return run_tool_execution_middleware(
|
||||
function_name,
|
||||
function_args,
|
||||
lambda next_args: _execute(next_args if isinstance(next_args, dict) else function_args),
|
||||
original_args=function_args,
|
||||
task_id=effective_task_id or "",
|
||||
session_id=getattr(agent, "session_id", "") or "",
|
||||
tool_call_id=tool_call_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1239,6 +1239,28 @@ def run_conversation(
|
|||
_sanitize_structure_non_ascii(api_kwargs)
|
||||
if agent.api_mode == "codex_responses":
|
||||
api_kwargs = agent._get_transport().preflight_kwargs(api_kwargs, allow_stream=False)
|
||||
try:
|
||||
from hermes_cli.middleware import apply_llm_request_middleware
|
||||
|
||||
_llm_request_mw = apply_llm_request_middleware(
|
||||
api_kwargs,
|
||||
task_id=effective_task_id,
|
||||
turn_id=turn_id,
|
||||
api_request_id=api_request_id,
|
||||
session_id=agent.session_id or "",
|
||||
platform=agent.platform or "",
|
||||
model=agent.model,
|
||||
provider=agent.provider,
|
||||
base_url=agent.base_url,
|
||||
api_mode=agent.api_mode,
|
||||
api_call_count=api_call_count,
|
||||
)
|
||||
api_kwargs = _llm_request_mw.payload
|
||||
_original_api_kwargs = _llm_request_mw.original_payload
|
||||
_llm_middleware_trace = _llm_request_mw.trace
|
||||
except Exception:
|
||||
_original_api_kwargs = dict(api_kwargs)
|
||||
_llm_middleware_trace = []
|
||||
|
||||
try:
|
||||
from hermes_cli.plugins import (
|
||||
|
|
@ -1291,6 +1313,7 @@ def run_conversation(
|
|||
request_char_count=total_chars,
|
||||
max_tokens=agent.max_tokens,
|
||||
started_at=api_start_time,
|
||||
middleware_trace=list(_llm_middleware_trace),
|
||||
request=_request_payload,
|
||||
)
|
||||
except Exception:
|
||||
|
|
@ -1349,7 +1372,24 @@ def run_conversation(
|
|||
)
|
||||
return agent._interruptible_api_call(next_api_kwargs)
|
||||
|
||||
response = _perform_api_call(api_kwargs)
|
||||
from hermes_cli.middleware import run_llm_execution_middleware
|
||||
|
||||
response = run_llm_execution_middleware(
|
||||
api_kwargs,
|
||||
_perform_api_call,
|
||||
original_request=_original_api_kwargs,
|
||||
task_id=effective_task_id,
|
||||
turn_id=turn_id,
|
||||
api_request_id=api_request_id,
|
||||
session_id=agent.session_id or "",
|
||||
platform=agent.platform or "",
|
||||
model=agent.model,
|
||||
provider=agent.provider,
|
||||
base_url=agent.base_url,
|
||||
api_mode=agent.api_mode,
|
||||
api_call_count=api_call_count,
|
||||
middleware_trace=list(_llm_middleware_trace),
|
||||
)
|
||||
|
||||
api_duration = time.time() - api_start_time
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ def _emit_terminal_post_tool_call(
|
|||
status: str | None = None,
|
||||
error_type: str | None = None,
|
||||
error_message: str | None = None,
|
||||
middleware_trace: Optional[list[dict[str, Any]]] = None,
|
||||
) -> None:
|
||||
try:
|
||||
from model_tools import _emit_post_tool_call_hook
|
||||
|
|
@ -86,6 +87,7 @@ def _emit_terminal_post_tool_call(
|
|||
status=status,
|
||||
error_type=error_type,
|
||||
error_message=error_message,
|
||||
middleware_trace=list(middleware_trace or []),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -111,6 +113,7 @@ def _emit_cancelled_terminal_post_tool_call(
|
|||
start_time: float,
|
||||
reason: str = "user interrupt",
|
||||
error_type: str = "keyboard_interrupt",
|
||||
middleware_trace: Optional[list[dict[str, Any]]] = None,
|
||||
) -> str:
|
||||
result = _cancelled_tool_result(reason)
|
||||
_emit_terminal_post_tool_call(
|
||||
|
|
@ -124,6 +127,7 @@ def _emit_cancelled_terminal_post_tool_call(
|
|||
status="cancelled",
|
||||
error_type=error_type,
|
||||
error_message=f"Tool execution cancelled by {reason}",
|
||||
middleware_trace=list(middleware_trace or []),
|
||||
)
|
||||
return result
|
||||
|
||||
|
|
@ -177,6 +181,65 @@ def _tool_search_scoped_names(agent) -> frozenset:
|
|||
return names
|
||||
|
||||
|
||||
def _apply_tool_request_middleware_for_agent(
|
||||
agent,
|
||||
*,
|
||||
function_name: str,
|
||||
function_args: dict,
|
||||
effective_task_id: str,
|
||||
tool_call_id: str,
|
||||
) -> tuple[dict, list[dict[str, Any]]]:
|
||||
try:
|
||||
from hermes_cli.middleware import apply_tool_request_middleware
|
||||
|
||||
result = apply_tool_request_middleware(
|
||||
function_name,
|
||||
function_args,
|
||||
task_id=effective_task_id or "",
|
||||
session_id=getattr(agent, "session_id", "") or "",
|
||||
tool_call_id=tool_call_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
)
|
||||
payload = result.payload if isinstance(result.payload, dict) else function_args
|
||||
return payload, list(result.trace)
|
||||
except Exception as exc:
|
||||
logger.debug("tool_request middleware error: %s", exc)
|
||||
return function_args, []
|
||||
|
||||
|
||||
def _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
*,
|
||||
function_name: str,
|
||||
function_args: dict,
|
||||
effective_task_id: str,
|
||||
tool_call_id: str,
|
||||
execute,
|
||||
) -> tuple[Any, dict]:
|
||||
observed_args = function_args
|
||||
|
||||
def _execute(next_args: dict) -> Any:
|
||||
nonlocal observed_args
|
||||
observed_args = next_args if isinstance(next_args, dict) else function_args
|
||||
return execute(observed_args)
|
||||
|
||||
from hermes_cli.middleware import run_tool_execution_middleware
|
||||
|
||||
result = run_tool_execution_middleware(
|
||||
function_name,
|
||||
function_args,
|
||||
_execute,
|
||||
original_args=function_args,
|
||||
task_id=effective_task_id or "",
|
||||
session_id=getattr(agent, "session_id", "") or "",
|
||||
tool_call_id=tool_call_id or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
)
|
||||
return result, observed_args
|
||||
|
||||
|
||||
def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effective_task_id: str, api_call_count: int = 0) -> None:
|
||||
"""Execute multiple tool calls concurrently using a thread pool.
|
||||
|
||||
|
|
@ -198,7 +261,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
return
|
||||
|
||||
# ── Parse args + pre-execution bookkeeping ───────────────────────
|
||||
parsed_calls = [] # list of (tool_call, function_name, function_args)
|
||||
parsed_calls = [] # list of (tool_call, function_name, function_args, middleware_trace, block_result, blocked_by_guardrail)
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
|
||||
|
|
@ -250,6 +313,14 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
function_args, middleware_trace = _apply_tool_request_middleware_for_agent(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
)
|
||||
|
||||
# ── Block evaluation (BEFORE checkpoint preflight) ───────────
|
||||
# We must know whether the tool will execute before touching
|
||||
# checkpoint state (dedup slot, real snapshots).
|
||||
|
|
@ -268,6 +339,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
status="blocked",
|
||||
error_type="tool_scope_block",
|
||||
error_message=_ts_scope_block,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
|
|
@ -280,6 +352,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
except Exception:
|
||||
block_message = None
|
||||
|
|
@ -296,6 +369,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
status="blocked",
|
||||
error_type="plugin_block",
|
||||
error_message=block_message,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
else:
|
||||
guardrail_decision = agent._tool_guardrails.before_call(function_name, function_args)
|
||||
|
|
@ -312,6 +386,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
status="blocked",
|
||||
error_type="guardrail_block",
|
||||
error_message=getattr(guardrail_decision, "message", None) or "Tool blocked by guardrail policy",
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
|
||||
# ── Checkpoint preflight (only for tools that will execute) ──
|
||||
|
|
@ -338,13 +413,13 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
parsed_calls.append((tool_call, function_name, function_args, block_result, blocked_by_guardrail))
|
||||
parsed_calls.append((tool_call, function_name, function_args, middleware_trace, block_result, blocked_by_guardrail))
|
||||
|
||||
# ── Logging / callbacks ──────────────────────────────────────────
|
||||
tool_names_str = ", ".join(name for _, name, _, _, _ in parsed_calls)
|
||||
tool_names_str = ", ".join(name for _, name, _, _, _, _ in parsed_calls)
|
||||
if not agent.quiet_mode:
|
||||
print(f" ⚡ Concurrent: {num_tools} tool calls — {tool_names_str}")
|
||||
for i, (tc, name, args, block_result, blocked_by_guardrail) in enumerate(parsed_calls, 1):
|
||||
for i, (tc, name, args, middleware_trace, block_result, blocked_by_guardrail) in enumerate(parsed_calls, 1):
|
||||
args_str = json.dumps(args, ensure_ascii=False)
|
||||
if agent.verbose_logging:
|
||||
print(f" 📞 Tool {i}: {name}({list(args.keys())})")
|
||||
|
|
@ -353,7 +428,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
args_preview = args_str[:agent.log_prefix_chars] + "..." if len(args_str) > agent.log_prefix_chars else args_str
|
||||
print(f" 📞 Tool {i}: {name}({list(args.keys())}) - {args_preview}")
|
||||
|
||||
for tc, name, args, block_result, blocked_by_guardrail in parsed_calls:
|
||||
for tc, name, args, middleware_trace, block_result, blocked_by_guardrail in parsed_calls:
|
||||
if block_result is not None:
|
||||
continue
|
||||
if agent.tool_progress_callback:
|
||||
|
|
@ -363,7 +438,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
except Exception as cb_err:
|
||||
logging.debug(f"Tool progress callback error: {cb_err}")
|
||||
|
||||
for tc, name, args, block_result, blocked_by_guardrail in parsed_calls:
|
||||
for tc, name, args, middleware_trace, block_result, blocked_by_guardrail in parsed_calls:
|
||||
if block_result is not None:
|
||||
continue
|
||||
if agent.tool_start_callback:
|
||||
|
|
@ -373,18 +448,18 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
logging.debug(f"Tool start callback error: {cb_err}")
|
||||
|
||||
# ── Concurrent execution ─────────────────────────────────────────
|
||||
# Each slot holds (function_name, function_args, function_result, duration, error_flag, blocked_flag)
|
||||
# Each slot holds (function_name, function_args, function_result, duration, error_flag, blocked_flag, middleware_trace)
|
||||
results = [None] * num_tools
|
||||
for i, (tc, name, args, block_result, blocked_by_guardrail) in enumerate(parsed_calls):
|
||||
for i, (tc, name, args, middleware_trace, block_result, blocked_by_guardrail) in enumerate(parsed_calls):
|
||||
if block_result is not None:
|
||||
results[i] = (name, args, block_result, 0.0, True, True)
|
||||
results[i] = (name, args, block_result, 0.0, True, True, middleware_trace)
|
||||
|
||||
# Touch activity before launching workers so the gateway knows
|
||||
# we're executing tools (not stuck).
|
||||
agent._current_tool = tool_names_str
|
||||
agent._touch_activity(f"executing {num_tools} tools concurrently: {tool_names_str}")
|
||||
|
||||
def _run_tool(index, tool_call, function_name, function_args):
|
||||
def _run_tool(index, tool_call, function_name, function_args, middleware_trace):
|
||||
"""Worker function executed in a thread."""
|
||||
# Register this worker tid so the agent can fan out an interrupt
|
||||
# to it — see AIAgent.interrupt(). Must happen first thing, and
|
||||
|
|
@ -423,6 +498,8 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
tool_call.id,
|
||||
messages=messages,
|
||||
pre_tool_block_checked=True,
|
||||
skip_tool_request_middleware=True,
|
||||
tool_request_middleware_trace=list(middleware_trace),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
try:
|
||||
|
|
@ -436,10 +513,11 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
start_time=start,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
duration = time.time() - start
|
||||
logger.info("tool %s cancelled (%.2fs)", function_name, duration)
|
||||
results[index] = (function_name, function_args, result, duration, True, False)
|
||||
results[index] = (function_name, function_args, result, duration, True, False, middleware_trace)
|
||||
return
|
||||
except Exception as tool_error:
|
||||
result = f"Error executing tool '{function_name}': {tool_error}"
|
||||
|
|
@ -450,7 +528,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
logger.info("tool %s failed (%.2fs): %s", function_name, duration, result[:200])
|
||||
else:
|
||||
logger.info("tool %s completed (%.2fs, %d chars)", function_name, duration, len(result))
|
||||
results[index] = (function_name, function_args, result, duration, is_error, False)
|
||||
results[index] = (function_name, function_args, result, duration, is_error, False, middleware_trace)
|
||||
finally:
|
||||
# Tear down worker-tid tracking. Clear any interrupt bit we may
|
||||
# have set so the next task scheduled onto this recycled tid
|
||||
|
|
@ -475,7 +553,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
try:
|
||||
runnable_calls = [
|
||||
(i, tc, name, args)
|
||||
for i, (tc, name, args, block_result, blocked_by_guardrail) in enumerate(parsed_calls)
|
||||
for i, (tc, name, args, middleware_trace, block_result, blocked_by_guardrail) in enumerate(parsed_calls)
|
||||
if block_result is None
|
||||
]
|
||||
futures = []
|
||||
|
|
@ -487,7 +565,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
# _approval_session_key) AND thread-local approval/sudo
|
||||
# callbacks into the worker thread; clears callbacks on exit.
|
||||
f = executor.submit(
|
||||
propagate_context_to_thread(_run_tool), i, tc, name, args
|
||||
propagate_context_to_thread(_run_tool), i, tc, name, args, parsed_calls[i][3]
|
||||
)
|
||||
futures.append(f)
|
||||
|
||||
|
|
@ -545,7 +623,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
spinner.stop(f"⚡ {completed}/{num_tools} tools completed in {total_dur:.1f}s total")
|
||||
|
||||
# ── Post-execution: display per-tool results ─────────────────────
|
||||
for i, (tc, name, args, block_result, blocked_by_guardrail) in enumerate(parsed_calls):
|
||||
for i, (tc, name, args, middleware_trace, block_result, blocked_by_guardrail) in enumerate(parsed_calls):
|
||||
r = results[i]
|
||||
blocked = False
|
||||
if r is None:
|
||||
|
|
@ -562,6 +640,7 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
status="cancelled",
|
||||
error_type="keyboard_interrupt",
|
||||
error_message="Tool execution cancelled by user interrupt",
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
else:
|
||||
function_result = f"Error executing tool '{name}': thread did not return a result"
|
||||
|
|
@ -575,10 +654,11 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
status="error",
|
||||
error_type="thread_missing_result",
|
||||
error_message=function_result,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
tool_duration = 0.0
|
||||
else:
|
||||
function_name, function_args, function_result, tool_duration, is_error, blocked = r
|
||||
function_name, function_args, function_result, tool_duration, is_error, blocked, middleware_trace = r
|
||||
|
||||
if not blocked:
|
||||
function_result = agent._append_guardrail_observation(
|
||||
|
|
@ -738,6 +818,14 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
function_args, middleware_trace = _apply_tool_request_middleware_for_agent(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
)
|
||||
|
||||
# Check plugin hooks for a block directive before executing.
|
||||
_block_msg: Optional[str] = None
|
||||
_block_error_type = "plugin_block"
|
||||
|
|
@ -755,6 +843,7 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
turn_id=getattr(agent, "_current_turn_id", "") or "",
|
||||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -853,6 +942,7 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
status="blocked",
|
||||
error_type=_block_error_type,
|
||||
error_message=_block_msg,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
elif _guardrail_block_decision is not None:
|
||||
# Tool blocked by tool-loop guardrail — synthesize exactly one
|
||||
|
|
@ -869,71 +959,108 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
status="blocked",
|
||||
error_type="guardrail_block",
|
||||
error_message=getattr(_guardrail_block_decision, "message", None) or "Tool blocked by guardrail policy",
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
elif function_name == "todo":
|
||||
from tools.todo_tool import todo_tool as _todo_tool
|
||||
function_result = _todo_tool(
|
||||
todos=function_args.get("todos"),
|
||||
merge=function_args.get("merge", False),
|
||||
store=agent._todo_store,
|
||||
def _execute(next_args: dict) -> Any:
|
||||
from tools.todo_tool import todo_tool as _todo_tool
|
||||
return _todo_tool(
|
||||
todos=next_args.get("todos"),
|
||||
merge=next_args.get("merge", False),
|
||||
store=agent._todo_store,
|
||||
)
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
tool_duration = time.time() - tool_start_time
|
||||
if agent._should_emit_quiet_tool_messages():
|
||||
agent._vprint(f" {_get_cute_tool_message_impl('todo', function_args, tool_duration, result=function_result)}")
|
||||
elif function_name == "session_search":
|
||||
session_db = agent._get_session_db_for_recall()
|
||||
if not session_db:
|
||||
from hermes_state import format_session_db_unavailable
|
||||
function_result = json.dumps({"success": False, "error": format_session_db_unavailable()})
|
||||
else:
|
||||
def _execute(next_args: dict) -> Any:
|
||||
session_db = agent._get_session_db_for_recall()
|
||||
if not session_db:
|
||||
from hermes_state import format_session_db_unavailable
|
||||
return json.dumps({"success": False, "error": format_session_db_unavailable()})
|
||||
from tools.session_search_tool import session_search as _session_search
|
||||
function_result = _session_search(
|
||||
query=function_args.get("query", ""),
|
||||
role_filter=function_args.get("role_filter"),
|
||||
limit=function_args.get("limit", 3),
|
||||
session_id=function_args.get("session_id"),
|
||||
around_message_id=function_args.get("around_message_id"),
|
||||
window=function_args.get("window", 5),
|
||||
sort=function_args.get("sort"),
|
||||
return _session_search(
|
||||
query=next_args.get("query", ""),
|
||||
role_filter=next_args.get("role_filter"),
|
||||
limit=next_args.get("limit", 3),
|
||||
session_id=next_args.get("session_id"),
|
||||
around_message_id=next_args.get("around_message_id"),
|
||||
window=next_args.get("window", 5),
|
||||
sort=next_args.get("sort"),
|
||||
db=session_db,
|
||||
current_session_id=agent.session_id,
|
||||
)
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
tool_duration = time.time() - tool_start_time
|
||||
if agent._should_emit_quiet_tool_messages():
|
||||
agent._vprint(f" {_get_cute_tool_message_impl('session_search', function_args, tool_duration, result=function_result)}")
|
||||
elif function_name == "memory":
|
||||
target = function_args.get("target", "memory")
|
||||
from tools.memory_tool import memory_tool as _memory_tool
|
||||
function_result = _memory_tool(
|
||||
action=function_args.get("action"),
|
||||
target=target,
|
||||
content=function_args.get("content"),
|
||||
old_text=function_args.get("old_text"),
|
||||
store=agent._memory_store,
|
||||
def _execute(next_args: dict) -> Any:
|
||||
target = next_args.get("target", "memory")
|
||||
from tools.memory_tool import memory_tool as _memory_tool
|
||||
result = _memory_tool(
|
||||
action=next_args.get("action"),
|
||||
target=target,
|
||||
content=next_args.get("content"),
|
||||
old_text=next_args.get("old_text"),
|
||||
store=agent._memory_store,
|
||||
)
|
||||
# Bridge: notify external memory provider of built-in memory writes
|
||||
if agent._memory_manager and next_args.get("action") in {"add", "replace"}:
|
||||
try:
|
||||
agent._memory_manager.on_memory_write(
|
||||
next_args.get("action", ""),
|
||||
target,
|
||||
next_args.get("content", ""),
|
||||
metadata=agent._build_memory_write_metadata(
|
||||
task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", None),
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
# Bridge: notify external memory provider of built-in memory writes
|
||||
if agent._memory_manager and function_args.get("action") in {"add", "replace"}:
|
||||
try:
|
||||
agent._memory_manager.on_memory_write(
|
||||
function_args.get("action", ""),
|
||||
target,
|
||||
function_args.get("content", ""),
|
||||
metadata=agent._build_memory_write_metadata(
|
||||
task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", None),
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
tool_duration = time.time() - tool_start_time
|
||||
if agent._should_emit_quiet_tool_messages():
|
||||
agent._vprint(f" {_get_cute_tool_message_impl('memory', function_args, tool_duration, result=function_result)}")
|
||||
elif function_name == "clarify":
|
||||
from tools.clarify_tool import clarify_tool as _clarify_tool
|
||||
function_result = _clarify_tool(
|
||||
question=function_args.get("question", ""),
|
||||
choices=function_args.get("choices"),
|
||||
callback=agent.clarify_callback,
|
||||
def _execute(next_args: dict) -> Any:
|
||||
from tools.clarify_tool import clarify_tool as _clarify_tool
|
||||
return _clarify_tool(
|
||||
question=next_args.get("question", ""),
|
||||
choices=next_args.get("choices"),
|
||||
callback=agent.clarify_callback,
|
||||
)
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
tool_duration = time.time() - tool_start_time
|
||||
if agent._should_emit_quiet_tool_messages():
|
||||
|
|
@ -957,7 +1084,16 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
agent._delegate_spinner = spinner
|
||||
_delegate_result = None
|
||||
try:
|
||||
function_result = agent._dispatch_delegate_task(function_args)
|
||||
def _execute(next_args: dict) -> Any:
|
||||
return agent._dispatch_delegate_task(next_args)
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
_delegate_result = function_result
|
||||
finally:
|
||||
agent._delegate_spinner = None
|
||||
|
|
@ -978,7 +1114,16 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
spinner.start()
|
||||
_ce_result = None
|
||||
try:
|
||||
function_result = agent.context_compressor.handle_tool_call(function_name, function_args, messages=messages)
|
||||
def _execute(next_args: dict) -> Any:
|
||||
return agent.context_compressor.handle_tool_call(function_name, next_args, messages=messages)
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
_ce_result = function_result
|
||||
except Exception as tool_error:
|
||||
function_result = json.dumps({"error": f"Context engine tool '{function_name}' failed: {tool_error}"})
|
||||
|
|
@ -1002,7 +1147,16 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
spinner.start()
|
||||
_mem_result = None
|
||||
try:
|
||||
function_result = agent._memory_manager.handle_tool_call(function_name, function_args)
|
||||
def _execute(next_args: dict) -> Any:
|
||||
return agent._memory_manager.handle_tool_call(function_name, next_args)
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
execute=_execute,
|
||||
)
|
||||
_mem_result = function_result
|
||||
except Exception as tool_error:
|
||||
function_result = json.dumps({"error": f"Memory tool '{function_name}' failed: {tool_error}"})
|
||||
|
|
@ -1032,8 +1186,10 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
enabled_tools=list(agent.valid_tool_names) if agent.valid_tool_names else None,
|
||||
skip_pre_tool_call_hook=True,
|
||||
skip_tool_request_middleware=True,
|
||||
enabled_toolsets=getattr(agent, "enabled_toolsets", None),
|
||||
disabled_toolsets=getattr(agent, "disabled_toolsets", None),
|
||||
tool_request_middleware_trace=list(middleware_trace),
|
||||
)
|
||||
_spinner_result = function_result
|
||||
except KeyboardInterrupt:
|
||||
|
|
@ -1044,6 +1200,7 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
start_time=tool_start_time,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
_spinner_result = function_result
|
||||
try:
|
||||
|
|
@ -1071,8 +1228,10 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
|
||||
enabled_tools=list(agent.valid_tool_names) if agent.valid_tool_names else None,
|
||||
skip_pre_tool_call_hook=True,
|
||||
skip_tool_request_middleware=True,
|
||||
enabled_toolsets=getattr(agent, "enabled_toolsets", None),
|
||||
disabled_toolsets=getattr(agent, "disabled_toolsets", None),
|
||||
tool_request_middleware_trace=list(middleware_trace),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
_emit_cancelled_terminal_post_tool_call(
|
||||
|
|
@ -1082,6 +1241,7 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
start_time=tool_start_time,
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
try:
|
||||
agent.interrupt("keyboard interrupt")
|
||||
|
|
@ -1126,6 +1286,7 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
effective_task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", "") or "",
|
||||
duration_ms=int(tool_duration * 1000),
|
||||
middleware_trace=list(middleware_trace),
|
||||
)
|
||||
if not _execution_blocked:
|
||||
function_result = agent._append_guardrail_observation(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue