Skip to content

feat: implement ExitPlanMode HITL for Tool Permission Model#1589

Open
quay-devel wants to merge 5 commits into
ambient-code:mainfrom
quay-devel:feat/exit-plan-mode-hitl
Open

feat: implement ExitPlanMode HITL for Tool Permission Model#1589
quay-devel wants to merge 5 commits into
ambient-code:mainfrom
quay-devel:feat/exit-plan-mode-hitl

Conversation

@quay-devel
Copy link
Copy Markdown
Contributor

@quay-devel quay-devel commented May 14, 2026

Summary

Implements the Tool Permission Model spec from PR #1586, adding ExitPlanMode as a HITL (human-in-the-loop) tool that halts the event stream and waits for user approval — the same mechanism already used by AskUserQuestion.

  • Runner: ExitPlanMode added to BUILTIN_FRONTEND_TOOLS halt set; plan file content injected into tool args; Tier 1 allowlist completed with all missing tools
  • Backend: isAskUserQuestionToolCallisHITLToolCall to detect both tools for status derivation and snapshot compaction; new test cases for ExitPlanMode
  • Frontend: HITL detection generalized via shared hitl-tools.ts; new ExitPlanModeMessage component with approve/reject/request-changes actions

Closes #1583
Spec: #1586

Test plan

  • Backend Go tests pass (go test ./websocket/...)
  • Frontend build passes with 0 errors, 0 warnings (npm run build)
  • Frontend tests pass (668 passed, 12 skipped via npx vitest run)
  • Runner adapter syntax verified
  • No panic() in Go code, no any types in TypeScript
  • Manual: verify ExitPlanMode halts stream and shows plan approval UI
  • Manual: verify approve/reject/request-changes sends correct tool result

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Plan Review UI: users can approve, reject, or request changes to plans with optional feedback.
  • Improvements

    • Broader human-in-the-loop tool support: interactions now include an additional tool type.
    • More consistent detection and handling of tool-driven prompts so the UI reliably shows waiting-for-input states.

quay-devel and others added 2 commits May 14, 2026 18:41
Add ExitPlanMode as a human-in-the-loop tool alongside AskUserQuestion,
enabling plan approval workflows in ACP sessions. This implements the
spec from PR ambient-code#1586 (closes ambient-code#1583).

Runner:
- Add ExitPlanMode to BUILTIN_FRONTEND_TOOLS halt set
- Enrich ExitPlanMode tool args with plan file content from .claude/plans/
- Complete Tier 1 tool allowlist (NotebookEdit, WebFetch, TodoWrite, etc.)

Backend:
- Generalize isAskUserQuestionToolCall → isHITLToolCall to detect both
  AskUserQuestion and ExitPlanMode for status derivation and compaction
- Add ExitPlanMode test cases for waiting_input detection

Frontend:
- Generalize HITL detection in use-agent-status and stream-message
- Add ExitPlanModeMessage component with approve/reject/request-changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract shared hitl-tools.ts with normalizeToolName, isHITLTool,
  isAskUserQuestionTool, isExitPlanModeTool, and hasToolResult helpers
- Remove duplicated hasResult and tool detection functions from
  ask-user-question.tsx, exit-plan-mode.tsx, stream-message.tsx,
  and use-agent-status.ts
- Add 100KB size guard to _read_plan_file to prevent oversized events
- Log JSON errors during ExitPlanMode plan enrichment instead of
  silently swallowing them
- Use stable composite key for allowedPrompts list rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 14, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit bf81358
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/6a0ce99122737e00083dd937

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d0f3eb00-f7da-47ef-a2c9-31a6009956b3

📥 Commits

Reviewing files that changed from the base of the PR and between 45eb7fc and bf81358.

📒 Files selected for processing (3)
  • components/frontend/src/components/session/exit-plan-mode.tsx
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py
  • components/frontend/src/components/session/exit-plan-mode.tsx
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py

📝 Walkthrough

Walkthrough

Adds unified HITL (AskUserQuestion + ExitPlanMode) detection and handling across backend, frontend, and runner: backend generalizes HITL detection and preserves state, frontend centralizes helpers, updates status logic, and adds ExitPlanMode UI, and the runner enriches ExitPlanMode calls and expands allowed tools.

Changes

HITL Tool Support

Layer / File(s) Summary
Backend HITL detection & tests
components/backend/websocket/agui_proxy.go, components/backend/websocket/agui_store.go, components/backend/websocket/agui_store_test.go
Replaces AskUserQuestion-only checks with isHITLToolCall (normalizes names and matches AskUserQuestion/ExitPlanMode). DeriveAgentStatus and compactFinishedRun now preserve/infer waiting_input for HITL tools. Tests updated to include ExitPlanMode and casing variants.
Frontend: hitl-tools helpers & status wiring
components/frontend/src/lib/hitl-tools.ts, components/frontend/src/hooks/use-agent-status.ts, components/frontend/src/components/session/ask-user-question.tsx
New tool-name normalization and predicates (normalizeToolName, isAskUserQuestionTool, isExitPlanModeTool, isHITLTool) and hasToolResult. useAgentStatus now scans for HITL tools; AskUserQuestion component uses hasToolResult.
Frontend: stream rendering & ExitPlanMode UI
components/frontend/src/components/ui/stream-message.tsx, components/frontend/src/components/session/exit-plan-mode.tsx
stream-message imports HITL predicates and renders ExitPlanModeMessage for ExitPlanMode tool uses. New ExitPlanModeMessage component renders plan-review UI, handles approve/reject/request-changes with optional feedback and submission state.
Runner: ExitPlanMode enrichment & allowlist
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py, components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py
Adds ExitPlanMode to builtin frontend tools; _read_plan_file() reads latest .claude/plans/*.md (UTF-8, truncated to 100KB) and injects planContent into tool call args. Expands DEFAULT_ALLOWED_TOOLS to include ExitPlanMode and other tools to avoid unhandled permission prompts.

Sequence Diagram

sequenceDiagram
    participant Claude as Claude (AI)
    participant Adapter as Adapter
    participant Backend as Backend (WebSocket)
    participant Frontend as Frontend
    participant User as User

    Claude->>Adapter: Invoke ExitPlanMode (tool call)
    Adapter->>Adapter: Read latest .claude/plans/*.md
    Adapter->>Adapter: Inject planContent into arguments
    Adapter->>Backend: Emit TOOL_CALL_START (ExitPlanMode)
    Backend->>Backend: isHITLToolCall() matches ExitPlanMode
    Backend->>Frontend: Stream event: TOOL_CALL_START / waiting_input
    Frontend->>Frontend: useAgentStatus() detects HITL tool
    Frontend->>Frontend: stream-message checks isExitPlanModeTool()
    Frontend->>Frontend: Render ExitPlanModeMessage with plan content
    Frontend->>User: Display "Plan Review" UI (approve/reject/changes)
    User->>Frontend: Click approve/reject + optional feedback
    Frontend->>Backend: Submit tool result via onSubmitAnswer
    Backend->>Backend: Snapshot TOOL_CALL_START to preserve waiting_input
    Backend->>Claude: Resume execution with user result
Loading
🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (feat scope: description) and clearly describes adding ExitPlanMode HITL support.
Linked Issues check ✅ Passed PR comprehensively addresses issue #1583: implements ExitPlanMode as HITL tool, updates runner allowlist, adds plan content injection, generalizes backend detection, and provides frontend UI component.
Out of Scope Changes check ✅ Passed All changes directly support ExitPlanMode HITL implementation: runner registration, plan file handling, backend detection generalization, frontend components, and related tool allowlist expansion.
Performance And Algorithmic Complexity ✅ Passed No blocking perf issues. All algorithms bounded: string normalization O(n) on short names, message scan with early exit, plan file read 100KB capped, event log 20MB tail-bounded.
Security And Secret Handling ✅ Passed No hardcoded secrets or tokens. Auth/authz properly enforced. No injection vulnerabilities. Plan file reading safely bounded. No sensitive data leaks. All security controls intact.
Kubernetes Resource Safety ✅ Passed PR modifies only application source code (Go, TypeScript, Python); no Kubernetes manifest files or resource definitions are changed. Check not applicable.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/frontend/src/components/session/exit-plan-mode.tsx`:
- Around line 35-36: The variables planContent and allowedPrompts currently use
TypeScript assertions only; add runtime guards so planContent is set to
input.planContent if typeof input.planContent === "string" otherwise "" and set
allowedPrompts to Array.isArray(input.allowedPrompts) ? input.allowedPrompts as
AllowedPrompt[] : [] (or validate each element) before using ReactMarkdown and
.map; update the initialization of planContent and allowedPrompts in
exit-plan-mode.tsx to perform these checks so ReactMarkdown always gets a string
and .map runs on a real array.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 105-108: The truncation checks byte length but slices by character
count, which can exceed _PLAN_FILE_MAX_BYTES for multi-byte UTF-8 chars; fix by
performing the truncation in bytes: read the file text into content, encode to
bytes (e.g., content_bytes = content.encode("utf-8")), if len(content_bytes) >
_PLAN_FILE_MAX_BYTES then slice the bytes to _PLAN_FILE_MAX_BYTES, decode back
to a string with a safe error handler (e.g., errors="ignore" or "replace") and
append the "\n\n[truncated]" marker before returning; update the logic around
plan_files, content, and _PLAN_FILE_MAX_BYTES accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 767b3627-8743-45b6-a69e-396e31eb5da9

📥 Commits

Reviewing files that changed from the base of the PR and between 63545c7 and 45eb7fc.

📒 Files selected for processing (10)
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/backend/websocket/agui_store_test.go
  • components/frontend/src/components/session/ask-user-question.tsx
  • components/frontend/src/components/session/exit-plan-mode.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/hooks/use-agent-status.ts
  • components/frontend/src/lib/hitl-tools.ts
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py

Comment thread components/frontend/src/components/session/exit-plan-mode.tsx Outdated
Comment thread components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
quay-devel and others added 2 commits May 14, 2026 19:11
- Add runtime type guards for planContent (typeof string) and
  allowedPrompts (Array.isArray) in ExitPlanModeMessage to prevent
  runtime errors from unexpected backend data
- Fix byte-accurate truncation in _read_plan_file: slice encoded bytes
  instead of character count to respect the 100KB limit for multi-byte
  UTF-8 content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ExitPlanMode hangs indefinitely in ACP sessions — runner lacks plan approval handler

1 participant