fix/cli hitl (#202)
* feat(display): enhance approval prompt with questionary for better navigation * feat(cli): add HumanInTheLoopMiddleware for user approval in main agent
This commit is contained in:
@@ -23,7 +23,7 @@ import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langchain.agents.middleware import AgentMiddleware, HumanInTheLoopMiddleware
|
||||
|
||||
from . import paths as _paths_mod
|
||||
from .config import apply_config_to_env, get_effective_config
|
||||
@@ -503,16 +503,16 @@ def create_cli_agent(
|
||||
|
||||
mw.insert(0, AskUserMiddleware())
|
||||
|
||||
# HITL on main agent only — passing `interrupt_on=` to create_deep_agent
|
||||
# would propagate it to every subagent, breaking parallel execute calls
|
||||
# (multi-pending-interrupt LangGraph error).
|
||||
if not cfg.auto_approve:
|
||||
mw.append(HumanInTheLoopMiddleware(interrupt_on={"execute": True}))
|
||||
|
||||
# Re-load MCP tools from current config (picks up /mcp add changes)
|
||||
kwargs = load_mcp_and_build_kwargs(be, mw, on_mcp_progress=on_mcp_progress)
|
||||
|
||||
# HITL: gate shell execution for user approval
|
||||
_interrupt_on: dict[str, bool] | None = None
|
||||
if not cfg.auto_approve:
|
||||
_interrupt_on = {"execute": True}
|
||||
|
||||
return create_deep_agent(
|
||||
**kwargs,
|
||||
checkpointer=checkpointer,
|
||||
interrupt_on=_interrupt_on,
|
||||
).with_config({"recursion_limit": 1000})
|
||||
|
||||
@@ -977,9 +977,18 @@ def _prompt_hitl_approval(action_requests: list) -> list[dict] | None:
|
||||
"""Display approval prompt and get user decision.
|
||||
|
||||
Returns list of decisions if approved, None if rejected.
|
||||
|
||||
Uses ``questionary.select()`` for arrow-key navigation, matching the
|
||||
style used by ``_resolve_ask_user_prompt``. Imports are lazy so the
|
||||
auto-approve / shell-allow-list fast paths in ``_resolve_hitl_approval``
|
||||
don't pay for them.
|
||||
"""
|
||||
global _session_auto_approve
|
||||
|
||||
import questionary # type: ignore[import-untyped]
|
||||
|
||||
from ..cli.widgets.thread_selector import PICKER_STYLE as _PICKER_STYLE
|
||||
|
||||
console.print()
|
||||
panel_text = Text()
|
||||
for i, req in enumerate(action_requests):
|
||||
@@ -993,10 +1002,6 @@ def _prompt_hitl_approval(action_requests: list) -> list[dict] | None:
|
||||
if panel_text.plain:
|
||||
panel_text.append("\n")
|
||||
panel_text.append(f" {i + 1}. {desc}", style="yellow")
|
||||
panel_text.append("\n\n")
|
||||
panel_text.append(
|
||||
" [1] Approve [2] Reject [3] Approve all (session)", style="dim"
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
@@ -1007,21 +1012,37 @@ def _prompt_hitl_approval(action_requests: list) -> list[dict] | None:
|
||||
)
|
||||
)
|
||||
|
||||
n = len(action_requests)
|
||||
if n <= 1:
|
||||
approve_label = "Approve"
|
||||
reject_label = "Reject"
|
||||
else:
|
||||
approve_label = f"Approve all {n}"
|
||||
reject_label = f"Reject all {n}"
|
||||
auto_label = "Approve all (session)"
|
||||
|
||||
try:
|
||||
choice = input(" Choose [1/2/3, Enter=Approve]: ").strip() or "1"
|
||||
selected = questionary.select(
|
||||
"Approval required",
|
||||
choices=[approve_label, reject_label, auto_label],
|
||||
style=_PICKER_STYLE,
|
||||
).ask()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
console.print("[dim] Rejected.[/dim]")
|
||||
return None
|
||||
|
||||
if choice == "1":
|
||||
return [{"type": "approve"} for _ in action_requests]
|
||||
elif choice == "3":
|
||||
_session_auto_approve = True
|
||||
return [{"type": "approve"} for _ in action_requests]
|
||||
else:
|
||||
if selected is None: # Ctrl+C inside questionary
|
||||
console.print("[dim] Rejected.[/dim]")
|
||||
return None
|
||||
|
||||
if selected == approve_label:
|
||||
return [{"type": "approve"} for _ in action_requests]
|
||||
if selected == auto_label:
|
||||
_session_auto_approve = True
|
||||
return [{"type": "approve"} for _ in action_requests]
|
||||
console.print("[dim] Rejected.[/dim]")
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async-to-sync bridge
|
||||
|
||||
Reference in New Issue
Block a user