{"record":{"id":"fcd7c7c07ad2c30e","repo":"langchain-ai/deepagents","slug":"manual-mode-callback-is-unavailable","errorCode":null,"errorMessage":"Manual mode callback is unavailable","messagePattern":"Manual mode callback is unavailable","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/tui/textual_adapter.py","lineNumber":3660,"sourceCode":"                                    _set_running_unless_deferred(tool_msg)\n                                    adapter._sync_tool_widget(tool_msg)\n                                for action_request in action_requests:\n                                    tool_name = action_request.get(\"name\")\n                                    if tool_name in {\n                                        \"write_file\",\n                                        \"edit_file\",\n                                        \"delete\",\n                                    }:\n                                        args = action_request.get(\"args\", {})\n                                        if isinstance(args, dict):\n                                            file_op_tracker.mark_hitl_approved(\n                                                tool_name, args\n                                            )\n\n                            elif decision_type == \"switch_manual\":\n                                if adapter._on_switch_to_manual is None:\n                                    msg = \"Manual mode callback is unavailable\"\n                                    raise RuntimeError(msg)  # noqa: TRY301  # shared turn error rendering owns this failure\n                                callback_result = adapter._on_switch_to_manual()\n                                switched = (\n                                    await callback_result\n                                    if inspect.isawaitable(callback_result)\n                                    else callback_result\n                                )\n                                if not switched:\n                                    msg = \"Manual mode could not be persisted\"\n                                    raise RuntimeError(msg)  # noqa: TRY301  # shared turn error rendering owns this failure\n                                decisions = [\n                                    cast(\"HITLDecision\", {\"type\": \"switch_manual\"})\n                                    for _ in action_requests\n                                ]\n\n                            elif decision_type == \"approve\":\n                                decisions = [\n                                    ApproveDecision(type=\"approve\")\n                                    for _ in action_requests","sourceCodeStart":3642,"sourceCodeEnd":3678,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/tui/textual_adapter.py#L3642-L3678","documentation":"When the model emits a 'switch_manual' HITL decision, execute_task_textual invokes adapter._on_switch_to_manual. If that callback is None (the UI never wired a manual-mode switch handler), RuntimeError is raised instead of silently ignoring the decision, and the shared turn-error rendering reports the failure.","triggerScenarios":"An interrupt decision of type 'switch_manual' arrives from the agent (e.g. a request to switch out of auto-approve), but the adapter was constructed without _on_switch_to_manual being set.","commonSituations":"Embedding the textual adapter headlessly or in tests without the full TUI wiring; a UI refactor that dropped the callback registration; running auto-approve flows where manual switching was never expected.","solutions":["Register adapter._on_switch_to_manual when constructing the adapter for sessions that can enter manual approval mode.","Handle the 'switch_manual' decision differently (e.g. reject with a message) when the callback is absent.","Update the TUI/adapter integration so the callback is always provided for interactive sessions."],"exampleFix":"// before\nadapter = TextualAdapter(..., _on_switch_to_manual=None)\n// after\nadapter = TextualAdapter(..., _on_switch_to_manual=self._switch_to_manual)","handlingStrategy":"type-guard","validationCode":"if adapter._on_switch_to_manual is None:\n    decisions = [{\"type\": \"reject\", \"message\": \"manual switch unsupported\"}]\nelse:\n    decisions = [{\"type\": \"switch_manual\"}]","typeGuard":"def supports_manual_switch(adapter: object) -> TypeGuard[Adapter]:\n    return getattr(adapter, \"_on_switch_to_manual\", None) is not None","tryCatchPattern":"try:\n    handle_hitl_decision(adapter, decision)\nexcept RuntimeError as exc:\n    render_turn_error(f\"cannot switch to manual: {exc}\")","preventionTips":["Always register _on_switch_to_manual when constructing adapters for interactive sessions.","Add an adapter self-check at startup that required HITL callbacks are present.","Cover 'switch_manual' decisions in integration tests."],"tags":["hitl","approval-mode","callback"],"backgroundTag":"missing-callback-registration","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}