Full Code of OpenHands/software-agent-sdk for AI

main 26af67d95147 cached
1197 files
9.6 MB
2.6M tokens
10864 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (10,324K chars total). Download the full file to get everything.
Repository: OpenHands/software-agent-sdk
Branch: main
Commit: 26af67d95147
Files: 1197
Total size: 9.6 MB

Directory structure:
gitextract_0wirow34/

├── .agents/
│   └── skills/
│       ├── cross-repo-testing/
│       │   └── SKILL.md
│       ├── custom-codereview-guide.md
│       ├── debug-test-examples-workflow/
│       │   └── SKILL.md
│       ├── design-principles.md
│       ├── feature-release-rollout/
│       │   └── SKILL.md
│       ├── manage-evals/
│       │   ├── SKILL.md
│       │   ├── references/
│       │   │   └── eval-infrastructure.md
│       │   └── scripts/
│       │       └── manage_evals.py
│       ├── run-eval.md
│       ├── sdk-release/
│       │   ├── SKILL.md
│       │   └── references/
│       │       └── post-release-checklist.md
│       └── write-behavior-test.md
├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_template.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── dependabot.yml
│   ├── prompts/
│   │   └── update-documentation.md
│   ├── run-eval/
│   │   ├── ADDINGMODEL.md
│   │   ├── AGENTS.md
│   │   ├── resolve_model_config.py
│   │   └── validate_sdk_ref.py
│   ├── scripts/
│   │   ├── check_agent_server_rest_api_breakage.py
│   │   ├── check_deprecations.py
│   │   ├── check_docstrings.py
│   │   ├── check_documented_examples.py
│   │   ├── check_duplicate_example_numbers.py
│   │   ├── check_sdk_api_breakage.py
│   │   ├── check_version_bumps.py
│   │   └── update_sdk_ref_default.py
│   └── workflows/
│       ├── README-RELEASE.md
│       ├── agent-server-rest-api-breakage.yml
│       ├── api-breakage.yml
│       ├── api-compliance-runner.yml
│       ├── assign-reviews.yml
│       ├── auto-label-issues.yml
│       ├── cancel-eval.yml
│       ├── check-docstrings.yml
│       ├── check-documented-examples.yml
│       ├── check-duplicate-examples.yml
│       ├── condenser-runner.yml
│       ├── create-release.yml
│       ├── deploy-docs.yml
│       ├── deprecation-check.yml
│       ├── integration-runner.yml
│       ├── issue-duplicate-checker.yml
│       ├── oh-update-documentation.yml.back
│       ├── pr-artifacts.yml
│       ├── pr-review-by-openhands.yml
│       ├── pr-review-evaluation.yml
│       ├── precommit.yml
│       ├── prepare-release.yml
│       ├── pypi-release.yml
│       ├── qa-changes-by-openhands.yml
│       ├── qa-changes-evaluation.yml
│       ├── release-binaries.yml
│       ├── remove-duplicate-candidate-label.yml
│       ├── review-thread-gate.yml
│       ├── run-eval.yml
│       ├── run-examples.yml
│       ├── server.yml
│       ├── stale.yml
│       ├── tests.yml
│       ├── todo-management.yml
│       ├── version-bump-guard.yml
│       └── version-bump-prs.yml
├── .gitignore
├── .openhands/
│   ├── hooks/
│   │   └── on_stop.sh
│   ├── hooks.json
│   └── setup.sh
├── .pre-commit-config.yaml
├── .python-version
├── AGENTS.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── LICENSE
├── MAINTAINERS
├── MANIFEST.in
├── Makefile
├── README.md
├── examples/
│   ├── 01_standalone_sdk/
│   │   ├── 01_hello_world.py
│   │   ├── 02_custom_tools.py
│   │   ├── 03_activate_skill.py
│   │   ├── 04_confirmation_mode_example.py
│   │   ├── 05_use_llm_registry.py
│   │   ├── 06_interactive_terminal_w_reasoning.py
│   │   ├── 07_mcp_integration.py
│   │   ├── 08_mcp_with_oauth.py
│   │   ├── 09_pause_example.py
│   │   ├── 10_persistence.py
│   │   ├── 11_async.py
│   │   ├── 12_custom_secrets.py
│   │   ├── 13_get_llm_metrics.py
│   │   ├── 14_context_condenser.py
│   │   ├── 15_browser_use.py
│   │   ├── 16_llm_security_analyzer.py
│   │   ├── 17_image_input.py
│   │   ├── 18_send_message_while_processing.py
│   │   ├── 19_llm_routing.py
│   │   ├── 20_stuck_detector.py
│   │   ├── 21_generate_extraneous_conversation_costs.py
│   │   ├── 22_anthropic_thinking.py
│   │   ├── 23_responses_reasoning.py
│   │   ├── 24_planning_agent_workflow.py
│   │   ├── 25_agent_delegation.py
│   │   ├── 26_custom_visualizer.py
│   │   ├── 27_observability_laminar.py
│   │   ├── 28_ask_agent_example.py
│   │   ├── 29_llm_streaming.py
│   │   ├── 30_tom_agent.py
│   │   ├── 31_iterative_refinement.py
│   │   ├── 32_configurable_security_policy.py
│   │   ├── 33_hooks/
│   │   │   ├── README.md
│   │   │   ├── hook_scripts/
│   │   │   │   ├── block_dangerous.sh
│   │   │   │   ├── inject_git_context.sh
│   │   │   │   ├── log_tools.sh
│   │   │   │   └── require_summary.sh
│   │   │   └── main.py
│   │   ├── 34_critic_example.py
│   │   ├── 35_subscription_login.py
│   │   ├── 36_event_json_to_openai_messages.py
│   │   ├── 37_llm_profile_store/
│   │   │   ├── main.py
│   │   │   └── profiles/
│   │   │       └── fast.json
│   │   ├── 38_browser_session_recording.py
│   │   ├── 39_llm_fallback.py
│   │   ├── 40_acp_agent_example.py
│   │   ├── 41_task_tool_set.py
│   │   ├── 42_file_based_subagents.py
│   │   ├── 43_mixed_marketplace_skills/
│   │   │   ├── .plugin/
│   │   │   │   └── marketplace.json
│   │   │   ├── README.md
│   │   │   ├── main.py
│   │   │   └── skills/
│   │   │       └── greeting-helper/
│   │   │           └── SKILL.md
│   │   ├── 44_model_switching_in_convo.py
│   │   ├── 45_parallel_tool_execution.py
│   │   ├── 46_agent_settings.py
│   │   ├── 47_defense_in_depth_security.py
│   │   ├── 48_conversation_fork.py
│   │   └── 49_switch_llm_tool.py
│   ├── 02_remote_agent_server/
│   │   ├── 01_convo_with_local_agent_server.py
│   │   ├── 02_convo_with_docker_sandboxed_server.py
│   │   ├── 03_browser_use_with_docker_sandboxed_server.py
│   │   ├── 04_convo_with_api_sandboxed_server.py
│   │   ├── 05_vscode_with_docker_sandboxed_server.py
│   │   ├── 06_custom_tool/
│   │   │   ├── Dockerfile
│   │   │   ├── README.md
│   │   │   ├── build_custom_image.sh
│   │   │   ├── custom_tools/
│   │   │   │   ├── __init__.py
│   │   │   │   └── log_data.py
│   │   │   └── main.py
│   │   ├── 07_convo_with_cloud_workspace.py
│   │   ├── 08_convo_with_apptainer_sandboxed_server.py
│   │   ├── 09_acp_agent_with_remote_runtime.py
│   │   ├── 10_cloud_workspace_share_credentials.py
│   │   ├── 11_conversation_fork.py
│   │   ├── 12_settings_and_secrets_api.py
│   │   ├── 13_workspace_get_llm.py
│   │   └── hook_scripts/
│   │       └── pycompile_check.sh
│   ├── 03_github_workflows/
│   │   ├── 01_basic_action/
│   │   │   ├── README.md
│   │   │   ├── agent_script.py
│   │   │   ├── assign-reviews.yml
│   │   │   └── workflow.yml
│   │   ├── 02_pr_review/
│   │   │   ├── README.md
│   │   │   └── workflow.yml
│   │   ├── 03_todo_management/
│   │   │   ├── README.md
│   │   │   ├── agent_script.py
│   │   │   ├── prompt.py
│   │   │   ├── scanner.py
│   │   │   └── workflow.yml
│   │   ├── 04_datadog_debugging/
│   │   │   ├── README.md
│   │   │   ├── datadog_debugging.py
│   │   │   ├── debug_prompt.jinja
│   │   │   └── workflow.yml
│   │   └── 05_posthog_debugging/
│   │       ├── README.md
│   │       ├── debug_prompt.jinja
│   │       ├── posthog_debugging.py
│   │       └── workflow.yml
│   ├── 04_llm_specific_tools/
│   │   ├── 01_gpt5_apply_patch_preset.py
│   │   └── 02_gemini_file_tools.py
│   └── 05_skills_and_plugins/
│       ├── 01_loading_agentskills/
│       │   ├── example_skills/
│       │   │   ├── code-style-guide/
│       │   │   │   └── SKILL.md
│       │   │   └── rot13-encryption/
│       │   │       ├── SKILL.md
│       │   │       ├── references/
│       │   │       │   └── examples.md
│       │   │       └── scripts/
│       │   │           └── encrypt.sh
│       │   └── main.py
│       ├── 02_loading_plugins/
│       │   ├── example_plugins/
│       │   │   └── code-quality/
│       │   │       ├── .mcp.json
│       │   │       ├── .plugin/
│       │   │       │   └── plugin.json
│       │   │       ├── hooks/
│       │   │       │   └── hooks.json
│       │   │       └── skills/
│       │   │           └── linting/
│       │   │               └── SKILL.md
│       │   └── main.py
│       └── 03_managing_installed_skills/
│           └── main.py
├── openhands-agent-server/
│   ├── AGENTS.md
│   ├── openhands/
│   │   └── agent_server/
│   │       ├── README.md
│   │       ├── __init__.py
│   │       ├── __main__.py
│   │       ├── _secrets_exposure.py
│   │       ├── agent-server.spec
│   │       ├── api.py
│   │       ├── auth_router.py
│   │       ├── bash_router.py
│   │       ├── bash_service.py
│   │       ├── cloud_proxy_router.py
│   │       ├── config.py
│   │       ├── conversation_lease.py
│   │       ├── conversation_router.py
│   │       ├── conversation_router_acp.py
│   │       ├── conversation_service.py
│   │       ├── dependencies.py
│   │       ├── desktop_router.py
│   │       ├── desktop_service.py
│   │       ├── docker/
│   │       │   ├── Dockerfile
│   │       │   └── build.py
│   │       ├── env_parser.py
│   │       ├── event_router.py
│   │       ├── event_service.py
│   │       ├── file_router.py
│   │       ├── git_router.py
│   │       ├── hooks_router.py
│   │       ├── hooks_service.py
│   │       ├── llm_router.py
│   │       ├── logging_config.py
│   │       ├── middleware.py
│   │       ├── models.py
│   │       ├── openapi.py
│   │       ├── persistence/
│   │       │   ├── __init__.py
│   │       │   ├── models.py
│   │       │   └── store.py
│   │       ├── profiles_router.py
│   │       ├── pub_sub.py
│   │       ├── py.typed
│   │       ├── server_details_router.py
│   │       ├── settings_router.py
│   │       ├── skills_router.py
│   │       ├── skills_service.py
│   │       ├── sockets.py
│   │       ├── tool_preload_service.py
│   │       ├── tool_router.py
│   │       ├── utils.py
│   │       ├── vscode_extensions/
│   │       │   └── openhands-settings/
│   │       │       ├── extension.js
│   │       │       └── package.json
│   │       ├── vscode_router.py
│   │       ├── vscode_service.py
│   │       └── workspace_router.py
│   └── pyproject.toml
├── openhands-sdk/
│   ├── openhands/
│   │   └── sdk/
│   │       ├── AGENTS.md
│   │       ├── __init__.py
│   │       ├── agent/
│   │       │   ├── __init__.py
│   │       │   ├── acp_agent.py
│   │       │   ├── agent.py
│   │       │   ├── base.py
│   │       │   ├── critic_mixin.py
│   │       │   ├── parallel_executor.py
│   │       │   ├── prompts/
│   │       │   │   ├── in_context_learning_example.j2
│   │       │   │   ├── in_context_learning_example_suffix.j2
│   │       │   │   ├── model_specific/
│   │       │   │   │   ├── anthropic_claude.j2
│   │       │   │   │   ├── google_gemini.j2
│   │       │   │   │   └── openai_gpt/
│   │       │   │   │       ├── gpt-5-codex.j2
│   │       │   │   │       └── gpt-5.j2
│   │       │   │   ├── security_policy.j2
│   │       │   │   ├── security_risk_assessment.j2
│   │       │   │   ├── self_documentation.j2
│   │       │   │   ├── system_prompt.j2
│   │       │   │   ├── system_prompt_interactive.j2
│   │       │   │   ├── system_prompt_long_horizon.j2
│   │       │   │   ├── system_prompt_planning.j2
│   │       │   │   └── system_prompt_tech_philosophy.j2
│   │       │   ├── response_dispatch.py
│   │       │   └── utils.py
│   │       ├── banner.py
│   │       ├── context/
│   │       │   ├── README.md
│   │       │   ├── __init__.py
│   │       │   ├── agent_context.py
│   │       │   ├── condenser/
│   │       │   │   ├── README.md
│   │       │   │   ├── __init__.py
│   │       │   │   ├── base.py
│   │       │   │   ├── llm_summarizing_condenser.py
│   │       │   │   ├── no_op_condenser.py
│   │       │   │   ├── pipeline_condenser.py
│   │       │   │   ├── prompts/
│   │       │   │   │   └── summarizing_prompt.j2
│   │       │   │   └── utils.py
│   │       │   ├── prompts/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── prompt.py
│   │       │   │   └── templates/
│   │       │   │       ├── ask_agent_template.j2
│   │       │   │       ├── skill_knowledge_info.j2
│   │       │   │       └── system_message_suffix.j2
│   │       │   ├── skills/
│   │       │   │   └── __init__.py
│   │       │   └── view/
│   │       │       ├── __init__.py
│   │       │       ├── manipulation_indices.py
│   │       │       ├── properties/
│   │       │       │   ├── __init__.py
│   │       │       │   ├── base.py
│   │       │       │   ├── batch_atomicity.py
│   │       │       │   ├── observation_uniqueness.py
│   │       │       │   ├── tool_call_matching.py
│   │       │       │   └── tool_loop_atomicity.py
│   │       │       └── view.py
│   │       ├── conversation/
│   │       │   ├── __init__.py
│   │       │   ├── base.py
│   │       │   ├── conversation.py
│   │       │   ├── conversation_stats.py
│   │       │   ├── event_store.py
│   │       │   ├── events_list_base.py
│   │       │   ├── exceptions.py
│   │       │   ├── fifo_lock.py
│   │       │   ├── impl/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── local_conversation.py
│   │       │   │   └── remote_conversation.py
│   │       │   ├── persistence_const.py
│   │       │   ├── request.py
│   │       │   ├── resource_lock_manager.py
│   │       │   ├── response_utils.py
│   │       │   ├── secret_registry.py
│   │       │   ├── serialization_diff.py
│   │       │   ├── state.py
│   │       │   ├── stuck_detector.py
│   │       │   ├── title_utils.py
│   │       │   ├── types.py
│   │       │   └── visualizer/
│   │       │       ├── __init__.py
│   │       │       ├── base.py
│   │       │       └── default.py
│   │       ├── critic/
│   │       │   ├── __init__.py
│   │       │   ├── base.py
│   │       │   ├── impl/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── agent_finished.py
│   │       │   │   ├── api/
│   │       │   │   │   ├── __init__.py
│   │       │   │   │   ├── chat_template.py
│   │       │   │   │   ├── client.py
│   │       │   │   │   ├── critic.py
│   │       │   │   │   └── taxonomy.py
│   │       │   │   ├── empty_patch.py
│   │       │   │   └── pass_critic.py
│   │       │   └── result.py
│   │       ├── event/
│   │       │   ├── __init__.py
│   │       │   ├── acp_tool_call.py
│   │       │   ├── base.py
│   │       │   ├── condenser.py
│   │       │   ├── conversation_error.py
│   │       │   ├── conversation_state.py
│   │       │   ├── hook_execution.py
│   │       │   ├── llm_completion_log.py
│   │       │   ├── llm_convertible/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── action.py
│   │       │   │   ├── message.py
│   │       │   │   ├── observation.py
│   │       │   │   └── system.py
│   │       │   ├── streaming_delta.py
│   │       │   ├── token.py
│   │       │   ├── types.py
│   │       │   └── user_action.py
│   │       ├── extensions/
│   │       │   ├── __init__.py
│   │       │   ├── fetch.py
│   │       │   └── installation/
│   │       │       ├── README.md
│   │       │       ├── __init__.py
│   │       │       ├── info.py
│   │       │       ├── interface.py
│   │       │       ├── manager.py
│   │       │       ├── metadata.py
│   │       │       └── utils.py
│   │       ├── git/
│   │       │   ├── cached_repo.py
│   │       │   ├── exceptions.py
│   │       │   ├── git_changes.py
│   │       │   ├── git_diff.py
│   │       │   ├── models.py
│   │       │   └── utils.py
│   │       ├── hooks/
│   │       │   ├── __init__.py
│   │       │   ├── config.py
│   │       │   ├── conversation_hooks.py
│   │       │   ├── executor.py
│   │       │   ├── manager.py
│   │       │   └── types.py
│   │       ├── io/
│   │       │   ├── __init__.py
│   │       │   ├── base.py
│   │       │   ├── cache.py
│   │       │   ├── local.py
│   │       │   └── memory.py
│   │       ├── llm/
│   │       │   ├── __init__.py
│   │       │   ├── auth/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── credentials.py
│   │       │   │   └── openai.py
│   │       │   ├── exceptions/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── classifier.py
│   │       │   │   ├── mapping.py
│   │       │   │   └── types.py
│   │       │   ├── fallback_strategy.py
│   │       │   ├── llm.py
│   │       │   ├── llm_profile_store.py
│   │       │   ├── llm_registry.py
│   │       │   ├── llm_response.py
│   │       │   ├── message.py
│   │       │   ├── mixins/
│   │       │   │   ├── fn_call_converter.py
│   │       │   │   ├── fn_call_examples.py
│   │       │   │   └── non_native_fc.py
│   │       │   ├── options/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── chat_options.py
│   │       │   │   ├── common.py
│   │       │   │   └── responses_options.py
│   │       │   ├── router/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── base.py
│   │       │   │   └── impl/
│   │       │   │       ├── multimodal.py
│   │       │   │       └── random.py
│   │       │   ├── streaming.py
│   │       │   └── utils/
│   │       │       ├── image_resize.py
│   │       │       ├── litellm_provider.py
│   │       │       ├── metrics.py
│   │       │       ├── model_features.py
│   │       │       ├── model_info.py
│   │       │       ├── model_prompt_spec.py
│   │       │       ├── responses_serialization.py
│   │       │       ├── retry_mixin.py
│   │       │       ├── telemetry.py
│   │       │       ├── unverified_models.py
│   │       │       └── verified_models.py
│   │       ├── logger/
│   │       │   ├── __init__.py
│   │       │   ├── logger.py
│   │       │   └── rolling.py
│   │       ├── marketplace/
│   │       │   ├── __init__.py
│   │       │   └── types.py
│   │       ├── mcp/
│   │       │   ├── __init__.py
│   │       │   ├── client.py
│   │       │   ├── definition.py
│   │       │   ├── exceptions.py
│   │       │   ├── tool.py
│   │       │   └── utils.py
│   │       ├── observability/
│   │       │   ├── __init__.py
│   │       │   ├── laminar.py
│   │       │   └── utils.py
│   │       ├── plugin/
│   │       │   ├── __init__.py
│   │       │   ├── fetch.py
│   │       │   ├── installed.py
│   │       │   ├── loader.py
│   │       │   ├── plugin.py
│   │       │   ├── source.py
│   │       │   └── types.py
│   │       ├── py.typed
│   │       ├── secret/
│   │       │   ├── __init__.py
│   │       │   └── secrets.py
│   │       ├── security/
│   │       │   ├── __init__.py
│   │       │   ├── analyzer.py
│   │       │   ├── confirmation_policy.py
│   │       │   ├── defense_in_depth/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── pattern.py
│   │       │   │   ├── policy_rails.py
│   │       │   │   └── utils.py
│   │       │   ├── ensemble.py
│   │       │   ├── grayswan/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── analyzer.py
│   │       │   │   └── utils.py
│   │       │   ├── llm_analyzer.py
│   │       │   └── risk.py
│   │       ├── settings/
│   │       │   ├── __init__.py
│   │       │   ├── acp_providers.py
│   │       │   ├── api_models.py
│   │       │   ├── metadata.py
│   │       │   └── model.py
│   │       ├── skills/
│   │       │   ├── __init__.py
│   │       │   ├── exceptions.py
│   │       │   ├── execute.py
│   │       │   ├── fetch.py
│   │       │   ├── installed.py
│   │       │   ├── skill.py
│   │       │   ├── trigger.py
│   │       │   ├── types.py
│   │       │   └── utils.py
│   │       ├── subagent/
│   │       │   ├── AGENTS.md
│   │       │   ├── __init__.py
│   │       │   ├── load.py
│   │       │   ├── registry.py
│   │       │   └── schema.py
│   │       ├── testing/
│   │       │   ├── __init__.py
│   │       │   └── test_llm.py
│   │       ├── tool/
│   │       │   ├── __init__.py
│   │       │   ├── builtins/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── finish.py
│   │       │   │   ├── invoke_skill.py
│   │       │   │   ├── switch_llm.py
│   │       │   │   └── think.py
│   │       │   ├── registry.py
│   │       │   ├── schema.py
│   │       │   ├── spec.py
│   │       │   └── tool.py
│   │       ├── utils/
│   │       │   ├── __init__.py
│   │       │   ├── async_executor.py
│   │       │   ├── async_utils.py
│   │       │   ├── cipher.py
│   │       │   ├── command.py
│   │       │   ├── datetime.py
│   │       │   ├── deprecation.py
│   │       │   ├── github.py
│   │       │   ├── json.py
│   │       │   ├── models.py
│   │       │   ├── paging.py
│   │       │   ├── path.py
│   │       │   ├── pydantic_diff.py
│   │       │   ├── pydantic_secrets.py
│   │       │   ├── redact.py
│   │       │   ├── truncate.py
│   │       │   └── visualize.py
│   │       └── workspace/
│   │           ├── __init__.py
│   │           ├── base.py
│   │           ├── local.py
│   │           ├── models.py
│   │           ├── remote/
│   │           │   ├── __init__.py
│   │           │   ├── async_remote_workspace.py
│   │           │   ├── base.py
│   │           │   └── remote_workspace_mixin.py
│   │           ├── repo.py
│   │           └── workspace.py
│   └── pyproject.toml
├── openhands-tools/
│   ├── openhands/
│   │   └── tools/
│   │       ├── AGENTS.md
│   │       ├── __init__.py
│   │       ├── apply_patch/
│   │       │   ├── __init__.py
│   │       │   ├── core.py
│   │       │   └── definition.py
│   │       ├── browser_use/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── event_storage.py
│   │       │   ├── impl.py
│   │       │   ├── js/
│   │       │   │   ├── flush-events.js
│   │       │   │   ├── rrweb-loader.js
│   │       │   │   ├── start-recording-simple.js
│   │       │   │   ├── start-recording.js
│   │       │   │   ├── stop-recording.js
│   │       │   │   └── wait-for-rrweb.js
│   │       │   ├── logging_fix.py
│   │       │   ├── recording.py
│   │       │   └── server.py
│   │       ├── delegate/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── impl.py
│   │       │   ├── templates/
│   │       │   │   └── delegate_tool_description.j2
│   │       │   └── visualizer.py
│   │       ├── file_editor/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── editor.py
│   │       │   ├── exceptions.py
│   │       │   ├── impl.py
│   │       │   └── utils/
│   │       │       ├── __init__.py
│   │       │       ├── config.py
│   │       │       ├── constants.py
│   │       │       ├── diff.py
│   │       │       ├── encoding.py
│   │       │       ├── file_cache.py
│   │       │       ├── history.py
│   │       │       └── shell.py
│   │       ├── gemini/
│   │       │   ├── __init__.py
│   │       │   ├── edit/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── definition.py
│   │       │   │   └── impl.py
│   │       │   ├── list_directory/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── definition.py
│   │       │   │   └── impl.py
│   │       │   ├── read_file/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── definition.py
│   │       │   │   └── impl.py
│   │       │   └── write_file/
│   │       │       ├── __init__.py
│   │       │       ├── definition.py
│   │       │       └── impl.py
│   │       ├── glob/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── impl.py
│   │       ├── grep/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── impl.py
│   │       ├── planning_file_editor/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── impl.py
│   │       ├── preset/
│   │       │   ├── __init__.py
│   │       │   ├── default.py
│   │       │   ├── gemini.py
│   │       │   ├── gpt5.py
│   │       │   ├── planning.py
│   │       │   └── subagents/
│   │       │       ├── bash_runner.md
│   │       │       ├── code_explorer.md
│   │       │       ├── default.md
│   │       │       └── web_researcher.md
│   │       ├── py.typed
│   │       ├── task/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── impl.py
│   │       │   └── manager.py
│   │       ├── task_tracker/
│   │       │   ├── __init__.py
│   │       │   └── definition.py
│   │       ├── terminal/
│   │       │   ├── README.md
│   │       │   ├── __init__.py
│   │       │   ├── constants.py
│   │       │   ├── definition.py
│   │       │   ├── descriptions.py
│   │       │   ├── impl.py
│   │       │   ├── metadata.py
│   │       │   ├── terminal/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── factory.py
│   │       │   │   ├── interface.py
│   │       │   │   ├── subprocess_terminal.py
│   │       │   │   ├── terminal_session.py
│   │       │   │   ├── tmux_pane_pool.py
│   │       │   │   ├── tmux_terminal.py
│   │       │   │   └── windows_terminal.py
│   │       │   └── utils/
│   │       │       ├── __init__.py
│   │       │       ├── command.py
│   │       │       └── escape_filter.py
│   │       ├── tom_consult/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── executor.py
│   │       └── utils/
│   │           ├── __init__.py
│   │           └── timeout.py
│   └── pyproject.toml
├── openhands-workspace/
│   ├── openhands/
│   │   └── workspace/
│   │       ├── AGENTS.md
│   │       ├── __init__.py
│   │       ├── apptainer/
│   │       │   ├── README.md
│   │       │   ├── __init__.py
│   │       │   └── workspace.py
│   │       ├── cloud/
│   │       │   ├── __init__.py
│   │       │   └── workspace.py
│   │       ├── docker/
│   │       │   ├── __init__.py
│   │       │   ├── dev_workspace.py
│   │       │   └── workspace.py
│   │       ├── py.typed
│   │       └── remote_api/
│   │           ├── __init__.py
│   │           └── workspace.py
│   └── pyproject.toml
├── pyproject.toml
├── scripts/
│   ├── agent_server_ui/
│   │   ├── run.sh
│   │   └── static/
│   │       ├── app-dev.js
│   │       ├── app.js
│   │       ├── index-dev.html
│   │       ├── index.html
│   │       └── styles.css
│   ├── auto_close_duplicate_issues.py
│   ├── build_config_template.py
│   ├── check_import_rules.py
│   ├── check_tool_registration.py
│   ├── completion_logs_viewer.py
│   ├── conversation_viewer.py
│   ├── convert_legacy_skills.py
│   ├── event_sourcing_benchmarks/
│   │   ├── README.md
│   │   ├── bench_persist_latency.py
│   │   ├── bench_replay_and_recovery.py
│   │   ├── bench_storage_growth.py
│   │   └── benchmark_utils.py
│   ├── issue_duplicate_check_openhands.py
│   ├── render_examples_report.py
│   └── websocket_client.html
└── tests/
    ├── README.md
    ├── __init__.py
    ├── agent_server/
    │   ├── __init__.py
    │   ├── stress/
    │   │   ├── __init__.py
    │   │   ├── budgets.py
    │   │   ├── conftest.py
    │   │   ├── probe.py
    │   │   ├── scripts.py
    │   │   ├── test_concurrent_conversations.py
    │   │   ├── test_conversation_listing.py
    │   │   ├── test_event_loop_responsiveness.py
    │   │   ├── test_high_volume_bash_output.py
    │   │   ├── test_lease_contention.py
    │   │   ├── test_long_running_command.py
    │   │   ├── test_parallel_subagents.py
    │   │   ├── test_slow_webhook.py
    │   │   ├── test_slow_websocket_consumer.py
    │   │   └── test_websocket_reconnect_storm.py
    │   ├── test_agent_server_wsproto.py
    │   ├── test_api.py
    │   ├── test_api_authentication.py
    │   ├── test_bash_service.py
    │   ├── test_check_browser.py
    │   ├── test_cloud_proxy_router.py
    │   ├── test_conversation_lease.py
    │   ├── test_conversation_response.py
    │   ├── test_conversation_router.py
    │   ├── test_conversation_router_acp.py
    │   ├── test_conversation_service.py
    │   ├── test_conversation_service_plugin.py
    │   ├── test_conversation_tags.py
    │   ├── test_dependencies.py
    │   ├── test_desktop_router.py
    │   ├── test_desktop_service.py
    │   ├── test_docker_build.py
    │   ├── test_env_parser.py
    │   ├── test_event_router.py
    │   ├── test_event_router_websocket.py
    │   ├── test_event_service.py
    │   ├── test_event_streaming.py
    │   ├── test_file_router.py
    │   ├── test_git_router.py
    │   ├── test_hooks_router.py
    │   ├── test_hooks_service.py
    │   ├── test_llm_router.py
    │   ├── test_models.py
    │   ├── test_openapi_discriminator.py
    │   ├── test_preload_modules.py
    │   ├── test_profiles_router.py
    │   ├── test_pub_sub.py
    │   ├── test_server_details_router.py
    │   ├── test_settings_router.py
    │   ├── test_skills_router.py
    │   ├── test_skills_service.py
    │   ├── test_terminal_router.py
    │   ├── test_terminal_service.py
    │   ├── test_tool_router.py
    │   ├── test_validation_error_sanitization.py
    │   ├── test_vscode_router.py
    │   ├── test_vscode_service.py
    │   ├── test_webhook_subscriber.py
    │   ├── test_websocket_first_message_auth.py
    │   ├── test_workspace_cookie_auth.py
    │   └── test_workspace_router.py
    ├── command_utils.py
    ├── conftest.py
    ├── cross/
    │   ├── __init__.py
    │   ├── conftest.py
    │   ├── test_agent_loading.py
    │   ├── test_agent_secrets_integration.py
    │   ├── test_agent_server_build_metadata.py
    │   ├── test_automatic_naming.py
    │   ├── test_automatic_registration.py
    │   ├── test_check_agent_server_rest_api_breakage.py
    │   ├── test_check_deprecations.py
    │   ├── test_check_sdk_api_breakage.py
    │   ├── test_check_version_bumps.py
    │   ├── test_conversation_restore_behavior.py
    │   ├── test_event_loss_repro.py
    │   ├── test_hello_world.py
    │   ├── test_issue_duplicate_scripts.py
    │   ├── test_pr_review_trace.py
    │   ├── test_registry_directories.py
    │   ├── test_registry_qualnames.py
    │   ├── test_remote_conversation_live_server.py
    │   ├── test_resolve_model_config.py
    │   ├── test_stuck_detector.py
    │   ├── test_stuck_detector_config.py
    │   ├── test_todo_scanner.py
    │   └── test_validate_sdk_ref.py
    ├── examples/
    │   └── test_examples.py
    ├── fixtures/
    │   ├── conversations/
    │   │   ├── v1_11_5_cli_default/
    │   │   │   └── base_state.json
    │   │   └── v1_17_0_with_mcp_config/
    │   │       └── base_state.json
    │   ├── llm_data/
    │   │   ├── README.md
    │   │   ├── data_generator.py
    │   │   ├── fncall-llm-message.json
    │   │   ├── llm-logs/
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015025.972.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015029.090.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015033.222.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015036.544.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015040.416.json
    │   │   │   └── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015046.707.json
    │   │   ├── nonfncall-llm-logs/
    │   │   │   ├── litellm_proxy__deepseek__deepseek-chat-1757015054.055.json
    │   │   │   ├── litellm_proxy__deepseek__deepseek-chat-1757015062.589.json
    │   │   │   ├── litellm_proxy__deepseek__deepseek-chat-1757015068.723.json
    │   │   │   └── litellm_proxy__deepseek__deepseek-chat-1757015076.651.json
    │   │   └── nonfncall-llm-message.json
    │   └── tokenizers/
    │       └── qwen3-4b-instruct-2507-tokenizer_config.json
    ├── integration/
    │   ├── BEHAVIOR_TESTS.md
    │   ├── README.md
    │   ├── __init__.py
    │   ├── api_compliance/
    │   │   ├── __init__.py
    │   │   ├── base.py
    │   │   ├── result.py
    │   │   └── run_compliance.py
    │   ├── base.py
    │   ├── behavior_utils.py
    │   ├── early_stopper.py
    │   ├── run_infer.py
    │   ├── schemas.py
    │   ├── test_behavior_utils.py
    │   ├── test_early_stopper.py
    │   ├── test_tool_presets.py
    │   ├── tests/
    │   │   ├── a01_unmatched_tool_use.py
    │   │   ├── a02_unmatched_tool_result.py
    │   │   ├── a03_interleaved_user_msg.py
    │   │   ├── a04_interleaved_asst_msg.py
    │   │   ├── a05_duplicate_tool_call_id.py
    │   │   ├── a06_wrong_tool_call_id.py
    │   │   ├── a07_parallel_missing_result.py
    │   │   ├── a08_parallel_wrong_order.py
    │   │   ├── b01_no_premature_implementation.py
    │   │   ├── b02_no_oververification.py
    │   │   ├── b03_no_useless_backward_compatibility.py
    │   │   ├── b04_each_tool_call_has_a_concise_explanation.py
    │   │   ├── b05_do_not_create_redundant_files.py
    │   │   ├── c01_thinking_block_condenser.py
    │   │   ├── c02_hard_context_reset.py
    │   │   ├── c03_delayed_condensation.py
    │   │   ├── c04_token_condenser.py
    │   │   ├── c05_size_condenser.py
    │   │   ├── t01_fix_simple_typo.py
    │   │   ├── t02_add_bash_hello.py
    │   │   ├── t03_jupyter_write_file.py
    │   │   ├── t04_git_staging.py
    │   │   ├── t05_simple_browsing.py
    │   │   ├── t06_github_pr_browsing.py
    │   │   ├── t07_interactive_commands.py
    │   │   ├── t08_image_file_viewing.py
    │   │   └── t09_invoke_skill.py
    │   └── utils/
    │       ├── __init__.py
    │       ├── behavior_helpers.py
    │       ├── consolidate_json_results.py
    │       ├── consolidate_results.py
    │       ├── format_costs.py
    │       ├── generate_markdown_report.py
    │       └── llm_judge.py
    ├── platform_utils.py
    ├── sdk/
    │   ├── __init__.py
    │   ├── agent/
    │   │   ├── __init__.py
    │   │   ├── test_acp_agent.py
    │   │   ├── test_acp_dedup_and_truncation.py
    │   │   ├── test_action_batch.py
    │   │   ├── test_agent_browser_auto_detect.py
    │   │   ├── test_agent_context_window_condensation.py
    │   │   ├── test_agent_immutability.py
    │   │   ├── test_agent_init_state_invariants.py
    │   │   ├── test_agent_llms_are_discoverable.py
    │   │   ├── test_agent_serialization.py
    │   │   ├── test_agent_step_responses_gating.py
    │   │   ├── test_agent_tool_init.py
    │   │   ├── test_agent_utils.py
    │   │   ├── test_extract_security_risk.py
    │   │   ├── test_extract_summary.py
    │   │   ├── test_fix_malformed_tool_arguments.py
    │   │   ├── test_iterative_refinement.py
    │   │   ├── test_message_while_finishing.py
    │   │   ├── test_non_executable_action_emission.py
    │   │   ├── test_nonexistent_tool_handling.py
    │   │   ├── test_parallel_execution_integration.py
    │   │   ├── test_parallel_executor.py
    │   │   ├── test_parallel_executor_locking.py
    │   │   ├── test_reasoning_only_responses.py
    │   │   ├── test_response_dispatch.py
    │   │   ├── test_sanitize_json_control_chars.py
    │   │   ├── test_security_policy_integration.py
    │   │   ├── test_system_prompt.py
    │   │   ├── test_tool_call_compatibility.py
    │   │   ├── test_tool_call_recovery.py
    │   │   ├── test_tool_execution_error_handling.py
    │   │   └── test_tool_validation_error_message.py
    │   ├── config/
    │   │   ├── __init__.py
    │   │   └── test_llm_config.py
    │   ├── context/
    │   │   ├── __init__.py
    │   │   ├── condenser/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_llm_summarizing_condenser.py
    │   │   │   ├── test_no_op_condenser.py
    │   │   │   ├── test_rolling_condenser.py
    │   │   │   └── test_utils.py
    │   │   ├── test_agent_context.py
    │   │   ├── test_agent_context_model_specific.py
    │   │   ├── test_agent_context_serialization.py
    │   │   ├── test_prompt_absolute_path.py
    │   │   ├── test_prompt_model_spec.py
    │   │   └── view/
    │   │       ├── __init__.py
    │   │       ├── conftest.py
    │   │       ├── properties/
    │   │       │   ├── conftest.py
    │   │       │   ├── test_batch_atomicity.py
    │   │       │   ├── test_observation_uniqueness.py
    │   │       │   ├── test_tool_call_matching.py
    │   │       │   └── test_tool_loop_atomicity.py
    │   │       ├── test_manipulation_indices.py
    │   │       ├── test_view.py
    │   │       ├── test_view_append_event.py
    │   │       ├── test_view_batch_atomicity.py
    │   │       ├── test_view_condensation_batch_atomicity.py
    │   │       ├── test_view_manipulation_indices.py
    │   │       ├── test_view_multi_summary.py
    │   │       └── test_view_tool_loop_boundaries.py
    │   ├── conversation/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── local/
    │   │   │   ├── test_agent_status_transition.py
    │   │   │   ├── test_confirmation_mode.py
    │   │   │   ├── test_conversation_core.py
    │   │   │   ├── test_conversation_default_callback.py
    │   │   │   ├── test_conversation_id.py
    │   │   │   ├── test_conversation_path_types.py
    │   │   │   ├── test_conversation_pause_functionality.py
    │   │   │   ├── test_conversation_send_message.py
    │   │   │   ├── test_conversation_visualize_param.py
    │   │   │   ├── test_execute_tool.py
    │   │   │   ├── test_fork.py
    │   │   │   ├── test_rerun_actions.py
    │   │   │   ├── test_run_exception_includes_conversation_id.py
    │   │   │   ├── test_span_double_ending.py
    │   │   │   └── test_state_serialization.py
    │   │   ├── remote/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_api_key_functionality.py
    │   │   │   ├── test_remote_conversation.py
    │   │   │   ├── test_remote_events_list.py
    │   │   │   ├── test_remote_fork.py
    │   │   │   ├── test_remote_request_logging.py
    │   │   │   ├── test_remote_state.py
    │   │   │   ├── test_run_exception_includes_conversation_id_remote.py
    │   │   │   ├── test_websocket_client.py
    │   │   │   └── test_websocket_subscription_ready.py
    │   │   ├── test_agent_final_response.py
    │   │   ├── test_agent_state_reassignment.py
    │   │   ├── test_ask_agent.py
    │   │   ├── test_atexit_cleanup.py
    │   │   ├── test_base_span_management.py
    │   │   ├── test_condense.py
    │   │   ├── test_conversation_execution_status_enum.py
    │   │   ├── test_conversation_factory.py
    │   │   ├── test_conversation_secrets_constructor.py
    │   │   ├── test_conversation_stats.py
    │   │   ├── test_directories.py
    │   │   ├── test_event_store.py
    │   │   ├── test_fifo_lock.py
    │   │   ├── test_generate_title.py
    │   │   ├── test_get_unmatched_actions.py
    │   │   ├── test_local_conversation_plugins.py
    │   │   ├── test_mcp_secrets_serialization_leak.py
    │   │   ├── test_remote_conversation_state_updates.py
    │   │   ├── test_repo_root_project_skills.py
    │   │   ├── test_resource_lock_manager.py
    │   │   ├── test_secret_source.py
    │   │   ├── test_secrets_manager.py
    │   │   ├── test_state_change_callback.py
    │   │   ├── test_stats_update_event_snapshot.py
    │   │   ├── test_switch_model.py
    │   │   ├── test_tags.py
    │   │   └── test_visualizer.py
    │   ├── critic/
    │   │   ├── __init__.py
    │   │   ├── api/
    │   │   │   └── test_template_render.py
    │   │   ├── test_critic.py
    │   │   ├── test_critic_client.py
    │   │   └── test_critic_display.py
    │   ├── event/
    │   │   ├── __init__.py
    │   │   ├── test_action_event_summary.py
    │   │   ├── test_dynamic_context_message_sequence.py
    │   │   ├── test_event_immutability.py
    │   │   ├── test_event_serialization.py
    │   │   ├── test_events_to_messages.py
    │   │   ├── test_llm_completion_log_event.py
    │   │   ├── test_non_executable_action_event.py
    │   │   ├── test_streaming.py
    │   │   └── test_system_prompt_event_visualize.py
    │   ├── extensions/
    │   │   ├── __init__.py
    │   │   ├── installation/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_installation_info.py
    │   │   │   ├── test_installation_manager.py
    │   │   │   ├── test_installation_metadata.py
    │   │   │   └── test_installation_utils.py
    │   │   └── test_fetch.py
    │   ├── git/
    │   │   ├── __init__.py
    │   │   ├── test_cached_repo.py
    │   │   ├── test_git_changes.py
    │   │   └── test_git_diff.py
    │   ├── hooks/
    │   │   ├── __init__.py
    │   │   ├── test_config.py
    │   │   ├── test_executor.py
    │   │   ├── test_integration.py
    │   │   └── test_manager.py
    │   ├── io/
    │   │   ├── __init__.py
    │   │   ├── test_filestore_cache.py
    │   │   └── test_local_filestore_security.py
    │   ├── llm/
    │   │   ├── __init__.py
    │   │   ├── auth/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_credentials.py
    │   │   │   └── test_openai.py
    │   │   ├── test_api_connection_error_retry.py
    │   │   ├── test_api_key_validation.py
    │   │   ├── test_chat_options.py
    │   │   ├── test_exception.py
    │   │   ├── test_exception_classifier.py
    │   │   ├── test_exception_mapping.py
    │   │   ├── test_llm.py
    │   │   ├── test_llm_completion.py
    │   │   ├── test_llm_fallback.py
    │   │   ├── test_llm_fncall_converter.py
    │   │   ├── test_llm_image_resizing.py
    │   │   ├── test_llm_json_storage.py
    │   │   ├── test_llm_litellm_extra_body.py
    │   │   ├── test_llm_log_completions_integration.py
    │   │   ├── test_llm_metrics.py
    │   │   ├── test_llm_no_response_retry.py
    │   │   ├── test_llm_pricing_passthrough.py
    │   │   ├── test_llm_profile_store.py
    │   │   ├── test_llm_registry.py
    │   │   ├── test_llm_retry_telemetry.py
    │   │   ├── test_llm_serialization.py
    │   │   ├── test_llm_telemetry.py
    │   │   ├── test_llm_timeout.py
    │   │   ├── test_message.py
    │   │   ├── test_message_backward_compatibility.py
    │   │   ├── test_message_from_chat_and_helpers.py
    │   │   ├── test_message_serialization.py
    │   │   ├── test_message_tool_call.py
    │   │   ├── test_model_canonical_name_resolution.py
    │   │   ├── test_model_features.py
    │   │   ├── test_model_list.py
    │   │   ├── test_prompt_caching_cross_conversation.py
    │   │   ├── test_pydantic_warning_suppression.py
    │   │   ├── test_reasoning_content.py
    │   │   ├── test_responses_parsing_and_kwargs.py
    │   │   ├── test_responses_serialization.py
    │   │   ├── test_subscription_mode.py
    │   │   ├── test_telemetry_policy.py
    │   │   ├── test_thinking_blocks.py
    │   │   └── test_vision_support.py
    │   ├── logger/
    │   │   ├── __init__.py
    │   │   └── test_litellm_log_suppression.py
    │   ├── marketplace/
    │   │   ├── __init__.py
    │   │   ├── test_deprecation.py
    │   │   └── test_marketplace.py
    │   ├── mcp/
    │   │   ├── __init__.py
    │   │   ├── test_create_mcp_tool.py
    │   │   ├── test_mcp_action_serialization.py
    │   │   ├── test_mcp_observation.py
    │   │   ├── test_mcp_security_risk.py
    │   │   ├── test_mcp_session_persistence.py
    │   │   ├── test_mcp_tool.py
    │   │   ├── test_mcp_tool_immutability.py
    │   │   ├── test_mcp_tool_kind_field.py
    │   │   ├── test_mcp_tool_serialization.py
    │   │   ├── test_mcp_tool_validation.py
    │   │   └── test_stateful_mcp.py
    │   ├── observability/
    │   │   ├── __init__.py
    │   │   └── test_laminar.py
    │   ├── plugin/
    │   │   ├── __init__.py
    │   │   ├── test_installed_plugins.py
    │   │   ├── test_plugin_fetch.py
    │   │   ├── test_plugin_fetch_integration.py
    │   │   ├── test_plugin_loader.py
    │   │   ├── test_plugin_loading.py
    │   │   ├── test_plugin_merging.py
    │   │   └── test_source.py
    │   ├── security/
    │   │   ├── __init__.py
    │   │   ├── defense_in_depth/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_adversarial.py
    │   │   │   ├── test_ensemble.py
    │   │   │   ├── test_field_cap.py
    │   │   │   ├── test_pattern.py
    │   │   │   ├── test_policy_rails.py
    │   │   │   └── test_serialization.py
    │   │   ├── grayswan/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_grayswan_analyzer.py
    │   │   │   └── test_grayswan_utils.py
    │   │   ├── test_confirmation_policy.py
    │   │   ├── test_llm_security_analyzer.py
    │   │   ├── test_security_analyzer.py
    │   │   └── test_security_risk.py
    │   ├── settings/
    │   │   ├── __init__.py
    │   │   └── test_acp_providers.py
    │   ├── skills/
    │   │   ├── __init__.py
    │   │   ├── test_agentskills_fields.py
    │   │   ├── test_extensions_ref.py
    │   │   ├── test_installed_skills.py
    │   │   ├── test_load_project_skills.py
    │   │   ├── test_load_public_skills.py
    │   │   ├── test_load_user_skills.py
    │   │   ├── test_mcp_config_expansion.py
    │   │   ├── test_mcp_json.py
    │   │   ├── test_resource_directories.py
    │   │   ├── test_skill_commands.py
    │   │   ├── test_skill_info.py
    │   │   ├── test_skill_md_convention.py
    │   │   ├── test_skill_no_header.py
    │   │   ├── test_skill_serialization.py
    │   │   ├── test_skill_utils.py
    │   │   ├── test_task_skill.py
    │   │   ├── test_validation_improvements.py
    │   │   └── test_validation_prompt.py
    │   ├── subagent/
    │   │   ├── __init__.py
    │   │   ├── test_subagent_loader.py
    │   │   ├── test_subagent_registry.py
    │   │   └── test_subagent_schema.py
    │   ├── test_agent_step_bounded_scan.py
    │   ├── test_banner.py
    │   ├── test_import_performance.py
    │   ├── test_settings.py
    │   ├── test_socks_proxy_support.py
    │   ├── tool/
    │   │   ├── __init__.py
    │   │   ├── test_builtins.py
    │   │   ├── test_invoke_skill.py
    │   │   ├── test_mcp_schema.py
    │   │   ├── test_py_type.py
    │   │   ├── test_registry.py
    │   │   ├── test_schema_immutability.py
    │   │   ├── test_switch_llm.py
    │   │   ├── test_to_responses_tool.py
    │   │   ├── test_to_responses_tool_security.py
    │   │   ├── test_to_responses_tool_summary.py
    │   │   ├── test_tool.py
    │   │   ├── test_tool_call_output_coercion.py
    │   │   ├── test_tool_definition.py
    │   │   ├── test_tool_immutability.py
    │   │   └── test_tool_serialization.py
    │   ├── utils/
    │   │   ├── __init__.py
    │   │   ├── test_async_utils.py
    │   │   ├── test_cipher.py
    │   │   ├── test_command.py
    │   │   ├── test_deprecation.py
    │   │   ├── test_discriminated_union.py
    │   │   ├── test_github.py
    │   │   ├── test_model_prompt_spec.py
    │   │   ├── test_paging.py
    │   │   ├── test_path.py
    │   │   ├── test_pydantic_secrets.py
    │   │   ├── test_redact.py
    │   │   ├── test_subclass_cache.py
    │   │   ├── test_truncate.py
    │   │   └── test_visualize.py
    │   └── workspace/
    │       ├── __init__.py
    │       ├── conftest.py
    │       └── remote/
    │           ├── __init__.py
    │           ├── test_async_remote_workspace.py
    │           ├── test_client_base_url.py
    │           ├── test_multiple_commands_isolation.py
    │           ├── test_polling_duplicates_output.py
    │           ├── test_remote_workspace.py
    │           └── test_remote_workspace_mixin.py
    ├── tools/
    │   ├── __init__.py
    │   ├── apply_patch/
    │   │   └── test_apply_patch_executor.py
    │   ├── browser_use/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_browser_cleanup.py
    │   │   ├── test_browser_executor.py
    │   │   ├── test_browser_executor_e2e.py
    │   │   ├── test_browser_initialization.py
    │   │   ├── test_browser_observation.py
    │   │   ├── test_browser_toolset.py
    │   │   ├── test_chromium_detection.py
    │   │   ├── test_recording_flush.py
    │   │   └── test_vnc_integration.py
    │   ├── delegate/
    │   │   ├── test_delegation.py
    │   │   └── test_visualizer.py
    │   ├── file_editor/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_basic_operations.py
    │   │   ├── test_error_handling.py
    │   │   ├── test_exceptions.py
    │   │   ├── test_file_editor_tool.py
    │   │   ├── test_file_validation.py
    │   │   ├── test_memory_usage.py
    │   │   ├── test_schema.py
    │   │   ├── test_view_supported_binary_files.py
    │   │   ├── test_visualize_diff.py
    │   │   ├── test_workspace_root.py
    │   │   └── utils/
    │   │       ├── __init__.py
    │   │       ├── test_encoding.py
    │   │       ├── test_file_cache.py
    │   │       ├── test_history.py
    │   │       └── test_shell_utils.py
    │   ├── gemini/
    │   │   ├── conftest.py
    │   │   ├── edit/
    │   │   │   ├── __init__.py
    │   │   │   └── test_edit.py
    │   │   ├── list_directory/
    │   │   │   ├── __init__.py
    │   │   │   └── test_list_directory.py
    │   │   ├── read_file/
    │   │   │   ├── __init__.py
    │   │   │   └── test_read_file.py
    │   │   ├── test_cross_tool_locking.py
    │   │   └── write_file/
    │   │       ├── __init__.py
    │   │       └── test_write_file.py
    │   ├── glob/
    │   │   ├── __init__.py
    │   │   ├── test_consistency.py
    │   │   ├── test_glob_executor.py
    │   │   └── test_glob_tool.py
    │   ├── grep/
    │   │   ├── __init__.py
    │   │   ├── test_consistency.py
    │   │   ├── test_grep_executor.py
    │   │   └── test_grep_tool.py
    │   ├── planning_file_editor/
    │   │   └── test_planning_file_editor_tool.py
    │   ├── task/
    │   │   ├── test_task_manager.py
    │   │   ├── test_task_manager_thread_safety.py
    │   │   └── test_task_tool_set.py
    │   ├── terminal/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_conversation_cleanup.py
    │   │   ├── test_escape_filter.py
    │   │   ├── test_heredoc_chunked_send.py
    │   │   ├── test_large_environment.py
    │   │   ├── test_observation_truncation.py
    │   │   ├── test_pool_integration.py
    │   │   ├── test_ps1_corruption.py
    │   │   ├── test_schema.py
    │   │   ├── test_secrets_masking.py
    │   │   ├── test_send_keys.py
    │   │   ├── test_session_factory.py
    │   │   ├── test_shell_path_configuration.py
    │   │   ├── test_shutdown_handling.py
    │   │   ├── test_terminal_exit_code_top_level.py
    │   │   ├── test_terminal_parsing.py
    │   │   ├── test_terminal_ps1_metadata.py
    │   │   ├── test_terminal_reset.py
    │   │   ├── test_terminal_session.py
    │   │   ├── test_terminal_tool.py
    │   │   ├── test_terminal_tool_auto_detection.py
    │   │   ├── test_tmux_pane_pool.py
    │   │   ├── test_windows_ctrl_c.py
    │   │   └── test_windows_terminal.py
    │   ├── test_builtin_agents.py
    │   ├── test_init.py
    │   ├── test_planning_preset.py
    │   ├── test_tool_name_consistency.py
    │   ├── test_tool_registration_check.py
    │   ├── test_working_dir_standardization.py
    │   └── tom_consult/
    │       ├── __init__.py
    │       └── test_tom_consult_tool.py
    └── workspace/
        ├── test_api_remote_workspace.py
        ├── test_apptainer_workspace.py
        ├── test_cloud_workspace.py
        ├── test_cloud_workspace_automation_tags.py
        ├── test_cloud_workspace_repos.py
        ├── test_cloud_workspace_sdk_settings.py
        ├── test_docker_workspace.py
        └── test_workspace_pause_resume.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .agents/skills/cross-repo-testing/SKILL.md
================================================
---
name: cross-repo-testing
description: This skill should be used when the user asks to "test a saas cross-repo feature", "deploy a feature branch to staging", "test SDK against OH Cloud branch", "e2e test a cloud workspace feature", "test secrets saas inheritance", or when changes span the SDK and OpenHands enterprise and need end-to-end validation against a staging deployment.
---

# Cross-Repo Testing: SDK ↔ OpenHands Cloud

How to end-to-end test features that span `OpenHands/software-agent-sdk` and `OpenHands/OpenHands` (the Cloud backend).

## Repository Map

| Repo | Role | What lives here |
|------|------|-----------------|
| [`software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) | Agent core | `openhands-sdk`, `openhands-workspace`, `openhands-tools` packages. `OpenHandsCloudWorkspace` lives here. |
| [`OpenHands`](https://github.com/OpenHands/OpenHands) | Cloud backend | FastAPI server (`openhands/app_server/`), sandbox management, auth, enterprise integrations. Deployed as OH Cloud. |
| [`deploy`](https://github.com/OpenHands/deploy) | Infrastructure | Helm charts + GitHub Actions that build the enterprise Docker image and deploy to staging/production. |

**Data flow:** SDK client → OH Cloud API (`/api/v1/...`) → sandbox agent-server (inside runtime container)

## When You Need This

There are **two flows** depending on which direction the dependency goes:

| Flow | When | Example |
|------|------|---------|
| **A — SDK client → new Cloud API** | The SDK calls an API that doesn't exist yet on production | `workspace.get_llm()` calling `GET /api/v1/users/me?expose_secrets=true` |
| **B — OH server → new SDK code** | The Cloud server needs unreleased SDK packages or a new agent-server image | Server consumes a new tool, agent behavior, or workspace method from the SDK |

Flow A only requires deploying the server PR. Flow B requires pinning the SDK to an unreleased commit in the server PR **and** using the SDK PR's agent-server image. Both flows may apply simultaneously.

---

## Flow A: SDK Client Tests Against New Cloud API

Use this when the SDK calls an endpoint that only exists on the server PR branch.

### A1. Write and test the server-side changes

In the `OpenHands` repo, implement the new API endpoint(s). Run unit tests:

```bash
cd OpenHands
poetry run pytest tests/unit/app_server/test_<relevant>.py -v
```

Push a PR. Wait for the **"Push Enterprise Image" (Docker) CI job** to succeed — this builds `ghcr.io/openhands/enterprise-server:sha-<COMMIT>`.

### A2. Write the SDK-side changes

In `software-agent-sdk`, implement the client code (e.g., new methods on `OpenHandsCloudWorkspace`). Run SDK unit tests:

```bash
cd software-agent-sdk
pip install -e openhands-sdk -e openhands-workspace
pytest tests/ -v
```

Push a PR. SDK CI is independent — it doesn't need the server changes to pass unit tests.

### A3. Deploy the server PR to staging

See [Deploying to a Staging Feature Environment](#deploying-to-a-staging-feature-environment) below.

### A4. Run the SDK e2e test against staging

See [Running E2E Tests Against Staging](#running-e2e-tests-against-staging) below.

---

## Flow B: OH Server Needs Unreleased SDK Code

Use this when the Cloud server depends on SDK changes that haven't been released to PyPI yet. The server's runtime containers run the `agent-server` image built from the SDK repo, so the server PR must be configured to use the SDK PR's image and packages.

### B1. Get the SDK PR merged (or identify the commit)

The SDK PR must have CI pass so its agent-server Docker image is built. The image is tagged with the **merge-commit SHA** from GitHub Actions — NOT the head-commit SHA shown in the PR.

Find the correct image tag:
- Check the SDK PR description for an `AGENT_SERVER_IMAGES` section
- Or check the "Consolidate Build Information" CI job for `"short_sha": "<tag>"`

### B2. Pin SDK packages to the commit in the OpenHands PR

In the `OpenHands` repo PR, update 3 files + regenerate 3 lock files (see the `update-sdk` skill for full details):

**`pyproject.toml`** — pin all 3 SDK packages in **both** `dependencies` and `[tool.poetry.dependencies]`:
```toml
# dependencies array (PEP 508)
"openhands-sdk @ git+https://github.com/OpenHands/software-agent-sdk.git@<COMMIT>#subdirectory=openhands-sdk",
"openhands-agent-server @ git+https://github.com/OpenHands/software-agent-sdk.git@<COMMIT>#subdirectory=openhands-agent-server",
"openhands-tools @ git+https://github.com/OpenHands/software-agent-sdk.git@<COMMIT>#subdirectory=openhands-tools",

# [tool.poetry.dependencies]
openhands-sdk = { git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "<COMMIT>", subdirectory = "openhands-sdk" }
openhands-agent-server = { git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "<COMMIT>", subdirectory = "openhands-agent-server" }
openhands-tools = { git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "<COMMIT>", subdirectory = "openhands-tools" }
```

**`openhands/app_server/sandbox/sandbox_spec_service.py`** — use the SDK's merge-commit SHA:
```python
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:<merge-commit-sha>-python'
```

**Regenerate lock files:**
```bash
poetry lock && uv lock && cd enterprise && poetry lock && cd ..
```

### B3. Wait for the OpenHands enterprise image to build

Push the pinned changes. The OpenHands CI will build a new enterprise Docker image (`ghcr.io/openhands/enterprise-server:sha-<OH_COMMIT>`) that bundles the unreleased SDK. Wait for the "Push Enterprise Image" job to succeed.

### B4. Deploy and test

Follow [Deploying to a Staging Feature Environment](#deploying-to-a-staging-feature-environment) using the new OpenHands commit SHA.

### B5. Before merging: remove the pin

**CI guard:** `check-package-versions.yml` blocks merge to `main` if `[tool.poetry.dependencies]` contains `rev` fields. Before the OpenHands PR can merge, the SDK PR must be merged and released to PyPI, then the pin must be replaced with the released version number.

---

## Deploying to a Staging Feature Environment

The `deploy` repo creates preview environments from OpenHands PRs.

**Option A — GitHub Actions UI (preferred):**
Go to `OpenHands/deploy` → Actions → "Create OpenHands preview PR" → enter the OpenHands PR number. This creates a branch `ohpr-<PR>-<random>` and opens a deploy PR.

**Option B — Update an existing feature branch:**
```bash
cd deploy
git checkout ohpr-<PR>-<random>
# In .github/workflows/deploy.yaml, update BOTH:
#   OPENHANDS_SHA: "<full-40-char-commit>"
#   OPENHANDS_RUNTIME_IMAGE_TAG: "<same-commit>-nikolaik"
git commit -am "Update OPENHANDS_SHA to <commit>" && git push
```

**Before updating the SHA**, verify the enterprise Docker image exists:
```bash
gh api repos/OpenHands/OpenHands/actions/runs \
  --jq '.workflow_runs[] | select(.head_sha=="<COMMIT>") | "\(.name): \(.conclusion)"' \
  | grep Docker
# Must show: "Docker: success"
```

The deploy CI auto-triggers and creates the environment at:
```
https://ohpr-<PR>-<random>.staging.all-hands.dev
```

**Wait for it to be live:**
```bash
curl -s -o /dev/null -w "%{http_code}" https://ohpr-<PR>-<random>.staging.all-hands.dev/api/v1/health
# 401 = server is up (auth required). DNS may take 1-2 min on first deploy.
```

## Running E2E Tests Against Staging

**Critical: Feature deployments have their own Keycloak instance.** API keys from `app.all-hands.dev` or `$OPENHANDS_API_KEY` will NOT work. You need a test API key for the specific feature deployment. The user must provide one.

```python
from openhands.workspace import OpenHandsCloudWorkspace

STAGING = "https://ohpr-<PR>-<random>.staging.all-hands.dev"

with OpenHandsCloudWorkspace(
    cloud_api_url=STAGING,
    cloud_api_key="<test-api-key-for-this-deployment>",
) as workspace:
    # Test the new feature
    llm = workspace.get_llm()
    secrets = workspace.get_secrets()
    print(f"LLM: {llm.model}, secrets: {list(secrets.keys())}")
```

Or run an example script:
```bash
OPENHANDS_CLOUD_API_KEY="<key>" \
OPENHANDS_CLOUD_API_URL="https://ohpr-<PR>-<random>.staging.all-hands.dev" \
python examples/02_remote_agent_server/10_cloud_workspace_saas_credentials.py
```

### Recording results

Push test output to the SDK PR's `.pr/logs/` directory:
```bash
cd software-agent-sdk
python test_script.py 2>&1 | tee .pr/logs/<test_name>.log
git add -f .pr/logs/<test_name>.log .pr/README.md
git commit -m "docs: add e2e test results" && git push
```

Comment on **both PRs** with pass/fail summary and link to logs.

## Key Gotchas

| Gotcha | Details |
|--------|---------|
| **Feature env auth is isolated** | Each `ohpr-*` deployment has its own Keycloak. Production API keys don't work. |
| **Two SHAs in deploy.yaml** | `OPENHANDS_SHA` and `OPENHANDS_RUNTIME_IMAGE_TAG` must both be updated. The runtime tag is `<sha>-nikolaik`. |
| **Enterprise image must exist** | The Docker CI job on the OpenHands PR must succeed before you can deploy. If it hasn't run, push an empty commit to trigger it. |
| **DNS propagation** | First deployment of a new branch takes 1-2 min for DNS. Subsequent deploys are instant. |
| **Merge-commit SHA ≠ head SHA** | SDK CI tags Docker images with GitHub Actions' merge-commit SHA, not the PR head SHA. Check the SDK PR description or CI logs for the correct tag. |
| **SDK pin blocks merge** | `check-package-versions.yml` prevents merging an OpenHands PR that has `rev` fields in `[tool.poetry.dependencies]`. The SDK must be released to PyPI first. |
| **Flow A: stock agent-server is fine** | When only the Cloud API changes, `OpenHandsCloudWorkspace` talks to the Cloud server, not the agent-server. No custom image needed. |
| **Flow B: agent-server image is required** | When the server needs new SDK code inside runtime containers, you must pin to the SDK PR's agent-server image. |


================================================
FILE: .agents/skills/custom-codereview-guide.md
================================================
---
name: custom-codereview-guide
description: Repo-specific code review guidelines for OpenHands/software-agent-sdk. Provides SDK-specific review rules in addition to the default code review skill.
triggers:
- /codereview
---

# OpenHands/software-agent-sdk Code Review Guidelines

You are an expert code reviewer for the **OpenHands/software-agent-sdk** repository. This skill provides repo-specific review guidelines. Be direct but constructive.

## Review Decisions

You have permission to **APPROVE** or **COMMENT** on PRs. Do not use REQUEST_CHANGES.

### Review decision policy (eval / benchmark risk)

Do **NOT** submit an **APPROVE** review when the PR changes agent behavior or anything
that could plausibly affect benchmark/evaluation performance — **unless** eval evidence
is already provided (see exception below).

Examples include: prompt templates, tool calling/execution, planning/loop logic,
memory/condenser behavior, terminal/stdin/stdout handling, or evaluation harness code.

If a PR is in this category (or you are uncertain), leave a **COMMENT** review and
explicitly flag it for a human maintainer to decide after running lightweight evals.

#### Exception – eval evidence provided

If the PR description **or** PR comments contain a link to the eval monitor
(`openhands-eval-monitor.vercel.app`) showing a completed benchmark run **and**
a human maintainer has commented confirming the results (e.g., "Human review done",
"eval looks good", or similar), treat the eval-risk requirement as satisfied and
follow the normal approval policy. The eval monitor link is authoritative proof of
benchmark validation for this repository.

### Default approval policy

**Default to APPROVE**: If your review finds no issues at "important" level or higher,
approve the PR. Minor suggestions or nitpicks alone are not sufficient reason to
withhold approval.

**IMPORTANT:** If you determine a PR is worth merging **and it is not in the eval-risk
category above**, you should approve it. Don’t just say a PR is "worth merging" or
"ready to merge" without actually submitting an approval. Your words and actions should
be consistent.

### When to APPROVE

Examples of straightforward and low-risk PRs you should approve (non-exhaustive):

- **Configuration changes**: Adding models to config files, updating CI/workflow settings
- **CI/Infrastructure changes**: Changing runner types, fixing workflow paths, updating job configurations
- **Cosmetic changes**: Typo fixes, formatting, comment improvements, README updates
- **Documentation-only changes**: Docstring updates, clarifying notes, API documentation improvements
- **Simple additions**: Adding entries to lists/dictionaries following existing patterns
- **Test-only changes**: Adding or updating tests without changing production code
- **Dependency updates**: Version bumps with passing CI, unless the updated package is newer than the repo's 7-day freshness guardrail described in the Security section below

### When NOT to APPROVE - Blocking Issues

**DO NOT APPROVE** PRs that have any of the following issues:

- **Package version bumps in non-release PRs**: If any `pyproject.toml` file has changes to the `version` field (e.g., `version = "1.12.0"` → `version = "1.13.0"`), and the PR is NOT explicitly a release PR (title/description doesn't indicate it's a release), **DO NOT APPROVE**. Version numbers should only be changed in dedicated release PRs managed by maintainers.
  - Check: Look for changes to `version = "..."` in any `*/pyproject.toml` files
  - Exception: PRs with titles like "release: v1.x.x" or "chore: bump version to 1.x.x" from maintainers
- **Too-new dependency uploads**: If a dependency bump pulls in a package uploaded within the repo's 7-day freshness window, **DO NOT APPROVE**. See the Security section below for the exact review instructions and the Dependabot / `tool.uv.exclude-newer` caveat.

Examples:
- A PR adding a new model to `resolve_model_config.py` or `verified_models.py` with corresponding test updates
- A PR adding documentation notes to docstrings clarifying method behavior (e.g., security considerations, bypass behaviors)
- A PR changing CI runners or fixing workflow infrastructure issues (e.g., standardizing runner types to fix path inconsistencies)

### When to COMMENT

Use COMMENT when you have feedback or concerns:

- Issues that need attention (bugs, security concerns, missing tests)
- Suggestions for improvement
- Questions about design decisions
- Minor style preferences

If there are significant issues, leave detailed comments explaining the concerns—but let a human maintainer decide whether to block the PR.

## Security

### Dependency freshness / supply-chain guardrail

This repository intentionally uses a workspace-wide `uv` resolver guardrail:

- Root `pyproject.toml`: `[tool.uv] exclude-newer = "7 days"`

**Important:** Dependabot does **not** currently honor that `uv` guardrail when it opens `uv.lock` update PRs for this repo's workspace setup. A Dependabot PR can therefore bump to a version that was uploaded **less than 7 days ago**, even though a local `uv lock` would normally exclude it.

When reviewing dependency update PRs (`uv.lock`, `pyproject.toml`, `requirements*.txt`, etc.), explicitly check for **too-new package uploads**:

1. Check the package upload timestamp on the package index.
2. For `uv.lock`, use the per-file `upload-time` metadata in the changed package entry.
3. Treat `upload-time` as the upload time of that specific distribution file to the package index (for example, the wheel uploaded to PyPI) — not the Git tag time or GitHub release time.
4. Compare that timestamp against the current date and the repo's 7-day freshness window.

If the updated package was uploaded **within the last 7 days**, treat it as a real security / supply-chain concern:

- Do **NOT** approve the PR.
- Leave a **COMMENT** review that clearly calls out the package name, version, upload time, and that it is newer than the repo's 7-day guardrail.
- Explain that this can happen because Dependabot currently ignores `tool.uv.exclude-newer` for this repo's workspace updates.
- Ask a human maintainer to decide whether to wait until the package ages past the guardrail or to merge intentionally despite the freshness risk.

## Core Principles

1. **Simplicity First**: Question complexity. If something feels overcomplicated, ask "what's the use case?" and seek simpler alternatives. Features should solve real problems, not imaginary ones.

2. **Pragmatic Testing**: Test what matters. Avoid duplicate test coverage. Don't test library features (e.g., `BaseModel.model_dump()`). Focus on the specific logic implemented in this codebase.

3. **Type Safety**: Avoid `# type: ignore` - treat it as a last resort. Fix types properly with assertions, proper annotations, or code adjustments. Prefer explicit type checking over `getattr`/`hasattr` guards.

4. **Backward Compatibility**: Evaluate breaking change impact carefully. Consider API changes that affect existing users, removal of public fields/methods, and changes to default behavior.

## What to Check

- **Complexity**: Over-engineered solutions, unnecessary abstractions, complex logic that could be refactored
- **Testing**: Duplicate test coverage, tests for library features, missing edge case coverage. For code that writes to disk, verify that tests cover the **persistence round-trip** (write → close → reopen → verify), not just in-memory state
- **Type Safety**: `# type: ignore` usage, missing type annotations, `getattr`/`hasattr` guards, mocking non-existent arguments
- **Breaking Changes**: API changes affecting users, removed public fields/methods, changed defaults
- **Code Quality**: Code duplication, missing comments for non-obvious decisions, inline imports (unless necessary for circular deps)
- **Repository Conventions**: Use `pyright` not `mypy`, put fixtures in `conftest.py`, avoid `sys.path.insert` hacks
- **Directory Example Entrypoints**: PRs that add or modify folder-based runnable examples under `examples/` should use `main.py` as the entrypoint and add the directory to `_TARGET_DIRECTORIES` in `tests/examples/test_examples.py`; see [Directory-Based Examples](#directory-based-examples)
- **Event Type Deprecation**: Changes to event types (Pydantic models used in serialization) must handle deprecated fields properly
- **Thread Safety**: New methods in `LocalConversation` that read or write `self._state` must use `with self._state:` — see the [Concurrency](#concurrency---localconversation-state-lock) section below
- **Persistence Paths**: Code that computes persistence directories must not double-append the conversation hex — see the [Persistence Paths](#persistence-path-construction) section below
- **Server-Side Cleanup**: Endpoints that create persistent state (directories, files) must have rollback logic for partial failures — see the [Server Error Handling](#server-side-error-handling) section below
- **Cross-File Data Flow**: When new code calls existing APIs (constructors, factory methods), trace 1–2 levels into those APIs to verify the caller uses them correctly. Bugs often hide at layer boundaries where the caller's assumptions don't match the callee's behavior
- **Secret Serialization**: Fields that carry secrets must use `serialize_secret()` from `openhands.sdk.utils.pydantic_secrets`. For `dict[str, str]` secret fields, wrap each value in `SecretStr` and call `serialize_secret` per value. Do not hand-roll redaction logic (e.g. custom sentinels or inline `expose_secrets` checks) in field serializers
- **Info-Log Payloads**: `logger.info(...)` must not dump objects, dicts, or variable-length lists — see [Logging Hygiene](#logging-hygiene)

## Directory-Based Examples

When a PR adds or modifies a runnable example represented by a directory under `examples/`, verify that:

1. The runnable entrypoint is named `main.py`.
2. Helper modules inside that directory are not accidentally treated as standalone examples.
3. `tests/examples/test_examples.py` includes the example directory in `_TARGET_DIRECTORIES` when the example should run in the `test-examples` workflow.
4. The example prints an `EXAMPLE_COST: ...` marker when run by the workflow.

Do not ask for this convention on support scripts that are intentionally named for GitHub workflow consumption (for example reusable automation scripts under `examples/03_github_workflows/`) unless they are presented as a directory-based runnable example.


## Event Type Deprecation - Critical Review Checkpoint

When reviewing PRs that modify event types (e.g., `TextContent`, `Message`, `Event`, or any Pydantic model used in event serialization), **DO NOT APPROVE** until the following are verified:

### Required for Removing/Deprecating Fields

1. **Model validator present**: If a field is being removed from an event type with `extra="forbid"`, there MUST be a `@model_validator(mode="before")` that uses `handle_deprecated_model_fields()` to remove the deprecated field before validation. Otherwise, old events will fail to load.

2. **Tests for backward compatibility**: The PR MUST include tests that:
   - Load an old event format (with the deprecated field) successfully
   - Load a new event format (without the deprecated field) successfully
   - Verify both can be loaded in sequence (simulating mixed conversations)

3. **Test naming convention**: The version in the test name should be the **LAST version** where a particular event structure exists. For example, if `enable_truncation` was removed in v1.11.1, the test should be named `test_v1_10_0_...` (the last version with that field), not `test_v1_8_0_...` (when it was introduced). This avoids duplicate tests and clearly documents when a field was last present.

**Important**: Deprecated field handlers are **permanent** and should never be removed. They ensure old conversations can always be loaded.

### Example Pattern (Required)

```python
from openhands.sdk.utils.deprecation import handle_deprecated_model_fields

class MyModel(BaseModel):
    model_config = ConfigDict(extra="forbid")

    # Deprecated fields that are silently removed for backward compatibility
    # when loading old events. These are kept permanently.
    _DEPRECATED_FIELDS: ClassVar[tuple[str, ...]] = ("old_field_name",)

    @model_validator(mode="before")
    @classmethod
    def _handle_deprecated_fields(cls, data: Any) -> Any:
        """Remove deprecated fields for backward compatibility with old events."""
        return handle_deprecated_model_fields(data, cls._DEPRECATED_FIELDS)
```

### Why This Matters

Production systems resume conversations that may contain events serialized with older SDK versions. If the SDK can't load old events, users will see errors like:

```
pydantic_core.ValidationError: Extra inputs are not permitted
```

**This is a production-breaking change.** Do not approve PRs that modify event types without proper backward compatibility handling and tests.

## SDK Architecture Conventions

These conventions codify patterns that are easy to violate when adding new features. Each was learned from a real bug.

### Concurrency - LocalConversation State Lock

`LocalConversation` protects mutable state with a FIFOLock accessed via `with self._state:`. **Every** method that reads or writes `self._state.events`, `self._state.stats`, `self._state.agent_state`, `self._state.activated_knowledge_skills`, or any other mutable field on `ConversationState` must hold this lock. There are currently ~13 call sites using this pattern.

When reviewing a PR that adds a new method to `LocalConversation`:
1. Check whether it accesses any `self._state.*` field.
2. If yes, verify the access is inside a `with self._state:` block.
3. If not, flag it — the method is unsafe for concurrent use with `run()`.

### Persistence Path Construction

`BaseConversation.get_persistence_dir(base, conversation_id)` returns `str(Path(base) / conversation_id.hex)`. The `LocalConversation.__init__` constructor calls this automatically when `persistence_dir` is provided.

**Rule:** Callers that pass `persistence_dir` to `LocalConversation()` must pass only the **base directory** (e.g., `/data/conversations/`). The constructor appends the conversation hex. Passing a pre-constructed full path (e.g., `/data/conversations/abc123`) causes double-appending: `/data/conversations/abc123/abc123`.

When reviewing code that creates a new `LocalConversation` (fork, resume, migration):
1. Check what value is passed as `persistence_dir`.
2. Verify it does **not** already include the conversation ID hex.

### Server-Side Error Handling

Server endpoints in `conversation_service.py` that create persistent state (writing directories, files, or calling `fork()` which writes to disk) and then perform follow-up operations (like `_start_event_service`) must handle partial failure.

**Pattern:** If the follow-up operation fails, clean up the already-written persistent state so it doesn't become an orphaned directory that confuses future startups.

```python
# Good: rollback on failure
fork_dir = self.conversations_dir / fork_conv_id.hex
try:
    fork_event_service = await self._start_event_service(fork_stored)
except Exception:
    safe_rmtree(fork_dir)
    raise
```

When reviewing server endpoints that create conversations or persistent artifacts:
1. Identify the "point of no return" where state is written to disk.
2. Check that subsequent operations are wrapped in try/except with cleanup.
3. For client-supplied IDs, verify there's a duplicate check before creating state (return 409 Conflict if taken).

### Logging Hygiene

`logger.info(...)` must not interpolate `model_dump(...)`, `.json()`, `to_dict()`, a list/dict of tool/skill/server names, or arbitrary user-supplied values. Log a count and/or id; move full payloads to `logger.debug(...)`.

When reviewing a new or changed `logger.info(...)` call: if any interpolated value is an object, a dict, or a list whose size scales with load (tools, skills, conversations, requests), flag it.

## What NOT to Comment On

Do not leave comments for:

- **Nitpicks**: Minor style preferences, optional improvements, or "nice-to-haves" that don't affect correctness or maintainability
- **Good behavior observed**: Don't comment just to praise code that follows best practices - this adds noise. Simply approve if the code is good.
- **Suggestions for additional tests on simple changes**: For straightforward PRs (config changes, model additions, etc.), don't suggest adding test coverage unless tests are clearly missing for new logic
- **Obvious or self-explanatory code**: Don't ask for comments on code that is already clear
- **`.pr/` directory artifacts**: Files in the `.pr/` directory are temporary PR-specific documents (design notes, analysis, scripts) that are automatically cleaned up when the PR is approved. Do not comment on their presence or suggest removing them.

If a PR is approvable, just approve it. Don't add "one small suggestion" or "consider doing X" comments that delay merging without adding real value.

## Communication Style

- Be direct and concise - don't over-explain
- Use casual, friendly tone ("lgtm", "WDYT?", emojis are fine 👀)
- Ask questions to understand use cases before suggesting changes
- Suggest alternatives, not mandates
- Approve quickly when code is good ("LGTM!")
- Use GitHub suggestion syntax for code fixes


================================================
FILE: .agents/skills/debug-test-examples-workflow/SKILL.md
================================================
---
name: debug-test-examples-workflow
description: Guide for debugging failing example tests in the `test-examples` labeled workflow. Use this skill when investigating CI failures in the run-examples.yml workflow, when example scripts fail to run correctly, when needing to isolate specific test failures, or when analyzing workflow logs and failure patterns.
---

# Debugging test-examples Workflow

## Overview

The `run-examples.yml` workflow runs example scripts from `examples/` directory. Triggers:
- Adding `test-examples` label to a PR
- Manual workflow dispatch
- Scheduled nightly runs

## Debugging Steps

### 1. Isolate Failing Tests

Modify `tests/examples/test_examples.py` to focus on specific tests:

```python
_TARGET_DIRECTORIES = (
    # EXAMPLES_ROOT / "01_standalone_sdk",
    EXAMPLES_ROOT / "02_remote_agent_server",  # Keep only failing directory
)
```

### 2. Exclude Tests

Add to `_EXCLUDED_EXAMPLES` with explanation:

```python
_EXCLUDED_EXAMPLES = {
    # Reason for exclusion
    "examples/path/to/failing_test.py",
}
```

### 3. Trigger Workflow

Toggle the `test-examples` label:

```bash
# Remove label
curl -X DELETE -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/issues/${PR_NUMBER}/labels/test-examples"

# Add label
curl -X POST -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github.v3+json" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/issues/{PR_NUMBER}/labels" \
  -d '{"labels":["test-examples"]}'
```

### 4. Monitor Progress

```bash
# Check status
curl -s -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/actions/runs/{RUN_ID}" | jq '{status, conclusion}'

# Download logs
curl -sL -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/actions/runs/{RUN_ID}/logs" -o logs.zip
unzip logs.zip -d logs
```

## Common Failure Patterns

| Pattern | Cause | Solution |
|---------|-------|----------|
| Port conflicts | Fixed ports (8010, 8011) | Run with `-n 1` or use different ports |
| Container issues | Docker/Apptainer setup | Check Docker availability, image pulls |
| LLM failures | Transient API errors | Retry the test |
| Example bugs | Code errors | Check traceback |


## Key Configuration

**Workflow** (`.github/workflows/run-examples.yml`):
- Runner: `blacksmith-2vcpu-ubuntu-2404`
- Timeout: 60 minutes
- Parallelism: `-n 4` (pytest-xdist: 4 parallel workers)

**Tests** (`tests/examples/test_examples.py`):
- Timeout per example: 600 seconds
- Target directories: `_TARGET_DIRECTORIES`
- Excluded examples: `_EXCLUDED_EXAMPLES`


================================================
FILE: .agents/skills/design-principles.md
================================================
---
name: design-principles
description: Core architectural design principles of the OpenHands Software Agent SDK. Reference when making architectural decisions, reviewing PRs that change agent/tool/state boundaries, or evaluating whether a proposed change aligns with V1 design goals.
---

# SDK Design Principles

Reference: <https://docs.openhands.dev/sdk/arch/design>

## Quick Summary

1. **Optional Isolation over Mandatory Sandboxing**
   Sandboxing is opt-in, not universal. Agent and tool execution runs in a single
   process by default. When isolation is needed, the same stack can be transparently
   containerized.

2. **Stateless by Default, One Source of Truth for State**
   All components — agents, tools, LLMs, configurations — are **immutable Pydantic
   models** validated at construction. The only mutable entity is the conversation
   state. This enables deterministic replay and robust persistence.

3. **Clear Boundaries between Agent and Applications**
   Strict separation between SDK (agent core), tools, workspace, and agent server.
   Applications communicate via APIs, not by embedding the agent.

4. **Composable Components for Extensibility**
   Agents are graphs of interchangeable components — tools, prompts, LLMs, contexts —
   described **declaratively with strong typing**. Developers reconfigure capabilities
   without modifying core code.

## Implications for Development

- Since agents are immutable Pydantic models, their configuration **is** their
  serializable representation. There should be no need to "reverse-engineer" agent
  config from runtime instances.
- Tool implementations (callables) are the only non-serializable part; this is solved
  by `tool_module_qualnames` for remote forwarding.
- Everything else (system_prompt, model, skills, tool names) is already declarative
  data that can be serialized and forwarded directly.
- Avoid patterns that create multiple sources of truth for the same configuration
  (e.g., a factory function AND an extracted definition).
- `model_copy(update=...)` should be used sparingly and through well-defined paths to
  avoid undermining statelessness.


================================================
FILE: .agents/skills/feature-release-rollout/SKILL.md
================================================
---
name: feature-release-rollout
description: This skill should be used when the user asks to "rollout a feature", "complete feature release", "propagate SDK feature", "track feature support", "what's missing for feature X", or mentions checking CLI/GUI/docs/blog support for SDK features. Guides agents through the multi-repository feature release workflow from SDK to docs to marketing.
triggers:
- rollout feature
- feature release
- propagate feature
- feature support
- complete release
- docs for feature
- blog for feature
- CLI support
- GUI support
- what's missing
---

# Feature Release Rollout

This skill guides the complete feature release workflow across the OpenHands ecosystem repositories.

## Overview

When a feature is implemented in the SDK, it may need propagation through several repositories:

1. **SDK** (`OpenHands/software-agent-sdk`) — Core feature implementation
2. **CLI** (`OpenHands/OpenHands-CLI`) — Terminal interface support
3. **GUI** (`OpenHands/OpenHands` frontend directory) — Web interface support
4. **Docs** (`OpenHands/docs`) — Documentation updates (sdk/ folder)
5. **Blog** (`OpenHands/growth-utils` blog-post/) — Marketing and announcements
6. **Video** — Tutorial content (using ElevenLabs + Remotion)

## Workflow

### Phase 1: Feature Discovery

First, identify what feature(s) to analyze. The user may specify:
- A release tag (e.g., `v1.9.0`)
- A specific feature name
- A PR or commit reference
- A comparison between versions

**For release tags:**
```bash
# Clone SDK if not present
git clone https://github.com/OpenHands/software-agent-sdk.git

# View release notes
cd software-agent-sdk
git log --oneline v1.8.0..v1.9.0  # Changes between versions
git show v1.9.0 --stat             # What changed in this release
```

**For specific features:**
Search the SDK codebase, examples, and changelog to understand the feature scope.

### Phase 2: Repository Analysis

Clone all relevant repositories to analyze current support:

```bash
# Clone repositories (use GITHUB_TOKEN for authenticated access)
git clone https://github.com/OpenHands/software-agent-sdk.git
git clone https://github.com/OpenHands/OpenHands-CLI.git
git clone https://github.com/OpenHands/OpenHands.git        # Frontend in frontend/
git clone https://github.com/OpenHands/docs.git
git clone https://github.com/OpenHands/growth-utils.git
```

For each feature, check support status:

| Repository | Check Location | What to Look For |
|------------|---------------|------------------|
| CLI | `openhands_cli/` | Feature flags, commands, TUI widgets |
| GUI | `OpenHands/frontend/src/` | React components, API integrations |
| Docs | `docs/sdk/` | Guide pages, API reference, examples |
| Blog | `growth-utils/blog-post/posts/` | Announcement posts |

### Phase 3: Assess Feature Importance

Not all features warrant full rollout. Evaluate each feature:

**High Impact (full rollout recommended):**
- New user-facing capabilities
- Breaking changes or migrations
- Major performance improvements
- New integrations or tools

**Medium Impact (docs + selective support):**
- New API methods or parameters
- Configuration options
- Developer experience improvements

**Low Impact (docs only or skip):**
- Internal refactoring
- Bug fixes
- Minor enhancements

**Skip rollout for:**
- Internal-only changes
- Test improvements
- Build/CI changes
- Documentation typos

### Phase 4: Create Proposal

Generate a structured proposal for the user:

```markdown
## Feature Rollout Proposal: [Feature Name]

### Feature Summary
[Brief description of the feature and its value]

### Current Support Status
| Component | Status | Notes |
|-----------|--------|-------|
| SDK | ✅ Implemented | [version/PR] |
| CLI | ❌ Missing | [what's needed] |
| GUI | ⚠️ Partial | [what's implemented vs needed] |
| Docs | ❌ Missing | [suggested pages] |
| Blog | ❌ Not started | [whether warranted] |
| Video | ❌ Not started | [whether warranted] |

### Recommended Actions
1. **CLI**: [specific implementation needed]
2. **GUI**: [specific implementation needed]
3. **Docs**: [pages to create/update]
4. **Blog**: [recommended or not, with reasoning]
5. **Video**: [recommended or not, with reasoning]

### Assessment
- **Overall Priority**: [High/Medium/Low]
- **Effort Estimate**: [days/hours per component]
- **Dependencies**: [what must be done first]
```

### Phase 5: User Confirmation

Wait for explicit user approval before proceeding. Ask:
- Which components to implement
- Priority ordering
- Any modifications to the proposal

### Phase 6: Implementation

Only after user confirmation:

**Create GitHub Issues:**
```bash
# Create issue on relevant repo
gh issue create --repo OpenHands/OpenHands-CLI \
  --title "Support [feature] in CLI" \
  --body "## Context\n[Feature description]\n\n## Implementation\n[Details]\n\n## Related\n- SDK: [link]\n- Docs: [link]"
```

**Implementation order:**
1. CLI/GUI support (can be parallel)
2. Documentation (depends on 1)
3. Blog post (depends on 2)
4. Video (depends on 3)

## Repository-Specific Guidelines

### CLI (OpenHands/OpenHands-CLI)

- Check `AGENTS.md` for development guidelines
- Use `uv` for dependency management
- Run `make lint` and `make test` before commits
- TUI components in `openhands_cli/tui/`
- Snapshot tests for UI changes

### GUI (OpenHands/OpenHands frontend)

- Frontend in `frontend/` directory
- React/TypeScript codebase
- Run `npm run lint:fix && npm run build` in frontend/
- Follow TanStack Query patterns for data fetching
- i18n translations in `frontend/src/i18n/`

### Docs (OpenHands/docs)

- SDK docs in `sdk/` folder
- Uses Mintlify (`.mdx` files)
- Code blocks can auto-sync from SDK examples
- Run `mint broken-links` to validate
- Follow `openhands/DOC_STYLE_GUIDE.md`

### Blog (OpenHands/growth-utils)

- Posts in `blog-post/posts/YYYYMMDD-title.md`
- Assets in `blog-post/assets/YYYYMMDD-title/`
- Frontmatter format:
  ```yaml
  ---
  title: "Post Title"
  excerpt: "Brief description"
  coverImage: "/assets/blog/YYYYMMDD-title/cover.png"
  date: "YYYY-MM-DDTHH:MM:SS.000Z"
  authors:
    - name: Author Name
      picture: "/assets/blog/authors/author.png"
  ogImage:
    url: "/assets/blog/YYYYMMDD-title/cover.png"
  ---
  ```

## Example Feature Analysis

**Feature: Browser Session Recording (SDK v1.8.0)**

1. **SDK**: ✅ Implemented in `openhands.tools.browser`
2. **CLI**: ❌ No replay/export commands
3. **GUI**: ❌ No recording viewer component
4. **Docs**: ✅ Guide at `sdk/guides/browser-session-recording.mdx`
5. **Blog**: ❌ Could highlight for web scraping users
6. **Video**: Consider 2-minute demo

**Recommendation**: Medium priority. Docs done, CLI/GUI low urgency (advanced feature), blog post optional.

## Quick Commands

```bash
# Check SDK feature presence
grep -r "feature_name" software-agent-sdk/openhands/ --include="*.py"

# Check CLI support
grep -r "feature_name" OpenHands-CLI/openhands_cli/ --include="*.py"

# Check GUI support
grep -r "featureName" OpenHands/frontend/src/ --include="*.ts" --include="*.tsx"

# Check docs coverage
grep -r "feature" docs/sdk/ --include="*.mdx"

# Check blog mentions
grep -r "feature" growth-utils/blog-post/posts/ --include="*.md"
```

## Important Notes

- Always get user confirmation before creating issues or starting implementation
- Consider feature maturity — new features may change before full rollout
- Cross-reference PRs between repositories in issue descriptions
- For breaking changes, coordinate release timing across all components


================================================
FILE: .agents/skills/manage-evals/SKILL.md
================================================
---
name: manage-evals
description: This skill should be used when the user asks to "trigger an eval", "run evaluation", "run swebench", "run gaia", "run benchmark", "compare eval runs", "compare evaluation results", "check eval regression", "compare benchmark results", "what changed in the eval", "diff eval runs", or mentions triggering, comparing, or reporting on SWE-bench, GAIA, or other benchmark evaluation results. Provides workflow for triggering evaluations on different benchmarks, finding and comparing runs, and reporting performance differences.
---

# Managing Evaluations

## Overview

OpenHands evaluations produce results stored on a CDN at `https://results.eval.all-hands.dev/`. Each run is identified by a path: `{benchmark}/{model_slug}/{github_run_id}/`. This skill enables triggering evaluation runs, comparing results between runs, and posting performance reports as GitHub PR comments.

## Quick Start

### Trigger an Evaluation

```bash
python .agents/skills/manage-evals/scripts/manage_evals.py trigger \
    --sdk-ref <BRANCH_OR_TAG> --benchmark swebench --eval-limit 50
```

### Compare Runs

```bash
python .agents/skills/manage-evals/scripts/manage_evals.py compare \
    "<benchmark>/<model_slug>/<run_id>/" \
    --auto-baseline
```

### Compare and Post to PR

```bash
python .agents/skills/manage-evals/scripts/manage_evals.py compare \
    "<benchmark>/<model_slug>/<run_id>/" \
    --auto-baseline \
    --post-comment --pr <PR_NUMBER> --repo OpenHands/software-agent-sdk
```

## Triggering Evaluations

### Using the Script

```bash
# SWE-bench (default) on a PR branch
python .agents/skills/manage-evals/scripts/manage_evals.py trigger \
    --sdk-ref my-feature-branch --eval-limit 50

# GAIA benchmark
python .agents/skills/manage-evals/scripts/manage_evals.py trigger \
    --sdk-ref main --benchmark gaia --eval-limit 50

# With a specific model
python .agents/skills/manage-evals/scripts/manage_evals.py trigger \
    --sdk-ref v1.16.0 --benchmark swebench --model-ids gemini-3-flash --eval-limit 50

# Multiple benchmarks (run the command multiple times)
for bench in swebench gaia; do
    python .agents/skills/manage-evals/scripts/manage_evals.py trigger \
        --sdk-ref main --benchmark "$bench" --eval-limit 50 --reason "Multi-benchmark eval"
done
```

### Available Benchmarks

| Benchmark | Description |
|-----------|-------------|
| `swebench` | SWE-bench (default) — software engineering tasks |
| `swebenchpro` | SWE-Bench Pro — harder software engineering tasks |
| `gaia` | GAIA — general AI assistant tasks |
| `swtbench` | SWT-bench — software testing tasks |
| `commit0` | Commit0 — commit generation tasks |
| `swebenchmultimodal` | SWE-bench Multimodal — tasks with images |
| `terminalbench` | TerminalBench — terminal interaction tasks |

### Trigger Options

| Option | Default | Description |
|--------|---------|-------------|
| `--sdk-ref` | *(required)* | Branch, tag, or commit SHA to evaluate |
| `--benchmark` | `swebench` | Benchmark to run |
| `--eval-limit` | `50` | Number of instances to evaluate |
| `--model-ids` | *(first in config)* | Comma-separated model IDs from `resolve_model_config.py` |
| `--tool-preset` | `default` | Tool preset: `default`, `gemini`, `gpt5`, `planning` |
| `--agent-type` | `default` | Agent type: `default`, `acp-claude`, `acp-codex` |
| `--instance-ids` | | Specific instance IDs to evaluate (overrides eval-limit) |
| `--reason` | | Human-readable reason (shown in notifications) |
| `--benchmarks-branch` | `main` | Branch of the benchmarks repo |
| `--eval-branch` | `main` | Branch of the evaluation repo |

### Via PR Labels (Alternative)

Adding a label to a PR also triggers evaluations:
- `run-eval-1` — 1 instance (quick sanity check)
- `run-eval-50` — 50 instances (standard comparison)
- `run-eval-200` — 200 instances
- `run-eval-500` — 500 instances (full benchmark)

## Comparing Evaluation Runs

### Step 1: Find the Current PR's Eval Run

Eval runs are triggered by adding labels like `run-eval-50` to a PR. The `all-hands-bot` posts a comment with results when complete.

**Option A — From bot comments on the PR:**

```bash
gh api repos/OpenHands/software-agent-sdk/issues/<PR_NUMBER>/comments \
    --jq '.[] | select(.user.login == "all-hands-bot") | .body' \
    | grep -o 'Evaluation:.*' | head -1
```

The evaluation name follows the format `{github_run_id}-{model_slug_short}` (e.g., `23775164157-claude-son`). Extract the `github_run_id` from this.

**Option B — From the "Evaluation Triggered" bot comment:**

```bash
gh api repos/OpenHands/software-agent-sdk/issues/<PR_NUMBER>/comments \
    --jq '.[] | select(.body | test("Evaluation Triggered")) | .body'
```

This contains the SDK commit SHA. Cross-reference with daily metadata to find the run ID.

**Option C — From daily metadata:**

```bash
curl -s "https://results.eval.all-hands.dev/metadata/$(date -u +%Y-%m-%d).txt"
```

Each line is a run path. Match by benchmark and model to find the run.

### Step 2: Identify the Run Path Components

A run path has three components:
- **benchmark**: `swebench`, `swebenchpro`, `gaia`, `swtbench`, `commit0`, `swebenchmultimodal`, `terminalbench`
- **model_slug**: Derived from model name with `/:@.` replaced by `-` (e.g., `litellm_proxy-claude-sonnet-4-5-20250929`)
- **run_id**: The GitHub Actions workflow run ID from the `OpenHands/evaluation` repo

### Step 3: Verify Results Exist

```bash
curl -sI "https://results.eval.all-hands.dev/<benchmark>/<model_slug>/<run_id>/output.report.json" | head -1
```

A `200` status confirms the run completed and results are available.

### Step 4: Find a Baseline for Comparison

**Automatic**: The comparison script's `--auto-baseline` flag scans metadata files backward up to 14 days to find the most recent completed run with the same benchmark and model.

**Manual**: Inspect metadata files or other PR bot comments to identify a specific run:

```bash
# Check today's runs
curl -s "https://results.eval.all-hands.dev/metadata/$(date -u +%Y-%m-%d).txt" | grep "swebench/litellm_proxy-claude"

# Check yesterday's runs
curl -s "https://results.eval.all-hands.dev/metadata/$(date -u -d yesterday +%Y-%m-%d).txt" | grep "swebench/litellm_proxy-claude"
```

### Step 5: Run the Comparison

```bash
python .agents/skills/manage-evals/scripts/manage_evals.py compare \
    "swebench/litellm_proxy-claude-sonnet-4-5-20250929/23775164157/" \
    --baseline "swebench/litellm_proxy-claude-sonnet-4-5-20250929/23773892085/"
```

Or with auto-baseline and PR comment posting:

```bash
python .agents/skills/manage-evals/scripts/manage_evals.py compare \
    "swebench/litellm_proxy-claude-sonnet-4-5-20250929/23775164157/" \
    --auto-baseline \
    --post-comment --pr 2334 --repo OpenHands/software-agent-sdk
```

## Available Data Per Run

Each run stores files at `https://results.eval.all-hands.dev/{run_path}/`:

| File | Description |
|------|-------------|
| `metadata/params.json` | Run parameters: SDK commit, PR number, model, eval_limit, triggered_by |
| `output.report.json` | Aggregated results: resolved/submitted/total counts and instance IDs |
| `cost_report.jsonl` | Per-instance cost data |
| `results.tar.gz` | Full archive with all outputs |

## Dashboard

The eval monitor dashboard provides a visual view of runs:

```
https://openhands-eval-monitor.vercel.app/?run={benchmark}/{model_slug}/{run_id}/
```

## Interpreting Results

- **Success rate** = resolved / min(eval_limit, total_instances)
- A 50-instance sample has natural variance of ±2-4 resolved instances between runs
- Focus on **instance-level changes** (gained/lost) to understand regressions vs. noise
- If the same set of instances is resolved, the difference is likely noise

## Additional Resources

### Reference Files
- **`references/eval-infrastructure.md`** — Detailed documentation on the evaluation infrastructure, GCS paths, metadata format, and workflow triggers

### Scripts
- **`scripts/manage_evals.py`** — Standalone comparison script with auto-baseline detection and GitHub comment posting


================================================
FILE: .agents/skills/manage-evals/references/eval-infrastructure.md
================================================
# Evaluation Infrastructure Reference

## Architecture Overview

The evaluation pipeline spans three repositories:

1. **OpenHands/software-agent-sdk** — Triggers evaluations via `run-eval.yml` workflow
2. **OpenHands/evaluation** — Orchestrates the eval job via `eval-job.yml` workflow
3. **OpenHands/benchmarks** — Contains benchmark runners (inference + evaluation)

## Trigger Flow

### PR Label Trigger

1. A label (`run-eval-1`, `run-eval-50`, `run-eval-200`, `run-eval-500`) is added to a PR
2. `software-agent-sdk/.github/workflows/run-eval.yml` fires
3. It resolves model configs from `.github/run-eval/resolve_model_config.py`
4. Dispatches `eval-job.yml` in `OpenHands/evaluation` with:
   - `sdk_commit`: The PR's head SHA
   - `sdk_workflow_run_id`: The `run-eval.yml` workflow run ID
   - `eval_limit`: Extracted from label name
   - `models_json`: Resolved model configurations
   - `pr_number`: The PR number (for result posting)
5. Posts an "Evaluation Triggered" comment on the PR

### Release Trigger

Runs automatically on `release` events with `eval_limit=50`.

### Manual Trigger

Via `workflow_dispatch` on `run-eval.yml` with explicit parameters.

## Results Storage (GCS)

Results are stored in Google Cloud Storage bucket `openhands-evaluation-results`
and served via CDN at `https://results.eval.all-hands.dev/`.

### Run Path Format

```
{benchmark}/{model_slug}/{github_run_id}/
```

- **benchmark**: `swebench`, `swebenchpro`, `gaia`, `swtbench`, `commit0`, `swebenchmultimodal`, `terminalbench`
- **model_slug**: Model name with `/:@.` replaced by `-`
  - Example: `litellm_proxy/claude-sonnet-4-5-20250929` → `litellm_proxy-claude-sonnet-4-5-20250929`
- **github_run_id**: The GitHub Actions run ID from the `OpenHands/evaluation` repo

### Files Per Run

```
{run_path}/
├── metadata/
│   └── params.json          # Job parameters (uploaded at job start)
├── output.report.json       # Aggregated evaluation results
├── cost_report.jsonl        # Per-instance cost data
└── results.tar.gz           # Full archive
```

### params.json Schema

```json
{
    "timestamp": "2026-03-31T00:54:15Z",
    "sdk_commit": "42852dc2260a461536acc186cd918ad5a58910dd",
    "sdk_workflow_run_id": "23775150328",
    "eval_limit": 50,
    "benchmark": "swebench",
    "model_name": "litellm_proxy/claude-sonnet-4-5-20250929",
    "model_id": "claude-sonnet-4-5-20250929",
    "model_display_name": "Claude Sonnet 4.5",
    "unique_eval_name": "23775164157-claude-son",
    "commit": "42852dc2260a461536acc186cd918ad5a58910dd",
    "pr_number": "2334",
    "triggered_by": "enyst",
    "tool_preset": "default",
    "agent_type": "default",
    "github_run_id": "23775164157"
}
```

### output.report.json Schema

```json
{
    "total_instances": 500,
    "submitted_instances": 50,
    "completed_instances": 50,
    "resolved_instances": 35,
    "unresolved_instances": 15,
    "empty_patch_instances": 0,
    "error_instances": 0,
    "completed_ids": ["instance_id_1", "..."],
    "resolved_ids": ["instance_id_1", "..."],
    "unresolved_ids": ["instance_id_1", "..."],
    "empty_patch_ids": [],
    "error_ids": []
}
```

## Daily Metadata

All runs registered on a given day are listed in:

```
https://results.eval.all-hands.dev/metadata/YYYY-MM-DD.txt
```

Each line is a run path. Example:

```
swebench/litellm_proxy-claude-sonnet-4-5-20250929/23773892085/
swebench/litellm_proxy-gemini-3-flash-preview/23774756886/
gaia/litellm_proxy-claude-sonnet-4-5-20250929/23775142614/
```

Metadata files are updated atomically with generation preconditions and
have `Cache-Control: no-cache` set.

## Dashboard

The eval monitor dashboard at `https://openhands-eval-monitor.vercel.app/`
provides a visual view of runs. Construct URLs as:

```
https://openhands-eval-monitor.vercel.app/?run={benchmark}/{model_slug}/{run_id}/
```

## Bot Comments

When an eval completes, `all-hands-bot` posts a comment on the PR (if `pr_number` was provided) with:

- Evaluation name (e.g., `23775164157-claude-son`)
- Model name
- Results summary (total, submitted, resolved, unresolved, empty patch, error counts)
- Success rate
- Archive link

## Model Slug Computation

The model slug is derived from the LLM config's `model` field:

```python
model = config["model"]  # e.g., "litellm_proxy/claude-sonnet-4-5-20250929"
for ch in "/:@.":
    model = model.replace(ch, "-")
# Result: "litellm_proxy-claude-sonnet-4-5-20250929"
```

## Available Models

Models are defined in `software-agent-sdk/.github/run-eval/resolve_model_config.py`.
Each model has an `id`, `display_name`, and `llm_config` with the model path and parameters.

## Variance Between Runs

For 50-instance SWE-bench evaluations:
- Natural variance is typically ±2-4 resolved instances between identical configurations
- Focus on instance-level changes (which specific instances gained/lost) to distinguish real regressions from noise
- If the resolved instance set is identical, the runs are equivalent


================================================
FILE: .agents/skills/manage-evals/scripts/manage_evals.py
================================================
#!/usr/bin/env python3
"""Trigger, compare, and report on OpenHands evaluation runs.

Subcommands:
    trigger   Dispatch an evaluation workflow via the GitHub API
    compare   Compare two evaluation runs and produce a markdown report

Examples:
    # Trigger a swebench eval on a PR branch
    python manage_evals.py trigger --sdk-ref my-branch --benchmark swebench --eval-limit 50

    # Trigger a GAIA eval on a release tag
    python manage_evals.py trigger --sdk-ref v1.16.0 --benchmark gaia --eval-limit 50

    # Auto-find baseline and print comparison markdown
    python manage_evals.py compare swebench/litellm_proxy-claude-sonnet-4-5-20250929/23775164157/ --auto-baseline

    # Post comparison to PR
    python manage_evals.py compare swebench/.../23775164157/ --auto-baseline \\
        --post-comment --pr 2334 --repo OpenHands/software-agent-sdk
"""  # noqa: E501

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.request
from datetime import UTC, datetime, timedelta
from typing import Any


RESULTS_CDN = os.environ.get("RESULTS_CDN", "https://results.eval.all-hands.dev")
DASHBOARD_BASE = "https://openhands-eval-monitor.vercel.app"

SDK_REPO = "OpenHands/software-agent-sdk"
BENCHMARKS = [
    "swebench",
    "swebenchpro",
    "gaia",
    "swtbench",
    "commit0",
    "swebenchmultimodal",
    "terminalbench",
]
TOOL_PRESETS = ["default", "gemini", "gpt5", "planning"]
AGENT_TYPES = ["default", "acp-claude", "acp-codex"]


def fetch_json(url: str) -> dict[str, Any] | None:
    """Fetch JSON from a URL, returning None on 404."""
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=15) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return None
        raise
    except Exception as e:
        print(f"Warning: Failed to fetch {url}: {e}", file=sys.stderr)
        return None


def fetch_text(url: str) -> str | None:
    """Fetch text from a URL, returning None on 404."""
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=15) as resp:
            return resp.read().decode()
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return None
        raise
    except Exception as e:
        print(f"Warning: Failed to fetch {url}: {e}", file=sys.stderr)
        return None


def parse_run_path(path: str) -> tuple[str, str, str]:
    """Parse a run path into (benchmark, model_slug, run_id).

    Accepts formats:
        swebench/litellm_proxy-claude-sonnet-4-5-20250929/23775164157/
        swebench/litellm_proxy-claude-sonnet-4-5-20250929/23775164157
    """
    parts = path.strip("/").split("/")
    if len(parts) != 3:
        raise ValueError(
            f"Invalid run path: {path!r}. Expected: benchmark/model_slug/run_id"
        )
    return parts[0], parts[1], parts[2]


def get_report(run_path: str) -> dict[str, Any] | None:
    """Fetch output.report.json for a run."""
    url = f"{RESULTS_CDN}/{run_path.strip('/')}/output.report.json"
    return fetch_json(url)


def get_params(run_path: str) -> dict[str, Any] | None:
    """Fetch metadata/params.json for a run."""
    url = f"{RESULTS_CDN}/{run_path.strip('/')}/metadata/params.json"
    return fetch_json(url)


def get_metadata_for_date(date_str: str) -> list[str]:
    """Fetch the metadata listing for a given date (YYYY-MM-DD)."""
    url = f"{RESULTS_CDN}/metadata/{date_str}.txt"
    text = fetch_text(url)
    if not text:
        return []
    return [line.strip() for line in text.strip().split("\n") if line.strip()]


def find_baseline_run(
    benchmark: str,
    model_slug: str,
    current_run_id: str,
    lookback_days: int = 14,
    current_eval_limit: int | None = None,
) -> str | None:
    """Find the most recent previous run with matching benchmark/model.

    Scans metadata files backward from today, looking for a run with the
    same benchmark and model_slug but a different (earlier) run_id.
    Prefers runs with matching eval_limit when available.

    Returns the run path or None if no baseline found.
    """
    today = datetime.now(UTC).date()
    prefix = f"{benchmark}/{model_slug}/"

    # Two-pass: first look for matching eval_limit, then any completed run
    candidates: list[tuple[str, dict[str, Any] | None]] = []

    for day_offset in range(lookback_days + 1):
        date = today - timedelta(days=day_offset)
        date_str = date.strftime("%Y-%m-%d")
        entries = get_metadata_for_date(date_str)

        for entry in reversed(entries):
            if not entry.startswith(prefix):
                continue
            _, _, run_id = parse_run_path(entry)
            if run_id == current_run_id:
                continue

            report = get_report(entry)
            if report and report.get("submitted_instances", 0) > 0:
                params = get_params(entry)
                candidates.append((entry, params))
                # Stop after finding enough candidates
                if len(candidates) >= 10:
                    break
        if len(candidates) >= 10:
            break

    if not candidates:
        return None

    # Prefer runs with matching eval_limit
    if current_eval_limit is not None:
        for path, params in candidates:
            if params and params.get("eval_limit") == current_eval_limit:
                return path

    # Fall back to most recent completed run
    return candidates[0][0]


def compute_diff(
    current: dict[str, Any],
    baseline: dict[str, Any],
    current_params: dict[str, Any] | None,
    baseline_params: dict[str, Any] | None,
) -> str:
    """Produce a markdown comparison of two eval reports."""
    # Extract key metrics
    c_resolved = current.get("resolved_instances", 0)
    b_resolved = baseline.get("resolved_instances", 0)
    c_submitted = current.get("submitted_instances", 0)
    b_submitted = baseline.get("submitted_instances", 0)
    c_total = current.get("total_instances", 0)
    b_total = baseline.get("total_instances", 0)
    c_empty = current.get("empty_patch_instances", 0)
    b_empty = baseline.get("empty_patch_instances", 0)
    c_error = current.get("error_instances", 0)
    b_error = baseline.get("error_instances", 0)

    # Eval limit from params
    c_limit = (current_params or {}).get("eval_limit", c_submitted)
    b_limit = (baseline_params or {}).get("eval_limit", b_submitted)

    # Denominators for rate calculation
    c_denom = min(c_limit, c_total) if c_total > 0 else c_limit
    b_denom = min(b_limit, b_total) if b_total > 0 else b_limit

    c_rate = (c_resolved / c_denom * 100) if c_denom else 0
    b_rate = (b_resolved / b_denom * 100) if b_denom else 0
    rate_delta = c_rate - b_rate

    # Instance-level diff
    c_resolved_ids = set(current.get("resolved_ids", []))
    b_resolved_ids = set(baseline.get("resolved_ids", []))
    gained = sorted(c_resolved_ids - b_resolved_ids)
    lost = sorted(b_resolved_ids - c_resolved_ids)

    # Delta symbol
    def delta_str(val: float | int) -> str:
        if val > 0:
            return f"+{val}"
        return str(val)

    # Build markdown
    lines: list[str] = []
    lines.append("## 📊 Evaluation Comparison")
    lines.append("")

    # Summary line
    if rate_delta > 0:
        emoji = "📈"
        delta_pp = f"+{rate_delta:.1f}"
    elif rate_delta < 0:
        emoji = "📉"
        delta_pp = f"{rate_delta:.1f}"
    else:
        emoji = "➡️"
        delta_pp = "0.0"
    lines.append(
        f"{emoji} **Success rate: {c_rate:.1f}% "
        f"({delta_pp}pp vs baseline {b_rate:.1f}%)**"
    )
    lines.append("")

    # Metadata
    c_pr = (current_params or {}).get("pr_number")
    b_pr = (baseline_params or {}).get("pr_number")
    c_commit = (current_params or {}).get("sdk_commit", "unknown")[:12]
    b_commit = (baseline_params or {}).get("sdk_commit", "unknown")[:12]
    c_run_id = (current_params or {}).get("github_run_id", "")
    b_run_id = (baseline_params or {}).get("github_run_id", "")

    lines.append("| | Current | Baseline |")
    lines.append("|---|---|---|")
    if c_run_id or b_run_id:
        lines.append(f"| **Run ID** | `{c_run_id}` | `{b_run_id}` |")
    lines.append(f"| **SDK Commit** | `{c_commit}` | `{b_commit}` |")
    if c_pr or b_pr:
        c_pr_str = f"#{c_pr}" if c_pr else "—"
        b_pr_str = f"#{b_pr}" if b_pr else "— (main)" if not b_pr else f"#{b_pr}"
        lines.append(f"| **PR** | {c_pr_str} | {b_pr_str} |")
    lines.append(
        f"| **Resolved** | {c_resolved}/{c_denom} ({c_rate:.1f}%) "
        f"| {b_resolved}/{b_denom} ({b_rate:.1f}%) |"
    )
    lines.append(f"| **Δ Resolved** | {delta_str(c_resolved - b_resolved)} | — |")
    lines.append(f"| **Empty Patches** | {c_empty} | {b_empty} |")
    lines.append(f"| **Errors** | {c_error} | {b_error} |")
    lines.append("")

    # Instance-level changes
    if gained or lost:
        lines.append("### Instance-Level Changes")
        lines.append("")

    if gained:
        lines.append(
            f"**✅ Newly resolved ({len(gained)}):** "
            + ", ".join(f"`{g}`" for g in gained[:20])
        )
        if len(gained) > 20:
            lines.append(f"  ... and {len(gained) - 20} more")
        lines.append("")

    if lost:
        lines.append(
            f"**❌ Regressions ({len(lost)}):** "
            + ", ".join(f"`{g}`" for g in lost[:20])
        )
        if len(lost) > 20:
            lines.append(f"  ... and {len(lost) - 20} more")
        lines.append("")

    if not gained and not lost and c_resolved_ids and b_resolved_ids:
        lines.append(
            "*Identical set of resolved instances — no regressions or improvements.*"
        )
        lines.append("")

    # Dashboard links
    lines.append("### 🔗 Links")
    lines.append("")
    if c_run_id:
        benchmark = (current_params or {}).get("benchmark", "swebench")
        model_slug = (
            (current_params or {})
            .get("model_name", "")
            .replace("/", "-")
            .replace(":", "-")
            .replace("@", "-")
            .replace(".", "-")
        )
        c_dash = f"{DASHBOARD_BASE}/?run={benchmark}/{model_slug}/{c_run_id}/"
        lines.append(f"- [Current run dashboard]({c_dash})")
    if b_run_id:
        benchmark = (baseline_params or {}).get("benchmark", "swebench")
        model_slug = (
            (baseline_params or {})
            .get("model_name", "")
            .replace("/", "-")
            .replace(":", "-")
            .replace("@", "-")
            .replace(".", "-")
        )
        b_dash = f"{DASHBOARD_BASE}/?run={benchmark}/{model_slug}/{b_run_id}/"
        lines.append(f"- [Baseline run dashboard]({b_dash})")
    lines.append("")

    return "\n".join(lines)


def github_api_request(
    url: str,
    token: str,
    *,
    method: str = "GET",
    data: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
    """Make a GitHub API request. Returns parsed JSON or None for 204."""
    body = json.dumps(data).encode() if data else None
    req = urllib.request.Request(
        url,
        data=body,
        method=method,
        headers={
            "Authorization": f"token {token}",
            "Accept": "application/vnd.github+json",
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        if resp.status == 204:
            return None
        return json.loads(resp.read().decode())


def post_github_comment(repo: str, pr_number: int, body: str, token: str) -> None:
    """Post a comment on a GitHub PR."""
    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    result = github_api_request(url, token, method="POST", data={"body": body})
    if result:
        print(f"Posted comment: {result.get('html_url', 'unknown')}", file=sys.stderr)


def trigger_eval(
    token: str,
    *,
    sdk_ref: str,
    benchmark: str = "swebench",
    eval_limit: int = 50,
    model_ids: str = "",
    reason: str = "",
    repo: str = SDK_REPO,
    allow_unreleased: bool = True,
    benchmarks_branch: str = "main",
    eval_branch: str = "main",
    tool_preset: str = "default",
    agent_type: str = "default",
    instance_ids: str = "",
) -> None:
    """Dispatch an evaluation workflow via the GitHub Actions API."""
    inputs: dict[str, str] = {
        "benchmark": benchmark,
        "sdk_ref": sdk_ref,
        "eval_limit": str(eval_limit),
        "reason": reason,
        "benchmarks_branch": benchmarks_branch,
        "eval_branch": eval_branch,
        "tool_preset": tool_preset,
        "agent_type": agent_type,
        "allow_unreleased_branches": str(allow_unreleased).lower(),
    }
    if model_ids:
        inputs["model_ids"] = model_ids
    if instance_ids:
        inputs["instance_ids"] = instance_ids

    url = (
        f"https://api.github.com/repos/{repo}/actions/workflows/run-eval.yml/dispatches"
    )
    payload = {"ref": sdk_ref, "inputs": inputs}

    print(f"Dispatching eval workflow on {repo}...", file=sys.stderr)
    print(f"  benchmark:    {benchmark}", file=sys.stderr)
    print(f"  sdk_ref:      {sdk_ref}", file=sys.stderr)
    print(f"  eval_limit:   {eval_limit}", file=sys.stderr)
    print(f"  model_ids:    {model_ids or '(default)'}", file=sys.stderr)
    print(f"  tool_preset:  {tool_preset}", file=sys.stderr)
    print(f"  agent_type:   {agent_type}", file=sys.stderr)
    if instance_ids:
        print(f"  instance_ids: {instance_ids}", file=sys.stderr)
    if reason:
        print(f"  reason:       {reason}", file=sys.stderr)

    github_api_request(url, token, method="POST", data=payload)
    print("✓ Workflow dispatched successfully.", file=sys.stderr)
    print(
        f"  Monitor at: https://github.com/{repo}/actions/workflows/run-eval.yml",
        file=sys.stderr,
    )


def _require_token() -> str:
    """Return GITHUB_TOKEN or exit with error."""
    token = os.environ.get("GITHUB_TOKEN", "")
    if not token:
        print("ERROR: GITHUB_TOKEN environment variable not set", file=sys.stderr)
        sys.exit(1)
    return token


def cmd_trigger(args: argparse.Namespace) -> None:
    """Handle the 'trigger' subcommand."""
    token = _require_token()
    trigger_eval(
        token,
        sdk_ref=args.sdk_ref,
        benchmark=args.benchmark,
        eval_limit=args.eval_limit,
        model_ids=args.model_ids or "",
        reason=args.reason or "",
        repo=args.repo,
        benchmarks_branch=args.benchmarks_branch,
        eval_branch=args.eval_branch,
        tool_preset=args.tool_preset,
        agent_type=args.agent_type,
        instance_ids=args.instance_ids or "",
    )


def cmd_compare(args: argparse.Namespace) -> None:
    """Handle the 'compare' subcommand."""
    # Validate
    if args.post_comment and (not args.pr or not args.repo):
        print("ERROR: --post-comment requires --pr and --repo", file=sys.stderr)
        sys.exit(1)
    if not args.baseline and not args.auto_baseline:
        print("ERROR: Specify --baseline or --auto-baseline", file=sys.stderr)
        sys.exit(1)

    benchmark, model_slug, run_id = parse_run_path(args.current_run_path)
    print(f"Current run: {benchmark}/{model_slug}/{run_id}", file=sys.stderr)

    # Fetch current run data
    current_report = get_report(args.current_run_path)
    if not current_report:
        print(f"ERROR: No report found for {args.current_run_path}", file=sys.stderr)
        sys.exit(1)

    current_params = get_params(args.current_run_path)

    # Find baseline
    if args.baseline:
        baseline_path = args.baseline
    else:
        current_eval_limit = (
            current_params.get("eval_limit") if current_params else None
        )
        print(
            f"Searching for baseline (lookback: {args.lookback_days} days, "
            f"eval_limit: {current_eval_limit})...",
            file=sys.stderr,
        )
        baseline_path = find_baseline_run(
            benchmark, model_slug, run_id, args.lookback_days, current_eval_limit
        )

    if not baseline_path:
        print("No baseline run found. Cannot produce comparison.", file=sys.stderr)
        sys.exit(1)

    print(f"Baseline run: {baseline_path}", file=sys.stderr)

    baseline_report = get_report(baseline_path)
    if not baseline_report:
        print(f"ERROR: No report found for baseline {baseline_path}", file=sys.stderr)
        sys.exit(1)

    baseline_params = get_params(baseline_path)

    # Generate comparison
    markdown = compute_diff(
        current_report, baseline_report, current_params, baseline_params
    )
    print(markdown)

    # Post comment if requested
    if args.post_comment:
        token = _require_token()
        body = (
            markdown
            + "\n---\n"
            + "*This comparison was generated by an AI assistant "
            + "(OpenHands) on behalf of the user.*\n"
        )
        post_github_comment(args.repo, args.pr, body, token)


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Trigger, compare, and report on OpenHands evaluation runs",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # --- trigger subcommand ---
    p_trigger = subparsers.add_parser(
        "trigger",
        help="Dispatch an evaluation workflow",
        description="Trigger an eval run via the GitHub Actions workflow_dispatch API.",
    )
    p_trigger.add_argument(
        "--sdk-ref",
        required=True,
        help="SDK branch, tag, or commit to evaluate (e.g., main, v1.16.0, my-branch)",
    )
    p_trigger.add_argument(
        "--benchmark",
        default="swebench",
        choices=BENCHMARKS,
        help="Benchmark to run (default: swebench)",
    )
    p_trigger.add_argument(
        "--eval-limit",
        type=int,
        default=50,
        help="Number of instances to evaluate (default: 50)",
    )
    p_trigger.add_argument(
        "--model-ids",
        default="",
        help=(
            "Comma-separated model IDs "
            "(see .github/run-eval/resolve_model_config.py; default: first model)"
        ),
    )
    p_trigger.add_argument("--reason", default="", help="Human-readable trigger reason")
    p_trigger.add_argument(
        "--repo",
        default=SDK_REPO,
        help=f"Repository to trigger on (default: {SDK_REPO})",
    )
    p_trigger.add_argument(
        "--benchmarks-branch",
        default="main",
        help="Benchmarks repo branch (default: main)",
    )
    p_trigger.add_argument(
        "--eval-branch",
        default="main",
        help="Evaluation repo branch (default: main)",
    )
    p_trigger.add_argument(
        "--tool-preset",
        default="default",
        choices=TOOL_PRESETS,
        help="Tool preset for file editing (default: default)",
    )
    p_trigger.add_argument(
        "--agent-type",
        default="default",
        choices=AGENT_TYPES,
        help="Agent type (default: default)",
    )
    p_trigger.add_argument(
        "--instance-ids",
        default="",
        help="Comma-separated instance IDs to evaluate (overrides eval-limit)",
    )

    # --- compare subcommand ---
    p_compare = subparsers.add_parser(
        "compare",
        help="Compare two evaluation runs",
        description="Fetch results for two eval runs and produce a diff report.",
    )
    p_compare.add_argument(
        "current_run_path",
        help="Run path (e.g., swebench/litellm_proxy-claude-.../23775164157/)",
    )
    p_compare.add_argument("--baseline", help="Explicit baseline run path")
    p_compare.add_argument(
        "--auto-baseline",
        action="store_true",
        help="Auto-find the most recent previous run as baseline",
    )
    p_compare.add_argument(
        "--lookback-days",
        type=int,
        default=14,
        help="Days to search for baseline (default: 14)",
    )
    p_compare.add_argument(
        "--post-comment",
        action="store_true",
        help="Post result as a GitHub PR comment",
    )
    p_compare.add_argument("--pr", type=int, help="PR number for commenting")
    p_compare.add_argument("--repo", help="Repository (OWNER/REPO) for commenting")

    args = parser.parse_args()

    if args.command == "trigger":
        cmd_trigger(args)
    elif args.command == "compare":
        cmd_compare(args)


if __name__ == "__main__":
    main()


================================================
FILE: .agents/skills/run-eval.md
================================================
---
name: run-eval
description: Trigger and monitor evaluation runs for benchmarks like SWE-bench, GAIA, and others. Use when running evaluations via GitHub Actions or monitoring eval progress through Datadog and kubectl.
triggers:
- run eval
- trigger eval
- evaluation run
- swebench eval
---

# Running Evaluations

## Trigger via GitHub API

```bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/actions/workflows/run-eval.yml/dispatches" \
  -d '{
    "ref": "main",
    "inputs": {
      "benchmark": "swebench",
      "sdk_ref": "main",
      "eval_limit": "50",
      "model_ids": "claude-sonnet-4-5-20250929",
      "reason": "Description of eval run",
      "benchmarks_branch": "main"
    }
  }'
```

**Key parameters:**
- `benchmark`: `swebench`, `swebenchpro`, `swebenchmultimodal`, `gaia`, `swtbench`, `commit0`, `multiswebench`, `terminalbench`
- `eval_limit`: Any positive integer (e.g., `1`, `10`, `50`, `200`)
- `model_ids`: See `.github/run-eval/resolve_model_config.py` for available models
- `benchmarks_branch`: Use feature branch from the benchmarks repo to test benchmark changes before merging

**Note:** When running a full eval, you must select an `eval_limit` that is greater than or equal to the actual number of instances in the benchmark. If you specify a smaller limit, only that many instances will be evaluated (partial eval).

## Monitoring

**Datadog script** (requires `OpenHands/evaluation` repo; DD_API_KEY, DD_APP_KEY, and DD_SITE environment variables are set):
```bash
DD_API_KEY=$DD_API_KEY DD_APP_KEY=$DD_APP_KEY DD_SITE=$DD_SITE \
  python scripts/analyze_evals.py --job-prefix <EVAL_RUN_ID> --time-range 60
# EVAL_RUN_ID format: typically the workflow run ID from GitHub Actions
```

**kubectl** (for users with cluster access - the agent does not have kubectl access):
```bash
kubectl logs -f job/eval-eval-<RUN_ID>-<MODEL_SLUG> -n evaluation-jobs
```

## Common Errors

| Error | Cause | Fix |
|-------|-------|-----|
| `503 Service Unavailable` | Infrastructure overloaded | Ask user to stop some evaluation runs |
| `429 Too Many Requests` | Rate limiting | Wait or reduce concurrency |
| `failed after 3 retries` | Instance failures | Check Datadog logs for root cause |

## Limits

- Max 256 parallel runtimes (jobs will queue if this limit is exceeded)
- Full evals typically take 1-3 hours depending on benchmark size


================================================
FILE: .agents/skills/sdk-release/SKILL.md
================================================
---
name: sdk-release
description: >-
  This skill should be used when the user asks to "release the SDK",
  "prepare a release", "publish a new version", "cut a release",
  "do a release", or mentions the SDK release checklist or release process.
  Guides through the full software-agent-sdk release workflow
  from version bump to PyPI publication, emphasizing human checkpoints.
---

# SDK Release Guide

This skill walks through the software-agent-sdk release process step by step.

> **🚨 CRITICAL**: NEVER merge the release PR or create/publish a GitHub
> release without the human's explicit approval. Release is the last line
> of human defense. Always present the current status and ask for
> confirmation before performing any irreversible action.

## Phase 1: Trigger the Prepare-Release Workflow

Determine the target version (SemVer `X.Y.Z`). Then trigger the
`prepare-release.yml` workflow, which creates a release branch and PR
automatically.

### Via GitHub UI

Navigate to
<https://github.com/OpenHands/software-agent-sdk/actions/workflows/prepare-release.yml>,
click **Run workflow**, enter the version (e.g. `1.16.0`), and run it.

### Via GitHub API

```bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/actions/workflows/prepare-release.yml/dispatches" \
  -d '{
    "ref": "main",
    "inputs": {
      "version": "1.16.0"
    }
  }'
```

The workflow will:
1. Validate version format
2. Create branch `rel-<version>`
3. Run `make set-package-version version=<version>` across all packages
4. Update the `sdk_ref` default in the eval workflow
5. Open a PR titled **"Release v\<version\>"** with labels
   `integration-test`, `behavior-test`, and `test-examples`

### ⏸ Checkpoint — Confirm PR Created

Verify the PR exists and the version changes look correct before continuing.

```bash
gh pr list --repo OpenHands/software-agent-sdk \
  --head "rel-<version>" --json number,title,url
```

## Phase 2: Address Deprecation Deadlines

The `deprecation-check` CI job runs on every PR. If the release version
crosses any deprecation deadline declared in the codebase, the check will
fail.

Review the failing check output and either:
- Remove the deprecated code if the deadline has passed, **or**
- Extend the deadline with justification.

Push fixes to the release branch. The check must pass before merging.

## Phase 3: Wait for CI — Tests Must Pass

The release PR triggers three labeled test suites. **All three must pass.**

| Label | Suite | What it covers |
|-------|-------|----------------|
| `integration-test` | Integration tests | End-to-end agent scenarios |
| `behavior-test` | Behavior tests | Agent behavioral guardrails |
| `test-examples` | Example tests | All runnable examples in `examples/` |

Monitor status:

```bash
gh pr checks <PR_NUMBER> --repo OpenHands/software-agent-sdk
```

### ⏸ Checkpoint — Human Judgment on Failures

Some test failures may be pre-existing or flaky. Decide with the team
whether each failure is:
- **Blocking** — must fix before release
- **Known / pre-existing** — acceptable to release with a follow-up issue
- **Flaky** — re-run the workflow

Re-run failed jobs:

```bash
# Find the run ID
gh run list --repo OpenHands/software-agent-sdk \
  --branch "rel-<version>" --limit 5

# Re-run failed jobs
gh run rerun <RUN_ID> --repo OpenHands/software-agent-sdk --failed
```

## Phase 4: Run Evaluation (Optional but Recommended)

Trigger an evaluation run on SWE-bench (or another benchmark) against the
release branch to catch regressions. See the `run-eval` skill for full
details.

```bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/actions/workflows/run-eval.yml/dispatches" \
  -d '{
    "ref": "main",
    "inputs": {
      "benchmark": "swebench",
      "sdk_ref": "v<version>",
      "eval_limit": "50",
      "reason": "Pre-release eval for v<version>",
      "allow_unreleased_branches": "true"
    }
  }'
```

### ⏸ Checkpoint — Evaluate Results

Compare the eval results against the previous release. Significant score
drops should block the release.

## Phase 5: Merge the Release PR

> **🚨 STOP — Do NOT merge without explicit human approval.**
> Present the CI status summary and ask the human to confirm before merging.
> Merging is effectively irreversible — it automatically triggers the full
> release pipeline (GitHub release → PyPI publish → downstream version bumps).

Once the human approves:

```bash
gh pr merge <PR_NUMBER> --repo OpenHands/software-agent-sdk --merge
```

## Phase 6: Automated Release Pipeline (no action needed)

When the release PR is merged, the following happens automatically:

1. **`create-release.yml`** detects the merged `rel-*` branch, creates a
   GitHub release with tag `v<version>` and auto-generated release notes.
2. **`pypi-release.yml`** triggers on the published release and publishes
   all four packages to PyPI:
   - `openhands-sdk`
   - `openhands-tools`
   - `openhands-workspace`
   - `openhands-agent-server`
3. **`version-bump-prs.yml`** triggers after successful PyPI publish and
   creates downstream version bump PRs.

### ⏸ Checkpoint — Verify PyPI Publication

```bash
# Check each package is available (allow a few minutes for indexing)
for pkg in openhands-sdk openhands-tools openhands-workspace openhands-agent-server; do
  curl -s -o /dev/null -w "$pkg: %{http_code}\n" \
    "https://pypi.org/pypi/$pkg/<version>/json"
done
```

All should return `200`.

## Phase 7: Post-Release Announcements

After the automated pipeline completes, compose a Slack message for the
human to post, including links to the downstream version bump PRs:

```
🚀 *SDK v<version> published to PyPI!*

Version bump PRs:
• <https://github.com/All-Hands-AI/OpenHands/pulls?q=is%3Apr+bump-sdk-<version>|OpenHands>
• <https://github.com/OpenHands/openhands-cli/pulls?q=is%3Apr+bump-sdk-<version>|OpenHands-CLI>

Release: <https://github.com/OpenHands/software-agent-sdk/releases/tag/v<version>|v<version>>
```

See `references/post-release-checklist.md` for details on reviewing
downstream PRs and handling any issues.

## Quick Reference — Full Checklist

- [ ] Trigger `prepare-release.yml` with target version
- [ ] Verify release PR is created
- [ ] Fix deprecation deadline failures (if any)
- [ ] Integration tests pass
- [ ] Behavior tests pass
- [ ] Example tests pass
- [ ] (Optional) Evaluation run shows no regressions
- [ ] **🚨 Get human approval**, then merge the release PR
- [ ] _(Automated)_ GitHub release created with auto-generated notes
- [ ] _(Automated)_ Packages published to PyPI
- [ ] _(Automated)_ Downstream version bump PRs created
- [ ] Verify packages appear on PyPI
- [ ] Send Slack message with downstream version bump PR links


================================================
FILE: .agents/skills/sdk-release/references/post-release-checklist.md
================================================
# Post-Release Checklist

After the GitHub release is published and PyPI packages are available,
several automated and manual follow-up steps occur.

## Automated: Downstream Version Bump PRs

The `version-bump-prs.yml` workflow runs automatically after `pypi-release`
succeeds. It creates PRs in two repositories:

### OpenHands-CLI (`OpenHands/openhands-cli`)

- Branch: `bump-sdk-<version>`
- Updates `openhands-sdk` and `openhands-tools` via `uv add`
- Verify the PR passes CLI tests before merging

```bash
gh pr list --repo OpenHands/openhands-cli \
  --search "bump-sdk-<version>" --json number,title,url
```

### OpenHands (`All-Hands-AI/OpenHands`)

- Branch: `bump-sdk-<version>`
- Updates `openhands-sdk`, `openhands-tools`, and `openhands-agent-server`
  in `pyproject.toml`
- Regenerates `poetry.lock`
- Updates `AGENT_SERVER_IMAGE` in `sandbox_spec_service.py`
- Verifies `enterprise/pyproject.toml` does not have explicit SDK pins

```bash
gh pr list --repo All-Hands-AI/OpenHands \
  --search "bump-sdk-<version>" --json number,title,url
```

## Manual Review of Downstream PRs

Both PRs require human review:

1. **Check CI passes** on each downstream PR
2. **Verify compatibility** — especially if the release includes breaking
   changes or new features that need adoption
3. **Merge** once satisfied

## Evaluation on OpenHands Index

If not already done pre-release, trigger a full evaluation run
against the published version:

```bash
curl -X POST \
  -H "Authorization: token $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/OpenHands/software-agent-sdk/actions/workflows/run-eval.yml/dispatches" \
  -d '{
    "ref": "main",
    "inputs": {
      "benchmark": "swebench",
      "sdk_ref": "v<version>",
      "eval_limit": "300",
      "reason": "Post-release eval v<version>"
    }
  }'
```

## Documentation Updates

If the release includes user-facing features, verify documentation is
updated in `OpenHands/docs` (SDK docs live under `sdk/`). See the
`feature-release-rollout` skill for the full downstream propagation
workflow.

## Troubleshooting

### PyPI publication failed

Re-run the `pypi-release.yml` workflow manually. It uses `--check-url`
to skip already-published packages, so partial reruns are safe.

```bash
gh workflow run pypi-release.yml --repo OpenHands/software-agent-sdk
```

### Version bump PR has conflicts

The automated PR may conflict if the downstream repo changed dependency
pins since the workflow ran. Resolve conflicts manually on the bump branch,
or re-trigger `version-bump-prs.yml` with the version input.

```bash
gh workflow run version-bump-prs.yml \
  --repo OpenHands/software-agent-sdk \
  -f version=<version>
```

### Downstream tests fail after bump

If a downstream repo's tests fail on the version bump PR, investigate
whether the failure is a breaking change in the SDK release. If so,
either:
- Fix the downstream code on the bump branch, or
- Publish a patch release of the SDK with the fix


================================================
FILE: .agents/skills/write-behavior-test.md
================================================
---
name: write-behavior-test
description: Guide for writing behavior tests that verify agents follow system message guidelines and avoid undesirable behaviors. Use when creating integration tests for agent behavior validation.
triggers:
- /write_behavior_test
---

# Behavior Test Writing Guide

You are helping to create **behavior tests** for the agent-sdk integration test suite. These tests verify that agents follow system message guidelines and avoid undesirable behaviors.

The tests are for the agent powered by this SDK, so you may need to refer the codebase for details on how the agent works in order to write effective tests.

## Behavior Tests vs Task Tests

**Task Tests (t*.py)** - REQUIRED tests that verify task completion:
- Focus: Can the agent successfully complete the task?
- Example: Fix typos in a file, create a script, implement a feature

**Behavior Tests (b*.py)** - OPTIONAL tests that verify proper behavior:
- Focus: Does the agent follow best practices and system guidelines?
- Example: Don't implement when asked for advice, don't over-verify, avoid redundant files

## Key Principles for Writing Behavior Tests

### ✅ DO:

1. **Use Real Repositories**
   - Clone actual GitHub repositories that represent real-world scenarios
   - Pin to a specific historical commit (before a fix/feature was added)
   - Example: `clone_pinned_software_agent_repo(workspace)` helper

2. **Test Realistic Complex, Nuanced Behaviors**
   - Try to make the task as realistic as possible to real HUMAN interactions, from file naming, (somewhat lazy) instruction style, etc
   - Focus on subtle behavioral issues that require judgment
   - Test scenarios where the "right" behavior isn't immediately obvious
   - Examples: When to implement vs advise, when to stop testing, whether to add backward compatibility

3. **Clean Up Repository History**
   - Check out to a commit BEFORE the solution exists
   - Reset/remove future commits (see existing tests for examples)
   - Ensures the agent experiences the same context as real users

4. **Use Helper Functions**
   - `find_file_editing_operations(events)` - Find file create/edit operations
   - `find_tool_calls(events, tool_name)` - Find specific tool usage
   - `get_conversation_summary(events)` - Get summary for LLM judge
   - `judge_agent_behavior(...)` - Use LLM to evaluate behavior quality

5. **Leverage LLM Judges**
   - Use `judge_agent_behavior()` for subjective evaluations
   - Provide clear evaluation criteria in the judge prompt
   - Track judge usage costs: `self.add_judge_usage(prompt_tokens, completion_tokens, cost)`

6. **Adaptation of Problem Description to Task**
   - If you find the problem description is not easy to adapt to a behavior test, e.g. it requires complex environment setup like kubernetes, try to come up with a simpler problem description that still captures the essence of the behavior you want to test but is easier to implement in the test framework.
   - Ensure the instructions naturally lead to the behavior you want to evaluate

### ❌ DO NOT:

1. **Avoid Simple Synthetic Tests**
   - Don't create artificial scenarios with minimal setup
   - Don't test behaviors that are too obvious or straightforward
   - Example: Don't create a single-file test with trivial content

2. **Don't Test Basic Functionality**
   - Behavior tests are NOT for testing if the agent can use tools
   - Task tests handle basic capability verification
   - Focus on HOW the agent approaches problems, not IF it can solve them

3. **Don't Overcomplicate Static Assertions**
   - Use assertions for clear-cut checks (e.g., no file edits)
   - Rely on LLM judges for nuanced behavior evaluations
   - Avoid trying to encode subjective judgments purely in code or too much static logic

## Tips for Test Difficulty Calibration

**Make tests challenging but not impossible and too long:**

1. **Context Complexity**: Use real codebases with multiple files and dependencies, either the software-agent-sdk or other popular open-source repos you find suitable
2. **Ambiguity**: Prefer instructions that could be interpreted multiple ways
3. **Temptation**: Set up scenarios where the "easy wrong path" is tempting
4. **Realism**: Mirror real user interactions and expectations

**Examples of Good Complexity:**
- "How to implement X?" (tests if agent implements vs advises)
- "Update constant Y" (tests if agent over-verifies with excessive test runs)
- "Rename method A to B" (tests if agent adds unnecessary backward compatibility)

## Example Behavior Test Patterns

1. **Premature Implementation** - Tests if agent implements when asked for advice only
2. **Over-verification** - Tests if agent runs excessive tests beyond what's needed
3. **Unnecessary Compatibility** - Tests if agent adds backward compatibility shims when not needed
4. **Redundant Artifacts** - Tests if agent creates extra files (docs, READMEs) without being asked
5. **Communication Quality** - Tests if agent provides explanations for actions

## File Naming Convention

Name your test file: `b##_descriptive_name.py`
- `b` prefix indicates behavior test (auto-detected)
- `##` is a zero-padded number (e.g., 01, 02, 03)
- Use snake_case for the descriptive name

## Final Checklist

Before submitting your behavior test, verify:

- [ ] Uses a real repository or complex codebase
- [ ] Tests a nuanced behavior, not basic functionality
- [ ] Includes clear and not overly complex verification logic (assertions or LLM judge)
- [ ] Has a descriptive docstring explaining what behavior is tested
- [ ] Properly tracks judge usage costs if using LLM evaluation
- [ ] Follows naming convention: `b##_descriptive_name.py`
- [ ] Test is realistic and based on actual behavioral issues observed

Remember: The goal is to catch subtle behavioral issues that would appear in real-world usage, serving as regression tests for system message improvements.


================================================
FILE: .dockerignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
# Note: We keep our custom spec file in version control
# *.spec

# PyInstaller build directories
build/
dist/

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be added to the global gitignore or merged into this project gitignore.  For a PyCharm
#  project, it is recommended to ignore the entire .idea directory.
.idea/

# VS Code
.vscode/

# macOS
.DS_Store
.AppleDouble
.LSOverride

# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/

# Linux
*~

# Temporary files
*.tmp
*.temp
*.swp
*.swo

# UV specific
.uv/

# Project specific
*.log
.coverage
.pytest_cache/

workspace/
.client
.docker


.git
.git/**

# VS Code: Ignore all but certain files that specify repo-specific settings.
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
.vscode/**/*
!.vscode/extensions.json
!.vscode/tasks.json

# VS Code extensions/forks:
.cursorignore
.rooignore
.clineignore
.windsurfignore
.cursorrules
.roorules
.clinerules
.windsurfrules
.cursor/rules
.roo/rules
.cline/rules
.windsurf/rules
.repomix
repomix-output.txt

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

logs

# agent
.envrc
cache
.jinja_cache/

.conversations*
workspace/

# Build optimization: exclude files not needed for building agent-server
tests/
*.log
.github/
scripts/
examples/
.ruff_cache/
.uv-cache/
Makefile
docs/
*.md
!README.md
.pre-commit-config.yaml
.python-version


================================================
FILE: .github/ISSUE_TEMPLATE/bug_template.yml
================================================
---
name: Bug
description: Report a problem with OpenHands SDK
title: '[Bug]: '
labels: [bug]
body:
    - type: markdown
      attributes:
          value: |
              ## Thank you for reporting a bug! 🐛

              **Please fill out all required fields.** Issues missing critical information (version, installation method, reproduction steps, etc.) will be delayed or closed until complete details are provided.

              Clear, detailed reports help us resolve issues faster.

    - type: checkboxes
      attributes:
          label: Is there an existing issue for the same bug?
          description: Please search existing issues before creating a new one. If found, react or comment to the duplicate issue instead of making a 
              new one. <!-- TODO-openhands -->
          options:
              - label: I have searched existing issues and this is not a duplicate.
                required: true

    - type: textarea
      id: bug-description
      attributes:
          label: Bug Description
          description: Clearly describe what went wrong. Be specific and concise.
          placeholder: Example - When I use the SDK to create an agent with custom tools, the agent fails to register the tools with a TypeError.
      validations:
          required: true

    - type: textarea
      id: expected-behavior
      attributes:
          label: Expected Behavior
          description: What did you expect to happen?
          placeholder: Example - The agent should successfully register custom tools and make them available for use.
      validations:
          required: false

    - type: textarea
      id: actual-behavior
      attributes:
          label: Actual Behavior
          description: What actually happened?
          placeholder: "Example - TypeError: 'NoneType' object is not iterable when calling agent.register_tool()"
      validations:
          required: false

    - type: textarea
      id: reproduction-steps
      attributes:
          label: Steps to Reproduce
          description: Provide clear, step-by-step instructions to reproduce the bug.
          placeholder: |
              1. Install openhands-sdk using pip
              2. Import and create an agent instance
              3. Define a custom tool function
              4. Call agent.register_tool(custom_tool)
              5. Error appears
      validations:
          required: false

    - type: input
      id: installation
      attributes:
          label: Installation Method
          description: How did you install the OpenHands SDK?
          placeholder: ex. pip install openhands-sdk, uv pip install openhands-sdk, pip install -e ., etc.

    - type: input
      id: installation-other
      attributes:
          label: If you selected "Other", please specify
          description: Describe your installation method
          placeholder: ex. Poetry, conda, custom setup, etc.

    - type: input
      id: sdk-version
      attributes:
          label: SDK Version
          description: What version are you using? Check with `pip show openhands-sdk` or similar for other packages.
          placeholder: ex. 0.1.0, 0.2.0, main branch, commit hash, etc.
      validations:
          required: false

    - type: checkboxes
      id: version-confirmation
      attributes:
          label: Version Confirmation
          description: Bugs on older versions may already be fixed. Please upgrade before submitting.
          options:
              - label: I have confirmed this bug exists on the LATEST version of OpenHands SDK
                required: false

    - type: input
      id: python-version
      attributes:
          label: Python Version
          description: Which Python version are you using?
          placeholder: ex. 3.10.12, 3.11.5, 3.12.0
      validations:
          required: false

    - type: input
      id: model-name
      attributes:
          label: Model Name (if applicable)
          description: Which model(s) are you using?
          placeholder: ex. gpt-4o, claude-3-5-sonnet-20241022, openrouter/deepseek-r1, etc.
      validations:
          required: false

    - type: dropdown
      id: os
      attributes:
          label: Operating System
          options:
              - MacOS
              - Linux
              - WSL on Windows
              - Windows
              - Other
      validations:
          required: false

    - type: textarea
      id: logs
      attributes:
          label: Logs and Error Messages
          description: |
              **Paste relevant logs, error messages, or stack traces.** Use code blocks (```) for formatting.

              Include full stack traces when available.
          placeholder: |
              ```
              Paste error logs here
              ```

    - type: textarea
      id: code-sample
      attributes:
          label: Minimal Code Sample
          description: |
              If possible, provide a minimal code sample that reproduces the issue.
          placeholder: |
              ```python
              from openhands.sdk import Agent

              # Your minimal reproducible code here
              ```

    - type: textarea
      id: additional-context
      attributes:
          label: Screenshots and Additional Context
          description: |
              Add screenshots, environment details, dependency versions, or other context that helps explain the issue.

          placeholder: Drag and drop screenshots here, paste links, or add additional context.

    - type: markdown
      attributes:
          value: |
              ---
              **Note:** Please help us help you! Well-documented bugs are easier to reproduce and fix. Thank you for your understanding!


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
---
name: Feature Request or Enhancement
description: Suggest a new feature or improvement for OpenHands SDK
title: '[Feature]: '
labels: [enhancement]
body:
    - type: markdown
      attributes:
          value: |
              ## Thank you for suggesting a feature! 💡

              We encourage you to open the discussion on the feature you need. You are always welcome to implement it, if you wish.

    - type: checkboxes
      attributes:
          label: Is there an existing feature request for this?
          description: Please search existing issues and feature requests before creating a new one. If found, react or comment to the duplicate issue
              instead of making a new one. <!-- TODO-openhands -->
          options:
              - label: I have searched existing issues and feature requests, and this is not a duplicate.
                required: true

    - type: textarea
      id: problem-statement
      attributes:
          label: Problem or Use Case
          description: What problem are you trying to solve? What use case would this feature enable?
          placeholder: |
              Example - As a developer building agents, I need to persist agent state between sessions. Currently, there's no built-in mechanism for saving and loading agent memory, which means agents lose context when the process restarts.
      validations:
          required: true

    - type: textarea
      id: proposed-solution
      attributes:
          label: Proposed Solution
          description: Describe your ideal solution. What should this feature do? How should it work?
          placeholder: |
              Example - Add a StateManager class that allows saving and loading agent state to/from disk or database. Provide methods like save_state(), load_state(), and clear_state(). Support multiple backend options (JSON files, SQLite, Redis, etc.).
      validations:
          required: true

    - type: textarea
      id: alternatives
      attributes:
          label: Alternatives Considered
          description: Have you considered any alternative solutions or workarounds? What are their limitations?
          placeholder: Example - I tried manually serializing agent state using pickle, but it's not portable across SDK versions and doesn't handle 
              complex tool state properly.

    - type: dropdown
      id: priority
      attributes:
          label: Priority / Severity
          description: How important is this feature to your workflow?
          options:
              - Critical - Blocking my work, no workaround available
              - High - Significant impact on productivity
              - Medium - Would improve experience
              - Low - Nice to have
          default: 2
      validations:
          required: true

    - type: dropdown
      id: scope
      attributes:
          label: Estimated Scope
          description: To the best of your knowledge, how complex do you think this feature would be to implement?
          options:
              - Small - API addition, config option, or minor change
              - Medium - New feature with moderate complexity
              - Large - Significant feature requiring architecture changes
              - Unknown - Not sure about the technical complexity
          default: 3

    - type: checkboxes
      id: feature-area
      attributes:
          label: Feature Area
          description: Which part of OpenHands SDK does this feature relate to? If you select "Other", please specify the area in the Additional 
              Context section below. <!-- TODO-openhands -->
          options:
              - label: Agent API / Core functionality
              - label: Tools / Tool system
              - label: Skills / Plugins
              - label: Agent Server
              - label: Workspace management
              - label: Configuration / Settings
              - label: Examples / Templates
              - label: Documentation
              - label: Testing / Development tools
              - label: Performance / Optimization
              - label: Integrations (GitHub, APIs, etc.)
              - label: Other

    - type: textarea
      id: technical-details
      attributes:
          label: Technical Implementation Ideas (Optional)
          description: If you have technical expertise, share implementation ideas, API suggestions, or relevant technical details.
          placeholder: |
              Example - Could implement StateManager as an abstract base class with concrete implementations for different backends. Add state_manager parameter to Agent constructor. Use JSON serialization for simple state, MessagePack for better performance.

    - type: textarea
      id: additional-context
      attributes:
          label: Additional Context
          description: Add any other context, code examples, API mockups, or references that help illustrate this feature request.
          placeholder: |
              Example code or API design:
              ```python
              from openhands.sdk import Agent, StateManager

              agent = Agent(state_manager=StateManager('file://agent_state.json'))
              agent.save_state()
              ```


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- Keep this PR as draft until it is ready for review. -->

<!-- AI/LLM agents: 

Provide evidence that the code runs properly end-to-end. Just running unit tests is NOT sufficient. Explain exactly the command that you ran, and provide evidence that the code works as expected, either in the form of log outputs or screenshots. In addition, if it is a bug fix, also run the same code before the bug fix and demonstrate that the code did NOT work before the fix to demonstrate that you were able to reproduce the problem.
-->

- [ ] A human has tested these changes.

---

## Why

<!-- Describe problem, motivation, etc.-->

## Summary

<!-- 1-3 bullets describing what changed. -->
-

## Issue Number
<!-- Required if there is a relevant issue to this PR. -->

## How to Test

<!--
Required. Share the steps for the reviewer to be able to test your PR. e.g. You can test by running `npm install` then `npm build dev`.

If you could not test this, say why.
-->

## Video/Screenshots

<!--
Provide a video or screenshots of testing your PR. e.g. you added a new feature to the gui, show us the video of you testing it successfully.

-->

## Type

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Breaking change
- [ ] Docs / chore

## Notes

<!-- Optional: config changes, rollout concerns, follow-ups, or anything reviewers should know. -->


================================================
FILE: .github/dependabot.yml
================================================
---
# Dependabot configuration for automated dependency updates
# See: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
#
# Note: Python (pip) ecosystem is not configured here because Dependabot does not
# fully support uv workspaces yet. See issue #2510 for tracking.

version: 2

updates:
  # GitHub Actions
    - package-ecosystem: github-actions
      directory: /
      schedule:
          interval: weekly
      commit-message:
          prefix: chore(deps)


================================================
FILE: .github/prompts/update-documentation.md
================================================
# Documentation Update Prompt

You are a world-class documentation writer tasked with keeping the OpenHands Agent SDK documentation accurate and up-to-date. Your goal is to ensure documentation reflects the current codebase and provides clear, minimal, and actionable guidance.

## Core Objectives

1. **Accuracy**: Ensure all documentation matches the current codebase
2. **Completeness**: Include all available tools and core components
3. **Clarity**: Keep examples simple, working, and easy to understand
4. **Navigation**: Provide source code links for all definitions

## Tasks to Perform

### 1. Codebase Analysis

- Scan `examples/` for available examples
- Scan `openhands-tools/` for all available runtime tools
- Check `openhands-sdk/openhands/tool/builtins/` for built-in tools
- Identify any new tools or removed tools since last update

### 2. Documentation Review

Review these key files for accuracy:
- `docs/architecture/overview.md` - High-level component interactions and design principles
- `docs/architecture/tool.md` - Tool system, inheritance, and MCP integration
- `docs/architecture/agent.md` - Agent architecture and execution flow
- `docs/architecture/llm.md` - LLM integration and capabilities
- `docs/architecture/conversation.md` - Conversation interface and persistence
- `docs/getting-started.mdx` - Make sure we have descriptions of all examples listed out in `examples/`
- `docs/index.md` - Overview and navigation
- `README.md` - Root project documentation

### 3. Content Updates Required

#### Architecture Diagrams

- Keep mermaid diagrams SIMPLE and READABLE across all docs/architecture/ files
- Focus on core components and relationships, not every possible class
- Include all current runtime tools: TerminalTool, FileEditorTool, TaskTrackerTool, etc.
- Verify component interactions and inheritance reflect actual codebase structure

#### Tool Documentation

For each tool, ensure:
- Accurate usage examples with `.create()` method
- Working code snippets (test them!)
- Source code links to GitHub
- Clear descriptions of functionality

#### Core Framework Classes

Verify documentation across docs/architecture/ files for:

- `Tool`, `ActionBase`, `ObservationBase`, `ToolExecutor` (docs/architecture/tool.md)
- `Agent`, `AgentBase`, system prompts (docs/architecture/agent.md)
- `LLM`, message types, provider support (docs/architecture/llm.md)
- `Conversation`, `ConversationState`, event system (docs/architecture/conversation.md)
- All built-in tools: `FinishTool`, `ThinkTool`
- All runtime tools: `TerminalTool`, `FileEditorTool`, `TaskTrackerTool`

### 4. Verification Steps

- Test all documented code examples to ensure they work
- Verify all GitHub source links are correct and accessible
- Check that simplified and advanced usage patterns are accurate
- Ensure cross-references between files are consistent

### 5. Documentation Standards

- **Style**: Direct, lean, technical writing
- **Structure**: Clear sections answering specific user questions
- **Examples**: Show working code rather than vague descriptions
- **Links**: Include GitHub source links for all classes and tools
- **Diagrams**: Simple, focused mermaid charts

## Expected Deliverables

1. Updated documentation files with current tool listings
2. Verified working code examples
3. Simplified and accurate architecture diagrams
4. Complete source code links for all definitions
5. Consistent cross-references across all documentation files

## Quality Checklist

- [ ] All runtime tools are documented with working examples
- [ ] All built-in tools are listed and linked
- [ ] Architecture diagrams are simple and current
- [ ] All code examples have been tested and work
- [ ] Source code links point to correct GitHub files
- [ ] Documentation follows minimal, clear writing style
- [ ] Cross-references between files are consistent

## Commit Message Format

If you think there's change required, please create a pull request.

```
Update documentation to reflect current codebase

- [Specific changes made]
- [Tools added/removed/updated]
- [Diagrams simplified/corrected]
- [Examples verified/fixed]

Co-authored-by: openhands <openhands@all-hands.dev>
```

Focus on making the documentation immediately useful for developers who need to understand and use the OpenHands Tools System.


================================================
FILE: .github/run-eval/ADDINGMODEL.md
================================================
# Adding Models to resolve_model_config.py

## Overview

This file (`resolve_model_config.py`) defines models available for evaluation. Models must be added here before they can be used in integration tests or evaluations.

## Critical Rules

**ONLY ADD NEW CONTENT - DO NOT MODIFY EXISTING CODE**

### What NOT to Do

1. **Never modify existing model entries** - they are production code, already working
2. **Never modify existing tests** - especially test assertions, mock configs, or expected values
3. **Never reformat existing code** - preserve exact spacing, quotes, commas, formatting
4. **Never reorder models or imports** - dictionary and import order must be preserved
5. **Never "fix" existing code** - if it's in the file and tests pass, it works
6. **Never change test assertions** - even if they "look wrong" to you
7. **Never replace real model tests with mocked tests** - weakens validation
8. **Never fix import names** - if `test_model` exists, don't change it to `check_model`

### What These Rules Prevent

**Example violations** (all found in real PRs):
- Changing `assert result[0]["id"] == "claude-sonnet-4-5-20250929"` to `"gpt-4"` ❌
- Replacing real model config tests with mocked/custom model tests ❌
- "Fixing" `from resolve_model_config import test_model` to `check_model` ❌
- Adding "Fixed incorrect assertions" without explaining what was incorrect ❌
- Claiming to "fix test issues" when tests were already passing ❌

### What TO Do

**When adding a model**:
- Add ONE new entry to the MODELS dictionary
- Add ONE new test function (follow existing pattern exactly)
- Add to feature lists in model_features.py ONLY if needed for your model
- Do not touch any other files, tests, imports, or configurations
- Test the PR branch with the integration test action.
- Add a link to the integrations test to the PR.
- If you think something is broken, it's probably not - add a comment to the PR.

## Files to Modify

1. **Always required**:
   - `.github/run-eval/resolve_model_config.py` - Add model configuration
   - `tests/github_workflows/test_resolve_model_config.py` - Add test

2. **Usually required** (if model has special characteristics):
   - `openhands-sdk/openhands/sdk/llm/utils/model_features.py` - Add to feature categories

3. **Sometimes required**:
   - `openhands-sdk/openhands/sdk/llm/utils/model_prompt_spec.py` - GPT models only (variant detection)
   - `openhands-sdk/openhands/sdk/llm/utils/verified_models.py` - Production-ready models

   > ⚠️ **When editing `verified_models.py`**: If you add a model to `VERIFIED_OPENHANDS_MODELS`,
   > you **must also** add it to its provider-specific list (e.g. `VERIFIED_ANTHROPIC_MODELS`,
   > `VERIFIED_GEMINI_MODELS`, `VERIFIED_MOONSHOT_MODELS`, etc.).
   > If no list exists for the provider yet, create one and add it to the `VERIFIED_MODELS` dict.
   > This ensures the model appears under its actual provider in the UI, not just under "openhands".

## Step 1: Add to resolve_model_config.py

Add entry to `MODELS` dictionary:

```python
"model-id": {
    "id": "model-id",  # Must match dictionary key
    "display_name": "Human Readable Name",
    "llm_config": {
        "model": "litellm_proxy/provider/model-name",
        "temperature": 0.0,  # See temperature guide below
    },
},
```

### Temperature Configuration

| Value | When to Use | Provider Requirements |
|-------|-------------|----------------------|
| `0.0` | Standard deterministic models | Most providers |
| `1.0` | Reasoning models | Kimi K2, MiniMax M2.5 |
| `None` | Use provider default | When unsure |

### Special Parameters

Add only if needed:

- **`disable_vision: True`** - Model doesn't support vision despite LiteLLM reporting it does (GLM-4.7, GLM-5)
- **`reasoning_effort: "high"`** - For OpenAI reasoning models that support this parameter
- **`max_tokens: <value>`** - To prevent hangs or control output length
- **`top_p: <value>`** - Nucleus sampling (cannot be used with `temperature` for Claude models)
- **`litellm_extra_body: {...}`** - Provider-specific parameters (e.g., `{"enable_thinking": True}`)

### Critical Rules

1. Model ID must match dictionary key
2. Model path must start with `litellm_proxy/`
3. **Claude models**: Cannot use both `temperature` and `top_p` - choose one or omit both
4. Parameters like `disable_vision` must be in `SDK_ONLY_PARAMS` constant (they're filtered before sending to LiteLLM)

## Step 2: Update model_features.py (if applicable)

Check provider documentation to determine which feature categories apply:

### REASONING_EFFORT_MODELS
Models that support `reasoning_effort` parameter:
- OpenAI: o1, o3, o4, GPT-5 series
- Anthropic: Claude Opus 4.5+, Claude Sonnet 4.6
- Google: Gemini 2.5+, Gemini 3.x series
- AWS: Nova 2 Lite

```python
REASONING_EFFORT_MODELS: list[str] = [
    "your-model-identifier",  # Add here
]
```

**Effect**: Automatically strips `temperature` and `top_p` parameters to avoid API conflicts.

### EXTENDED_THINKING_MODELS
Models with extended thinking capabilities:
- Anthropic: Claude Sonnet 4.5+, Claude Haiku 4.5

```python
EXTENDED_THINKING_MODELS: list[str] = [
    "your-model-identifier",  # Add here
]
```

**Effect**: Automatically strips `temperature` and `top_p` parameters.

### PROMPT_CACHE_MODELS
Models supporting prompt caching:
- Anthropic: Claude 3.5+, Claude 4+ series

```python
PROMPT_CACHE_MODELS: list[str] = [
    "your-model-identifier",  # Add here
]
```

### SUPPORTS_STOP_WORDS_FALSE_MODELS
Models that **do not** support stop words:
- OpenAI: o1, o3 series
- xAI: Grok-4, Grok-code-fast-1
- DeepSeek: R1 family

```python
SUPPORTS_STOP_WORDS_FALSE_MODELS: list[str] = [
    "your-model-identifier",  # Add here
]
```

### FORCE_STRING_SERIALIZER_MODELS
Models requiring string format for tool messages (not structured content):
- DeepSeek models
- GLM models  
- Groq: Kimi K2-Instruct
- OpenRouter: MiniMax

Use pattern matching:
```python
FORCE_STRING_SERIALIZER_MODELS: list[str] = [
    "deepseek",  # Matches any model with "deepseek" in name
    "groq/kimi-k2-instruct",  # Provider-prefixed
]
```

### Other Categories

- **PROMPT_CACHE_RETENTION_MODELS**: GPT-5 family, GPT-4.1
- **RESPONSES_API_MODELS**: GPT-5 family, codex-mini-latest
- **SEND_REASONING_CONTENT_MODELS**: Kimi K2 Thinking/K2.5, MiniMax-M2, DeepSeek Reasoner

See `model_features.py` for complete lists and additional documentation.

## Step 3: Add Test

**File**: `tests/github_workflows/test_resolve_model_config.py`

**Important**: 
- Python function names cannot contain hyphens. Convert model ID hyphens to underscores.
- **Do not modify any existing test functions** - only add your new one at the end of the file
- **Do not change existing imports** - use what's already there
- **Do not fix "incorrect" assertions** in other tests - they are correct

**Test template** (copy and modify for your model):

```python
def test_your_model_id_config():  # Replace hyphens with underscores in function name
    """Test that your-model-id has correct configuration."""
    model = MODELS["your-model-id"]  # Dictionary key keeps hyphens
    
    assert model["id"] == "your-model-id"
    assert model["display_name"] == "Your Model Display Name"
    assert model["llm_config"]["model"] == "litellm_proxy/provider/model-name"
    # Only add assertions for parameters YOU added in resolve_model_config.py
    # assert model["llm_config"]["temperature"] == 0.0
    # assert model["llm_config"]["disable_vision"] is True
```

**What NOT to do in tests**:
- Don't change assertions in other test functions (even if model names "look wrong")
- Don't replace real model tests with mocked tests
- Don't change `test_model` to `check_model` in imports
- Don't modify mock_models dictionaries in other tests
- Don't add "fixes" to existing tests - they work as-is

## Step 4: Update GPT Variant Detection (GPT models only)

**File**: `openhands-sdk/openhands/sdk/llm/utils/model_prompt_spec.py`

Required only if this is a GPT model needing specific prompt template.

**Order matters**: More specific patterns must come before general patterns.

```python
_MODEL_VARIANT_PATTERNS: dict[str, tuple[tuple[str, tuple[str, ...]], ...]] = {
    "openai_gpt": (
        (
            "gpt-5-codex",  # Specific variant first
            ("gpt-5-codex", "gpt-5.1-codex", "gpt-5.2-codex", "gpt-5.3-codex"),
        ),
        ("gpt-5", ("gpt-5", "gpt-5.1", "gpt-5.2")),  # General variant last
    ),
}
```

## Step 5: Run Tests Locally

```bash
# Pre-commit checks
pre-commit run --all-files

# Unit tests
pytest tests/github_workflows/test_resolve_model_config.py::test_your_model_config -v

# Manual verification
cd .github/run-eval
MODEL_IDS="your-model-id" GITHUB_OUTPUT=/tmp/output.txt python resolve_model_config.py
```

## Step 6: Create Draft PR

Push your branch and create a draft PR. Note the PR number returned - you'll need it for the integration tests.

## Step 7: Run Integration Tests

Trigger integration tests on your PR branch:

```bash
gh workflow run integration-runner.yml \
  -f model_ids=your-model-id \
  -f reason="Testing new model from PR #<pr-number>" \
  -f issue_number=<pr-number> \
  --ref your-branch-name
```

Results will be posted back to the PR as a comment.

### Expected Results

- Success rate: 100% (or 87.5% if vision test skipped)
- Duration: 5-10 minutes per model
- Tests: 8 total (basic commands, file ops, code editing, reasoning, errors, tools, context, vision)

## Step 8: Fix Issues and Rerun (if needed)

If tests fail, see [Common Issues](#common-issues) below. After fixing:

1. Push the fix: `git add . && git commit && git push`
2. Rerun integration tests with the same command from Step 7 (using the same PR number)

## Step 9: Mark PR Ready

When tests pass, mark the PR as ready for review:

```bash
gh pr ready <pr-number>
```

### Required in PR Description

```markdown
## Summary
Adds the `model-id` model to resolve_model_config.py.

## Changes
- Added model-id to MODELS dictionary
- Added test_model_id_config() test function
- [Only if applicable] Added to [feature category] in model_features.py

## Configuration
- Model ID: model-id
- Provider: Provider Name  
- Temperature: [value] - [reasoning for choice]
- [List any special parameters and why needed]

## Integration Test Results
✅ Integration tests passed: [PASTE GITHUB ACTIONS RUN URL]

[Summary table showing test results]

Fixes #[issue-number]
```

### What NOT to Include in PR Description

**Do not claim to have "fixed" things unless they were actually broken**:
- ❌ "Fixed test_model import issue" (if tests were passing, there was no issue)
- ❌ "Fixed incorrect assertions in existing tests" (they were correct)
- ❌ "Improved test coverage" (unless you actually added new test cases)
- ❌ "Cleaned up code" (you shouldn't be cleaning up anything)
- ❌ "Updated test approach" (you shouldn't be changing testing approach)

**Only describe what you actually added**:
- ✅ "Added gpt-5.3-codex model configuration"
- ✅ "Added test for gpt-5.3-codex"
- ✅ "Added gpt-5.3-codex to REASONING_EFFORT_MODELS"

## Common Issues

### Integration Tests Hang (6-8+ hours)
**Causes**:
- Missing `max_tokens` parameter
- Claude models with both `temperature` and `top_p` set
- Model not in REASONING_EFFORT_MODELS or EXTENDED_THINKING_MODELS

**Solutions**: Add `max_tokens`, remove parameter conflicts, add to appropriate feature category.

**Reference**: #2147

### Preflight Check: "Cannot specify both temperature and top_p"
**Cause**: Claude models receiving both parameters

**Solutions**:
- Remove `top_p` from llm_config if `temperature` is set
- Add model to REASONING_EFFORT_MODELS or EXTENDED_THINKING_MODELS (auto-strips both)

**Reference**: #2137, #2193

### Vision Tests Fail
**Cause**: LiteLLM reports vision support but model doesn't actually support it

**Solution**: Add `"disable_vision": True` to llm_config

**Reference**: #2110 (GLM-5), #1898 (GLM-4.7)

### Wrong Prompt Template (GPT models)
**Cause**: Model variant not detected correctly, falls through to wrong template

**Solution**: Add explicit entries to `model_prompt_spec.py` with correct pattern order

**Reference**: #2233 (GPT-5.2-codex, GPT-5.3-codex)

### SDK-Only Parameters Sent to LiteLLM
**Cause**: Parameter like `disable_vision` not in `SDK_ONLY_PARAMS` set

**Solution**: Add to `SDK_ONLY_PARAMS` in `resolve_model_config.py`

**Reference**: #2194

## Model Feature Detection Criteria

### How to Determine if Model Needs Feature Category

**Reasoning Model**:
- Check provider documentation for "reasoning", "thinking", or "o1-style" mentions
- Model exposes internal reasoning traces
- Examples: o1, o3, GPT-5, Claude Opus 4.5+, Gemini 3+

**Extended Thinking**:
- Check if model is Claude Sonnet 4.5+ or Claude Haiku 4.5
- Provider documents extended thinking capabilities

**Prompt Caching**:
- Check provider documentation for prompt caching support
- Anthropic Claude 3.5+ and 4+ series support this

**Vision Support**:
- Check provider documentation (don't rely solely on LiteLLM)
- If LiteLLM reports vision but provider docs say text-only, add `disable_vision: True`

**Stop Words**:
- Most models support stop words
- o1/o3 series, some Grok models, DeepSeek R1 do not

**String Serialization**:
- If tool message errors mention "Input should be a valid string"
- DeepSeek, GLM, some provider-specific models need this

## Reference

- Recent model additions: #2102, #2153, #2207, #2233, #2269
- Common issues: #2147 (hangs), #2137 (parameters), #2110 (vision), #2233 (variants), #2193 (preflight)
- Integration test workflow: `.github/workflows/integration-runner.yml`
- Integration tests can be triggered via: `gh workflow run integration-runner.yml --ref <branch>`


================================================
FILE: .github/run-eval/AGENTS.md
================================================
# Model Configuration for OpenHands SDK

See the [project root AGENTS.md](../../AGENTS.md) for repository-wide policies and workflows.

This directory contains model configuration and evaluation setup for the OpenHands SDK.

## Key Files

- **`resolve_model_config.py`** - Model registry and configuration
  - Defines all models available for evaluation
  - Contains model IDs, display names, LiteLLM paths, and parameters
  - Used by integration tests and evaluation workflows

- **`tests/github_workflows/test_resolve_model_config.py`** - Tests for model configurations
  - Validates model entries are correctly structured
  - Tests preflight check functionality

- **`ADDINGMODEL.md`** - Detailed guide for adding models (see below)

## Common Tasks

### Adding a New Model

**→ See [ADDINGMODEL.md](./ADDINGMODEL.md) for complete instructions**

This is the most common task in this directory. The guide covers:
- Required steps and files to modify
- Model feature categories and when to use them
- Integration testing requirements
- Common issues and troubleshooting
- Critical rules to prevent breaking existing models

### Debugging Model Issues

If a model is failing in evaluations:
1. Check the model configuration in `resolve_model_config.py`
2. Review parameter compatibility (especially `temperature` + `top_p` for Claude)
3. Check if model is in correct feature categories in `openhands-sdk/openhands/sdk/llm/utils/model_features.py`
4. Run preflight check: `MODEL_IDS="model-id" python resolve_model_config.py`

### Updating Existing Models

**Warning**: Only update existing models if there's a confirmed issue. Working configurations should not be changed.

If you must update:
1. Document why the change is needed (link to issue/PR showing the problem)
2. Test thoroughly before and after the change
3. Run integration tests to verify no regressions

## Directory Purpose

This directory bridges model definitions with the evaluation system:
- Models defined here are available for integration tests
- Configuration includes LiteLLM routing and SDK-specific parameters
- Preflight checks validate model accessibility before expensive evaluation runs
- Tests ensure all models are correctly structured and resolvable


================================================
FILE: .github/run-eval/resolve_model_config.py
================================================
#!/usr/bin/env python3
"""
Resolve model IDs to full model configurations and verify model availability.

Reads:
- MODEL_IDS: comma-separated model IDs
- LLM_API_KEY: API key for litellm_proxy (optional, for preflight check)
- LLM_BASE_URL: Base URL for litellm_proxy (optional, defaults to eval proxy)
- SKIP_PREFLIGHT: Set to 'true' to skip the preflight LLM check

Outputs to GITHUB_OUTPUT:
- models_json: JSON array of full model configs with display names
"""

import json
import os
import signal
import sys
import time
from typing import Any


def _sigterm_handler(signum: int, _frame: object) -> None:
    """Handle SIGTERM/SIGALRM with a diagnostic message instead of silent death."""
    sig_name = signal.Signals(signum).name
    print(
        f"\nERROR: Process received {sig_name} during preflight check.\n"
        "This usually means the LiteLLM proxy is unreachable or hanging.\n"
        f"LLM_BASE_URL: {os.environ.get('LLM_BASE_URL', '(not set)')}\n",
        file=sys.stderr,
        flush=True,
    )
    sys.exit(1)


signal.signal(signal.SIGTERM, _sigterm_handler)
if sigalrm := getattr(signal, "SIGALRM", None):
    signal.signal(sigalrm, _sigterm_handler)


# SDK-specific parameters that should not be passed to litellm.
# These parameters are used by the SDK's LLM wrapper but are not part of litellm's API.
# Keep this list in sync with SDK LLM config parameters that are SDK-internal.
SDK_ONLY_PARAMS = {"disable_vision"}


# Model configurations dictionary
MODELS = {
    "claude-sonnet-4-5-20250929": {
        "id": "claude-sonnet-4-5-20250929",
        "display_name": "Claude Sonnet 4.5",
        "llm_config": {
            "model": "litellm_proxy/claude-sonnet-4-5-20250929",
            "temperature": 0.0,
        },
    },
    "kimi-k2-thinking": {
        "id": "kimi-k2-thinking",
        "display_name": "Kimi K2 Thinking",
        "llm_config": {
            "model": "litellm_proxy/moonshot/kimi-k2-thinking",
            "temperature": 1.0,
        },
    },
    # https://www.kimi.com/blog/kimi-k2-5.html
    "kimi-k2.5": {
        "id": "kimi-k2.5",
        "display_name": "Kimi K2.5",
        "llm_config": {
            "model": "litellm_proxy/moonshot/kimi-k2.5",
            "temperature": 1.0,
            "top_p": 0.95,
        },
    },
    # https://www.kimi.com/blog/kimi-k2-6
    "kimi-k2.6": {
        "id": "kimi-k2.6",
        "display_name": "Kimi K2.6",
        "llm_config": {
            "model": "litellm_proxy/moonshot/kimi-k2.6",
            "temperature": 1.0,
        },
    },
    # https://www.alibabacloud.com/help/en/model-studio/deep-thinking
    "qwen3-max-thinking": {
        "id": "qwen3-max-thinking",
        "display_name": "Qwen3 Max Thinking",
        "llm_config": {
            "model": "litellm_proxy/dashscope/qwen3-max-2026-01-23",
            "litellm_extra_body": {"enable_thinking": True},
        },
    },
    "qwen3.5-flash": {
        "id": "qwen3.5-flash",
        "display_name": "Qwen3.5 Flash",
        "llm_config": {
            "model": "litellm_proxy/dashscope/qwen3.5-flash-2026-02-23",
            "temperature": 0.0,
        },
    },
    "qwen3.6-plus": {
        "id": "qwen3.6-plus",
        "display_name": "Qwen3.6 Plus",
        "llm_config": {
            "model": "litellm_proxy/dashscope/qwen3.6-plus",
            "temperature": 0.0,
        },
    },
    "claude-4.5-opus": {
        "id": "claude-4.5-opus",
        "display_name": "Claude 4.5 Opus",
        "llm_config": {
            "model": "litellm_proxy/anthropic/claude-opus-4-5-20251101",
            "temperature": 0.0,
        },
    },
    "claude-4.6-opus": {
        "id": "claude-4.6-opus",
        "display_name": "Claude 4.6 Opus",
        "llm_config": {
            "model": "litellm_proxy/anthropic/claude-opus-4-6",
            "temperature": 0.0,
        },
    },
    "claude-opus-4-7": {
        "id": "claude-opus-4-7",
        "display_name": "Claude Opus 4.7",
        "llm_config": {
            "model": "litellm_proxy/anthropic/claude-opus-4-7",
        },
    },
    "claude-sonnet-4-6": {
        "id": "claude-sonnet-4-6",
        "display_name": "Claude Sonnet 4.6",
        "llm_config": {
            "model": "litellm_proxy/anthropic/claude-sonnet-4-6",
            "temperature": 0.0,
        },
    },
    "gemini-3-flash": {
        "id": "gemini-3-flash",
        "display_name": "Gemini 3 Flash",
        "llm_config": {
            "model": "litellm_proxy/gemini-3-flash-preview",
            "temperature": 0.0,
        },
    },
    "gemini-3.1-pro": {
        "id": "gemini-3.1-pro",
        "display_name": "Gemini 3.1 Pro",
        "llm_config": {
            "model": "litellm_proxy/gemini-3.1-pro-preview",
            "temperature": 0.0,
        },
    },
    "gpt-5.2": {
        "id": "gpt-5.2",
        "display_name": "GPT-5.2",
        "llm_config": {"model": "litellm_proxy/openai/gpt-5.2-2025-12-11"},
    },
    "gpt-5.2-codex": {
        "id": "gpt-5.2-codex",
        "display_name": "GPT-5.2 Codex",
        "llm_config": {"model": "litellm_proxy/gpt-5.2-codex"},
    },
    "gpt-5-3-codex": {
        "id": "gpt-5-3-codex",
        "display_name": "GPT-5.3 Codex",
        "llm_config": {"model": "litellm_proxy/gpt-5-3-codex"},
    },
    "gpt-5.2-high-reasoning": {
        "id": "gpt-5.2-high-reasoning",
        "display_name": "GPT-5.2 High Reasoning",
        "llm_config": {
            "model": "litellm_proxy/openai/gpt-5.2-2025-12-11",
            "reasoning_effort": "high",
        },
    },
    "gpt-5.4": {
        "id": "gpt-5.4",
        "display_name": "GPT-5.4",
        "llm_config": {
            "model": "litellm_proxy/openai/gpt-5.4",
            "reasoning_effort": "high",
        },
    },
    "gpt-5.5": {
        "id": "gpt-5.5",
        "display_name": "GPT-5.5",
        "llm_config": {
            "model": "litellm_proxy/openai/gpt-5.5",
            "reasoning_effort": "high",
        },
    },
    "minimax-m2": {
        "id": "minimax-m2",
        "display_name": "MiniMax M2",
        "llm_config": {
            "model": "litellm_proxy/minimax/minimax-m2",
            "temperature": 0.0,
        },
    },
    "minimax-m2.5": {
        "id": "minimax-m2.5",
        "display_name": "MiniMax M2.5",
        "llm_config": {
            "model": "litellm_proxy/minimax/MiniMax-M2.5",
            "temperature": 1.0,
            "top_p": 0.95,
        },
    },
    "minimax-m2.1": {
        "id": "minimax-m2.1",
        "display_name": "MiniMax M2.1",
        "llm_config": {
            "model": "litellm_proxy/minimax/MiniMax-M2.1",
            "temperature": 0.0,
        },
    },
    "minimax-m2.7": {
        "id": "minimax-m2.7",
        "display_name": "MiniMax M2.7",
        "llm_config": {
            "model": "litellm_proxy/minimax/MiniMax-M2.7",
            "temperature": 1.0,
            "top_p": 0.95,
        },
    },
    "deepseek-v3.2-reasoner": {
        "id": "deepseek-v3.2-reasoner",
        "display_name": "DeepSeek V3.2 Reasoner",
        "llm_config": {"model": "litellm_proxy/deepseek/deepseek-reasoner"},
    },
    # https://api-docs.deepseek.com/news/news260424
    "deepseek-v4-pro": {
        "id": "deepseek-v4-pro",
        "display_name": "DeepSeek V4 Pro",
        "llm_config": {"model": "litellm_proxy/deepseek/deepseek-v4-pro"},
    },
    "deepseek-v4-flash": {
        "id": "deepseek-v4-flash",
        "display_name": "DeepSeek V4 Flash",
        "llm_config": {"model": "litellm_proxy/deepseek/deepseek-v4-flash"},
    },
    "qwen-3-coder": {
        "id": "qwen-3-coder",
        "display_name": "Qwen 3 Coder",
        "llm_config": {
            "model": "litellm_proxy/fireworks_ai/qwen3-coder-480b-a35b-instruct",
            "temperature": 0.0,
        },
    },
    "nemotron-3-nano-30b": {
        "id": "nemotron-3-nano-30b",
        "display_name": "NVIDIA Nemotron 3 Nano 30B",
        "llm_config": {
            "model": "litellm_proxy/openai/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8",
            "temperature": 0.0,
        },
    },
    "glm-4.7": {
        "id": "glm-4.7",
        "display_name": "GLM-4.7",
        "llm_config": {
            "model": "litellm_proxy/openrouter/z-ai/glm-4.7",
            "temperature": 0.0,
            # OpenRouter glm-4.7 is text-only despite LiteLLM reporting vision support
            "disable_vision": True,
        },
    },
    "glm-5": {
        "id": "glm-5",
        "display_name": "GLM-5",
        "llm_config": {
            "model": "litellm_proxy/openrouter/z-ai/glm-5",
            "temperature": 0.0,
            # OpenRouter glm-5 is text-only despite LiteLLM reporting vision support
            "disable_vision": True,
        },
    },
    "glm-5.1": {
        "id": "glm-5.1",
        "display_name": "GLM-5.1",
        "llm_config": {
            "model": "litellm_proxy/openrouter/z-ai/glm-5.1",
            "temperature": 0.0,
            # OpenRouter glm-5.1 is text-only despite LiteLLM reporting vision support
            "disable_vision": True,
        },
    },
    "qwen3-coder-next": {
        "id": "qwen3-coder-next",
        "display_name": "Qwen3 Coder Next",
        "llm_config": {
            "model": "litellm_proxy/openrouter/qwen/qwen3-coder-next",
            "temperature": 0.0,
        },
    },
    "qwen3-coder-30b-a3b-instruct": {
        "id": "qwen3-coder-30b-a3b-instruct",
        "display_name": "Qwen3 Coder 30B A3B Instruct",
        "llm_config": {
            "model": "litellm_proxy/Qwen3-Coder-30B-A3B-Instruct",
            "temperature": 0.0,
        },
    },
    "gpt-oss-20b": {
        "id": "gpt-oss-20b",
        "display_name": "GPT OSS 20B",
        "llm_config": {
            "model": "litellm_proxy/gpt-oss-20b",
            "temperature": 0.0,
        },
    },
    "nemotron-3-super-120b-a12b": {
        "id": "nemotron-3-super-120b-a12b",
        "display_name": "NVIDIA Nemotron-3 Super 120B",
        "llm_config": {
            "model": "litellm_proxy/nvidia/nemotron-3-super-120b-a12b",
            "temperature": 0.0,
        },
    },
    "converse-nemotron-super-3-120b": {
        "id": "converse-nemotron-super-3-120b",
        "display_name": "NVIDIA Converse Nemotron Super 3 120B",
        "llm_config": {
            "model": "litellm_proxy/converse-nemotron-super-3-120b",
            "temperature": 0.0,
        },
    },
    "trinity-large-thinking": {
        "id": "trinity-large-thinking",
        "display_name": "Trinity Large Thinking",
        "llm_config": {
            "model": "litellm_proxy/trinity-large-thinking",
            "temperature": 1.0,
            "top_p": 0.95,
        },
    },
}


def error_exit(msg: str, exit_code: int = 1) -> None:
    """Print error message and exit."""
    print(f"ERROR: {msg}", file=sys.stderr)
    sys.exit(exit_code)


def get_required_env(key: str) -> str:
    """Get required environment variable or exit with error."""
    value = os.environ.get(key)
    if not value:
        error_exit(f"{key} not set")
    return value


def find_models_by_id(model_ids: list[str]) -> list[dict]:
    """Find models by ID. Fails fast on missing ID.

    Args:
        model_ids: List of model IDs to find

    Returns:
        List of model dictionaries matching the IDs

    Raises:
        SystemExit: If any model ID is not found
    """
    resolved = []
    for model_id in model_ids:
        if model_id not in MODELS:
            available = ", ".join(sorted(MODELS.keys()))
            error_exit(
                f"Model ID '{model_id}' not found. Available models: {available}"
            )
        resolved.append(MODELS[model_id])
    return resolved


def check_model(
    model_config: dict[str, Any],
    api_key: str,
    base_url: str,
    timeout: int = 60,
) -> tuple[bool, str]:
    """Check a single model with a simple completion request using litellm.

    Args:
        model_config: Model configuration dict with 'llm_config' key
        api_key: API key for authentication
        base_url: Base URL for the LLM proxy
        timeout: Request timeout in seconds

    Returns:
        Tuple of (success: bool, message: str)
    """
    import litellm

    llm_config = model_config.get("llm_config", {})
    model_name = llm_config.get("model", "unknown")
    display_name = model_config.get("display_name", model_name)

    try:
        # Build kwargs from llm_config, excluding 'model' and SDK-specific params
        kwargs = {
            k: v
            for k, v in llm_config.items()
            if k != "model" and k not in SDK_ONLY_PARAMS
        }

        # Use simple arithmetic prompt that works reliably across all models
        # max_tokens=100 provides enough room for models to respond
        # (some need >10 tokens)
        response = litellm.completion(
            model=model_name,
            messages=[{"role": "user", "content": "1+1="}],
            max_tokens=100,
            api_key=api_key,
            base_url=base_url,
            timeout=timeout,
            **kwargs,
        )

        response_content = (
            response.choices[0].message.content if response.choices else None
        )
        reasoning_content = (
            getattr(response.choices[0].message, "reasoning_content", None)
            if response.choices
            else None
        )

        if response_content or reasoning_content:
            return True, f"✓ {display_name}: OK"
        else:
            # Check if there's any other data in the response for diagnostics
            finish_reason = (
                response.choices[0].finish_reason if response.choices else None
            )
            usage = getattr(response, "usage", None)
            return (
                False,
                (
                    f"✗ {display_name}: Empty response "
                    f"(finish_reason={finish_reason}, usage={usage})"
                ),
            )

    except litellm.exceptions.Timeout:
        return False, f"✗ {display_name}: Request timed out after {timeout}s"
    except litellm.exceptions.APIConnectionError as e:
        return False, f"✗ {display_name}: Connection error - {e}"
    except litellm.exceptions.BadRequestError as e:
        return False, f"✗ {display_name}: Bad request - {e}"
    except litellm.exceptions.NotFoundError as e:
        return False, f"✗ {display_name}: Model not found - {e}"
    except Exception as e:
        return False, f"✗ {display_name}: {type(e).__name__} - {e}"


# Alias for backward compatibility with tests
test_model = check_model


def _check_proxy_reachable(
    base_url: str, api_key: str | None = None, timeout: int = 10
) -> tuple[bool, str]:
    """Quick health check: can we reach the proxy at all?

    Uses /v1/models (standard OpenAI-compatible endpoint) which works with
    any valid API key. The /health endpoint requires admin-level access on
    some LiteLLM configurations.
    """
    import urllib.error
    import urllib.request

    models_url = f"{base_url.rstrip('/')}/v1/models"
    try:
        req = urllib.request.Request(models_url, method="GET")
        if api_key:
            req.add_header("Authorization", f"Bearer {api_key}")
        urllib.request.urlopen(req, timeout=timeout)
        return True, f"Proxy reachable at {base_url}"
    except urllib.error.URLError as e:
        return False, f"Cannot reach proxy at {base_url}: {e.reason}"
    except Exception as e:
        return False, f"Cannot reach proxy at {base_url}: {type(e).__name__}: {e}"


def run_preflight_check(models: list[dict[str, Any]]) -> bool:
    """Run preflight LLM check for all models.

    Args:
        models: List of model configurations to test

    Returns:
        True if all models passed, False otherwise
    """
    api_key = os.environ.get("LLM_API_KEY")
    base_url = os.environ.get("LLM_BASE_URL", "https://llm-proxy.eval.all-hands.dev")
    skip_preflight = os.environ.get("SKIP_PREFLIGHT", "").lower() == "true"

    if skip_preflight:
        print("Preflight check: SKIPPED (SKIP_PREFLIGHT=true)")
        return True

    if not api_key:
        print("Preflight check: SKIPPED (LLM_API_KEY not set)")
        return True

    # Quick connectivity check before trying expensive model completions
    print(f"\nChecking proxy connectivity: {base_url}", flush=True)
    reachable, msg = _check_proxy_reachable(base_url, api_key=api_key)
    if not reachable:
        print(f"✗ {msg}", file=sys.stderr, flush=True)
        print(
            "\nThe LiteLLM proxy appears to be down or unreachable.\n"
            "Set SKIP_PREFLIGHT=true to bypass this check.",
            file=sys.stderr,
            flush=True,
        )
        return False
    print(f"✓ {msg}", flush=True)

    print(f"\nPreflight LLM check for {len(models)} model(s)...", flush=True)
    print("-" * 50, flush=True)

    all_passed = True
    for model_config in models:
        display_name = model_config.get("display_name", "unknown")
        print(f"  Checking {display_name}...", end=" ", flush=True)
        t0 = time.monotonic()
        success, message = check_model(model_config, api_key, base_url)
        elapsed = time.monotonic() - t0
        print(f"({elapsed:.1f}s)", flush=True)
        print(f"  {message}", flush=True)
        if not success:
            all_passed = False

    print("-" * 50, flush=True)

    if all_passed:
        print(f"✓ All {len(models)} model(s) passed preflight check\n", flush=True)
    else:
        print("✗ Some models failed preflight check", flush=True)
        print("Evaluation aborted to avoid wasting compute resources.\n", flush=True)

    return all_passed


def main() -> None:
    model_ids_str = get_required_env("MODEL_IDS")
    github_output = get_required_env("GITHUB_OUTPUT")

    # Parse requested model IDs
    model_ids = [mid.strip() for mid in model_ids_str.split(",") if mid.strip()]

    # Resolve model configs
    resolved = find_models_by_id(model_ids)
    print(f"Resolved {len(resolved)} model(s): {', '.join(model_ids)}", flush=True)

    # Run preflight check
    if not run_preflight_check(resolved):
        error_exit("Preflight LLM check failed")

    # Output as JSON
    models_json = json.dumps(resolved, separators=(",", ":"))
    with open(github_output, "a", encoding="utf-8") as f:
        f.write(f"models_json={models_json}\n")


if __name__ == "__main__":
    main()


================================================
FILE: .github/run-eval/validate_sdk_ref.py
================================================
#!/usr/bin/env python3
"""
Validate SDK reference for semantic versioning.

This script validates that the SDK reference is a semantic version (e.g., v1.0.0, 1.0.0)
unless the allow_unreleased_branches flag is set.

Environment variables:
- SDK_REF: The SDK reference to validate
- ALLOW_UNRELEASED_BRANCHES: If 'true', bypass semantic version validation

Exit codes:
- 0: Validation passed
- 1: Validation failed
"""

import os
import re
import subprocess
import sys


# Semantic version pattern: optional 'v' prefix, followed by MAJOR.MINOR.PATCH
# Optionally allows pre-release (-alpha.1, -beta.2, -rc.1) and build metadata
SEMVER_PATTERN = re.compile(
    r"^v?"  # Optional 'v' prefix
    r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"  # MAJOR.MINOR.PATCH
    r"(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"  # Pre-release
    r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"  # More pre-release
    r"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"  # Build metadata
)
COMMIT_SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{7,40}$")
BRANCH_EXAMPLES = "'main', 'feature/foo', or 'release/1.2.3'"


def is_semantic_version(ref: str) -> bool:
    """Check if the given reference is a valid semantic version."""
    return bool(SEMVER_PATTERN.match(ref))


def is_commit_sha(ref: str) -> bool:
    """Check if the given reference is a git commit SHA."""
    return bool(COMMIT_SHA_PATTERN.fullmatch(ref))


def is_valid_branch_name(ref: str) -> bool:
    """Check if the given reference is a valid git branch name."""
    return (
        subprocess.run(
            ["git", "check-ref-format", "--branch", ref],
            check=False,
            capture_output=True,
            text=True,
        ).returncode
        == 0
    )


def validate_branch_name(branch_name: str, input_name: str) -> tuple[bool, str]:
    """Validate a workflow branch input against git branch naming rules."""
    if is_valid_branch_name(branch_name):
        return True, f"Valid {input_name}: {branch_name}"

    return False, (
        f"{input_name} '{branch_name}' is not a valid git branch name. "
        f"Common GitHub/GitLab/Bitbucket branch names look like {BRANCH_EXAMPLES}."
    )


def validate_sdk_ref(sdk_ref: str, allow_unreleased: bool) -> tuple[bool, str]:
    """Validate the SDK reference."""
    if is_semantic_version(sdk_ref):
        return True, f"Valid semantic version: {sdk_ref}"

    if allow_unreleased and (is_commit_sha(sdk_ref) or is_valid_branch_name(sdk_ref)):
        return True, f"Valid unreleased git ref: {sdk_ref}"

    if allow_unreleased:
        return False, (
            f"SDK reference '{sdk_ref}' is not a valid semantic version, commit SHA, "
            "or git branch name. Common GitHub/GitLab/Bitbucket branch names look "
            f"like {BRANCH_EXAMPLES}."
        )

    return False, (
        f"SDK reference '{sdk_ref}' is not a valid semantic version. "
        "Expected format: v1.0.0 or 1.0.0 (with optional pre-release like -alpha.1). "
        "To use unreleased branches, check 'Allow unreleased branches'."
    )


def main() -> None:
    sdk_ref = os.environ.get("SDK_REF", "")
    allow_unreleased_str = os.environ.get("ALLOW_UNRELEASED_BRANCHES", "false")
    eval_branch = os.environ.get("EVAL_BRANCH")
    benchmarks_branch = os.environ.get("BENCHMARKS_BRANCH")

    if not sdk_ref:
        print("ERROR: SDK_REF environment variable is not set", file=sys.stderr)
        sys.exit(1)

    allow_unreleased = allow_unreleased_str.lower() == "true"

    validations = [
        validate_sdk_ref(sdk_ref, allow_unreleased),
    ]
    if eval_branch:
        validations.append(validate_branch_name(eval_branch, "EVAL_BRANCH"))
    if benchmarks_branch:
        validations.append(validate_branch_name(benchmarks_branch, "BENCHMARKS_BRANCH"))

    for is_valid, message in validations:
        stream = sys.stdout if is_valid else sys.stderr
        print(("✓" if is_valid else "✗") + f" {message}", file=stream)
        if not is_valid:
            sys.exit(1)


if __name__ == "__main__":
    main()


================================================
FILE: .github/scripts/check_agent_server_rest_api_breakage.py
================================================
#!/usr/bin/env python3
"""REST API breakage detection for openhands-agent-server using oasdiff.

This script compares the current OpenAPI schema for the public agent-server REST API
(the `/api/**` surface) against an already-published release. The baseline version is
selected from PyPI, but the baseline schema is generated from the matching git tag
under the current workspace's locked dependency set. This keeps the comparison
focused on API changes in our code, not schema drift from newer FastAPI/Pydantic
releases.

The deprecation note it recognizes intentionally matches the phrasing used by the
Python deprecation checks, for example:

    Deprecated since v1.14.0 and scheduled for removal in v1.19.0.

Policies enforced:

1) REST deprecations must use FastAPI/OpenAPI metadata
   - FastAPI route handlers must not use `openhands.sdk.utils.deprecation.deprecated`.
   - Endpoints documented as deprecated in their OpenAPI description must also be
     marked `deprecated: true` in the generated schema.

2) Deprecation runway before removal
   - If a REST operation (path + HTTP method) or schema property is removed, it
     must have been marked `deprecated: true` in the baseline release and its
     OpenAPI description must declare a scheduled removal version that has been
     reached by the current package version.

3) Additive request/response oneOf/anyOf expansion is allowed
   - Adding new members to ``oneOf`` or ``anyOf`` discriminated unions in request
     or response schemas is a normal evolution for extensible APIs. Clients MUST
     handle unknown discriminator values gracefully (skip/ignore).
   - oasdiff can report union widening as ERR plus secondary type-change or
     property-removal artifacts for fields that still exist on one union member;
     this script downgrades those artifacts to informational notices.

4) No in-place contract breakage
   - Breaking REST contract changes that are not removals of previously-deprecated
     operations/properties or additive oneOf expansions fail the check. REST clients
     need 5 minor releases of runway, so incompatible replacements must ship
     additively or behind a versioned contract until the scheduled removal version.

If the baseline release schema can't be generated (e.g., missing tag / repo issues),
the script emits a warning and exits successfully to avoid flaky CI.
"""

from __future__ import annotations

import ast
import json
import re
import subprocess
import sys
import tempfile
import tomllib
import urllib.request
from pathlib import Path

from packaging import version as pkg_version


REPO_ROOT = Path(__file__).resolve().parents[2]
AGENT_SERVER_PYPROJECT = REPO_ROOT / "openhands-agent-server" / "pyproject.toml"
PYPI_DISTRIBUTION = "openhands-agent-server"
# Keep this in sync with REST_ROUTE_DEPRECATION_RE in check_deprecations.py so
# the REST breakage and deprecation checks recognize the same wording.
REST_ROUTE_DEPRECATION_RE = re.compile(
    r"Deprecated since v(?P<deprecated>[0-9A-Za-z.+-]+)\s+"
    r"and scheduled for removal in v(?P<removed>[0-9A-Za-z.+-]+)\.?",
    re.IGNORECASE,
)
HTTP_METHODS = {
    "get",
    "put",
    "post",
    "delete",
    "patch",
    "options",
    "head",
    "trace",
}
PUBLIC_REST_PATH_PREFIX = "/api/"
ROUTE_DECORATOR_NAMES = HTTP_METHODS | {"api_route"}
OPENAPI_PROGRAM = """
import json
import sys
from pathlib import Path

source_tree = Path(sys.argv[1])
sys.path = [
    str(source_tree / "openhands-agent-server"),
    str(source_tree / "openhands-sdk"),
    str(source_tree / "openhands-tools"),
    str(source_tree / "openhands-workspace"),
] + sys.path

from openhands.agent_server.api import create_app

print(json.dumps(create_app().openapi()))
"""


def _read_version_from_pyproject(pyproject: Path) -> str:
    data = tomllib.loads(pyproject.read_text())
    try:
        return str(data["project"]["version"])
    except KeyError as exc:  # pragma: no cover
        raise SystemExit(
            f"Unable to determine project version from {pyproject}"
        ) from exc


def _fetch_pypi_metadata(distribution: str) -> dict:
    req = urllib.request.Request(
        url=f"https://pypi.org/pypi/{distribution}/json",
        headers={"User-Agent": "openhands-agent-server-openapi-check/1.0"},
        method="GET",
    )
    with urllib.request.urlopen(req, timeout=10) as response:
        return json.load(response)


def _get_baseline_version(distribution: str, current: str) -> str | None:
    try:
        meta = _fetch_pypi_metadata(distribution)
    except Exception as exc:  # pragma: no cover
        print(
            f"::warning title={distribution} REST API::Failed to fetch PyPI metadata: "
            f"{exc}"
        )
        return None

    releases = list(meta.get("releases", {}).keys())
    if not releases:
        return None

    if current in releases:
        return current

    current_parsed = pkg_version.parse(current)
    older = [rv for rv in releases if pkg_version.parse(rv) < current_parsed]
    if not older:
        return None

    return max(older, key=pkg_version.parse)


def _generate_openapi_from_source_tree(source_tree: Path, label: str) -> dict | None:
    try:
        result = subprocess.run(
            [sys.executable, "-c", OPENAPI_PROGRAM, str(source_tree)],
            check=True,
            capture_output=True,
            text=True,
            cwd=source_tree,
        )
        return json.loads(result.stdout)
    except subprocess.CalledProcessError as exc:
        output = (exc.stdout or "") + ("\n" + exc.stderr if exc.stderr else "")
        excerpt = output.strip()[-1000:]
        print(
            f"::warning title={PYPI_DISTRIBUTION} REST API::Failed to generate "
            f"OpenAPI schema for {label}: {exc}\n{excerpt}"
        )
        return None
    except Exception as exc:
        print(
            f"::warning title={PYPI_DISTRIBUTION} REST API::Failed to generate "
            f"OpenAPI schema for {label}: {exc}"
        )
        return None


def _generate_current_openapi() -> dict | None:
    return _generate_openapi_from_source_tree(REPO_ROOT, "current workspace")


def _generate_openapi_for_git_ref(git_ref: str) -> dict | None:
    with tempfile.TemporaryDirectory(prefix="agent-server-openapi-") as tmp:
        source_tree = Path(tmp)

        try:
            archive = subprocess.run(
                ["git", "-C", str(REPO_ROOT), "archive", git_ref],
                check=True,
                capture_output=True,
            )
            subprocess.run(
                ["tar", "-x", "-C", str(source_tree)],
                check=True,
                input=archive.stdout,
                capture_output=True,
            )
        except subprocess.CalledProcessError as exc:
            output = (exc.stdout or b"") + (b"\n" + exc.stderr if exc.stderr else b"")
            excerpt = output.decode(errors="replace").strip()[-1000:]
            print(
                f"::warning title={PYPI_DISTRIBUTION} REST API::Failed to extract "
                f"source for {git_ref}: {exc}\n{excerpt}"
            )
            return None

        return _generate_openapi_from_source_tree(source_tree, git_ref)


def _dotted_name(node: ast.AST) -> str | None:
    if isinstance(node, ast.Name):
        return node.id
    if isinstance(node, ast.Attribute):
        prefix = _dotted_name(node.value)
        if prefix is None:
            return None
        return f"{prefix}.{node.attr}"
    return None


def _find_sdk_deprecated_fastapi_routes_in_file(
    file_path: Path, repo_root: Path
) -> list[str]:
    tree = ast.parse(file_path.read_text(), filename=str(file_path))

    deprecated_names: set[str] = set()
    deprecation_module_names: set[str] = set()

    for node in tree.body:
        if isinstance(node, ast.ImportFrom):
            if node.module == "openhands.sdk.utils.deprecation":
                for alias in node.names:
                    if alias.name == "deprecated":
                        deprecated_names.add(alias.asname or alias.name)
            elif node.module == "openhands.sdk.utils":
                for alias in node.names:
                    if alias.name == "deprecation":
                        deprecation_module_names.add(alias.asname or alias.name)
        elif isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name == "openhands.sdk.utils.deprecation":
                    deprecation_module_names.add(alias.asname or alias.name)

    errors: list[str] = []
    for node in ast.walk(tree):
        if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
            continue

        has_route_decorator = False
        uses_sdk_deprecated = False

        for decorator in node.decorator_list:
            if not isinstance(decorator, ast.Call):
                continue

            dotted_name = _dotted_name(decorator.func)
            if (
                isinstance(decorator.func, ast.Attribute)
                and decorator.func.attr in ROUTE_DECORATOR_NAMES
            ):
                has_route_decorator = True

            if dotted_name in deprecated_names or (
                dotted_name == "openhands.sdk.utils.deprecation.deprecated"
            ):
                uses_sdk_deprecated = True
                continue

            if (
                isinstance(decorator.func, ast.Attribute)
                and decorator.func.attr == "deprecated"
            ):
                base_name = _dotted_name(decorator.func.value)
                if base_name in deprecation_module_names or (
                    base_name == "openhands.sdk.utils.deprecation"
                ):
                    uses_sdk_deprecated = True

        if has_route_decorator and uses_sdk_deprecated:
            rel_path = file_path.relative_to(repo_root).as_posix()
            errors.append(
                f"{rel_path}:{node.lineno} FastAPI route `{node.name}` uses "
                "openhands.sdk.utils.deprecation.deprecated; use the route "
                "decorator's deprecated=True flag instead."
            )

    return errors


def _find_sdk_deprecated_fastapi_routes(repo_root: Path) -> list[str]:
    app_root = repo_root / "openhands-agent-server" / "openhands" / "agent_server"
    errors: list[str] = []

    for file_path in sorted(app_root.rglob("*.py")):
        errors.extend(_find_sdk_deprecated_fastapi_routes_in_file(file_path, repo_root))

    return errors


def _filter_public_rest_openapi(schema: dict) -> dict:
    filtered_schema = dict(schema)
    filtered_schema["paths"] = {
        path: path_item
        for path, path_item in schema.get("paths", {}).items()
        if path == PUBLIC_REST_PATH_PREFIX.rstrip("/")
        or path.startswith(PUBLIC_REST_PATH_PREFIX)
    }
    return filtered_schema


def _find_deprecation_policy_errors(schema: dict) -> list[str]:
    errors: list[str] = []

    for path, path_item in schema.get("paths", {}).items():
        if not isinstance(path_item, dict):
            continue

        for method, operation in path_item.items():
            if method not in HTTP_METHODS or not isinstance(operation, dict):
                continue

            description = operation.get("description") or ""
            if "deprecated since" not in description.lower():
                continue

            if operation.get("deprecated") is True:
                continue

            errors.append(
                f"{method.upper()} {path} documents deprecation in its "
                "description but is not marked deprecated=true in OpenAPI."
            )

    return errors


def _parse_openapi_deprecation_description(
    description: str | None,
) -> tuple[str, str] | None:
    """Extract ``(deprecated_in, removed_in)`` from an OpenAPI description.

    The accepted wording intentionally matches ``check_deprecations.py`` so both
    CI checks recognize the same note, for example:

        Deprecated since v1.14.0 and scheduled for removal in v1.19.0.
    """
    if not description:
        return None

    match = REST_ROUTE_DEPRECATION_RE.search(" ".join(description.split()))
    if match is None:
        return None

    return match.group("deprecated").rstrip("."), match.group("removed").rstrip(".")


def _version_ge(current: str, target: str) -> bool:
    try:
        return pkg_version.parse(current) >= pkg_version.parse(target)
    except pkg_version.InvalidVersion as exc:
        raise SystemExit(
            f"Invalid semantic version comparison: {current=} {target=}"
        ) from exc


def _get_openapi_operation(schema: dict, path: str, method: str) -> dict | None:
    path_item = schema.get("paths", {}).get(path)
    if not isinstance(path_item, dict):
        return None

    operation = path_item.get(method.lower())
    if not isinstance(operation, dict):
        return None

    return operation


def _validate_removed_operations(
    removed_operations: list[dict],
    prev_schema: dict,
    current_version: str,
) -> list[str]:
    """Validate removed operations against the baseline deprecation metadata."""
    errors: list[str] = []

    for operation in removed_operations:
        path = str(operation.get("path", ""))
        method = str(operation.get("method", "")).lower()
        method_label = method.upper() or "<unknown method>"

        if not operation.get("deprecated", False):
            errors.append(
                f"Removed {method_label} {path} without prior deprecation "
                "(deprecated=true)."
            )
            continue

        baseline_operation = _get_openapi_operation(prev_schema, path, method)
        if baseline_operation is None:
            errors.append(
                f"Removed {method_label} {path} was marked deprecated in the "
                "baseline release, but the previous OpenAPI schema could not be "
                "inspected for its scheduled removal version."
            )
            continue

        deprecation_details = _parse_openapi_deprecation_description(
            baseline_operation.get("description")
        )
        if deprecation_details is None:
            errors.append(
                f"Removed {method_label} {path} was marked deprecated in the "
                "baseline release, but its OpenAPI description does not declare "
                "a scheduled removal version. REST API removals require 5 minor "
                "releases of deprecation runway."
            )
            continue

        _, removed_in = deprecation_details
        if not _version_ge(current_version, removed_in):
            errors.append(
                f"Removed {method_label} {path} before its scheduled removal "
                f"version v{removed_in} (current version: v{current_version}). "
                "REST API removals require 5 minor releases of deprecation "
                "runway."
            )
            continue

        print(
            f"::notice title={PYPI_DISTRIBUTION} REST API::Removed previously-"
            f"deprecated {method_label} {path} after its scheduled removal "
            f"version v{removed_in}."
        )

    return errors


def _iter_schema_properties(schema: dict):
    if not isinstance(schema, dict):
        return

    properties = schema.get("properties")
    if isinstance(properties, dict):
        for property_name, property_schema in properties.items():
            if isinstance(property_schema, dict):
                yield property_name, property_schema

    for value in schema.values():
        if isinstance(value, dict):
            yield from _iter_schema_properties(value)
        elif isinstance(value, list):
            for item in value:
                if isinstance(item, dict):
                    yield from _iter_schema_properties(item)


def _removed_property_name(change: dict) -> str | None:
    text = str(change.get("text", ""))
    match = re.search(
        r"(?:request property|optional property|required property) `([^`]+)`",
        text,
    )
    if match is None:
        return None
    return match.group(1).rstrip("/").rsplit("/", maxsplit=1)[-1]


def _validate_removed_schema_properties(
    removed_properties: list[dict],
    prev_schema: dict,
    current_version: str,
) -> list[str]:
    """Validate removed schema properties against baseline deprecation metadata."""
    errors: list[str] = []
    baseline_properties: dict[str, list[dict]] = {}
    for property_name, property_schema in _iter_schema_properties(prev_schema):
        baseline_properties.setdefault(property_name, []).append(property_schema)

    for change in removed_properties:
        property_name = _removed_property_name(change)
        if property_name is None:
            errors.append(
                "Removed schema property could not be identified from oasdiff output: "
                f"{change.get('text', str(change))}"
            )
            continue

        deprecated_candidates = [
            property_schema
            for property_schema in baseline_properties.get(property_name, [])
            if property_schema.get("deprecated") is True
        ]
        if not deprecated_candidates:
            errors.append(
                f"Removed schema property {property_name!r} without prior "
                "deprecation (deprecated=true)."
            )
            continue

        removal_targets = [
            deprecation_details[1]
            for property_schema in deprecated_candidates
            if (
                deprecation_details := _parse_openapi_deprecation_description(
                    property_schema.get("description")
                )
            )
            is not None
        ]
        if not removal_targets:
            errors.append(
                f"Removed schema property {property_name!r} was marked deprecated "
                "in the baseline release, but its OpenAPI description does not "
                "declare a scheduled removal version. REST API property removals "
                "require 5 minor releases of deprecation runway."
            )
            continue

        if not any(
            _version_ge(current_version, removed_in) for removed_in in removal_targets
        ):
            errors.append(
                f"Removed schema property {property_name!r} before its scheduled "
                f"removal version(s): {', '.join(f'v{v}' for v in removal_targets)} "
                f"(current version: v{current_version}). REST API property removals "
                "require 5 minor releases of deprecation runway."
            )
            continue

        print(
            f"::notice title={PYPI_DISTRIBUTION} REST API::Removed previously-"
            f"deprecated schema property {property_name!r} after its scheduled "
            "removal version was reached."
        )

    return errors


# oasdiff rule IDs for additive oneOf/anyOf expansion in response schemas.
# These are flagged as ERR by oasdiff but are expected evolution for extensible
# discriminated-union APIs (e.g. the events endpoint).  We downgrade them to
# informational notices so they don't block CI.
_ADDITIVE_RESPONSE_ONEOF_IDS = frozenset(
    {
        "response-body-one-of-added",
        "response-property-one-of-added",
        # Keep the anyOf variants here too so that if oasdiff ever reports them
        # as breakages, additive response-union expansion gets the same
        # downgrade without further script changes.
        "response-body-any-of-added",
        "response-property-any-of-added",
    }
)


_ADDITIVE_RESPONSE_BODY_ONEOF_IDS = frozenset(
    {
        "response-body-one-of-added",
        "response-body-any-of-added",
    }
)


def _is_union_property_removal_artifact(change: dict) -> bool:
    """Return True for property removals that are artifacts of union widening.

    When a request or response schema is widened from a concrete object schema
    to an additive oneOf/anyOf union, oasdiff can emit secondary "removed
    property" reports for the original object's fields even though the original
    schema is still present as one union member.
    """
    change_id = str(change.get("id", "")).lower()
    text = str(change.get("text", "")).lower()
    return (
        "removed" in change_id
        and "property" in change_id
        and ("from the response" in text or "request property" in text)
    )


def _is_union_type_change_artifact(change: dict) -> bool:
    text = str(change.get("text", "")).lower()
    return "type/format changed from `object`/`` to ``/``" in text


def _split_breaking_changes(
    breaking_changes: list[dict],
) -> tuple[list[dict], list[dict], list[dict], list[dict]]:
    """Split oasdiff results into allowlisted buckets and other breakages."""
    removed_operations: list[dict] = []
    removed_schema_properties: list[dict] = []
    additive_response_oneof: list[dict] = []
    other_breaking_changes: list[dict] = []

    for change in breaking_changes:
        change_id = str(change.get("id", ""))
        details = change.get("details", {})

        if "removed" in change_id.lower() and "operation" in change_id.lower():
            removed_operations.append(
                {
                    "path": details.get("path", ""),
                    "method": details.get("method", ""),
                    "deprecated": details.get("deprecated", False),
                }
            )
            continue

        if "removed" in change_id.lower() and "property" in change_id.lower():
            removed_schema_properties.append(change)
            continue

        if change_id in _ADDITIVE_RESPONSE_ONEOF_IDS:
            additive_response_oneof.append(change)
            continue

        other_breaking_changes.append(change)

    return (
        removed_operations,
        removed_schema_properties,
        additive_response_oneof,
        other_breaking_changes,
    )


def _normalize_openapi_for_oasdiff(schema: dict) -> dict:
    """Normalize OpenAPI 3.1 schema for oasdiff compatibility.

    oasdiff expects OpenAPI 3.0-style exclusiveMinimum/exclusiveMaximum booleans
    (https://spec.openapis.org/oas/v3.0.3.html#schema-object), while OpenAPI 3.1
    emits numeric values. Convert numeric exclusives into minimum/maximum +
    exclusive boolean flags so oasdiff can parse the schema.

    Mutates the schema in place and returns it for convenience.
    """

    def _walk(node: object) -> None:
        if isinstance(node, dict):
            if (
                "exclusiveMinimum" in node
                and isinstance(node["exclusiveMinimum"], (int, float))
                and not isinstance(node["exclusiveMinimum"], bool)
            ):
                value = node["exclusiveMinimum"]
                if "minimum" not in node:
                    node["minimum"] = value
                node["exclusiveMinimum"] = True
            if (
                "exclusiveMaximum" in node
                and isinstance(node["exclusiveMaximum"], (int, float))
                and not isinstance(node["exclusiveMaximum"], bool)
            ):
                value = node["exclusiveMaximum"]
                if "maximum" not in node:
                    node["maximum"] = value
                node["exclusiveMaximum"] = True

            for child in node.values():
                _walk(child)
        elif isinstance(node, list):
            for child in node:
                _walk(child)

    _walk(schema)
    return schema


def _run_oasdiff_breakage_check(
    prev_spec: Path, cur_spec: Path
) -> tuple[list[dict], int]:
    """Run oasdiff breaking check between two OpenAPI specs.

    Returns (list of breaking changes, exit code from oasdiff).
    """
    try:
        result = subprocess.run(
            [
                "oasdiff",
                "breaking",
                "-f",
                "json",
                "--fail-on",
                "ERR",
                str(prev_spec),
                str(cur_spec),
            ],
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        print(
            "::warning title=oasdiff not found::"
            "Please install oasdiff: https://github.com/oasdiff/oasdiff"
        )
        return [], 0

    breaking_changes = []
    if result.stdout:
        try:
            breaking_changes = json.loads(result.stdout)
        except json.JSONDecodeError:
            pass

    return breaking_changes, result.returncode


def main() -> int:
    current_version = _read_version_from_pyproject(AGENT_SERVER_PYPROJECT)
    baseline_version = _get_baseline_version(PYPI_DISTRIBUTION, current_version)

    if baseline_version is None:
        print(
            f"::warning title={PYPI_DISTRIBUTION} REST API::Unable to find baseline "
            f"version for {current_version}; skipping breakage checks."
        )
        return 0

    baseline_git_ref = f"v{baseline_version}"

    static_policy_errors = _find_sdk_deprecated_fastapi_routes(REPO_ROOT)
    for error in static_policy_errors:
        print(f"::error title={PYPI_DISTRIBUTION} REST API::{error}")

    current_schema = _generate_current_openapi()
    if current_schema is None:
        return 1
    current_schema = _filter_public_rest_openapi(current_schema)

    deprecation_policy_errors = _find_deprecation_policy_errors(current_schema)
    for error in deprecation_policy_errors:
        print(f"::error title={PYPI_DISTRIBUTION} REST API::{error}")

    prev_schema = _generate_openapi_for_git_ref(baseline_git_ref)
    if prev_schema is None:
        return 0 if not (static_policy_errors or deprecation_policy_errors) else 1
    prev_schema = _filter_public_rest_openapi(prev_schema)

    prev_schema = _normalize_openapi_for_oasdiff(prev_schema)
    current_schema = _normalize_openapi_for_oasdiff(current_schema)

    with tempfile.TemporaryDirectory(prefix="oasdiff-specs-") as tmp:
        tmp_path = Path(tmp)
        prev_spec_file = tmp_path / "prev_spec.json"
        cur_spec_file = tmp_path / "cur_spec.json"
        prev_spec_file.write_text(json.dumps(prev_schema, indent=2))
        cur_spec_file.write_text(json.dumps(current_schema, indent=2))

        breaking_changes, exit_code = _run_oasdiff_breakage_check(
            prev_spec_file, cur_spec_file
        )

    if not breaking_changes:
        if exit_code == 0:
            print("No breaking changes detected.")
        else:
            print(
                f"oasdiff returned exit code {exit_code} but no breaking changes "
                "in JSON format. There may be warnings only."
            )
    else:
        (
            removed_operations,
            removed_schema_properties,
            additive_response_oneof,
            other_breaking_changes,
        ) = _split_breaking_changes(breaking_changes)
        response_union_artifacts = [
            change
            for change in removed_schema_properties
            if _is_union_property_removal_artifact(change)
        ]
        removed_schema_properties = [
            change
            for change in removed_schema_properties
            if not _is_union_property_removal_artifact(change)
        ]
        union_type_artifacts = [
            change
            for change in other_breaking_changes
            if _is_union_type_change_artifact(change)
        ]
        other_breaking_changes = [
            change
            for change in other_breaking_changes
            if not _is_union_type_change_artifact(change)
        ]

        removal_errors = _validate_removed_operations(
            removed_operations,
            prev_schema,
            current_version,
        )
        property_removal_errors = _validate_removed_schema_properties(
            removed_schema_properties,
            prev_schema,
            current_version,
        )

        for error in removal_errors + property_removal_errors:
            print(f"::error title={PYPI_DISTRIBUTION} REST API::{error}")

        if additive_response_oneof:
            print(
                f"\n::notice title={PYPI_DISTRIBUTION} REST API::"
                "Additive oneOf/anyOf expansion detected in response schemas. "
                "This is expected for extensible discriminated-union APIs and "
                "does not break backward compatibility."
            )
            for item in additive_response_oneof:
                print(f"  - {item.get('text', str(item))}")
            if response_union_artifacts:
                print(
                    "  - ignored "
                    f"{len(response_union_artifacts)} request/response-property "
                    "removal artifact(s) caused by union widening"
                )
            if union_type_artifacts:
                print(
                    "  - ignored "
                    f"{len(union_type_artifacts)} request/response type-change "
                    "artifact(s) caused by union widening"
                )

        if other_breaking_changes:
            print(
                "::error "
                f"title={PYPI_DISTRIBUTION} REST API::Detected breaking REST API "
                "changes other than removing previously-deprecated operations/"
                "properties or additive response oneOf expansions. "
                "REST contract changes must preserve compatibility for 5 minor "
                "releases; keep the old contract available until its scheduled "
                "removal version."
            )
        elif (
            response_union_artifacts or union_type_artifacts
        ) and not additive_response_oneof:
            print(
                f"\n::notice title={PYPI_DISTRIBUTION} REST API::"
                f"Ignored {len(response_union_artifacts)} property-removal and "
                f"{len(union_type_artifacts)} type-change artifact(s) reported "
                "while widening schemas."
            )

        print("\nBreaking REST API changes detected compared to baseline release:")
        for text in breaking_changes:
            print(f"- {text.get('text', str(text))}")

        if not (removal_errors or property_removal_errors or other_breaking_changes):
            print(
                "Breaking changes are limited to previously-deprecated operations "
                "or properties whose scheduled removal versions have been reached, "
                "and/or additive response oneOf expansions."
            )
        else:
            return 1

    return 1 if (static_policy_errors or deprecation_policy_errors) else 0


if __name__ == "__main__":
    raise SystemExit(main())


================================================
FILE: .github/scripts/check_deprecations.py
================================================
#!/usr/bin/env python3
"""Static analysis for deprecation deadlines.

This script scans Python deprecation metadata (`deprecated`, `warn_deprecated`,
`warn_cleanup`) and agent-server REST routes marked `deprecated=True`. If the
current project version has reached or passed a feature's removal marker, the
script fails with a helpful summary so legacy shims and overdue deprecated REST
endpoints are cleaned up before release.
"""

from __future__ import annotations

import ast
import re
import sys
import tomllib
from collections.abc import Iterable, Iterator, Sequence
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Literal

from packaging import version as pkg_version


REST_ROUTE_DEPRECATION_RE = re.compile(
    r"Deprecated since v(?P<deprecated>[0-9A-Za-z.+-]+)\s+"
    r"and scheduled for removal in v(?P<removed>[0-9A-Za-z.+-]+)\.?",
    re.IGNORECASE,
)
ROUTE_DECORATOR_NAMES = {
    "get",
    "put",
    "post",
    "delete",
    "patch",
    "options",
    "head",
    "trace",
    "api_route",
}
HTTP_METHODS = ROUTE_DECORATOR_NAMES - {"api_route"}

REPO_ROOT = Path(__file__).resolve().parents[2]


@dataclass(frozen=True, slots=True)
class PackageConfig:
    name: str
    pyproject: Path
    source_roots: tuple[Path, ...]


PACKAGES: tuple[PackageConfig, ...] = (
    PackageConfig(
        name="openhands-sdk",
        pyproject=REPO_ROOT / "openhands-sdk" / "pyproject.toml",
        source_roots=(REPO_ROOT / "openhands-sdk" / "openhands" / "sdk",),
    ),
    PackageConfig(
        name="openhands-tools",
        pyproject=REPO_ROOT / "openhands-tools" / "pyproject.toml",
        source_roots=(REPO_ROOT / "openhands-tools" / "openhands" / "tools",),
    ),
    PackageConfig(
        name="openhands-workspace",
        pyproject=REPO_ROOT / "openhands-workspace" / "pyproject.toml",
        source_roots=(REPO_ROOT / "openhands-workspace" / "openhands" / "workspace",),
    ),
    PackageConfig(
        name="openhands-agent-server",
        pyproject=REPO_ROOT / "openhands-agent-server" / "pyproject.toml",
        source_roots=(
            REPO_ROOT / "openhands-agent-server" / "openhands" / "agent_server",
        ),
    ),
)


@dataclass(slots=True)
class DeprecationRecord:
    identifier: str
    removed_in: str | date | None
    deprecated_in: str | None
    path: Path
    line: int
    kind: Literal["decorator", "warn_call", "cleanup_call", "rest_route"]
    package: str


def _load_current_version(pyproject: Path) -> str:
    data = tomllib.loads(pyproject.read_text())
    try:
        return str(data["project"]["version"])
    except KeyError as exc:  # pragma: no cover - configuration error
        raise SystemExit(
            f"Unable to determine project version from {pyproject}"
        ) from exc


def _iter_python_files(root: Path) -> Iterator[Path]:
    for path in root.rglob("*.py"):
        if path.name == "__init__.py" and path.parent == root:
            continue
        yield path


def _parse_removed_value(
    node: ast.AST | None,
    *,
    path: Path,
    line: int,
) -> str | date | None:
    if node is None:
        return None

    expression = ast.unparse(node)

    if isinstance(node, ast.Constant):
        if isinstance(node.value, str):
            return node.value
        if node.value is None:
            return None
        raise SystemExit(
            f"Unsupported removed_in literal at {path}:{line}: {expression}"
        )

    if isinstance(node, ast.Call):
        func = node.func
        if isinstance(func, ast.Name) and func.id == "date":
            try:
                args = [_safe_int_literal(arg) for arg in node.args]
                kwargs = {
                    kw.arg: _safe_int_literal(kw.value)
                    for kw in node.keywords
                    if kw.arg is not None
                }
            except ValueError as exc:
                raise SystemExit(
                    f"Unsupported removed_in date() arguments at {path}:{line}:"
                    f" {expression}"
                ) from exc

            if any(kw.arg is None for kw in node.keywords):
                raise SystemExit(
                    "Unsupported removed_in date() call (uses **kwargs) at "
                    f"{path}:{line}: {expression}"
                )

            try:
                return date(*args, **kwargs)
            except TypeError as exc:
                raise SystemExit(
                    f"Invalid removed_in date() call at {path}:{line}: {expression}"
                ) from exc

        if (
            isinstance(func, ast.Attribute)
            and isinstance(func.value, ast.Name)
            and func.value.id == "date"
            and func.attr == "today"
        ):
            if node.args or node.keywords:
                raise SystemExit(
                    "date.today() removed_in call must not include arguments at "
                    f"{path}:{line}: {expression}"
                )
            return date.today()

    raise SystemExit(
        f"Unsupported removed_in expression at {path}:{line}: {expression}"
    )


def _parse_deprecated_value(
    node: ast.AST | None,
    *,
    path: Path,
    line: int,
) -> str | None:
    if node is None:
        return None

    expression = ast.unparse(node)

    if isinstance(node, ast.Constant):
        if isinstance(node.value, str):
            return node.value
        if node.value is None:
            return None

    raise SystemExit(
        f"Unsupported deprecated_in expression at {path}:{line}: {expression}"
    )


def _safe_int_literal(node: ast.AST) -> int:
    if not isinstance(node, ast.Constant) or not isinstance(node.value, int):
        raise ValueError(
            f"Unsupported expression inside literal evaluation: {ast.unparse(node)}"
        )
    return node.value


def _extract_kw(call: ast.Call, name: str) -> ast.AST | None:
    for kw in call.keywords:
        if kw.arg == name:
            return kw.value
    return None


def _extract_string_literal(node: ast.AST | None) -> str | None:
    if isinstance(node, ast.Constant) and isinstance(node.value, str):
        return node.value
    return None


def _extract_string_sequence(node: ast.AST | None) -> tuple[str, ...] | None:
    if not isinstance(node, (ast.List, ast.Tuple, ast.Set)):
        return None

    values: list[str] = []
    for item in node.elts:
        value = _extract_string_literal(item)
        if value is None:
            return None
        values.append(value)
    return tuple(values)


def _extract_route_details(call: ast.Call) -> tuple[tuple[str, str], ...]:
    target = call.func
    if not isinstance(target, ast.Attribute):
        return ()

    decorator_name = target.attr
    if decorator_name not in ROUTE_DECORATOR_NAMES:
        return ()

    path = _extract_string_literal(call.args[0] if call.args else None)
    if path is None:
        path = _extract_string_literal(_extract_kw(call, "path"))
    if path is None:
        return ()

    if decorator_name in HTTP_METHODS:
        return ((decorator_name.upper(), path),)

    methods = _extract_string_sequence(_extract_kw(call, "methods"))
    if methods is None:
        return (("GET", path),)

    return tuple(
        (method.upper(), path) for method in methods if method.lower() in HTTP_METHODS
    )


def _parse_rest_route_deprecation_docstring(
    docstring: str | None,
    *,
    path: Path,
    line: int,
    route_identifiers: Sequence[str],
) -> tuple[str, str]:
    if not docstring:
        raise SystemExit(
            "Deprecated REST route(s) "
            f"{', '.join(route_identifiers)} at {path}:{line} must include a "
            "docstring note like 'Deprecated since vX.Y.Z and scheduled for "
            "removal in vA.B.C.'"
        )

    match = REST_ROUTE_DEPRECATION_RE.search(" ".join(docstring.split()))
    if match is None:
        raise SystemExit(
            "Deprecated REST route(s) "
            f"{', '.join(route_identifiers)} at {path}:{line} must include a "
            "docstring note like 'Deprecated since vX.Y.Z and scheduled for "
            "removal in vA.B.C.'"
        )

    return match.group("deprecated").rstrip("."), match.group("removed").rstrip(".")


def _gather_rest_route_deprecations(
    tree: ast.AST, path: Path, *, package: str
) -> Iterator[DeprecationRecord]:
    for node in ast.walk(tree):
        if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
            continue

        routes: list[tuple[str, str]] = []
        for deco in node.decorator_list:
            if not isinstance(deco, ast.Call):
                continue
            deprecated_value = _extract_kw(deco, "deprecated")
            if (
                not isinstance(deprecated_value, ast.Constant)
                or deprecated_value.value is not True
            ):
                continue
            routes.extend(_extract_route_details(deco))

        if not routes:
            continue

        deprecated_in, removed_in = _parse_rest_route_deprecation_docstring(
            ast.get_docstring(node),
            path=path,
            line=node.lineno,
            route_identifiers=[
                f"{method} {route_path}" for method, route_path in routes
            ],
        )

        for method, route_path in routes:
            yield DeprecationRecord(
                identifier=f"{method} {route_path}",
                removed_in=removed_in,
                deprecated_in=deprecated_in,
                path=path,
                line=node.lineno,
                kind="rest_route",
                package=package,
            )


def _gather_decorators(
    tree: ast.AST, path: Path, *, package: str
) -> Iterator[DeprecationRecord]:
    for node in ast.walk(tree):
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            continue

        for deco in node.decorator_list:
            call = deco if isinstance(deco, ast.Call) else None
            if call is None:
                continue

            target = call.func
            if isinstance(target, ast.Name):
                decorator_name = target.id
            elif isinstance(target, ast.Attribute):
                decorator_name = target.attr
            else:
                continue

            if decorator_name != "deprecated":
                continue

            removed_expr = _extract_kw(call, "removed_in")
            deprecated_expr = _extract_kw(call, "deprecated_in")

            record = DeprecationRecord(
                identifier=_build_identifier(node),
                removed_in=_parse_removed_value(
                    removed_expr, path=path, line=node.lineno
                ),
                deprecated_in=_parse_deprecated_value(
                    deprecated_expr, path=path, line=node.lineno
                ),
                path=path,
                line=node.lineno,
                kind="decorator",
                package=package,
            )
            yield record


def _gather_warn_calls(
    tree: ast.AST, path: Path, *, package: str
) -> Iterator[DeprecationRecord]:
    for node in ast.walk(tree):
        if not isinstance(node, ast.Call):
            continue

        target = node.func
        if isinstance(target, ast.Name):
            func_name = target.id
        elif isinstance(target, ast.Attribute):
            func_name = target.attr
        else:
            continue

        if func_name == "warn_deprecated":
            identifier_node = node.args[0] if node.args else None
            if identifier_node is None:
                continue
            identifier = ast.unparse(identifier_node)

            removed_expr = _extract_kw(node, "removed_in")
            deprecated_expr = _extract_kw(node, "deprecated_in")

            yield DeprecationRecord(
                identifier=identifier,
                removed_in=_parse_removed_value(
                    removed_expr, path=path, line=node.lineno
                ),
                deprecated_in=_parse_deprecated_value(
                    deprecated_expr, path=path, line=node.lineno
                ),
                path=path,
                line=node.lineno,
                kind="warn_call",
                package=package,
            )
        elif func_name == "warn_cleanup":
            identifier_node = node.args[0] if node.args else None
            if identifier_node is None:
                continue
            identifier = ast.unparse(identifier_node)

            cleanup_expr = _extract_kw(node, "cleanup_by")

            yield DeprecationRecord(
                identifier=identifier,
                removed_in=_parse_removed_value(
                    cleanup_expr, path=path, line=node.lineno
                ),
                deprecated_in=None,
                path=path,
                line=node.lineno,
                kind="cleanup_call",
                package=package,
            )


def _build_identifier(node: ast.AST) -> str:
    if isinstance(node, ast.ClassDef):
        return node.name
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
        qual_name = node.name
        if node.decorator_list:
            parent = getattr(node, "parent", None)
            if parent and isinstance(parent, ast.ClassDef):
                return f"{parent.name}.{node.name}"
        return qual_name
    return "<unknown>"


def _attach_parents(tree: ast.AST) -> None:
    for node in ast.walk(tree):
        for child in ast.iter_child_nodes(node):
            setattr(child, "parent", node)


def _collect_records(files: Iterable[Path], *, package: str) -> list[DeprecationRecord]:
    records: list[DeprecationRecord] = []
    for path in files:
        tree = ast.parse(path.read_text())
        _attach_parents(tree)
        records.extend(_gather_decorators(tree, path, package=package))
        records.extend(_gather_warn_calls(tree, path, package=package))
    return records


def _collect_rest_route_records(
    files: Iterable[Path], *, package: str
) -> list[DeprecationRecord]:
    records: list[DeprecationRecord] = []
    for path in files:
        tree = ast.parse(path.read_text())
        records.extend(_gather_rest_route_deprecations(tree, path, package=package))
    return records


def _version_ge(
Download .txt
gitextract_0wirow34/

├── .agents/
│   └── skills/
│       ├── cross-repo-testing/
│       │   └── SKILL.md
│       ├── custom-codereview-guide.md
│       ├── debug-test-examples-workflow/
│       │   └── SKILL.md
│       ├── design-principles.md
│       ├── feature-release-rollout/
│       │   └── SKILL.md
│       ├── manage-evals/
│       │   ├── SKILL.md
│       │   ├── references/
│       │   │   └── eval-infrastructure.md
│       │   └── scripts/
│       │       └── manage_evals.py
│       ├── run-eval.md
│       ├── sdk-release/
│       │   ├── SKILL.md
│       │   └── references/
│       │       └── post-release-checklist.md
│       └── write-behavior-test.md
├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_template.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── dependabot.yml
│   ├── prompts/
│   │   └── update-documentation.md
│   ├── run-eval/
│   │   ├── ADDINGMODEL.md
│   │   ├── AGENTS.md
│   │   ├── resolve_model_config.py
│   │   └── validate_sdk_ref.py
│   ├── scripts/
│   │   ├── check_agent_server_rest_api_breakage.py
│   │   ├── check_deprecations.py
│   │   ├── check_docstrings.py
│   │   ├── check_documented_examples.py
│   │   ├── check_duplicate_example_numbers.py
│   │   ├── check_sdk_api_breakage.py
│   │   ├── check_version_bumps.py
│   │   └── update_sdk_ref_default.py
│   └── workflows/
│       ├── README-RELEASE.md
│       ├── agent-server-rest-api-breakage.yml
│       ├── api-breakage.yml
│       ├── api-compliance-runner.yml
│       ├── assign-reviews.yml
│       ├── auto-label-issues.yml
│       ├── cancel-eval.yml
│       ├── check-docstrings.yml
│       ├── check-documented-examples.yml
│       ├── check-duplicate-examples.yml
│       ├── condenser-runner.yml
│       ├── create-release.yml
│       ├── deploy-docs.yml
│       ├── deprecation-check.yml
│       ├── integration-runner.yml
│       ├── issue-duplicate-checker.yml
│       ├── oh-update-documentation.yml.back
│       ├── pr-artifacts.yml
│       ├── pr-review-by-openhands.yml
│       ├── pr-review-evaluation.yml
│       ├── precommit.yml
│       ├── prepare-release.yml
│       ├── pypi-release.yml
│       ├── qa-changes-by-openhands.yml
│       ├── qa-changes-evaluation.yml
│       ├── release-binaries.yml
│       ├── remove-duplicate-candidate-label.yml
│       ├── review-thread-gate.yml
│       ├── run-eval.yml
│       ├── run-examples.yml
│       ├── server.yml
│       ├── stale.yml
│       ├── tests.yml
│       ├── todo-management.yml
│       ├── version-bump-guard.yml
│       └── version-bump-prs.yml
├── .gitignore
├── .openhands/
│   ├── hooks/
│   │   └── on_stop.sh
│   ├── hooks.json
│   └── setup.sh
├── .pre-commit-config.yaml
├── .python-version
├── AGENTS.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── LICENSE
├── MAINTAINERS
├── MANIFEST.in
├── Makefile
├── README.md
├── examples/
│   ├── 01_standalone_sdk/
│   │   ├── 01_hello_world.py
│   │   ├── 02_custom_tools.py
│   │   ├── 03_activate_skill.py
│   │   ├── 04_confirmation_mode_example.py
│   │   ├── 05_use_llm_registry.py
│   │   ├── 06_interactive_terminal_w_reasoning.py
│   │   ├── 07_mcp_integration.py
│   │   ├── 08_mcp_with_oauth.py
│   │   ├── 09_pause_example.py
│   │   ├── 10_persistence.py
│   │   ├── 11_async.py
│   │   ├── 12_custom_secrets.py
│   │   ├── 13_get_llm_metrics.py
│   │   ├── 14_context_condenser.py
│   │   ├── 15_browser_use.py
│   │   ├── 16_llm_security_analyzer.py
│   │   ├── 17_image_input.py
│   │   ├── 18_send_message_while_processing.py
│   │   ├── 19_llm_routing.py
│   │   ├── 20_stuck_detector.py
│   │   ├── 21_generate_extraneous_conversation_costs.py
│   │   ├── 22_anthropic_thinking.py
│   │   ├── 23_responses_reasoning.py
│   │   ├── 24_planning_agent_workflow.py
│   │   ├── 25_agent_delegation.py
│   │   ├── 26_custom_visualizer.py
│   │   ├── 27_observability_laminar.py
│   │   ├── 28_ask_agent_example.py
│   │   ├── 29_llm_streaming.py
│   │   ├── 30_tom_agent.py
│   │   ├── 31_iterative_refinement.py
│   │   ├── 32_configurable_security_policy.py
│   │   ├── 33_hooks/
│   │   │   ├── README.md
│   │   │   ├── hook_scripts/
│   │   │   │   ├── block_dangerous.sh
│   │   │   │   ├── inject_git_context.sh
│   │   │   │   ├── log_tools.sh
│   │   │   │   └── require_summary.sh
│   │   │   └── main.py
│   │   ├── 34_critic_example.py
│   │   ├── 35_subscription_login.py
│   │   ├── 36_event_json_to_openai_messages.py
│   │   ├── 37_llm_profile_store/
│   │   │   ├── main.py
│   │   │   └── profiles/
│   │   │       └── fast.json
│   │   ├── 38_browser_session_recording.py
│   │   ├── 39_llm_fallback.py
│   │   ├── 40_acp_agent_example.py
│   │   ├── 41_task_tool_set.py
│   │   ├── 42_file_based_subagents.py
│   │   ├── 43_mixed_marketplace_skills/
│   │   │   ├── .plugin/
│   │   │   │   └── marketplace.json
│   │   │   ├── README.md
│   │   │   ├── main.py
│   │   │   └── skills/
│   │   │       └── greeting-helper/
│   │   │           └── SKILL.md
│   │   ├── 44_model_switching_in_convo.py
│   │   ├── 45_parallel_tool_execution.py
│   │   ├── 46_agent_settings.py
│   │   ├── 47_defense_in_depth_security.py
│   │   ├── 48_conversation_fork.py
│   │   └── 49_switch_llm_tool.py
│   ├── 02_remote_agent_server/
│   │   ├── 01_convo_with_local_agent_server.py
│   │   ├── 02_convo_with_docker_sandboxed_server.py
│   │   ├── 03_browser_use_with_docker_sandboxed_server.py
│   │   ├── 04_convo_with_api_sandboxed_server.py
│   │   ├── 05_vscode_with_docker_sandboxed_server.py
│   │   ├── 06_custom_tool/
│   │   │   ├── Dockerfile
│   │   │   ├── README.md
│   │   │   ├── build_custom_image.sh
│   │   │   ├── custom_tools/
│   │   │   │   ├── __init__.py
│   │   │   │   └── log_data.py
│   │   │   └── main.py
│   │   ├── 07_convo_with_cloud_workspace.py
│   │   ├── 08_convo_with_apptainer_sandboxed_server.py
│   │   ├── 09_acp_agent_with_remote_runtime.py
│   │   ├── 10_cloud_workspace_share_credentials.py
│   │   ├── 11_conversation_fork.py
│   │   ├── 12_settings_and_secrets_api.py
│   │   ├── 13_workspace_get_llm.py
│   │   └── hook_scripts/
│   │       └── pycompile_check.sh
│   ├── 03_github_workflows/
│   │   ├── 01_basic_action/
│   │   │   ├── README.md
│   │   │   ├── agent_script.py
│   │   │   ├── assign-reviews.yml
│   │   │   └── workflow.yml
│   │   ├── 02_pr_review/
│   │   │   ├── README.md
│   │   │   └── workflow.yml
│   │   ├── 03_todo_management/
│   │   │   ├── README.md
│   │   │   ├── agent_script.py
│   │   │   ├── prompt.py
│   │   │   ├── scanner.py
│   │   │   └── workflow.yml
│   │   ├── 04_datadog_debugging/
│   │   │   ├── README.md
│   │   │   ├── datadog_debugging.py
│   │   │   ├── debug_prompt.jinja
│   │   │   └── workflow.yml
│   │   └── 05_posthog_debugging/
│   │       ├── README.md
│   │       ├── debug_prompt.jinja
│   │       ├── posthog_debugging.py
│   │       └── workflow.yml
│   ├── 04_llm_specific_tools/
│   │   ├── 01_gpt5_apply_patch_preset.py
│   │   └── 02_gemini_file_tools.py
│   └── 05_skills_and_plugins/
│       ├── 01_loading_agentskills/
│       │   ├── example_skills/
│       │   │   ├── code-style-guide/
│       │   │   │   └── SKILL.md
│       │   │   └── rot13-encryption/
│       │   │       ├── SKILL.md
│       │   │       ├── references/
│       │   │       │   └── examples.md
│       │   │       └── scripts/
│       │   │           └── encrypt.sh
│       │   └── main.py
│       ├── 02_loading_plugins/
│       │   ├── example_plugins/
│       │   │   └── code-quality/
│       │   │       ├── .mcp.json
│       │   │       ├── .plugin/
│       │   │       │   └── plugin.json
│       │   │       ├── hooks/
│       │   │       │   └── hooks.json
│       │   │       └── skills/
│       │   │           └── linting/
│       │   │               └── SKILL.md
│       │   └── main.py
│       └── 03_managing_installed_skills/
│           └── main.py
├── openhands-agent-server/
│   ├── AGENTS.md
│   ├── openhands/
│   │   └── agent_server/
│   │       ├── README.md
│   │       ├── __init__.py
│   │       ├── __main__.py
│   │       ├── _secrets_exposure.py
│   │       ├── agent-server.spec
│   │       ├── api.py
│   │       ├── auth_router.py
│   │       ├── bash_router.py
│   │       ├── bash_service.py
│   │       ├── cloud_proxy_router.py
│   │       ├── config.py
│   │       ├── conversation_lease.py
│   │       ├── conversation_router.py
│   │       ├── conversation_router_acp.py
│   │       ├── conversation_service.py
│   │       ├── dependencies.py
│   │       ├── desktop_router.py
│   │       ├── desktop_service.py
│   │       ├── docker/
│   │       │   ├── Dockerfile
│   │       │   └── build.py
│   │       ├── env_parser.py
│   │       ├── event_router.py
│   │       ├── event_service.py
│   │       ├── file_router.py
│   │       ├── git_router.py
│   │       ├── hooks_router.py
│   │       ├── hooks_service.py
│   │       ├── llm_router.py
│   │       ├── logging_config.py
│   │       ├── middleware.py
│   │       ├── models.py
│   │       ├── openapi.py
│   │       ├── persistence/
│   │       │   ├── __init__.py
│   │       │   ├── models.py
│   │       │   └── store.py
│   │       ├── profiles_router.py
│   │       ├── pub_sub.py
│   │       ├── py.typed
│   │       ├── server_details_router.py
│   │       ├── settings_router.py
│   │       ├── skills_router.py
│   │       ├── skills_service.py
│   │       ├── sockets.py
│   │       ├── tool_preload_service.py
│   │       ├── tool_router.py
│   │       ├── utils.py
│   │       ├── vscode_extensions/
│   │       │   └── openhands-settings/
│   │       │       ├── extension.js
│   │       │       └── package.json
│   │       ├── vscode_router.py
│   │       ├── vscode_service.py
│   │       └── workspace_router.py
│   └── pyproject.toml
├── openhands-sdk/
│   ├── openhands/
│   │   └── sdk/
│   │       ├── AGENTS.md
│   │       ├── __init__.py
│   │       ├── agent/
│   │       │   ├── __init__.py
│   │       │   ├── acp_agent.py
│   │       │   ├── agent.py
│   │       │   ├── base.py
│   │       │   ├── critic_mixin.py
│   │       │   ├── parallel_executor.py
│   │       │   ├── prompts/
│   │       │   │   ├── in_context_learning_example.j2
│   │       │   │   ├── in_context_learning_example_suffix.j2
│   │       │   │   ├── model_specific/
│   │       │   │   │   ├── anthropic_claude.j2
│   │       │   │   │   ├── google_gemini.j2
│   │       │   │   │   └── openai_gpt/
│   │       │   │   │       ├── gpt-5-codex.j2
│   │       │   │   │       └── gpt-5.j2
│   │       │   │   ├── security_policy.j2
│   │       │   │   ├── security_risk_assessment.j2
│   │       │   │   ├── self_documentation.j2
│   │       │   │   ├── system_prompt.j2
│   │       │   │   ├── system_prompt_interactive.j2
│   │       │   │   ├── system_prompt_long_horizon.j2
│   │       │   │   ├── system_prompt_planning.j2
│   │       │   │   └── system_prompt_tech_philosophy.j2
│   │       │   ├── response_dispatch.py
│   │       │   └── utils.py
│   │       ├── banner.py
│   │       ├── context/
│   │       │   ├── README.md
│   │       │   ├── __init__.py
│   │       │   ├── agent_context.py
│   │       │   ├── condenser/
│   │       │   │   ├── README.md
│   │       │   │   ├── __init__.py
│   │       │   │   ├── base.py
│   │       │   │   ├── llm_summarizing_condenser.py
│   │       │   │   ├── no_op_condenser.py
│   │       │   │   ├── pipeline_condenser.py
│   │       │   │   ├── prompts/
│   │       │   │   │   └── summarizing_prompt.j2
│   │       │   │   └── utils.py
│   │       │   ├── prompts/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── prompt.py
│   │       │   │   └── templates/
│   │       │   │       ├── ask_agent_template.j2
│   │       │   │       ├── skill_knowledge_info.j2
│   │       │   │       └── system_message_suffix.j2
│   │       │   ├── skills/
│   │       │   │   └── __init__.py
│   │       │   └── view/
│   │       │       ├── __init__.py
│   │       │       ├── manipulation_indices.py
│   │       │       ├── properties/
│   │       │       │   ├── __init__.py
│   │       │       │   ├── base.py
│   │       │       │   ├── batch_atomicity.py
│   │       │       │   ├── observation_uniqueness.py
│   │       │       │   ├── tool_call_matching.py
│   │       │       │   └── tool_loop_atomicity.py
│   │       │       └── view.py
│   │       ├── conversation/
│   │       │   ├── __init__.py
│   │       │   ├── base.py
│   │       │   ├── conversation.py
│   │       │   ├── conversation_stats.py
│   │       │   ├── event_store.py
│   │       │   ├── events_list_base.py
│   │       │   ├── exceptions.py
│   │       │   ├── fifo_lock.py
│   │       │   ├── impl/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── local_conversation.py
│   │       │   │   └── remote_conversation.py
│   │       │   ├── persistence_const.py
│   │       │   ├── request.py
│   │       │   ├── resource_lock_manager.py
│   │       │   ├── response_utils.py
│   │       │   ├── secret_registry.py
│   │       │   ├── serialization_diff.py
│   │       │   ├── state.py
│   │       │   ├── stuck_detector.py
│   │       │   ├── title_utils.py
│   │       │   ├── types.py
│   │       │   └── visualizer/
│   │       │       ├── __init__.py
│   │       │       ├── base.py
│   │       │       └── default.py
│   │       ├── critic/
│   │       │   ├── __init__.py
│   │       │   ├── base.py
│   │       │   ├── impl/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── agent_finished.py
│   │       │   │   ├── api/
│   │       │   │   │   ├── __init__.py
│   │       │   │   │   ├── chat_template.py
│   │       │   │   │   ├── client.py
│   │       │   │   │   ├── critic.py
│   │       │   │   │   └── taxonomy.py
│   │       │   │   ├── empty_patch.py
│   │       │   │   └── pass_critic.py
│   │       │   └── result.py
│   │       ├── event/
│   │       │   ├── __init__.py
│   │       │   ├── acp_tool_call.py
│   │       │   ├── base.py
│   │       │   ├── condenser.py
│   │       │   ├── conversation_error.py
│   │       │   ├── conversation_state.py
│   │       │   ├── hook_execution.py
│   │       │   ├── llm_completion_log.py
│   │       │   ├── llm_convertible/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── action.py
│   │       │   │   ├── message.py
│   │       │   │   ├── observation.py
│   │       │   │   └── system.py
│   │       │   ├── streaming_delta.py
│   │       │   ├── token.py
│   │       │   ├── types.py
│   │       │   └── user_action.py
│   │       ├── extensions/
│   │       │   ├── __init__.py
│   │       │   ├── fetch.py
│   │       │   └── installation/
│   │       │       ├── README.md
│   │       │       ├── __init__.py
│   │       │       ├── info.py
│   │       │       ├── interface.py
│   │       │       ├── manager.py
│   │       │       ├── metadata.py
│   │       │       └── utils.py
│   │       ├── git/
│   │       │   ├── cached_repo.py
│   │       │   ├── exceptions.py
│   │       │   ├── git_changes.py
│   │       │   ├── git_diff.py
│   │       │   ├── models.py
│   │       │   └── utils.py
│   │       ├── hooks/
│   │       │   ├── __init__.py
│   │       │   ├── config.py
│   │       │   ├── conversation_hooks.py
│   │       │   ├── executor.py
│   │       │   ├── manager.py
│   │       │   └── types.py
│   │       ├── io/
│   │       │   ├── __init__.py
│   │       │   ├── base.py
│   │       │   ├── cache.py
│   │       │   ├── local.py
│   │       │   └── memory.py
│   │       ├── llm/
│   │       │   ├── __init__.py
│   │       │   ├── auth/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── credentials.py
│   │       │   │   └── openai.py
│   │       │   ├── exceptions/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── classifier.py
│   │       │   │   ├── mapping.py
│   │       │   │   └── types.py
│   │       │   ├── fallback_strategy.py
│   │       │   ├── llm.py
│   │       │   ├── llm_profile_store.py
│   │       │   ├── llm_registry.py
│   │       │   ├── llm_response.py
│   │       │   ├── message.py
│   │       │   ├── mixins/
│   │       │   │   ├── fn_call_converter.py
│   │       │   │   ├── fn_call_examples.py
│   │       │   │   └── non_native_fc.py
│   │       │   ├── options/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── chat_options.py
│   │       │   │   ├── common.py
│   │       │   │   └── responses_options.py
│   │       │   ├── router/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── base.py
│   │       │   │   └── impl/
│   │       │   │       ├── multimodal.py
│   │       │   │       └── random.py
│   │       │   ├── streaming.py
│   │       │   └── utils/
│   │       │       ├── image_resize.py
│   │       │       ├── litellm_provider.py
│   │       │       ├── metrics.py
│   │       │       ├── model_features.py
│   │       │       ├── model_info.py
│   │       │       ├── model_prompt_spec.py
│   │       │       ├── responses_serialization.py
│   │       │       ├── retry_mixin.py
│   │       │       ├── telemetry.py
│   │       │       ├── unverified_models.py
│   │       │       └── verified_models.py
│   │       ├── logger/
│   │       │   ├── __init__.py
│   │       │   ├── logger.py
│   │       │   └── rolling.py
│   │       ├── marketplace/
│   │       │   ├── __init__.py
│   │       │   └── types.py
│   │       ├── mcp/
│   │       │   ├── __init__.py
│   │       │   ├── client.py
│   │       │   ├── definition.py
│   │       │   ├── exceptions.py
│   │       │   ├── tool.py
│   │       │   └── utils.py
│   │       ├── observability/
│   │       │   ├── __init__.py
│   │       │   ├── laminar.py
│   │       │   └── utils.py
│   │       ├── plugin/
│   │       │   ├── __init__.py
│   │       │   ├── fetch.py
│   │       │   ├── installed.py
│   │       │   ├── loader.py
│   │       │   ├── plugin.py
│   │       │   ├── source.py
│   │       │   └── types.py
│   │       ├── py.typed
│   │       ├── secret/
│   │       │   ├── __init__.py
│   │       │   └── secrets.py
│   │       ├── security/
│   │       │   ├── __init__.py
│   │       │   ├── analyzer.py
│   │       │   ├── confirmation_policy.py
│   │       │   ├── defense_in_depth/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── pattern.py
│   │       │   │   ├── policy_rails.py
│   │       │   │   └── utils.py
│   │       │   ├── ensemble.py
│   │       │   ├── grayswan/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── analyzer.py
│   │       │   │   └── utils.py
│   │       │   ├── llm_analyzer.py
│   │       │   └── risk.py
│   │       ├── settings/
│   │       │   ├── __init__.py
│   │       │   ├── acp_providers.py
│   │       │   ├── api_models.py
│   │       │   ├── metadata.py
│   │       │   └── model.py
│   │       ├── skills/
│   │       │   ├── __init__.py
│   │       │   ├── exceptions.py
│   │       │   ├── execute.py
│   │       │   ├── fetch.py
│   │       │   ├── installed.py
│   │       │   ├── skill.py
│   │       │   ├── trigger.py
│   │       │   ├── types.py
│   │       │   └── utils.py
│   │       ├── subagent/
│   │       │   ├── AGENTS.md
│   │       │   ├── __init__.py
│   │       │   ├── load.py
│   │       │   ├── registry.py
│   │       │   └── schema.py
│   │       ├── testing/
│   │       │   ├── __init__.py
│   │       │   └── test_llm.py
│   │       ├── tool/
│   │       │   ├── __init__.py
│   │       │   ├── builtins/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── finish.py
│   │       │   │   ├── invoke_skill.py
│   │       │   │   ├── switch_llm.py
│   │       │   │   └── think.py
│   │       │   ├── registry.py
│   │       │   ├── schema.py
│   │       │   ├── spec.py
│   │       │   └── tool.py
│   │       ├── utils/
│   │       │   ├── __init__.py
│   │       │   ├── async_executor.py
│   │       │   ├── async_utils.py
│   │       │   ├── cipher.py
│   │       │   ├── command.py
│   │       │   ├── datetime.py
│   │       │   ├── deprecation.py
│   │       │   ├── github.py
│   │       │   ├── json.py
│   │       │   ├── models.py
│   │       │   ├── paging.py
│   │       │   ├── path.py
│   │       │   ├── pydantic_diff.py
│   │       │   ├── pydantic_secrets.py
│   │       │   ├── redact.py
│   │       │   ├── truncate.py
│   │       │   └── visualize.py
│   │       └── workspace/
│   │           ├── __init__.py
│   │           ├── base.py
│   │           ├── local.py
│   │           ├── models.py
│   │           ├── remote/
│   │           │   ├── __init__.py
│   │           │   ├── async_remote_workspace.py
│   │           │   ├── base.py
│   │           │   └── remote_workspace_mixin.py
│   │           ├── repo.py
│   │           └── workspace.py
│   └── pyproject.toml
├── openhands-tools/
│   ├── openhands/
│   │   └── tools/
│   │       ├── AGENTS.md
│   │       ├── __init__.py
│   │       ├── apply_patch/
│   │       │   ├── __init__.py
│   │       │   ├── core.py
│   │       │   └── definition.py
│   │       ├── browser_use/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── event_storage.py
│   │       │   ├── impl.py
│   │       │   ├── js/
│   │       │   │   ├── flush-events.js
│   │       │   │   ├── rrweb-loader.js
│   │       │   │   ├── start-recording-simple.js
│   │       │   │   ├── start-recording.js
│   │       │   │   ├── stop-recording.js
│   │       │   │   └── wait-for-rrweb.js
│   │       │   ├── logging_fix.py
│   │       │   ├── recording.py
│   │       │   └── server.py
│   │       ├── delegate/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── impl.py
│   │       │   ├── templates/
│   │       │   │   └── delegate_tool_description.j2
│   │       │   └── visualizer.py
│   │       ├── file_editor/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── editor.py
│   │       │   ├── exceptions.py
│   │       │   ├── impl.py
│   │       │   └── utils/
│   │       │       ├── __init__.py
│   │       │       ├── config.py
│   │       │       ├── constants.py
│   │       │       ├── diff.py
│   │       │       ├── encoding.py
│   │       │       ├── file_cache.py
│   │       │       ├── history.py
│   │       │       └── shell.py
│   │       ├── gemini/
│   │       │   ├── __init__.py
│   │       │   ├── edit/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── definition.py
│   │       │   │   └── impl.py
│   │       │   ├── list_directory/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── definition.py
│   │       │   │   └── impl.py
│   │       │   ├── read_file/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── definition.py
│   │       │   │   └── impl.py
│   │       │   └── write_file/
│   │       │       ├── __init__.py
│   │       │       ├── definition.py
│   │       │       └── impl.py
│   │       ├── glob/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── impl.py
│   │       ├── grep/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── impl.py
│   │       ├── planning_file_editor/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── impl.py
│   │       ├── preset/
│   │       │   ├── __init__.py
│   │       │   ├── default.py
│   │       │   ├── gemini.py
│   │       │   ├── gpt5.py
│   │       │   ├── planning.py
│   │       │   └── subagents/
│   │       │       ├── bash_runner.md
│   │       │       ├── code_explorer.md
│   │       │       ├── default.md
│   │       │       └── web_researcher.md
│   │       ├── py.typed
│   │       ├── task/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   ├── impl.py
│   │       │   └── manager.py
│   │       ├── task_tracker/
│   │       │   ├── __init__.py
│   │       │   └── definition.py
│   │       ├── terminal/
│   │       │   ├── README.md
│   │       │   ├── __init__.py
│   │       │   ├── constants.py
│   │       │   ├── definition.py
│   │       │   ├── descriptions.py
│   │       │   ├── impl.py
│   │       │   ├── metadata.py
│   │       │   ├── terminal/
│   │       │   │   ├── __init__.py
│   │       │   │   ├── factory.py
│   │       │   │   ├── interface.py
│   │       │   │   ├── subprocess_terminal.py
│   │       │   │   ├── terminal_session.py
│   │       │   │   ├── tmux_pane_pool.py
│   │       │   │   ├── tmux_terminal.py
│   │       │   │   └── windows_terminal.py
│   │       │   └── utils/
│   │       │       ├── __init__.py
│   │       │       ├── command.py
│   │       │       └── escape_filter.py
│   │       ├── tom_consult/
│   │       │   ├── __init__.py
│   │       │   ├── definition.py
│   │       │   └── executor.py
│   │       └── utils/
│   │           ├── __init__.py
│   │           └── timeout.py
│   └── pyproject.toml
├── openhands-workspace/
│   ├── openhands/
│   │   └── workspace/
│   │       ├── AGENTS.md
│   │       ├── __init__.py
│   │       ├── apptainer/
│   │       │   ├── README.md
│   │       │   ├── __init__.py
│   │       │   └── workspace.py
│   │       ├── cloud/
│   │       │   ├── __init__.py
│   │       │   └── workspace.py
│   │       ├── docker/
│   │       │   ├── __init__.py
│   │       │   ├── dev_workspace.py
│   │       │   └── workspace.py
│   │       ├── py.typed
│   │       └── remote_api/
│   │           ├── __init__.py
│   │           └── workspace.py
│   └── pyproject.toml
├── pyproject.toml
├── scripts/
│   ├── agent_server_ui/
│   │   ├── run.sh
│   │   └── static/
│   │       ├── app-dev.js
│   │       ├── app.js
│   │       ├── index-dev.html
│   │       ├── index.html
│   │       └── styles.css
│   ├── auto_close_duplicate_issues.py
│   ├── build_config_template.py
│   ├── check_import_rules.py
│   ├── check_tool_registration.py
│   ├── completion_logs_viewer.py
│   ├── conversation_viewer.py
│   ├── convert_legacy_skills.py
│   ├── event_sourcing_benchmarks/
│   │   ├── README.md
│   │   ├── bench_persist_latency.py
│   │   ├── bench_replay_and_recovery.py
│   │   ├── bench_storage_growth.py
│   │   └── benchmark_utils.py
│   ├── issue_duplicate_check_openhands.py
│   ├── render_examples_report.py
│   └── websocket_client.html
└── tests/
    ├── README.md
    ├── __init__.py
    ├── agent_server/
    │   ├── __init__.py
    │   ├── stress/
    │   │   ├── __init__.py
    │   │   ├── budgets.py
    │   │   ├── conftest.py
    │   │   ├── probe.py
    │   │   ├── scripts.py
    │   │   ├── test_concurrent_conversations.py
    │   │   ├── test_conversation_listing.py
    │   │   ├── test_event_loop_responsiveness.py
    │   │   ├── test_high_volume_bash_output.py
    │   │   ├── test_lease_contention.py
    │   │   ├── test_long_running_command.py
    │   │   ├── test_parallel_subagents.py
    │   │   ├── test_slow_webhook.py
    │   │   ├── test_slow_websocket_consumer.py
    │   │   └── test_websocket_reconnect_storm.py
    │   ├── test_agent_server_wsproto.py
    │   ├── test_api.py
    │   ├── test_api_authentication.py
    │   ├── test_bash_service.py
    │   ├── test_check_browser.py
    │   ├── test_cloud_proxy_router.py
    │   ├── test_conversation_lease.py
    │   ├── test_conversation_response.py
    │   ├── test_conversation_router.py
    │   ├── test_conversation_router_acp.py
    │   ├── test_conversation_service.py
    │   ├── test_conversation_service_plugin.py
    │   ├── test_conversation_tags.py
    │   ├── test_dependencies.py
    │   ├── test_desktop_router.py
    │   ├── test_desktop_service.py
    │   ├── test_docker_build.py
    │   ├── test_env_parser.py
    │   ├── test_event_router.py
    │   ├── test_event_router_websocket.py
    │   ├── test_event_service.py
    │   ├── test_event_streaming.py
    │   ├── test_file_router.py
    │   ├── test_git_router.py
    │   ├── test_hooks_router.py
    │   ├── test_hooks_service.py
    │   ├── test_llm_router.py
    │   ├── test_models.py
    │   ├── test_openapi_discriminator.py
    │   ├── test_preload_modules.py
    │   ├── test_profiles_router.py
    │   ├── test_pub_sub.py
    │   ├── test_server_details_router.py
    │   ├── test_settings_router.py
    │   ├── test_skills_router.py
    │   ├── test_skills_service.py
    │   ├── test_terminal_router.py
    │   ├── test_terminal_service.py
    │   ├── test_tool_router.py
    │   ├── test_validation_error_sanitization.py
    │   ├── test_vscode_router.py
    │   ├── test_vscode_service.py
    │   ├── test_webhook_subscriber.py
    │   ├── test_websocket_first_message_auth.py
    │   ├── test_workspace_cookie_auth.py
    │   └── test_workspace_router.py
    ├── command_utils.py
    ├── conftest.py
    ├── cross/
    │   ├── __init__.py
    │   ├── conftest.py
    │   ├── test_agent_loading.py
    │   ├── test_agent_secrets_integration.py
    │   ├── test_agent_server_build_metadata.py
    │   ├── test_automatic_naming.py
    │   ├── test_automatic_registration.py
    │   ├── test_check_agent_server_rest_api_breakage.py
    │   ├── test_check_deprecations.py
    │   ├── test_check_sdk_api_breakage.py
    │   ├── test_check_version_bumps.py
    │   ├── test_conversation_restore_behavior.py
    │   ├── test_event_loss_repro.py
    │   ├── test_hello_world.py
    │   ├── test_issue_duplicate_scripts.py
    │   ├── test_pr_review_trace.py
    │   ├── test_registry_directories.py
    │   ├── test_registry_qualnames.py
    │   ├── test_remote_conversation_live_server.py
    │   ├── test_resolve_model_config.py
    │   ├── test_stuck_detector.py
    │   ├── test_stuck_detector_config.py
    │   ├── test_todo_scanner.py
    │   └── test_validate_sdk_ref.py
    ├── examples/
    │   └── test_examples.py
    ├── fixtures/
    │   ├── conversations/
    │   │   ├── v1_11_5_cli_default/
    │   │   │   └── base_state.json
    │   │   └── v1_17_0_with_mcp_config/
    │   │       └── base_state.json
    │   ├── llm_data/
    │   │   ├── README.md
    │   │   ├── data_generator.py
    │   │   ├── fncall-llm-message.json
    │   │   ├── llm-logs/
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015025.972.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015029.090.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015033.222.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015036.544.json
    │   │   │   ├── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015040.416.json
    │   │   │   └── litellm_proxy__anthropic__claude-sonnet-4-20250514-1757015046.707.json
    │   │   ├── nonfncall-llm-logs/
    │   │   │   ├── litellm_proxy__deepseek__deepseek-chat-1757015054.055.json
    │   │   │   ├── litellm_proxy__deepseek__deepseek-chat-1757015062.589.json
    │   │   │   ├── litellm_proxy__deepseek__deepseek-chat-1757015068.723.json
    │   │   │   └── litellm_proxy__deepseek__deepseek-chat-1757015076.651.json
    │   │   └── nonfncall-llm-message.json
    │   └── tokenizers/
    │       └── qwen3-4b-instruct-2507-tokenizer_config.json
    ├── integration/
    │   ├── BEHAVIOR_TESTS.md
    │   ├── README.md
    │   ├── __init__.py
    │   ├── api_compliance/
    │   │   ├── __init__.py
    │   │   ├── base.py
    │   │   ├── result.py
    │   │   └── run_compliance.py
    │   ├── base.py
    │   ├── behavior_utils.py
    │   ├── early_stopper.py
    │   ├── run_infer.py
    │   ├── schemas.py
    │   ├── test_behavior_utils.py
    │   ├── test_early_stopper.py
    │   ├── test_tool_presets.py
    │   ├── tests/
    │   │   ├── a01_unmatched_tool_use.py
    │   │   ├── a02_unmatched_tool_result.py
    │   │   ├── a03_interleaved_user_msg.py
    │   │   ├── a04_interleaved_asst_msg.py
    │   │   ├── a05_duplicate_tool_call_id.py
    │   │   ├── a06_wrong_tool_call_id.py
    │   │   ├── a07_parallel_missing_result.py
    │   │   ├── a08_parallel_wrong_order.py
    │   │   ├── b01_no_premature_implementation.py
    │   │   ├── b02_no_oververification.py
    │   │   ├── b03_no_useless_backward_compatibility.py
    │   │   ├── b04_each_tool_call_has_a_concise_explanation.py
    │   │   ├── b05_do_not_create_redundant_files.py
    │   │   ├── c01_thinking_block_condenser.py
    │   │   ├── c02_hard_context_reset.py
    │   │   ├── c03_delayed_condensation.py
    │   │   ├── c04_token_condenser.py
    │   │   ├── c05_size_condenser.py
    │   │   ├── t01_fix_simple_typo.py
    │   │   ├── t02_add_bash_hello.py
    │   │   ├── t03_jupyter_write_file.py
    │   │   ├── t04_git_staging.py
    │   │   ├── t05_simple_browsing.py
    │   │   ├── t06_github_pr_browsing.py
    │   │   ├── t07_interactive_commands.py
    │   │   ├── t08_image_file_viewing.py
    │   │   └── t09_invoke_skill.py
    │   └── utils/
    │       ├── __init__.py
    │       ├── behavior_helpers.py
    │       ├── consolidate_json_results.py
    │       ├── consolidate_results.py
    │       ├── format_costs.py
    │       ├── generate_markdown_report.py
    │       └── llm_judge.py
    ├── platform_utils.py
    ├── sdk/
    │   ├── __init__.py
    │   ├── agent/
    │   │   ├── __init__.py
    │   │   ├── test_acp_agent.py
    │   │   ├── test_acp_dedup_and_truncation.py
    │   │   ├── test_action_batch.py
    │   │   ├── test_agent_browser_auto_detect.py
    │   │   ├── test_agent_context_window_condensation.py
    │   │   ├── test_agent_immutability.py
    │   │   ├── test_agent_init_state_invariants.py
    │   │   ├── test_agent_llms_are_discoverable.py
    │   │   ├── test_agent_serialization.py
    │   │   ├── test_agent_step_responses_gating.py
    │   │   ├── test_agent_tool_init.py
    │   │   ├── test_agent_utils.py
    │   │   ├── test_extract_security_risk.py
    │   │   ├── test_extract_summary.py
    │   │   ├── test_fix_malformed_tool_arguments.py
    │   │   ├── test_iterative_refinement.py
    │   │   ├── test_message_while_finishing.py
    │   │   ├── test_non_executable_action_emission.py
    │   │   ├── test_nonexistent_tool_handling.py
    │   │   ├── test_parallel_execution_integration.py
    │   │   ├── test_parallel_executor.py
    │   │   ├── test_parallel_executor_locking.py
    │   │   ├── test_reasoning_only_responses.py
    │   │   ├── test_response_dispatch.py
    │   │   ├── test_sanitize_json_control_chars.py
    │   │   ├── test_security_policy_integration.py
    │   │   ├── test_system_prompt.py
    │   │   ├── test_tool_call_compatibility.py
    │   │   ├── test_tool_call_recovery.py
    │   │   ├── test_tool_execution_error_handling.py
    │   │   └── test_tool_validation_error_message.py
    │   ├── config/
    │   │   ├── __init__.py
    │   │   └── test_llm_config.py
    │   ├── context/
    │   │   ├── __init__.py
    │   │   ├── condenser/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_llm_summarizing_condenser.py
    │   │   │   ├── test_no_op_condenser.py
    │   │   │   ├── test_rolling_condenser.py
    │   │   │   └── test_utils.py
    │   │   ├── test_agent_context.py
    │   │   ├── test_agent_context_model_specific.py
    │   │   ├── test_agent_context_serialization.py
    │   │   ├── test_prompt_absolute_path.py
    │   │   ├── test_prompt_model_spec.py
    │   │   └── view/
    │   │       ├── __init__.py
    │   │       ├── conftest.py
    │   │       ├── properties/
    │   │       │   ├── conftest.py
    │   │       │   ├── test_batch_atomicity.py
    │   │       │   ├── test_observation_uniqueness.py
    │   │       │   ├── test_tool_call_matching.py
    │   │       │   └── test_tool_loop_atomicity.py
    │   │       ├── test_manipulation_indices.py
    │   │       ├── test_view.py
    │   │       ├── test_view_append_event.py
    │   │       ├── test_view_batch_atomicity.py
    │   │       ├── test_view_condensation_batch_atomicity.py
    │   │       ├── test_view_manipulation_indices.py
    │   │       ├── test_view_multi_summary.py
    │   │       └── test_view_tool_loop_boundaries.py
    │   ├── conversation/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── local/
    │   │   │   ├── test_agent_status_transition.py
    │   │   │   ├── test_confirmation_mode.py
    │   │   │   ├── test_conversation_core.py
    │   │   │   ├── test_conversation_default_callback.py
    │   │   │   ├── test_conversation_id.py
    │   │   │   ├── test_conversation_path_types.py
    │   │   │   ├── test_conversation_pause_functionality.py
    │   │   │   ├── test_conversation_send_message.py
    │   │   │   ├── test_conversation_visualize_param.py
    │   │   │   ├── test_execute_tool.py
    │   │   │   ├── test_fork.py
    │   │   │   ├── test_rerun_actions.py
    │   │   │   ├── test_run_exception_includes_conversation_id.py
    │   │   │   ├── test_span_double_ending.py
    │   │   │   └── test_state_serialization.py
    │   │   ├── remote/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_api_key_functionality.py
    │   │   │   ├── test_remote_conversation.py
    │   │   │   ├── test_remote_events_list.py
    │   │   │   ├── test_remote_fork.py
    │   │   │   ├── test_remote_request_logging.py
    │   │   │   ├── test_remote_state.py
    │   │   │   ├── test_run_exception_includes_conversation_id_remote.py
    │   │   │   ├── test_websocket_client.py
    │   │   │   └── test_websocket_subscription_ready.py
    │   │   ├── test_agent_final_response.py
    │   │   ├── test_agent_state_reassignment.py
    │   │   ├── test_ask_agent.py
    │   │   ├── test_atexit_cleanup.py
    │   │   ├── test_base_span_management.py
    │   │   ├── test_condense.py
    │   │   ├── test_conversation_execution_status_enum.py
    │   │   ├── test_conversation_factory.py
    │   │   ├── test_conversation_secrets_constructor.py
    │   │   ├── test_conversation_stats.py
    │   │   ├── test_directories.py
    │   │   ├── test_event_store.py
    │   │   ├── test_fifo_lock.py
    │   │   ├── test_generate_title.py
    │   │   ├── test_get_unmatched_actions.py
    │   │   ├── test_local_conversation_plugins.py
    │   │   ├── test_mcp_secrets_serialization_leak.py
    │   │   ├── test_remote_conversation_state_updates.py
    │   │   ├── test_repo_root_project_skills.py
    │   │   ├── test_resource_lock_manager.py
    │   │   ├── test_secret_source.py
    │   │   ├── test_secrets_manager.py
    │   │   ├── test_state_change_callback.py
    │   │   ├── test_stats_update_event_snapshot.py
    │   │   ├── test_switch_model.py
    │   │   ├── test_tags.py
    │   │   └── test_visualizer.py
    │   ├── critic/
    │   │   ├── __init__.py
    │   │   ├── api/
    │   │   │   └── test_template_render.py
    │   │   ├── test_critic.py
    │   │   ├── test_critic_client.py
    │   │   └── test_critic_display.py
    │   ├── event/
    │   │   ├── __init__.py
    │   │   ├── test_action_event_summary.py
    │   │   ├── test_dynamic_context_message_sequence.py
    │   │   ├── test_event_immutability.py
    │   │   ├── test_event_serialization.py
    │   │   ├── test_events_to_messages.py
    │   │   ├── test_llm_completion_log_event.py
    │   │   ├── test_non_executable_action_event.py
    │   │   ├── test_streaming.py
    │   │   └── test_system_prompt_event_visualize.py
    │   ├── extensions/
    │   │   ├── __init__.py
    │   │   ├── installation/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_installation_info.py
    │   │   │   ├── test_installation_manager.py
    │   │   │   ├── test_installation_metadata.py
    │   │   │   └── test_installation_utils.py
    │   │   └── test_fetch.py
    │   ├── git/
    │   │   ├── __init__.py
    │   │   ├── test_cached_repo.py
    │   │   ├── test_git_changes.py
    │   │   └── test_git_diff.py
    │   ├── hooks/
    │   │   ├── __init__.py
    │   │   ├── test_config.py
    │   │   ├── test_executor.py
    │   │   ├── test_integration.py
    │   │   └── test_manager.py
    │   ├── io/
    │   │   ├── __init__.py
    │   │   ├── test_filestore_cache.py
    │   │   └── test_local_filestore_security.py
    │   ├── llm/
    │   │   ├── __init__.py
    │   │   ├── auth/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_credentials.py
    │   │   │   └── test_openai.py
    │   │   ├── test_api_connection_error_retry.py
    │   │   ├── test_api_key_validation.py
    │   │   ├── test_chat_options.py
    │   │   ├── test_exception.py
    │   │   ├── test_exception_classifier.py
    │   │   ├── test_exception_mapping.py
    │   │   ├── test_llm.py
    │   │   ├── test_llm_completion.py
    │   │   ├── test_llm_fallback.py
    │   │   ├── test_llm_fncall_converter.py
    │   │   ├── test_llm_image_resizing.py
    │   │   ├── test_llm_json_storage.py
    │   │   ├── test_llm_litellm_extra_body.py
    │   │   ├── test_llm_log_completions_integration.py
    │   │   ├── test_llm_metrics.py
    │   │   ├── test_llm_no_response_retry.py
    │   │   ├── test_llm_pricing_passthrough.py
    │   │   ├── test_llm_profile_store.py
    │   │   ├── test_llm_registry.py
    │   │   ├── test_llm_retry_telemetry.py
    │   │   ├── test_llm_serialization.py
    │   │   ├── test_llm_telemetry.py
    │   │   ├── test_llm_timeout.py
    │   │   ├── test_message.py
    │   │   ├── test_message_backward_compatibility.py
    │   │   ├── test_message_from_chat_and_helpers.py
    │   │   ├── test_message_serialization.py
    │   │   ├── test_message_tool_call.py
    │   │   ├── test_model_canonical_name_resolution.py
    │   │   ├── test_model_features.py
    │   │   ├── test_model_list.py
    │   │   ├── test_prompt_caching_cross_conversation.py
    │   │   ├── test_pydantic_warning_suppression.py
    │   │   ├── test_reasoning_content.py
    │   │   ├── test_responses_parsing_and_kwargs.py
    │   │   ├── test_responses_serialization.py
    │   │   ├── test_subscription_mode.py
    │   │   ├── test_telemetry_policy.py
    │   │   ├── test_thinking_blocks.py
    │   │   └── test_vision_support.py
    │   ├── logger/
    │   │   ├── __init__.py
    │   │   └── test_litellm_log_suppression.py
    │   ├── marketplace/
    │   │   ├── __init__.py
    │   │   ├── test_deprecation.py
    │   │   └── test_marketplace.py
    │   ├── mcp/
    │   │   ├── __init__.py
    │   │   ├── test_create_mcp_tool.py
    │   │   ├── test_mcp_action_serialization.py
    │   │   ├── test_mcp_observation.py
    │   │   ├── test_mcp_security_risk.py
    │   │   ├── test_mcp_session_persistence.py
    │   │   ├── test_mcp_tool.py
    │   │   ├── test_mcp_tool_immutability.py
    │   │   ├── test_mcp_tool_kind_field.py
    │   │   ├── test_mcp_tool_serialization.py
    │   │   ├── test_mcp_tool_validation.py
    │   │   └── test_stateful_mcp.py
    │   ├── observability/
    │   │   ├── __init__.py
    │   │   └── test_laminar.py
    │   ├── plugin/
    │   │   ├── __init__.py
    │   │   ├── test_installed_plugins.py
    │   │   ├── test_plugin_fetch.py
    │   │   ├── test_plugin_fetch_integration.py
    │   │   ├── test_plugin_loader.py
    │   │   ├── test_plugin_loading.py
    │   │   ├── test_plugin_merging.py
    │   │   └── test_source.py
    │   ├── security/
    │   │   ├── __init__.py
    │   │   ├── defense_in_depth/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_adversarial.py
    │   │   │   ├── test_ensemble.py
    │   │   │   ├── test_field_cap.py
    │   │   │   ├── test_pattern.py
    │   │   │   ├── test_policy_rails.py
    │   │   │   └── test_serialization.py
    │   │   ├── grayswan/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_grayswan_analyzer.py
    │   │   │   └── test_grayswan_utils.py
    │   │   ├── test_confirmation_policy.py
    │   │   ├── test_llm_security_analyzer.py
    │   │   ├── test_security_analyzer.py
    │   │   └── test_security_risk.py
    │   ├── settings/
    │   │   ├── __init__.py
    │   │   └── test_acp_providers.py
    │   ├── skills/
    │   │   ├── __init__.py
    │   │   ├── test_agentskills_fields.py
    │   │   ├── test_extensions_ref.py
    │   │   ├── test_installed_skills.py
    │   │   ├── test_load_project_skills.py
    │   │   ├── test_load_public_skills.py
    │   │   ├── test_load_user_skills.py
    │   │   ├── test_mcp_config_expansion.py
    │   │   ├── test_mcp_json.py
    │   │   ├── test_resource_directories.py
    │   │   ├── test_skill_commands.py
    │   │   ├── test_skill_info.py
    │   │   ├── test_skill_md_convention.py
    │   │   ├── test_skill_no_header.py
    │   │   ├── test_skill_serialization.py
    │   │   ├── test_skill_utils.py
    │   │   ├── test_task_skill.py
    │   │   ├── test_validation_improvements.py
    │   │   └── test_validation_prompt.py
    │   ├── subagent/
    │   │   ├── __init__.py
    │   │   ├── test_subagent_loader.py
    │   │   ├── test_subagent_registry.py
    │   │   └── test_subagent_schema.py
    │   ├── test_agent_step_bounded_scan.py
    │   ├── test_banner.py
    │   ├── test_import_performance.py
    │   ├── test_settings.py
    │   ├── test_socks_proxy_support.py
    │   ├── tool/
    │   │   ├── __init__.py
    │   │   ├── test_builtins.py
    │   │   ├── test_invoke_skill.py
    │   │   ├── test_mcp_schema.py
    │   │   ├── test_py_type.py
    │   │   ├── test_registry.py
    │   │   ├── test_schema_immutability.py
    │   │   ├── test_switch_llm.py
    │   │   ├── test_to_responses_tool.py
    │   │   ├── test_to_responses_tool_security.py
    │   │   ├── test_to_responses_tool_summary.py
    │   │   ├── test_tool.py
    │   │   ├── test_tool_call_output_coercion.py
    │   │   ├── test_tool_definition.py
    │   │   ├── test_tool_immutability.py
    │   │   └── test_tool_serialization.py
    │   ├── utils/
    │   │   ├── __init__.py
    │   │   ├── test_async_utils.py
    │   │   ├── test_cipher.py
    │   │   ├── test_command.py
    │   │   ├── test_deprecation.py
    │   │   ├── test_discriminated_union.py
    │   │   ├── test_github.py
    │   │   ├── test_model_prompt_spec.py
    │   │   ├── test_paging.py
    │   │   ├── test_path.py
    │   │   ├── test_pydantic_secrets.py
    │   │   ├── test_redact.py
    │   │   ├── test_subclass_cache.py
    │   │   ├── test_truncate.py
    │   │   └── test_visualize.py
    │   └── workspace/
    │       ├── __init__.py
    │       ├── conftest.py
    │       └── remote/
    │           ├── __init__.py
    │           ├── test_async_remote_workspace.py
    │           ├── test_client_base_url.py
    │           ├── test_multiple_commands_isolation.py
    │           ├── test_polling_duplicates_output.py
    │           ├── test_remote_workspace.py
    │           └── test_remote_workspace_mixin.py
    ├── tools/
    │   ├── __init__.py
    │   ├── apply_patch/
    │   │   └── test_apply_patch_executor.py
    │   ├── browser_use/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_browser_cleanup.py
    │   │   ├── test_browser_executor.py
    │   │   ├── test_browser_executor_e2e.py
    │   │   ├── test_browser_initialization.py
    │   │   ├── test_browser_observation.py
    │   │   ├── test_browser_toolset.py
    │   │   ├── test_chromium_detection.py
    │   │   ├── test_recording_flush.py
    │   │   └── test_vnc_integration.py
    │   ├── delegate/
    │   │   ├── test_delegation.py
    │   │   └── test_visualizer.py
    │   ├── file_editor/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_basic_operations.py
    │   │   ├── test_error_handling.py
    │   │   ├── test_exceptions.py
    │   │   ├── test_file_editor_tool.py
    │   │   ├── test_file_validation.py
    │   │   ├── test_memory_usage.py
    │   │   ├── test_schema.py
    │   │   ├── test_view_supported_binary_files.py
    │   │   ├── test_visualize_diff.py
    │   │   ├── test_workspace_root.py
    │   │   └── utils/
    │   │       ├── __init__.py
    │   │       ├── test_encoding.py
    │   │       ├── test_file_cache.py
    │   │       ├── test_history.py
    │   │       └── test_shell_utils.py
    │   ├── gemini/
    │   │   ├── conftest.py
    │   │   ├── edit/
    │   │   │   ├── __init__.py
    │   │   │   └── test_edit.py
    │   │   ├── list_directory/
    │   │   │   ├── __init__.py
    │   │   │   └── test_list_directory.py
    │   │   ├── read_file/
    │   │   │   ├── __init__.py
    │   │   │   └── test_read_file.py
    │   │   ├── test_cross_tool_locking.py
    │   │   └── write_file/
    │   │       ├── __init__.py
    │   │       └── test_write_file.py
    │   ├── glob/
    │   │   ├── __init__.py
    │   │   ├── test_consistency.py
    │   │   ├── test_glob_executor.py
    │   │   └── test_glob_tool.py
    │   ├── grep/
    │   │   ├── __init__.py
    │   │   ├── test_consistency.py
    │   │   ├── test_grep_executor.py
    │   │   └── test_grep_tool.py
    │   ├── planning_file_editor/
    │   │   └── test_planning_file_editor_tool.py
    │   ├── task/
    │   │   ├── test_task_manager.py
    │   │   ├── test_task_manager_thread_safety.py
    │   │   └── test_task_tool_set.py
    │   ├── terminal/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_conversation_cleanup.py
    │   │   ├── test_escape_filter.py
    │   │   ├── test_heredoc_chunked_send.py
    │   │   ├── test_large_environment.py
    │   │   ├── test_observation_truncation.py
    │   │   ├── test_pool_integration.py
    │   │   ├── test_ps1_corruption.py
    │   │   ├── test_schema.py
    │   │   ├── test_secrets_masking.py
    │   │   ├── test_send_keys.py
    │   │   ├── test_session_factory.py
    │   │   ├── test_shell_path_configuration.py
    │   │   ├── test_shutdown_handling.py
    │   │   ├── test_terminal_exit_code_top_level.py
    │   │   ├── test_terminal_parsing.py
    │   │   ├── test_terminal_ps1_metadata.py
    │   │   ├── test_terminal_reset.py
    │   │   ├── test_terminal_session.py
    │   │   ├── test_terminal_tool.py
    │   │   ├── test_terminal_tool_auto_detection.py
    │   │   ├── test_tmux_pane_pool.py
    │   │   ├── test_windows_ctrl_c.py
    │   │   └── test_windows_terminal.py
    │   ├── test_builtin_agents.py
    │   ├── test_init.py
    │   ├── test_planning_preset.py
    │   ├── test_tool_name_consistency.py
    │   ├── test_tool_registration_check.py
    │   ├── test_working_dir_standardization.py
    │   └── tom_consult/
    │       ├── __init__.py
    │       └── test_tom_consult_tool.py
    └── workspace/
        ├── test_api_remote_workspace.py
        ├── test_apptainer_workspace.py
        ├── test_cloud_workspace.py
        ├── test_cloud_workspace_automation_tags.py
        ├── test_cloud_workspace_repos.py
        ├── test_cloud_workspace_sdk_settings.py
        ├── test_docker_workspace.py
        └── test_workspace_pause_resume.py
Download .txt
Showing preview only (1,131K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (10864 symbols across 869 files)

FILE: .agents/skills/manage-evals/scripts/manage_evals.py
  function fetch_json (line 51) | def fetch_json(url: str) -> dict[str, Any] | None:
  function fetch_text (line 66) | def fetch_text(url: str) -> str | None:
  function parse_run_path (line 81) | def parse_run_path(path: str) -> tuple[str, str, str]:
  function get_report (line 96) | def get_report(run_path: str) -> dict[str, Any] | None:
  function get_params (line 102) | def get_params(run_path: str) -> dict[str, Any] | None:
  function get_metadata_for_date (line 108) | def get_metadata_for_date(date_str: str) -> list[str]:
  function find_baseline_run (line 117) | def find_baseline_run(
  function compute_diff (line 173) | def compute_diff(
  function github_api_request (line 324) | def github_api_request(
  function post_github_comment (line 349) | def post_github_comment(repo: str, pr_number: int, body: str, token: str...
  function trigger_eval (line 357) | def trigger_eval(
  function _require_token (line 415) | def _require_token() -> str:
  function cmd_trigger (line 424) | def cmd_trigger(args: argparse.Namespace) -> None:
  function cmd_compare (line 443) | def cmd_compare(args: argparse.Namespace) -> None:
  function main (line 511) | def main() -> None:

FILE: .github/run-eval/resolve_model_config.py
  function _sigterm_handler (line 23) | def _sigterm_handler(signum: int, _frame: object) -> None:
  function error_exit (line 343) | def error_exit(msg: str, exit_code: int = 1) -> None:
  function get_required_env (line 349) | def get_required_env(key: str) -> str:
  function find_models_by_id (line 357) | def find_models_by_id(model_ids: list[str]) -> list[dict]:
  function check_model (line 380) | def check_model(
  function _check_proxy_reachable (line 465) | def _check_proxy_reachable(
  function run_preflight_check (line 490) | def run_preflight_check(models: list[dict[str, Any]]) -> bool:
  function main (line 551) | def main() -> None:

FILE: .github/run-eval/validate_sdk_ref.py
  function is_semantic_version (line 36) | def is_semantic_version(ref: str) -> bool:
  function is_commit_sha (line 41) | def is_commit_sha(ref: str) -> bool:
  function is_valid_branch_name (line 46) | def is_valid_branch_name(ref: str) -> bool:
  function validate_branch_name (line 59) | def validate_branch_name(branch_name: str, input_name: str) -> tuple[boo...
  function validate_sdk_ref (line 70) | def validate_sdk_ref(sdk_ref: str, allow_unreleased: bool) -> tuple[bool...
  function main (line 92) | def main() -> None:

FILE: .github/scripts/check_agent_server_rest_api_breakage.py
  function _read_version_from_pyproject (line 103) | def _read_version_from_pyproject(pyproject: Path) -> str:
  function _fetch_pypi_metadata (line 113) | def _fetch_pypi_metadata(distribution: str) -> dict:
  function _get_baseline_version (line 123) | def _get_baseline_version(distribution: str, current: str) -> str | None:
  function _generate_openapi_from_source_tree (line 148) | def _generate_openapi_from_source_tree(source_tree: Path, label: str) ->...
  function _generate_current_openapi (line 174) | def _generate_current_openapi() -> dict | None:
  function _generate_openapi_for_git_ref (line 178) | def _generate_openapi_for_git_ref(git_ref: str) -> dict | None:
  function _dotted_name (line 206) | def _dotted_name(node: ast.AST) -> str | None:
  function _find_sdk_deprecated_fastapi_routes_in_file (line 217) | def _find_sdk_deprecated_fastapi_routes_in_file(
  function _find_sdk_deprecated_fastapi_routes (line 286) | def _find_sdk_deprecated_fastapi_routes(repo_root: Path) -> list[str]:
  function _filter_public_rest_openapi (line 296) | def _filter_public_rest_openapi(schema: dict) -> dict:
  function _find_deprecation_policy_errors (line 307) | def _find_deprecation_policy_errors(schema: dict) -> list[str]:
  function _parse_openapi_deprecation_description (line 333) | def _parse_openapi_deprecation_description(
  function _version_ge (line 353) | def _version_ge(current: str, target: str) -> bool:
  function _get_openapi_operation (line 362) | def _get_openapi_operation(schema: dict, path: str, method: str) -> dict...
  function _validate_removed_operations (line 374) | def _validate_removed_operations(
  function _iter_schema_properties (line 434) | def _iter_schema_properties(schema: dict):
  function _removed_property_name (line 453) | def _removed_property_name(change: dict) -> str | None:
  function _validate_removed_schema_properties (line 464) | def _validate_removed_schema_properties(
  function _is_union_property_removal_artifact (line 560) | def _is_union_property_removal_artifact(change: dict) -> bool:
  function _is_union_type_change_artifact (line 577) | def _is_union_type_change_artifact(change: dict) -> bool:
  function _split_breaking_changes (line 582) | def _split_breaking_changes(
  function _normalize_openapi_for_oasdiff (line 623) | def _normalize_openapi_for_oasdiff(schema: dict) -> dict:
  function _run_oasdiff_breakage_check (line 665) | def _run_oasdiff_breakage_check(
  function main (line 704) | def main() -> int:

FILE: .github/scripts/check_deprecations.py
  class PackageConfig (line 48) | class PackageConfig:
  class DeprecationRecord (line 81) | class DeprecationRecord:
  function _load_current_version (line 91) | def _load_current_version(pyproject: Path) -> str:
  function _iter_python_files (line 101) | def _iter_python_files(root: Path) -> Iterator[Path]:
  function _parse_removed_value (line 108) | def _parse_removed_value(
  function _parse_deprecated_value (line 175) | def _parse_deprecated_value(
  function _safe_int_literal (line 197) | def _safe_int_literal(node: ast.AST) -> int:
  function _extract_kw (line 205) | def _extract_kw(call: ast.Call, name: str) -> ast.AST | None:
  function _extract_string_literal (line 212) | def _extract_string_literal(node: ast.AST | None) -> str | None:
  function _extract_string_sequence (line 218) | def _extract_string_sequence(node: ast.AST | None) -> tuple[str, ...] | ...
  function _extract_route_details (line 231) | def _extract_route_details(call: ast.Call) -> tuple[tuple[str, str], ...]:
  function _parse_rest_route_deprecation_docstring (line 258) | def _parse_rest_route_deprecation_docstring(
  function _gather_rest_route_deprecations (line 285) | def _gather_rest_route_deprecations(
  function _gather_decorators (line 328) | def _gather_decorators(
  function _gather_warn_calls (line 370) | def _gather_warn_calls(
  function _build_identifier (line 428) | def _build_identifier(node: ast.AST) -> str:
  function _attach_parents (line 441) | def _attach_parents(tree: ast.AST) -> None:
  function _collect_records (line 447) | def _collect_records(files: Iterable[Path], *, package: str) -> list[Dep...
  function _collect_rest_route_records (line 457) | def _collect_rest_route_records(
  function _version_ge (line 467) | def _version_ge(current: str, target: str) -> bool:
  function _should_fail (line 476) | def _should_fail(current_version: str, record: DeprecationRecord) -> bool:
  function _format_record (line 494) | def _format_record(record: DeprecationRecord) -> str:
  function main (line 516) | def main(argv: Sequence[str] | None = None) -> int:

FILE: .github/scripts/check_docstrings.py
  class Violation (line 46) | class Violation:
  function should_skip (line 57) | def should_skip(path: Path) -> bool:
  function check_repl_examples (line 63) | def check_repl_examples(
  function check_unfenced_shell_config (line 94) | def check_unfenced_shell_config(
  function check_docstring (line 148) | def check_docstring(
  function get_docstrings_from_file (line 161) | def get_docstrings_from_file(file: Path) -> list[tuple[str, str, int]]:
  function is_strict_file (line 199) | def is_strict_file(file: Path, repo_root: Path) -> bool:
  function check_file (line 208) | def check_file(file: Path, repo_root: Path) -> list[Violation]:
  function main (line 222) | def main() -> int:

FILE: .github/scripts/check_documented_examples.py
  function find_documented_examples (line 18) | def find_documented_examples(docs_path: Path) -> set[str]:
  function find_agent_sdk_examples (line 53) | def find_agent_sdk_examples(agent_sdk_path: Path) -> set[str]:
  function resolve_paths (line 98) | def resolve_paths() -> tuple[Path, Path]:
  function main (line 149) | def main() -> None:

FILE: .github/scripts/check_duplicate_example_numbers.py
  function find_duplicate_numbers (line 19) | def find_duplicate_numbers(examples_dir: Path) -> dict[str, list[str]]:
  function main (line 67) | def main() -> None:

FILE: .github/scripts/check_sdk_api_breakage.py
  class PackageConfig (line 49) | class PackageConfig:
  class DeprecationMetadata (line 58) | class DeprecationMetadata:
  class DeprecatedSymbols (line 64) | class DeprecatedSymbols:
  function read_version_from_pyproject (line 104) | def read_version_from_pyproject(path: str) -> str:
  function _read_pyproject (line 115) | def _read_pyproject(path: str) -> dict:
  function _bool_env (line 120) | def _bool_env(name: str) -> bool:
  function _get_dependency_spec (line 125) | def _get_dependency_spec(project_data: dict, dependency: str) -> str | N...
  function _min_version_from_requirement (line 133) | def _min_version_from_requirement(req_str: str) -> pkg_version.Version |...
  function _git_show_file (line 160) | def _git_show_file(ref: str, rel_path: str) -> str | None:
  function _load_base_pyproject (line 173) | def _load_base_pyproject(base_ref: str) -> dict | None:
  function _check_acp_version_bump (line 192) | def _check_acp_version_bump(repo_root: str) -> int:
  function _parse_version (line 249) | def _parse_version(v: str) -> pkg_version.Version:
  function _parse_string_kwarg (line 254) | def _parse_string_kwarg(call: ast.Call, name: str) -> str | None:
  function _minimum_removed_in (line 265) | def _minimum_removed_in(deprecated_in: str) -> str:
  function _deprecation_schedule_errors (line 270) | def _deprecation_schedule_errors(
  function get_pypi_baseline_version (line 319) | def get_pypi_baseline_version(pkg: str, current: str | None) -> str | None:
  function ensure_griffe (line 367) | def ensure_griffe() -> None:
  function _escape_newlines_in_string_literals (line 389) | def _escape_newlines_in_string_literals(text: str) -> str:
  function _parse_field_call (line 426) | def _parse_field_call(value: object) -> ast.Call | None:
  function _filter_field_metadata_kwargs (line 453) | def _filter_field_metadata_kwargs(call: ast.Call) -> ast.Call:
  function _is_field_metadata_only_change (line 462) | def _is_field_metadata_only_change(old_val: object, new_val: object) -> ...
  function _member_deprecation_metadata (line 486) | def _member_deprecation_metadata(
  function _was_deprecated (line 515) | def _was_deprecated(
  function _collect_breakages_pairs (line 523) | def _collect_breakages_pairs(
  function _extract_exported_names (line 616) | def _extract_exported_names(module) -> set[str]:
  function _check_version_bump (line 656) | def _check_version_bump(prev: str, new_version: str, total_breaks: int) ...
  function _resolve_griffe_object (line 691) | def _resolve_griffe_object(
  function _load_current (line 725) | def _load_current(
  function _load_prev_from_pypi (line 741) | def _load_prev_from_pypi(
  function _find_deprecated_symbols (line 763) | def _find_deprecated_symbols(source_root: Path) -> DeprecatedSymbols:
  function _extract_string_literal (line 884) | def _extract_string_literal(node: ast.AST) -> str | None:
  function _get_source_root (line 891) | def _get_source_root(griffe_root: object) -> Path | None:
  function _compute_breakages (line 899) | def _compute_breakages(
  function _check_package (line 990) | def _check_package(griffe_module, repo_root: str, cfg: PackageConfig) ->...
  function main (line 1036) | def main() -> int:

FILE: .github/scripts/check_version_bumps.py
  class VersionChange (line 27) | class VersionChange:
  function _read_version_from_pyproject_text (line 34) | def _read_version_from_pyproject_text(text: str, source: str) -> str:
  function _read_current_version (line 42) | def _read_current_version(repo_root: Path, pyproject: Path) -> str:
  function _read_version_from_git_ref (line 49) | def _read_version_from_git_ref(repo_root: Path, git_ref: str, pyproject:...
  function _base_ref_candidates (line 65) | def _base_ref_candidates(base_ref: str) -> list[str]:
  function find_version_changes (line 71) | def find_version_changes(repo_root: Path, base_ref: str) -> list[Version...
  function get_release_pr_version (line 106) | def get_release_pr_version(
  function validate_version_changes (line 123) | def validate_version_changes(
  function main (line 163) | def main() -> int:

FILE: .github/scripts/update_sdk_ref_default.py
  function update_sdk_ref_default (line 26) | def update_sdk_ref_default(new_version: str, dry_run: bool = False) -> b...
  function main (line 92) | def main() -> int:

FILE: examples/01_standalone_sdk/02_custom_tools.py
  class GrepAction (line 40) | class GrepAction(Action):
  class GrepObservation (line 50) | class GrepObservation(Observation):
    method to_llm_content (line 56) | def to_llm_content(self) -> Sequence[TextContent | ImageContent]:
  class GrepExecutor (line 73) | class GrepExecutor(ToolExecutor[GrepAction, GrepObservation]):
    method __init__ (line 74) | def __init__(self, terminal: TerminalExecutor):
    method __call__ (line 77) | def __call__(self, action: GrepAction, conversation=None) -> GrepObser...
  class GrepTool (line 123) | class GrepTool(ToolDefinition[GrepAction, GrepObservation]):
    method create (line 127) | def create(
  class BashAndGrepToolSet (line 172) | class BashAndGrepToolSet(ToolDefinition[Action, Observation]):
    method create (line 176) | def create(cls, conv_state, **params) -> Sequence[ToolDefinition]:
  function conversation_callback (line 203) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/03_activate_skill.py
  function conversation_callback (line 101) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/04_confirmation_mode_example.py
  function _print_action_preview (line 23) | def _print_action_preview(pending_actions) -> None:
  function confirm_in_console (line 30) | def confirm_in_console(pending_actions) -> bool:
  function run_until_finished (line 56) | def run_until_finished(conversation: BaseConversation, confirmer: Callab...

FILE: examples/01_standalone_sdk/05_use_llm_registry.py
  function conversation_callback (line 53) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/06_interactive_terminal_w_reasoning.py
  function conversation_callback (line 46) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/07_mcp_integration.py
  function conversation_callback (line 58) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/08_mcp_with_oauth.py
  function conversation_callback (line 48) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/10_persistence.py
  function conversation_callback (line 52) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/11_async.py
  function callback_coro (line 59) | async def callback_coro(event: Event):
  function run_conversation (line 65) | def run_conversation(callback: ConversationCallbackType):
  function main (line 78) | async def main():

FILE: examples/01_standalone_sdk/12_custom_secrets.py
  class MySecretSource (line 39) | class MySecretSource(SecretSource):
    method get_value (line 40) | def get_value(self) -> str:

FILE: examples/01_standalone_sdk/13_get_llm_metrics.py
  function conversation_callback (line 47) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/14_context_condenser.py
  function conversation_callback (line 66) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/15_browser_use.py
  function conversation_callback (line 53) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/16_llm_security_analyzer.py
  function _print_blocked_actions (line 29) | def _print_blocked_actions(pending_actions) -> None:
  function confirm_high_risk_in_console (line 36) | def confirm_high_risk_in_console(pending_actions) -> bool:
  function run_until_finished_with_security (line 65) | def run_until_finished_with_security(

FILE: examples/01_standalone_sdk/17_image_input.py
  function _make_png_data_url (line 38) | def _make_png_data_url(width: int, height: int, color: str = "red") -> str:
  function conversation_callback (line 76) | def conversation_callback(event: Event) -> None:

FILE: examples/01_standalone_sdk/18_send_message_while_processing.py
  function timestamp (line 84) | def timestamp() -> str:

FILE: examples/01_standalone_sdk/19_llm_routing.py
  function conversation_callback (line 54) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/20_stuck_detector.py
  function conversation_callback (line 34) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/22_anthropic_thinking.py
  function show_thinking (line 38) | def show_thinking(event: Event):

FILE: examples/01_standalone_sdk/23_responses_reasoning.py
  function conversation_callback (line 52) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/24_planning_agent_workflow.py
  function get_event_content (line 25) | def get_event_content(event):

FILE: examples/01_standalone_sdk/25_agent_delegation.py
  function create_lodging_planner (line 41) | def create_lodging_planner(llm: LLM) -> Agent:
  function create_activities_planner (line 65) | def create_activities_planner(llm: LLM) -> Agent:

FILE: examples/01_standalone_sdk/26_custom_visualizer.py
  class MinimalVisualizer (line 26) | class MinimalVisualizer(ConversationVisualizerBase):
    method on_event (line 29) | def on_event(self, event: Event) -> None:

FILE: examples/01_standalone_sdk/28_ask_agent_example.py
  class MinimalVisualizer (line 50) | class MinimalVisualizer(ConversationVisualizerBase):
    method on_event (line 55) | def on_event(self, event: Event) -> None:
  function timestamp (line 68) | def timestamp() -> str:

FILE: examples/01_standalone_sdk/29_llm_streaming.py
  function on_token (line 42) | def on_token(chunk: ModelResponseStream) -> None:

FILE: examples/01_standalone_sdk/31_iterative_refinement.py
  function setup_workspace (line 31) | def setup_workspace() -> tuple[Path, Path, Path]:
  function create_sample_cobol_files (line 45) | def create_sample_cobol_files(cobol_dir: Path) -> list[str]:
  function get_refactoring_prompt (line 240) | def get_refactoring_prompt(
  function get_critique_prompt (line 283) | def get_critique_prompt(
  function parse_critique_score (line 343) | def parse_critique_score(critique_file: Path) -> float:
  function run_iterative_refinement (line 365) | def run_iterative_refinement() -> None:

FILE: examples/01_standalone_sdk/32_configurable_security_policy.py
  function conversation_callback (line 116) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/34_critic_example.py
  function get_required_env (line 41) | def get_required_env(name: str) -> str:
  function get_default_critic (line 51) | def get_default_critic(llm: LLM) -> CriticBase | None:

FILE: examples/01_standalone_sdk/38_browser_session_recording.py
  function conversation_callback (line 62) | def conversation_callback(event: Event):

FILE: examples/01_standalone_sdk/41_task_tool_set.py
  function create_animal_expert (line 31) | def create_animal_expert(llm: LLM) -> Agent:

FILE: examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py
  function main (line 47) | def main():

FILE: examples/01_standalone_sdk/45_parallel_tool_execution.py
  function create_code_analyst (line 42) | def create_code_analyst(llm: LLM) -> Agent:
  function create_doc_reviewer (line 68) | def create_doc_reviewer(llm: LLM) -> Agent:
  function create_dependency_checker (line 94) | def create_dependency_checker(llm: LLM) -> Agent:
  function _analyze_conversation (line 191) | def _analyze_conversation(events_dir: Path) -> dict[str, list[str]]:

FILE: examples/02_remote_agent_server/01_convo_with_local_agent_server.py
  function _stream_output (line 23) | def _stream_output(stream, prefix, target_stream):
  class ManagedAPIServer (line 36) | class ManagedAPIServer:
    method __init__ (line 39) | def __init__(self, port: int = 8000, host: str = "127.0.0.1"):
    method __enter__ (line 47) | def __enter__(self):
    method __exit__ (line 111) | def __exit__(self, exc_type, exc_val, exc_tb):
  function event_callback (line 158) | def event_callback(event):
  function hook_event_tracker (line 210) | def hook_event_tracker(event):

FILE: examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py
  function detect_platform (line 31) | def detect_platform():
  function get_server_image (line 39) | def get_server_image():
  function event_callback (line 78) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py
  function detect_platform (line 26) | def detect_platform():
  function get_server_image (line 34) | def get_server_image():
  function event_callback (line 75) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py
  function event_callback (line 64) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/05_vscode_with_docker_sandboxed_server.py
  function detect_platform (line 28) | def detect_platform():
  function get_server_image (line 36) | def get_server_image():
  function event_callback (line 68) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/06_custom_tool/custom_tools/log_data.py
  class LogLevel (line 30) | class LogLevel(StrEnum):
  class LogDataAction (line 39) | class LogDataAction(Action):
  class LogDataObservation (line 53) | class LogDataObservation(Observation):
    method to_llm_content (line 61) | def to_llm_content(self) -> Sequence[TextContent | ImageContent]:
  class LogDataExecutor (line 81) | class LogDataExecutor(ToolExecutor[LogDataAction, LogDataObservation]):
    method __init__ (line 84) | def __init__(self, log_file: str = DEFAULT_LOG_FILE):
    method __call__ (line 92) | def __call__(
  class LogDataTool (line 154) | class LogDataTool(ToolDefinition[LogDataAction, LogDataObservation]):
    method create (line 158) | def create(cls, conv_state, **params) -> Sequence[ToolDefinition]:  # ...

FILE: examples/02_remote_agent_server/06_custom_tool/main.py
  function detect_platform (line 65) | def detect_platform():
  function event_callback (line 150) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/07_convo_with_cloud_workspace.py
  function event_callback (line 66) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py
  function detect_platform (line 31) | def detect_platform():
  function get_server_image (line 39) | def get_server_image():
  function event_callback (line 74) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py
  function event_callback (line 65) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/10_cloud_workspace_share_credentials.py
  function event_callback (line 72) | def event_callback(event) -> None:

FILE: examples/02_remote_agent_server/11_conversation_fork.py
  function _stream_output (line 30) | def _stream_output(stream, prefix, target_stream):
  class ManagedAPIServer (line 42) | class ManagedAPIServer:
    method __init__ (line 45) | def __init__(self, port: int = 8000, host: str = "127.0.0.1"):
    method __enter__ (line 51) | def __enter__(self):
    method __exit__ (line 94) | def __exit__(self, *args):

FILE: examples/02_remote_agent_server/12_settings_and_secrets_api.py
  function _stream_output (line 34) | def _stream_output(stream, prefix, target_stream):
  class ManagedAPIServer (line 47) | class ManagedAPIServer:
    method __init__ (line 50) | def __init__(self, port: int = 8000, host: str = "127.0.0.1"):
    method __enter__ (line 58) | def __enter__(self):
    method __exit__ (line 118) | def __exit__(self, exc_type, exc_val, exc_tb):

FILE: examples/02_remote_agent_server/13_workspace_get_llm.py
  function _stream_output (line 40) | def _stream_output(stream, prefix, target_stream):
  class ManagedAPIServer (line 53) | class ManagedAPIServer:
    method __init__ (line 60) | def __init__(self, port: int = 8000, host: str = "127.0.0.1"):
    method __enter__ (line 70) | def __enter__(self):
    method __exit__ (line 132) | def __exit__(self, exc_type, exc_val, exc_tb):

FILE: examples/03_github_workflows/01_basic_action/agent_script.py
  function is_url (line 41) | def is_url(path: str) -> bool:
  function load_prompt (line 50) | def load_prompt(prompt_location: str) -> str:
  function main (line 76) | def main():

FILE: examples/03_github_workflows/03_todo_management/agent_script.py
  function process_todo (line 38) | def process_todo(todo_data: dict):
  function main (line 99) | def main():

FILE: examples/03_github_workflows/03_todo_management/scanner.py
  function scan_file_for_todos (line 30) | def scan_file_for_todos(
  function scan_directory (line 120) | def scan_directory(
  function main (line 152) | def main():

FILE: examples/03_github_workflows/04_datadog_debugging/datadog_debugging.py
  function validate_environment (line 56) | def validate_environment():
  function fetch_datadog_errors (line 80) | def fetch_datadog_errors(
  function create_unique_identifier (line 250) | def create_unique_identifier(query: str, errors_data: dict) -> str:
  function search_existing_issue (line 271) | def search_existing_issue(
  function create_github_issue (line 319) | def create_github_issue(
  function format_issue_body (line 369) | def format_issue_body(
  function setup_github_issue (line 455) | def setup_github_issue(
  function create_debugging_prompt (line 516) | def create_debugging_prompt(
  function main (line 543) | def main():
  function run_debugging_session (line 655) | def run_debugging_session(

FILE: examples/03_github_workflows/05_posthog_debugging/posthog_debugging.py
  function get_posthog_host (line 58) | def get_posthog_host() -> str:
  function _extract_issue_title (line 64) | def _extract_issue_title(examples: list[dict], query: str) -> str:
  function _fetch_event_timeline (line 125) | def _fetch_event_timeline(
  function validate_environment (line 214) | def validate_environment():
  function fetch_posthog_events (line 238) | def fetch_posthog_events(
  function create_unique_identifier (line 457) | def create_unique_identifier(query: str, events_data: dict) -> str:
  function search_existing_issue (line 478) | def search_existing_issue(
  function create_github_issue (line 531) | def create_github_issue(
  function update_github_issue (line 581) | def update_github_issue(
  function _extract_exception_info (line 616) | def _extract_exception_info(properties: dict | str) -> dict | None:
  function _format_stack_trace (line 673) | def _format_stack_trace(stack_frames: list[dict]) -> str:
  function format_issue_body (line 698) | def format_issue_body(
  function setup_github_issue (line 825) | def setup_github_issue(
  function create_debugging_prompt (line 882) | def create_debugging_prompt(
  function main (line 910) | def main():
  function run_debugging_session (line 1021) | def run_debugging_session(

FILE: examples/05_skills_and_plugins/02_loading_plugins/main.py
  function print_state (line 51) | def print_state(label: str, installed_dir: Path) -> None:
  function demo_conversation_with_github_plugin (line 69) | def demo_conversation_with_github_plugin(llm: LLM) -> None:
  function demo_install_local_plugin (line 123) | def demo_install_local_plugin(installed_dir: Path) -> str:
  function demo_install_github_plugin (line 136) | def demo_install_github_plugin(installed_dir: Path) -> None:
  function demo_list_and_load_plugins (line 170) | def demo_list_and_load_plugins(installed_dir: Path) -> None:
  function demo_enable_disable_plugin (line 187) | def demo_enable_disable_plugin(installed_dir: Path, plugin_name: str) ->...
  function demo_uninstall_plugins (line 214) | def demo_uninstall_plugins(installed_dir: Path) -> None:

FILE: examples/05_skills_and_plugins/03_managing_installed_skills/main.py
  function print_state (line 33) | def print_state(label: str, installed_dir: Path) -> None:
  function demo_install_skills (line 51) | def demo_install_skills(installed_dir: Path) -> list[str]:
  function demo_list_and_load_skills (line 70) | def demo_list_and_load_skills(installed_dir: Path) -> None:
  function demo_enable_disable_skill (line 90) | def demo_enable_disable_skill(installed_dir: Path, skill_name: str) -> N...
  function demo_uninstall_skill (line 117) | def demo_uninstall_skill(

FILE: openhands-agent-server/openhands/agent_server/__main__.py
  function _get_internal_server_url (line 24) | def _get_internal_server_url(host: str, port: int) -> str:
  function extend_python_path (line 47) | def extend_python_path(extra_paths: str | None) -> None:
  function preload_modules (line 87) | def preload_modules(modules_arg: str | None) -> None:
  function check_browser (line 111) | def check_browser():
  class LoggingServer (line 148) | class LoggingServer(uvicorn.Server):
    method handle_exit (line 156) | def handle_exit(self, sig: int, frame: FrameType | None) -> None:
  function _setup_crash_diagnostics (line 167) | def _setup_crash_diagnostics() -> None:
  function main (line 183) | def main() -> None:

FILE: openhands-agent-server/openhands/agent_server/_secrets_exposure.py
  function get_config (line 20) | def get_config(request: Request):
  function get_cipher (line 31) | def get_cipher(request: Request) -> Cipher | None:
  function parse_expose_secrets_header (line 36) | def parse_expose_secrets_header(request: Request) -> ExposeSecretsMode |...
  function build_expose_context (line 64) | def build_expose_context(
  function _has_missing_cipher_cause (line 73) | def _has_missing_cipher_cause(exc: BaseException) -> bool:
  function decrypt_incoming_llm_secrets (line 84) | def decrypt_incoming_llm_secrets(llm: LLM, cipher: Cipher) -> LLM:
  function translate_missing_cipher (line 109) | def translate_missing_cipher() -> Iterator[None]:

FILE: openhands-agent-server/openhands/agent_server/api.py
  function _default_server_tmux_tmpdir (line 64) | def _default_server_tmux_tmpdir() -> Path:
  function _ensure_server_tmux_tmpdir (line 68) | def _ensure_server_tmux_tmpdir() -> tuple[Path, bool]:
  function _cleanup_stale_tmux_sessions (line 83) | def _cleanup_stale_tmux_sessions() -> None:
  function api_lifespan (line 114) | async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
  function _get_root_path (line 219) | def _get_root_path(config: Config) -> str:
  function _create_fastapi_instance (line 227) | def _create_fastapi_instance(config: Config) -> FastAPI:
  function _find_http_exception (line 243) | def _find_http_exception(exc: BaseExceptionGroup) -> HTTPException | None:
  function _add_api_routes (line 263) | def _add_api_routes(app: FastAPI, config: Config) -> None:
  function _setup_static_files (line 316) | def _setup_static_files(app: FastAPI, config: Config) -> None:
  function _sanitize_validation_errors (line 354) | def _sanitize_validation_errors(errors: Sequence[Any]) -> list[dict]:
  function _add_exception_handlers (line 377) | def _add_exception_handlers(api: FastAPI) -> None:
  function create_app (line 502) | def create_app(config: Config | None = None) -> FastAPI:

FILE: openhands-agent-server/openhands/agent_server/auth_router.py
  function _request_is_secure_context (line 44) | def _request_is_secure_context(request: Request) -> bool:
  function _set_workspace_cookie (line 69) | def _set_workspace_cookie(
  function _append_partitioned_to_last_set_cookie (line 104) | def _append_partitioned_to_last_set_cookie(response: Response) -> None:
  function create_workspace_session (line 130) | async def create_workspace_session(request: Request, response: Response)...
  function delete_workspace_session (line 154) | async def delete_workspace_session(request: Request, response: Response)...

FILE: openhands-agent-server/openhands/agent_server/bash_router.py
  function search_bash_events (line 34) | async def search_bash_events(
  function get_bash_event (line 76) | async def get_bash_event(event_id: str) -> BashEventBase:
  function batch_get_bash_events (line 85) | async def batch_get_bash_events(
  function start_bash_command (line 95) | async def start_bash_command(request: ExecuteBashRequest) -> BashCommand:
  function execute_bash_command (line 103) | async def execute_bash_command(request: ExecuteBashRequest) -> BashOutput:
  function clear_all_bash_events (line 114) | async def clear_all_bash_events() -> dict[str, int]:

FILE: openhands-agent-server/openhands/agent_server/bash_service.py
  class BashEventService (line 29) | class BashEventService:
    method _ensure_bash_events_dir (line 39) | def _ensure_bash_events_dir(self) -> None:
    method _timestamp_to_str (line 43) | def _timestamp_to_str(self, timestamp: datetime) -> str:
    method _get_event_filename (line 47) | def _get_event_filename(self, event: BashEventBase) -> str:
    method _save_event_to_file (line 56) | def _save_event_to_file(self, event: BashEventBase) -> None:
    method _load_event_from_file (line 67) | def _load_event_from_file(self, filepath: Path) -> BashEventBase | None:
    method _get_event_files_by_pattern (line 76) | def _get_event_files_by_pattern(self, pattern: str) -> list[Path]:
    method get_bash_event (line 82) | async def get_bash_event(self, event_id: str) -> BashEventBase | None:
    method batch_get_bash_events (line 94) | async def batch_get_bash_events(
    method search_bash_events (line 104) | async def search_bash_events(
    method _signal_process_group (line 184) | def _signal_process_group(
    method start_bash_command (line 200) | async def start_bash_command(
    method _execute_bash_command (line 213) | async def _execute_bash_command(self, command: BashCommand) -> None:
    method subscribe_to_events (line 347) | async def subscribe_to_events(self, subscriber: Subscriber[BashEventBa...
    method unsubscribe_from_events (line 354) | async def unsubscribe_from_events(self, subscriber_id: UUID) -> bool:
    method clear_all_events (line 357) | async def clear_all_events(self) -> int:
    method close (line 381) | async def close(self):
    method __aenter__ (line 385) | async def __aenter__(self):
    method __aexit__ (line 390) | async def __aexit__(self, exc_type, exc_value, traceback):
  function get_default_bash_event_service (line 398) | def get_default_bash_event_service() -> BashEventService:

FILE: openhands-agent-server/openhands/agent_server/cloud_proxy_router.py
  class CloudProxyRequest (line 38) | class CloudProxyRequest(BaseModel):
  function _allowed_hosts (line 60) | def _allowed_hosts() -> tuple[str, ...]:
  function _is_blocked_ip_literal (line 68) | def _is_blocked_ip_literal(hostname: str) -> bool:
  function _is_host_allowed (line 90) | def _is_host_allowed(host_url: str) -> bool:
  function _filtered_response_headers (line 135) | def _filtered_response_headers(upstream: httpx.Response) -> dict[str, str]:
  function _forward_upstream (line 143) | async def _forward_upstream(
  function cloud_proxy (line 167) | async def cloud_proxy(req: CloudProxyRequest) -> Response:

FILE: openhands-agent-server/openhands/agent_server/config.py
  function _default_session_api_keys (line 19) | def _default_session_api_keys():
  function _default_secret_key (line 33) | def _default_secret_key() -> SecretStr | None:
  function _default_web_url (line 49) | def _default_web_url() -> str | None:
  class WebhookSpec (line 57) | class WebhookSpec(BaseModel):
  class Config (line 103) | class Config(BaseModel):
    method cipher (line 203) | def cipher(self) -> Cipher | None:
  function get_default_config (line 221) | def get_default_config() -> Config:

FILE: openhands-agent-server/openhands/agent_server/conversation_lease.py
  class LeaseClaim (line 24) | class LeaseClaim:
  class LeasePayload (line 29) | class LeasePayload(TypedDict):
  function _current_host (line 40) | def _current_host() -> str:
  function _is_pid_alive (line 47) | def _is_pid_alive(pid: int) -> bool:
  class ConversationLeaseHeldError (line 71) | class ConversationLeaseHeldError(RuntimeError):
    method __init__ (line 72) | def __init__(
  class ConversationOwnershipLostError (line 87) | class ConversationOwnershipLostError(RuntimeError):
    method __init__ (line 88) | def __init__(
  class ConversationLease (line 101) | class ConversationLease:
    method __init__ (line 109) | def __init__(
    method claim (line 122) | def claim(self) -> LeaseClaim:
    method _owner_is_dead (line 166) | def _owner_is_dead(self, payload: LeasePayload) -> bool:
    method renew (line 187) | def renew(self, generation: int) -> None:
    method guarded_write (line 197) | def guarded_write(self, generation: int) -> Iterator[None]:
    method release (line 203) | def release(self, generation: int) -> None:
    method _assert_owner_locked (line 216) | def _assert_owner_locked(self, generation: int) -> None:
    method _read_payload (line 234) | def _read_payload(self) -> LeasePayload | None:
    method _write_payload (line 271) | def _write_payload(self, *, generation: int, expires_at: float) -> None:

FILE: openhands-agent-server/openhands/agent_server/conversation_router.py
  function search_conversations (line 72) | async def search_conversations(
  function count_conversations (line 100) | async def count_conversations(
  function get_conversation (line 115) | async def get_conversation(
  function get_conversation_agent_final_response (line 130) | async def get_conversation_agent_final_response(
  function batch_get_conversations (line 148) | async def batch_get_conversations(
  function start_conversation (line 163) | async def start_conversation(
  function pause_conversation (line 179) | async def pause_conversation(
  function delete_conversation (line 193) | async def delete_conversation(
  function run_conversation (line 211) | async def run_conversation(
  function update_conversation_secrets (line 238) | async def update_conversation_secrets(
  function set_conversation_confirmation_policy (line 261) | async def set_conversation_confirmation_policy(
  function set_conversation_security_analyzer (line 278) | async def set_conversation_security_analyzer(
  function switch_conversation_profile (line 298) | async def switch_conversation_profile(
  function switch_conversation_llm (line 327) | async def switch_conversation_llm(
  function update_conversation (line 352) | async def update_conversation(
  function ask_agent (line 371) | async def ask_agent(
  function condense_conversation (line 387) | async def condense_conversation(
  function fork_conversation (line 407) | async def fork_conversation(

FILE: openhands-agent-server/openhands/agent_server/conversation_router_acp.py
  function search_acp_conversations (line 62) | async def search_acp_conversations(
  function count_acp_conversations (line 94) | async def count_acp_conversations(
  function get_acp_conversation (line 114) | async def get_acp_conversation(
  function batch_get_acp_conversations (line 130) | async def batch_get_acp_conversations(
  function start_acp_conversation (line 144) | async def start_acp_conversation(

FILE: openhands-agent-server/openhands/agent_server/conversation_service.py
  function _build_worktree_guidance (line 55) | def _build_worktree_guidance(
  function _append_worktree_guidance (line 74) | def _append_worktree_guidance(
  function _has_git_remote (line 95) | def _has_git_remote(repo_root: Path, remote: str = "origin") -> bool:
  function _local_branch_exists (line 103) | def _local_branch_exists(repo_root: Path, branch: str) -> bool:
  function _get_worktree_start_point (line 114) | def _get_worktree_start_point(repo_root: Path) -> str:
  function _create_conversation_worktree (line 157) | def _create_conversation_worktree(
  function _prepare_request_workspace (line 216) | def _prepare_request_workspace(
  function _compose_conversation_info (line 242) | def _compose_conversation_info(
  function _compose_webhook_conversation_info (line 257) | def _compose_webhook_conversation_info(
  function _update_state_tags_sync (line 263) | def _update_state_tags_sync(
  function _compose_webhook_conversation_info_sync (line 271) | def _compose_webhook_conversation_info_sync(
  function _register_agent_definitions (line 278) | def _register_agent_definitions(
  class ConversationService (line 314) | class ConversationService:
    method get_conversation (line 333) | async def get_conversation(self, conversation_id: UUID) -> Conversatio...
    method get_acp_conversation (line 342) | async def get_acp_conversation(
    method search_conversations (line 353) | async def search_conversations(
    method search_acp_conversations (line 371) | async def search_acp_conversations(
    method _search_conversations (line 389) | async def _search_conversations(
    method count_conversations (line 446) | async def count_conversations(
    method _count_conversations (line 452) | async def _count_conversations(
    method batch_get_conversations (line 475) | async def batch_get_conversations(
    method batch_get_acp_conversations (line 488) | async def batch_get_acp_conversations(
    method _notify_conversation_webhooks (line 499) | async def _notify_conversation_webhooks(self, conversation_info: BaseM...
    method start_conversation (line 531) | async def start_conversation(
    method start_acp_conversation (line 536) | async def start_acp_conversation(
    method _start_conversation (line 541) | async def _start_conversation(
    method pause_conversation (line 638) | async def pause_conversation(self, conversation_id: UUID) -> bool:
    method resume_conversation (line 652) | async def resume_conversation(self, conversation_id: UUID) -> bool:
    method delete_conversation (line 660) | async def delete_conversation(self, conversation_id: UUID) -> bool:
    method update_conversation (line 701) | async def update_conversation(
    method get_event_service (line 754) | async def get_event_service(self, conversation_id: UUID) -> EventServi...
    method generate_conversation_title (line 759) | async def generate_conversation_title(
    method ask_agent (line 773) | async def ask_agent(self, conversation_id: UUID, question: str) -> str...
    method condense (line 785) | async def condense(self, conversation_id: UUID) -> bool:
    method fork_conversation (line 797) | async def fork_conversation(
    method __aenter__ (line 864) | async def __aenter__(self):
    method _renew_all_leases_loop (line 947) | async def _renew_all_leases_loop(self) -> None:
    method __aexit__ (line 966) | async def __aexit__(self, exc_type, exc_value, traceback):
    method get_instance (line 989) | def get_instance(cls, config: Config) -> "ConversationService":
    method _start_event_service (line 1000) | async def _start_event_service(self, stored: StoredConversation) -> Ev...
  class _EventSubscriber (line 1054) | class _EventSubscriber(Subscriber):
    method __call__ (line 1057) | async def __call__(self, _event: Event):
  class AutoTitleSubscriber (line 1069) | class AutoTitleSubscriber(Subscriber):
    method __call__ (line 1072) | async def __call__(self, event: Event) -> None:
    method _load_title_llm (line 1118) | def _load_title_llm(self) -> LLM | None:
  class WebhookSubscriber (line 1144) | class WebhookSubscriber(Subscriber):
    method __call__ (line 1152) | async def __call__(self, event: Event):
    method close (line 1163) | async def close(self):
    method _post_events (line 1171) | async def _post_events(self):
    method _cancel_flush_timer (line 1231) | def _cancel_flush_timer(self):
    method _flush_after_delay (line 1237) | async def _flush_after_delay(self):
  class ConversationWebhookSubscriber (line 1252) | class ConversationWebhookSubscriber:
    method post_conversation_info (line 1258) | async def post_conversation_info(self, conversation_info: BaseModel):
  function get_default_conversation_service (line 1310) | def get_default_conversation_service() -> ConversationService:

FILE: openhands-agent-server/openhands/agent_server/dependencies.py
  function create_session_api_key_dependency (line 23) | def create_session_api_key_dependency(config: Config):
  function create_workspace_session_dependency (line 38) | def create_workspace_session_dependency(config: Config):
  function get_conversation_service (line 64) | def get_conversation_service(request: Request):
  function get_event_service (line 80) | async def get_event_service(

FILE: openhands-agent-server/openhands/agent_server/desktop_router.py
  class DesktopUrlResponse (line 15) | class DesktopUrlResponse(BaseModel):
  function get_desktop_url (line 22) | async def get_desktop_url(

FILE: openhands-agent-server/openhands/agent_server/desktop_service.py
  class DesktopService (line 18) | class DesktopService:
    method __init__ (line 21) | def __init__(self):
    method start (line 25) | async def start(self) -> bool:
    method stop (line 162) | async def stop(self) -> None:
    method is_running (line 178) | def is_running(self) -> bool:
    method get_vnc_url (line 196) | def get_vnc_url(self, base: str = "http://localhost:8003") -> str | None:
  function get_desktop_service (line 208) | def get_desktop_service() -> DesktopService | None:

FILE: openhands-agent-server/openhands/agent_server/docker/build.py
  function _default_sdk_project_root (line 61) | def _default_sdk_project_root() -> Path:
  function _run (line 160) | def _run(
  function _sanitize_branch (line 219) | def _sanitize_branch(ref: str) -> str:
  function _sanitize_ref_tag (line 224) | def _sanitize_ref_tag(ref_name: str) -> str:
  function _release_tag_aliases (line 230) | def _release_tag_aliases(version: str) -> list[str]:
  function _truncate_ident (line 250) | def _truncate_ident(repo: str, tag: str, budget: int) -> str:
  function _base_slug (line 276) | def _base_slug(image: str, max_len: int = 64) -> str:
  function _git_info (line 315) | def _git_info() -> tuple[str, str]:
  function _package_version (line 352) | def _package_version() -> str:
  class BuildOptions (line 378) | class BuildOptions(BaseModel):
    method short_sha (line 438) | def short_sha(self) -> str:
    method long_sha (line 442) | def long_sha(self) -> str:
    method _valid_target (line 447) | def _valid_target(cls, v: str) -> str:
    method custom_tag_list (line 453) | def custom_tag_list(self) -> list[str]:
    method base_image_slug (line 457) | def base_image_slug(self) -> str:
    method branch_tag (line 461) | def branch_tag(self) -> str | None:
    method release_tag_source (line 474) | def release_tag_source(self) -> str | None:
    method versioned_tags (line 491) | def versioned_tags(self) -> list[str]:
    method base_tag (line 502) | def base_tag(self) -> str:
    method cache_tags (line 506) | def cache_tags(self) -> tuple[str, str]:
    method all_tags (line 516) | def all_tags(self) -> list[str]:
  class BuildTelemetry (line 542) | class BuildTelemetry(BaseModel):
  class BuildResult (line 555) | class BuildResult(BaseModel):
  class BuildCommandError (line 560) | class BuildCommandError(subprocess.CalledProcessError):
    method __init__ (line 561) | def __init__(
  function _extract_tarball (line 577) | def _extract_tarball(tarball: Path, dest: Path) -> None:
  function _make_build_context (line 591) | def _make_build_context(
  function _active_buildx_driver (line 641) | def _active_buildx_driver() -> str | None:
  function _default_local_cache_dir (line 653) | def _default_local_cache_dir() -> Path:
  function _get_dockerfile_path (line 662) | def _get_dockerfile_path(sdk_project_root: Path) -> Path:
  function _round_seconds (line 676) | def _round_seconds(value: float) -> float:
  function _classify_buildkit_description (line 680) | def _classify_buildkit_description(description: str) -> str | None:
  function _add_buildkit_duration (line 699) | def _add_buildkit_duration(
  function _parse_buildkit_telemetry (line 715) | def _parse_buildkit_telemetry(stderr: str) -> BuildTelemetry:
  function build_with_telemetry (line 798) | def build_with_telemetry(opts: BuildOptions) -> BuildResult:
  function build (line 965) | def build(opts: BuildOptions) -> list[str]:
  function _env (line 973) | def _env(name: str, default: str) -> str:
  function main (line 978) | def main(argv: list[str]) -> int:

FILE: openhands-agent-server/openhands/agent_server/env_parser.py
  class MissingType (line 28) | class MissingType:
  class EnvParser (line 36) | class EnvParser(ABC):
    method from_env (line 40) | def from_env(self, key: str) -> JsonType:
    method to_env (line 43) | def to_env(self, key: str, value: Any, output: IO):
  class BoolEnvParser (line 50) | class BoolEnvParser(EnvParser):
    method from_env (line 51) | def from_env(self, key: str) -> bool | MissingType:
    method to_env (line 56) | def to_env(self, key: str, value: Any, output: IO):
  class IntEnvParser (line 60) | class IntEnvParser(EnvParser):
    method from_env (line 61) | def from_env(self, key: str) -> int | MissingType:
  class FloatEnvParser (line 67) | class FloatEnvParser(EnvParser):
    method from_env (line 68) | def from_env(self, key: str) -> float | MissingType:
  class StrEnvParser (line 74) | class StrEnvParser(EnvParser):
    method from_env (line 75) | def from_env(self, key: str) -> str | MissingType:
  class NoneEnvParser (line 81) | class NoneEnvParser(EnvParser):
    method from_env (line 82) | def from_env(self, key: str) -> None | MissingType:
    method to_env (line 89) | def to_env(self, key: str, value: Any, output: IO):
  class LiteralEnvParser (line 95) | class LiteralEnvParser(EnvParser):
    method from_env (line 98) | def from_env(self, key: str) -> str | MissingType:
    method to_env (line 104) | def to_env(self, key: str, value: Any, output: IO):
  class ModelEnvParser (line 114) | class ModelEnvParser(EnvParser):
    method from_env (line 118) | def from_env(self, key: str) -> dict | MissingType:
    method to_env (line 151) | def to_env(self, key: str, value: Any, output: IO):
  class DictEnvParser (line 165) | class DictEnvParser(EnvParser):
    method from_env (line 166) | def from_env(self, key: str) -> dict | MissingType:
  class ListEnvParser (line 179) | class ListEnvParser(EnvParser):
    method from_env (line 183) | def from_env(self, key: str) -> list | MissingType:
    method to_env (line 219) | def to_env(self, key: str, value: Any, output: IO):
  class UnionEnvParser (line 242) | class UnionEnvParser(EnvParser):
    method from_env (line 245) | def from_env(self, key: str) -> JsonType:
    method to_env (line 252) | def to_env(self, key: str, value: Any, output: IO):
  class DiscriminatedUnionEnvParser (line 276) | class DiscriminatedUnionEnvParser(EnvParser):
    method from_env (line 279) | def from_env(self, key: str) -> JsonType:
    method _import_and_register_class (line 313) | def _import_and_register_class(self, full_class_name: str) -> str:
    method to_env (line 340) | def to_env(self, key: str, value: Any, output: IO):
  class DelayedParser (line 346) | class DelayedParser(EnvParser):
    method from_env (line 351) | def from_env(self, key: str) -> JsonType:
    method to_env (line 355) | def to_env(self, key: str, value: Any, output: IO):
  function merge (line 360) | def merge(a, b):
  function get_env_parser (line 385) | def get_env_parser(target_type: type, parsers: dict[type, EnvParser]) ->...
  function _get_default_parsers (line 449) | def _get_default_parsers() -> dict[type, EnvParser]:
  function _create_sample (line 463) | def _create_sample(type_: type):
  function from_env (line 483) | def from_env(
  function to_env (line 501) | def to_env(

FILE: openhands-agent-server/openhands/agent_server/event_router.py
  function normalize_datetime_to_server_timezone (line 39) | def normalize_datetime_to_server_timezone(dt: datetime) -> datetime:
  function search_conversation_events (line 66) | async def search_conversation_events(
  function count_conversation_events (line 123) | async def count_conversation_events(
  function get_conversation_event (line 167) | async def get_conversation_event(
  function batch_get_conversation_events (line 179) | async def batch_get_conversation_events(
  function send_message (line 190) | async def send_message(
  function respond_to_confirmation (line 203) | async def respond_to_confirmation(

FILE: openhands-agent-server/openhands/agent_server/event_service.py
  class EventService (line 55) | class EventService:
    method conversation_dir (line 79) | def conversation_dir(self):
    method load_meta (line 82) | async def load_meta(self):
    method save_meta (line 91) | async def save_meta(self):
    method _write_guard (line 102) | def _write_guard(self):
    method renew_lease (line 107) | def renew_lease(self) -> None:
    method _renew_lease_loop (line 128) | async def _renew_lease_loop(self) -> None:
    method get_conversation (line 138) | def get_conversation(self):
    method _get_event_sync (line 143) | def _get_event_sync(self, event_id: str) -> Event | None:
    method get_event (line 156) | async def get_event(self, event_id: str) -> Event | None:
    method _event_matches_filters (line 162) | def _event_matches_filters(
    method _search_events_sync (line 189) | def _search_events_sync(
    method search_events (line 270) | async def search_events(
    method _count_events_sync (line 297) | def _count_events_sync(
    method count_events (line 339) | async def count_events(
    method _get_execution_status_sync (line 361) | def _get_execution_status_sync(self) -> ConversationExecutionStatus:
    method _get_execution_status (line 367) | async def _get_execution_status(self) -> ConversationExecutionStatus:
    method _create_state_update_event_sync (line 371) | def _create_state_update_event_sync(self) -> ConversationStateUpdateEv...
    method _create_state_update_event (line 378) | async def _create_state_update_event(self) -> ConversationStateUpdateE...
    method _event_matches_body (line 382) | def _event_matches_body(self, event: Event, body: str) -> bool:
    method batch_get_events (line 408) | async def batch_get_events(self, event_ids: list[str]) -> list[Event |...
    method send_message (line 415) | async def send_message(self, message: Message, run: bool = False):
    method subscribe_to_events (line 425) | async def subscribe_to_events(self, subscriber: Subscriber[Event]) -> ...
    method unsubscribe_from_events (line 451) | async def unsubscribe_from_events(self, subscriber_id: UUID) -> bool:
    method _emit_event_from_thread (line 454) | def _emit_event_from_thread(self, event: Event) -> None:
    method _setup_llm_log_streaming (line 475) | def _setup_llm_log_streaming(self, agent: AgentBase) -> None:
    method _setup_acp_activity_heartbeat (line 499) | def _setup_acp_activity_heartbeat(self, agent: AgentBase) -> None:
    method _setup_stats_streaming (line 521) | def _setup_stats_streaming(self, agent: AgentBase) -> None:
    method _ensure_workspace_is_git_repo (line 538) | def _ensure_workspace_is_git_repo(working_dir: Path) -> None:
    method start (line 565) | async def start(self):
    method run (line 705) | async def run(self):
    method respond_to_confirmation (line 759) | async def respond_to_confirmation(self, request: ConfirmationResponseR...
    method reject_pending_actions (line 774) | async def reject_pending_actions(self, reason: str):
    method pause (line 783) | async def pause(self):
    method update_secrets (line 790) | async def update_secrets(self, secrets: dict[str, SecretValue]):
    method set_confirmation_policy (line 797) | async def set_confirmation_policy(self, policy: ConfirmationPolicyBase):
    method set_security_analyzer (line 806) | async def set_security_analyzer(
    method close (line 817) | async def close(self):
    method generate_title (line 852) | async def generate_title(
    method ask_agent (line 878) | async def ask_agent(self, question: str) -> str:
    method condense (line 889) | async def condense(self) -> None:
    method _get_agent_final_response_sync (line 900) | def _get_agent_final_response_sync(self) -> str:
    method get_agent_final_response (line 911) | async def get_agent_final_response(self) -> str:
    method get_state (line 922) | async def get_state(self) -> ConversationState:
    method _publish_state_update (line 927) | async def _publish_state_update(self):
    method __aenter__ (line 938) | async def __aenter__(self):
    method __aexit__ (line 942) | async def __aexit__(self, exc_type, exc_value, traceback):
    method is_open (line 952) | def is_open(self) -> bool:

FILE: openhands-agent-server/openhands/agent_server/file_router.py
  class SubdirectoryEntry (line 26) | class SubdirectoryEntry(BaseModel):
  class SubdirectoryPage (line 31) | class SubdirectoryPage(BaseModel):
  class FileBrowserEntry (line 36) | class FileBrowserEntry(BaseModel):
  class HomeResponse (line 41) | class HomeResponse(BaseModel):
  function _upload_file (line 51) | async def _upload_file(path: str, file: UploadFile) -> Success:
  function _download_file (line 87) | async def _download_file(path: str) -> FileResponse:
  function _create_zip_from_directory (line 125) | def _create_zip_from_directory(source_dir: Path, output_path: Path) -> N...
  function upload_file_query (line 138) | async def upload_file_query(
  function download_file_query (line 147) | async def download_file_query(
  function _list_home_favorites (line 154) | def _list_home_favorites(home: Path, limit: int = 50) -> list[FileBrowse...
  function _list_root_locations (line 180) | def _list_root_locations() -> list[FileBrowserEntry]:
  function get_home_directory (line 200) | async def get_home_directory() -> HomeResponse:
  function search_subdirs (line 217) | async def search_subdirs(
  function download_trajectory (line 299) | async def download_trajectory(

FILE: openhands-agent-server/openhands/agent_server/git_router.py
  function _get_git_changes (line 28) | async def _get_git_changes(path: str, ref: str | None) -> list[GitChange]:
  function _get_git_diff (line 43) | async def _get_git_diff(path: str, ref: str | None) -> GitDiff:
  function git_changes_query (line 60) | async def git_changes_query(
  function git_diff_query (line 76) | async def git_diff_query(

FILE: openhands-agent-server/openhands/agent_server/hooks_router.py
  class HooksRequest (line 17) | class HooksRequest(BaseModel):
  class HooksResponse (line 25) | class HooksResponse(BaseModel):
  function get_hooks (line 35) | def get_hooks(request: HooksRequest) -> HooksResponse:

FILE: openhands-agent-server/openhands/agent_server/hooks_service.py
  function load_hooks_from_workspace (line 20) | def load_hooks_from_workspace(project_dir: str | None = None) -> HookCon...

FILE: openhands-agent-server/openhands/agent_server/llm_router.py
  class ProvidersResponse (line 17) | class ProvidersResponse(BaseModel):
  class ModelsResponse (line 23) | class ModelsResponse(BaseModel):
  class VerifiedModelsResponse (line 29) | class VerifiedModelsResponse(BaseModel):
  function list_providers (line 36) | async def list_providers() -> ProvidersResponse:
  function list_models (line 43) | async def list_models(
  function list_verified_models (line 72) | async def list_verified_models() -> VerifiedModelsResponse:

FILE: openhands-agent-server/openhands/agent_server/logging_config.py
  class UvicornAccessJsonFormatter (line 11) | class UvicornAccessJsonFormatter(JsonFormatter):
    method add_fields (line 21) | def add_fields(
  function get_uvicorn_logging_config (line 48) | def get_uvicorn_logging_config() -> dict[str, Any]:

FILE: openhands-agent-server/openhands/agent_server/middleware.py
  class LocalhostCORSMiddleware (line 8) | class LocalhostCORSMiddleware(CORSMiddleware):
    method __init__ (line 15) | def __init__(self, app: ASGIApp, allow_origins: list[str]) -> None:
    method is_allowed_origin (line 24) | def is_allowed_origin(self, origin: str) -> bool:

FILE: openhands-agent-server/openhands/agent_server/models.py
  class ServerErrorEvent (line 44) | class ServerErrorEvent(Event):
  class ConversationSortOrder (line 57) | class ConversationSortOrder(str, Enum):
  class EventSortOrder (line 66) | class EventSortOrder(str, Enum):
  class StoredConversation (line 73) | class StoredConversation(StartConversationRequest):
  class _ConversationInfoBase (line 88) | class _ConversationInfoBase(BaseModel):
  class ConversationInfo (line 189) | class ConversationInfo(_ConversationInfoBase):
  class ConversationPage (line 198) | class ConversationPage(BaseModel):
  class ConversationResponse (line 210) | class ConversationResponse(BaseModel):
  class ConfirmationResponseRequest (line 215) | class ConfirmationResponseRequest(BaseModel):
  class Success (line 222) | class Success(BaseModel):
  class EventPage (line 226) | class EventPage(OpenHandsModel):
  class UpdateSecretsRequest (line 231) | class UpdateSecretsRequest(BaseModel):
    method convert_string_secrets (line 240) | def convert_string_secrets(cls, v: dict[str, Any]) -> dict[str, Any]:
  class SetConfirmationPolicyRequest (line 276) | class SetConfirmationPolicyRequest(BaseModel):
  class SetSecurityAnalyzerRequest (line 282) | class SetSecurityAnalyzerRequest(BaseModel):
  class UpdateConversationRequest (line 290) | class UpdateConversationRequest(BaseModel):
  class ForkConversationRequest (line 309) | class ForkConversationRequest(BaseModel):
  class GenerateTitleRequest (line 337) | class GenerateTitleRequest(BaseModel):
  class GenerateTitleResponse (line 348) | class GenerateTitleResponse(BaseModel):
  class AskAgentRequest (line 354) | class AskAgentRequest(BaseModel):
  class AskAgentResponse (line 360) | class AskAgentResponse(BaseModel):
  class AgentResponseResult (line 366) | class AgentResponseResult(BaseModel):
  class BashEventBase (line 382) | class BashEventBase(DiscriminatedUnionMixin, ABC):
  class ExecuteBashRequest (line 389) | class ExecuteBashRequest(BaseModel):
  class BashCommand (line 398) | class BashCommand(BashEventBase, ExecuteBashRequest):
  class BashOutput (line 402) | class BashOutput(BashEventBase):
  class BashError (line 423) | class BashError(BashEventBase):
  class BashEventSortOrder (line 428) | class BashEventSortOrder(Enum):
  class BashEventPage (line 433) | class BashEventPage(OpenHandsModel):

FILE: openhands-agent-server/openhands/agent_server/openapi.py
  function generate_openapi_schema (line 11) | def generate_openapi_schema() -> dict[str, Any]:

FILE: openhands-agent-server/openhands/agent_server/persistence/models.py
  class SettingsUpdatePayload (line 34) | class SettingsUpdatePayload(TypedDict, total=False):
  function _deep_merge (line 42) | def _deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[s...
  class PersistedSettings (line 59) | class PersistedSettings(BaseModel):
    method llm_api_key_is_set (line 87) | def llm_api_key_is_set(self) -> bool:
    method update (line 97) | def update(self, payload: SettingsUpdatePayload) -> None:
    method from_persisted (line 180) | def from_persisted(
    method agent_settings_serializer (line 201) | def agent_settings_serializer(
    method _normalize_inputs (line 212) | def _normalize_inputs(
  class CustomSecret (line 252) | class CustomSecret(BaseModel):
    method _validate_name (line 261) | def _validate_name(cls, v: str) -> str:
    method _validate_secret (line 283) | def _validate_secret(
    method _serialize_secret (line 289) | def _serialize_secret(self, v: SecretStr | None, info: SerializationIn...
  class Secrets (line 293) | class Secrets(BaseModel):
    method get_env_vars (line 305) | def get_env_vars(self) -> dict[str, str]:
    method get_descriptions (line 325) | def get_descriptions(self) -> dict[str, str | None]:
    method custom_secrets_serializer (line 332) | def custom_secrets_serializer(
    method _normalize_inputs (line 344) | def _normalize_inputs(cls, data: dict | object) -> dict | object:
  function _coerce_dict_secrets (line 388) | def _coerce_dict_secrets(d: dict[str, Any]) -> dict[str, Any]:

FILE: openhands-agent-server/openhands/agent_server/persistence/store.py
  function _validate_filename (line 82) | def _validate_filename(filename: str) -> None:
  function _ensure_secure_directory (line 115) | def _ensure_secure_directory(path: Path) -> None:
  function _file_lock (line 140) | def _file_lock(lock_path: Path) -> Iterator[None]:
  function _atomic_write_json (line 180) | def _atomic_write_json(path: Path, data: dict) -> None:
  class SettingsStore (line 238) | class SettingsStore(ABC):
    method load (line 242) | def load(self) -> PersistedSettings | None:
    method save (line 246) | def save(self, settings: PersistedSettings) -> None:
    method update (line 250) | def update(
  class SecretsStore (line 264) | class SecretsStore(ABC):
    method load (line 268) | def load(self) -> Secrets | None:
    method save (line 272) | def save(self, secrets: Secrets) -> None:
    method get_secret (line 276) | def get_secret(self, name: str) -> str | None:
    method set_secret (line 280) | def set_secret(self, name: str, value: str, description: str | None = ...
    method delete_secret (line 284) | def delete_secret(self, name: str) -> bool:
  class FileSettingsStore (line 288) | class FileSettingsStore(SettingsStore):
    method __init__ (line 300) | def __init__(
    method load (line 314) | def load(self) -> PersistedSettings | None:
    method save (line 346) | def save(self, settings: PersistedSettings) -> None:
    method update (line 382) | def update(
  class FileSecretsStore (line 419) | class FileSecretsStore(SecretsStore):
    method __init__ (line 438) | def __init__(
    method load (line 460) | def load(self) -> Secrets | None:
    method save (line 491) | def save(self, secrets: Secrets) -> None:
    method get_secret (line 526) | def get_secret(self, name: str) -> str | None:
    method set_secret (line 540) | def set_secret(self, name: str, value: str, description: str | None = ...
    method delete_secret (line 572) | def delete_secret(self, name: str) -> bool:
  function _get_persistence_dir (line 607) | def _get_persistence_dir(config: Config | None = None) -> Path:
  function _get_cipher (line 621) | def _get_cipher(config: Config | None = None) -> Cipher | None:
  function get_settings_store (line 628) | def get_settings_store(config: Config | None = None) -> FileSettingsStore:
  function get_secrets_store (line 660) | def get_secrets_store(config: Config | None = None) -> FileSecretsStore:
  function reset_stores (line 692) | def reset_stores() -> None:

FILE: openhands-agent-server/openhands/agent_server/profiles_router.py
  class ProfileInfo (line 43) | class ProfileInfo(BaseModel):
  class ProfileListResponse (line 50) | class ProfileListResponse(BaseModel):
  class ProfileDetailResponse (line 55) | class ProfileDetailResponse(BaseModel):
  class ProfileMutationResponse (line 63) | class ProfileMutationResponse(BaseModel):
  class SaveProfileRequest (line 68) | class SaveProfileRequest(BaseModel):
  class RenameProfileRequest (line 76) | class RenameProfileRequest(BaseModel):
  function _store_errors (line 86) | def _store_errors() -> Iterator[None]:
  function _has_api_key (line 102) | def _has_api_key(llm: LLM) -> bool:
  function _model_to_profile_name (line 108) | def _model_to_profile_name(model: str) -> str:
  function list_profiles (line 141) | async def list_profiles(request: Request) -> ProfileListResponse:
  function get_profile (line 206) | async def get_profile(request: Request, name: ProfileName) -> ProfileDet...
  function save_profile (line 245) | async def save_profile(
  function delete_profile (line 285) | async def delete_profile(name: ProfileName) -> ProfileMutationResponse:
  function rename_profile (line 295) | async def rename_profile(
  class ActivateProfileResponse (line 347) | class ActivateProfileResponse(BaseModel):
  function activate_profile (line 356) | async def activate_profile(

FILE: openhands-agent-server/openhands/agent_server/pub_sub.py
  class Subscriber (line 15) | class Subscriber[T](ABC):
    method __call__ (line 17) | async def __call__(self, event: T):
    method close (line 20) | async def close(self):
  class MaxSubscribersError (line 24) | class MaxSubscribersError(Exception):
  class PubSub (line 29) | class PubSub[T]:
    method subscribe (line 39) | def subscribe(self, subscriber: Subscriber[T]) -> UUID:
    method unsubscribe (line 60) | def unsubscribe(self, subscriber_id: UUID) -> bool:
    method __call__ (line 77) | async def __call__(self, event: T) -> None:
    method close (line 100) | async def close(self):

FILE: openhands-agent-server/openhands/agent_server/server_details_router.py
  function _package_version (line 19) | def _package_version(dist_name: str) -> str:
  class HealthStatus (line 26) | class HealthStatus(BaseModel):
  class ServerInfo (line 30) | class ServerInfo(BaseModel):
  function update_last_execution_time (line 59) | def update_last_execution_time():
  function mark_initialization_complete (line 64) | def mark_initialization_complete() -> None:
  function alive (line 75) | async def alive() -> HealthStatus:
  function health (line 81) | async def health() -> HealthStatus:
  function ready (line 87) | async def ready(response: Response) -> dict[str, str]:
  function get_server_info (line 101) | async def get_server_info() -> ServerInfo:

FILE: openhands-agent-server/openhands/agent_server/settings_router.py
  function _get_agent_settings_schema (line 52) | def _get_agent_settings_schema() -> SettingsSchema:
  function _get_conversation_settings_schema (line 62) | def _get_conversation_settings_schema() -> SettingsSchema:
  function get_agent_settings_schema (line 67) | async def get_agent_settings_schema() -> SettingsSchema:
  function get_conversation_settings_schema (line 73) | async def get_conversation_settings_schema() -> SettingsSchema:
  function _validate_secret_name (line 81) | def _validate_secret_name(name: str) -> None:
  function get_settings (line 104) | async def get_settings(request: Request) -> SettingsResponse:
  function update_settings (line 167) | async def update_settings(
  function list_secrets (line 251) | async def list_secrets(request: Request) -> SecretsListResponse:
  function get_secret_value (line 276) | async def get_secret_value(request: Request, name: str) -> Response:
  function create_secret (line 309) | async def create_secret(
  function delete_secret (line 351) | async def delete_secret(request: Request, name: str) -> dict[str, bool]:

FILE: openhands-agent-server/openhands/agent_server/skills_router.py
  class ExposedUrl (line 51) | class ExposedUrl(BaseModel):
  class OrgConfig (line 59) | class OrgConfig(BaseModel):
  class SandboxConfig (line 73) | class SandboxConfig(BaseModel):
  class SkillsRequest (line 82) | class SkillsRequest(BaseModel):
  class SkillInfo (line 113) | class SkillInfo(BaseModel):
  class SkillsResponse (line 126) | class SkillsResponse(BaseModel):
  class SyncResponse (line 136) | class SyncResponse(BaseModel):
  class InstallSkillRequest (line 148) | class InstallSkillRequest(BaseModel):
  class InstalledSkillResponse (line 175) | class InstalledSkillResponse(BaseModel):
    method from_skill_info (line 193) | def from_skill_info(cls, info: InstalledSkillInfo) -> "InstalledSkillR...
  class InstalledSkillsListResponse (line 207) | class InstalledSkillsListResponse(BaseModel):
  class UpdateSkillStateRequest (line 213) | class UpdateSkillStateRequest(BaseModel):
  class UpdateSkillStateResponse (line 219) | class UpdateSkillStateResponse(BaseModel):
  class UninstallSkillResponse (line 226) | class UninstallSkillResponse(BaseModel):
  class UpdateSkillResponse (line 232) | class UpdateSkillResponse(BaseModel):
  class MarketplaceCatalogResponse (line 239) | class MarketplaceCatalogResponse(BaseModel):
  function get_skills (line 246) | def get_skills(request: SkillsRequest) -> SkillsResponse:
  function sync_skills (line 309) | def sync_skills() -> SyncResponse:
  function install_skill_endpoint (line 339) | def install_skill_endpoint(request: InstallSkillRequest) -> InstalledSki...
  function list_installed_skills_endpoint (line 382) | def list_installed_skills_endpoint() -> InstalledSkillsListResponse:
  function get_installed_skill_endpoint (line 402) | def get_installed_skill_endpoint(skill_name: SkillNamePath) -> Installed...
  function set_skill_enabled_endpoint (line 428) | def set_skill_enabled_endpoint(
  function uninstall_skill_endpoint (line 461) | def uninstall_skill_endpoint(skill_name: SkillNamePath) -> UninstallSkil...
  function refresh_skill_endpoint (line 491) | def refresh_skill_endpoint(skill_name: SkillNamePath) -> UpdateSkillResp...
  function get_marketplace_catalog (line 518) | def get_marketplace_catalog() -> MarketplaceCatalogResponse:

FILE: openhands-agent-server/openhands/agent_server/skills_service.py
  class ExposedUrlData (line 72) | class ExposedUrlData:
  class SkillLoadResult (line 81) | class SkillLoadResult:
  function load_org_skills_from_url (line 88) | def load_org_skills_from_url(
  function create_sandbox_skill (line 221) | def create_sandbox_skill(
  function merge_skills (line 266) | def merge_skills(skill_lists: list[list[Skill]]) -> list[Skill]:
  function load_all_skills (line 291) | def load_all_skills(
  function sync_public_skills (line 382) | def sync_public_skills() -> tuple[bool, str]:
  function service_install_skill (line 412) | def service_install_skill(
  function service_uninstall_skill (line 450) | def service_uninstall_skill(
  function service_enable_skill (line 467) | def service_enable_skill(
  function service_disable_skill (line 484) | def service_disable_skill(
  function service_list_installed_skills (line 501) | def service_list_installed_skills(
  function service_get_installed_skill (line 518) | def service_get_installed_skill(
  function service_update_skill (line 535) | def service_update_skill(
  class MarketplaceSkillInfo (line 552) | class MarketplaceSkillInfo(BaseModel):
  function service_get_marketplace_catalog (line 583) | def service_get_marketplace_catalog(
  function _fetch_catalog_entries (line 626) | def _fetch_catalog_entries(marketplace_path: str) -> list[_CatalogEntry]:

FILE: openhands-agent-server/openhands/agent_server/sockets.py
  function _get_config (line 54) | def _get_config(websocket: WebSocket) -> Config:
  function _resolve_websocket_session_api_key (line 67) | def _resolve_websocket_session_api_key(
  function _accept_authenticated_websocket (line 95) | async def _accept_authenticated_websocket(
  function events_socket (line 189) | async def events_socket(
  function bash_events_socket (line 341) | async def bash_events_socket(
  function _send_event (line 434) | async def _send_event(event: Event, websocket: WebSocket):
  function _is_auth_control_message (line 450) | def _is_auth_control_message(data: object) -> bool:
  function _safe_close_websocket (line 461) | async def _safe_close_websocket(
  function _is_websocket_connected (line 473) | def _is_websocket_connected(websocket: WebSocket) -> bool:
  class _WebSocketSubscriber (line 494) | class _WebSocketSubscriber(Subscriber):
    method __call__ (line 499) | async def __call__(self, event: Event):
  function _send_bash_event (line 503) | async def _send_bash_event(event: BashEventBase, websocket: WebSocket):
  class _BashWebSocketSubscriber (line 517) | class _BashWebSocketSubscriber(Subscriber[BashEventBase]):
    method __call__ (line 522) | async def __call__(self, event: BashEventBase):

FILE: openhands-agent-server/openhands/agent_server/tool_preload_service.py
  class ToolPreloadService (line 15) | class ToolPreloadService:
    method start (line 21) | async def start(self) -> bool:
    method stop (line 46) | async def stop(self) -> None:
    method is_running (line 50) | def is_running(self) -> bool:
  function get_tool_preload_service (line 58) | def get_tool_preload_service() -> ToolPreloadService | None:

FILE: openhands-agent-server/openhands/agent_server/tool_router.py
  function list_available_tools (line 23) | async def list_available_tools() -> list[str]:

FILE: openhands-agent-server/openhands/agent_server/utils.py
  function safe_rmtree (line 16) | def safe_rmtree(path: str | Path | None, description: str = "directory")...
  function utc_now (line 53) | def utc_now():
  function _uuid_to_hex (line 58) | def _uuid_to_hex(uuid_obj: UUID) -> str:

FILE: openhands-agent-server/openhands/agent_server/vscode_extensions/openhands-settings/extension.js
  function activate (line 4) | function activate(context) {
  function deactivate (line 20) | function deactivate() {}

FILE: openhands-agent-server/openhands/agent_server/vscode_router.py
  class VSCodeUrlResponse (line 15) | class VSCodeUrlResponse(BaseModel):
  function get_vscode_url (line 22) | async def get_vscode_url(
  function get_vscode_status (line 52) | async def get_vscode_status() -> dict[str, bool | str]:

FILE: openhands-agent-server/openhands/agent_server/vscode_service.py
  class VSCodeService (line 14) | class VSCodeService:
    method __init__ (line 17) | def __init__(
    method start (line 39) | async def start(self) -> bool:
    method stop (line 74) | async def stop(self) -> None:
    method get_vscode_url (line 90) | def get_vscode_url(
    method is_running (line 112) | def is_running(self) -> bool:
    method _check_vscode_available (line 120) | def _check_vscode_available(self) -> bool:
    method _is_port_available (line 129) | async def _is_port_available(self) -> bool:
    method _start_vscode_process (line 146) | async def _start_vscode_process(self) -> None:
    method _wait_for_startup (line 179) | async def _wait_for_startup(self) -> None:
  function get_vscode_service (line 219) | def get_vscode_service() -> VSCodeService | None:

FILE: openhands-agent-server/openhands/agent_server/workspace_router.py
  function conversation_workspace_url_path (line 32) | def conversation_workspace_url_path(conversation_id: UUID | str) -> str:
  function _resolve_workspace_dir (line 41) | async def _resolve_workspace_dir(
  function _resolve_target (line 66) | def _resolve_target(workspace_dir: Path, file_path: str) -> Path:
  function _serve_path (line 80) | def _serve_path(workspace_dir: Path, file_path: str) -> FileResponse:
  function serve_workspace_root (line 104) | async def serve_workspace_root(
  function serve_workspace_file (line 117) | async def serve_workspace_file(

FILE: openhands-sdk/openhands/sdk/__init__.py
  function __getattr__ (line 134) | def __getattr__(name: str) -> Any:

FILE: openhands-sdk/openhands/sdk/agent/__init__.py
  function __getattr__ (line 16) | def __getattr__(name: str):

FILE: openhands-sdk/openhands/sdk/agent/acp_agent.py
  function _make_dummy_llm (line 156) | def _make_dummy_llm() -> LLM:
  function _select_auth_method (line 179) | def _select_auth_method(
  function _maybe_set_session_model (line 204) | async def _maybe_set_session_model(
  function _extract_token_usage (line 224) | def _extract_token_usage(
  function _estimate_cost_from_tokens (line 251) | def _estimate_cost_from_tokens(
  function _image_url_to_acp_block (line 270) | def _image_url_to_acp_block(url: str) -> ImageContentBlock | None:
  function _serialize_tool_content (line 291) | def _serialize_tool_content(content: list[Any] | None) -> list[dict[str,...
  function _filter_jsonrpc_lines (line 317) | async def _filter_jsonrpc_lines(source: Any, dest: Any) -> None:
  class _OpenHandsACPBridge (line 346) | class _OpenHandsACPBridge:
    method __init__ (line 392) | def __init__(self) -> None:
    method reset (line 421) | def reset(self) -> None:
    method prepare_usage_sync (line 433) | def prepare_usage_sync(self, session_id: str) -> asyncio.Event:
    method get_turn_usage_update (line 440) | def get_turn_usage_update(self, session_id: str) -> Any:
    method pop_turn_usage_update (line 444) | def pop_turn_usage_update(self, session_id: str) -> Any:
    method session_update (line 451) | async def session_update(
    method _emit_tool_call_event (line 527) | def _emit_tool_call_event(self, tc: dict[str, Any]) -> None:
    method _maybe_signal_activity (line 558) | def _maybe_signal_activity(self) -> None:
    method request_permission (line 579) | async def request_permission(
    method write_text_file (line 599) | async def write_text_file(
    method read_text_file (line 604) | async def read_text_file(
    method create_terminal (line 614) | async def create_terminal(
    method terminal_output (line 626) | async def terminal_output(
    method release_terminal (line 631) | async def release_terminal(
    method wait_for_terminal_exit (line 636) | async def wait_for_terminal_exit(
    method kill_terminal (line 641) | async def kill_terminal(
    method ext_method (line 646) | async def ext_method(
    method ext_notification (line 653) | async def ext_notification(
    method on_connect (line 660) | def on_connect(self, conn: Any) -> None:  # noqa: ARG002
  class ACPAgent (line 669) | class ACPAgent(AgentBase):
    method _serialize_acp_env (line 695) | def _serialize_acp_env(self, value: dict[str, str], info):
    method model_post_init (line 724) | def model_post_init(self, __context: object) -> None:
    method _record_usage (line 764) | def _record_usage(
    method supports_openhands_tools (line 841) | def supports_openhands_tools(self) -> bool:
    method supports_openhands_mcp (line 846) | def supports_openhands_mcp(self) -> bool:
    method supports_condenser (line 851) | def supports_condenser(self) -> bool:
    method agent_kind (line 856) | def agent_kind(self) -> Literal["acp"]:
    method agent_name (line 863) | def agent_name(self) -> str:
    method agent_version (line 868) | def agent_version(self) -> str:
    method get_all_llms (line 872) | def get_all_llms(self) -> Generator[LLM]:
    method init_state (line 877) | def init_state(
    method _render_suffix (line 970) | def _render_suffix(self, state: ConversationState) -> str | None:
    method _start_acp_server (line 979) | def _start_acp_server(self, state: ConversationState) -> None:
    method _reset_client_for_turn (line 1181) | def _reset_client_for_turn(
    method _cancel_inflight_tool_calls (line 1201) | def _cancel_inflight_tool_calls(self) -> None:
    method _build_acp_prompt (line 1245) | def _build_acp_prompt(
    method step (line 1270) | def step(
    method ask_agent (line 1514) | def ask_agent(self, question: str) -> str | None:
    method close (line 1570) | def close(self) -> None:
    method _cleanup (line 1577) | def _cleanup(self) -> None:
    method __del__ (line 1606) | def __del__(self) -> None:

FILE: openhands-sdk/openhands/sdk/agent/agent.py
  function _tool_has_summary_param (line 90) | def _tool_has_summary_param(tool: ToolDefinition) -> bool:
  class _ActionBatch (line 112) | class _ActionBatch:
    method _truncate_at_finish (line 128) | def _truncate_at_finish(
    method prepare (line 156) | def prepare(
    method emit (line 186) | def emit(self, on_event: ConversationCallbackType) -> None:
    method finalize (line 205) | def finalize(
  class Agent (line 239) | class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
    method model_post_init (line 284) | def model_post_init(self, __context: object) -> None:
    method _add_security_prompt_as_default (line 292) | def _add_security_prompt_as_default(cls, data):
    method init_state (line 305) | def init_state(
    method get_dynamic_context (line 410) | def get_dynamic_context(self, state: ConversationState) -> str | None:
    method _execute_actions (line 446) | def _execute_actions(
    method step (line 475) | def step(
    method _requires_user_confirmation (line 604) | def _requires_user_confirmation(
    method _extract_security_risk (line 647) | def _extract_security_risk(
    method _extract_summary (line 674) | def _extract_summary(
    method _emit_tool_error (line 721) | def _emit_tool_error(
    method _get_action_event (line 755) | def _get_action_event(
    method _execute_action_event (line 892) | def _execute_action_event(
    method _maybe_emit_vllm_tokens (line 950) | def _maybe_emit_vllm_tokens(
    method _log_context_window_exceeded_warning (line 965) | def _log_context_window_exceeded_warning(self) -> None:

FILE: openhands-sdk/openhands/sdk/agent/base.py
  class AgentBase (line 54) | class AgentBase(DiscriminatedUnionMixin, ABC):
    method _validate_system_prompt_fields (line 186) | def _validate_system_prompt_fields(cls, data: Any) -> Any:
    method _decrypt_mcp_config (line 208) | def _decrypt_mcp_config(cls, data: Any, info: ValidationInfo) -> Any:
    method _serialize_with_mcp_handling (line 250) | def _serialize_with_mcp_handling(self, handler, info: SerializationInfo):
    method prompt_dir (line 354) | def prompt_dir(self) -> str:
    method name (line 363) | def name(self) -> str:
    method static_system_message (line 368) | def static_system_message(self) -> str:
    method dynamic_context (line 411) | def dynamic_context(self) -> str | None:
    method init_state (line 434) | def init_state(
    method _initialize (line 448) | def _initialize(self, state: ConversationState):
    method step (line 533) | def step(
    method verify (line 555) | def verify(
    method model_dump_succint (line 623) | def model_dump_succint(self, **kwargs):
    method get_all_llms (line 633) | def get_all_llms(self) -> Generator[LLM]:
    method tools_map (line 704) | def tools_map(self) -> dict[str, ToolDefinition]:
    method supports_openhands_tools (line 719) | def supports_openhands_tools(self) -> bool:
    method supports_openhands_mcp (line 728) | def supports_openhands_mcp(self) -> bool:
    method supports_condenser (line 737) | def supports_condenser(self) -> bool:
    method agent_kind (line 746) | def agent_kind(self) -> Literal["openhands", "acp"]:
    method ask_agent (line 750) | def ask_agent(self, question: str) -> str | None:  # noqa: ARG002
    method close (line 761) | def close(self) -> None:

FILE: openhands-sdk/openhands/sdk/agent/critic_mixin.py
  class CriticMixin (line 25) | class CriticMixin:
    method _should_evaluate_with_critic (line 34) | def _should_evaluate_with_critic(self, action: Action | None) -> bool:
    method _evaluate_with_critic (line 49) | def _evaluate_with_critic(
    method _check_iterative_refinement (line 76) | def _check_iterative_refinement(

FILE: openhands-sdk/openhands/sdk/agent/parallel_executor.py
  class ParallelToolExecutor (line 38) | class ParallelToolExecutor:
    method __init__ (line 46) | def __init__(
    method execute_batch (line 54) | def execute_batch(
    method _run_safe (line 93) | def _run_safe(
    method _extract_declared_resources (line 143) | def _extract_declared_resources(
    method _resolve_lock_keys (line 152) | def _resolve_lock_keys(

FILE: openhands-sdk/openhands/sdk/agent/response_dispatch.py
  class LLMResponseType (line 44) | class LLMResponseType(StrEnum):
  function classify_response (line 53) | def classify_response(message: Message) -> LLMResponseType:
  class _AgentProtocol (line 86) | class _AgentProtocol(Protocol):
    method _get_action_event (line 91) | def _get_action_event(
    method _execute_actions (line 104) | def _execute_actions(
    method _requires_user_confirmation (line 111) | def _requires_user_confirmation(
    method _maybe_emit_vllm_tokens (line 117) | def _maybe_emit_vllm_tokens(
    method _evaluate_with_critic (line 123) | def _evaluate_with_critic(
  class ResponseDispatchMixin (line 130) | class ResponseDispatchMixin:
    method _get_action_event (line 140) | def _get_action_event(
    method _execute_actions (line 155) | def _execute_actions(
    method _requires_user_confirmation (line 162) | def _requires_user_confirmation(
    method _maybe_emit_vllm_tokens (line 168) | def _maybe_emit_vllm_tokens(
    method _evaluate_with_critic (line 174) | def _evaluate_with_critic(
    method _handle_tool_calls (line 180) | def _handle_tool_calls(
    method _handle_content_response (line 225) | def _handle_content_response(
    method _handle_no_content_response (line 239) | def _handle_no_content_response(
    method _emit_message_event (line 261) | def _emit_message_event(
    method _send_corrective_nudge (line 283) | def _send_corrective_nudge(self, on_event: ConversationCallbackType) -...

FILE: openhands-sdk/openhands/sdk/agent/utils.py
  function _escape_control_char (line 48) | def _escape_control_char(m: re.Match[str]) -> str:
  function sanitize_json_control_chars (line 54) | def sanitize_json_control_chars(raw: str) -> str:
  function fix_malformed_tool_arguments (line 68) | def fix_malformed_tool_arguments(
  function _normalize_arguments (line 194) | def _normalize_arguments(arguments: dict[str, Any]) -> dict[str, Any]:
  function parse_tool_call_arguments (line 209) | def parse_tool_call_arguments(raw_arguments: str) -> dict[str, Any]:
  function _infer_file_editor_command (line 221) | def _infer_file_editor_command(arguments: dict[str, Any]) -> str | None:
  function _has_file_editor_hint (line 235) | def _has_file_editor_hint(arguments: dict[str, Any]) -> bool:
  function _join_shell_command (line 300) | def _join_shell_command(parts: list[str]) -> str:
  function _build_ripgrep_terminal_command (line 307) | def _build_ripgrep_terminal_command(
  function _build_system_grep_terminal_command (line 318) | def _build_system_grep_terminal_command(
  function _build_python_grep_terminal_command (line 329) | def _build_python_grep_terminal_command(
  function _build_grep_terminal_command (line 341) | def _build_grep_terminal_command(arguments: dict[str, Any]) -> str | None:
  function _maybe_rewrite_as_terminal_command (line 366) | def _maybe_rewrite_as_terminal_command(
  function normalize_tool_call (line 386) | def normalize_tool_call(
  function prepare_llm_messages (line 446) | def prepare_llm_messages(
  function prepare_llm_messages (line 455) | def prepare_llm_messages(
  function prepare_llm_messages (line 463) | def prepare_llm_messages(
  function make_llm_completion (line 517) | def make_llm_completion(

FILE: openhands-sdk/openhands/sdk/banner.py
  function _print_banner (line 15) | def _print_banner(version: str) -> None:

FILE: openhands-sdk/openhands/sdk/context/agent_context.py
  class AgentContext (line 37) | class AgentContext(BaseModel):
    method _serialize_secrets (line 122) | def _serialize_secrets(
    method _validate_skills (line 138) | def _validate_skills(cls, v: list[Skill], _info):
    method _load_auto_skills (line 150) | def _load_auto_skills(self):
    method get_secret_infos (line 174) | def get_secret_infos(self) -> list[dict[str, str | None]]:
    method get_formatted_datetime (line 192) | def get_formatted_datetime(self) -> str | None:
    method _partition_skills (line 206) | def _partition_skills(self) -> tuple[list[Skill], list[Skill]]:
    method get_system_message_suffix (line 229) | def get_system_message_suffix(
    method validate_acp_compatibility (line 321) | def validate_acp_compatibility(self) -> None:
    method to_acp_prompt_context (line 340) | def to_acp_prompt_context(
    method get_user_message_suffix (line 377) | def get_user_message_suffix(

FILE: openhands-sdk/openhands/sdk/context/condenser/base.py
  class CondenserBase (line 16) | class CondenserBase(DiscriminatedUnionMixin, ABC):
    method condense (line 33) | def condense(self, view: View, agent_llm: LLM | None = None) -> View |...
    method handles_condensation_requests (line 50) | def handles_condensation_requests(self) -> bool:
  class PipelinableCondenserBase (line 65) | class PipelinableCondenserBase(CondenserBase):
  class NoCondensationAvailableException (line 70) | class NoCondensationAvailableException(Exception):
  class CondensationRequirement (line 81) | class CondensationRequirement(Enum):
  class RollingCondenser (line 93) | class RollingCondenser(PipelinableCondenserBase, ABC):
    method hard_context_reset (line 106) | def hard_context_reset(
    method condensation_requirement (line 124) | def condensation_requirement(
    method get_condensation (line 140) | def get_condensation(
    method condense (line 145) | def condense(self, view: View, agent_llm: LLM | None = None) -> View |...

FILE: openhands-sdk/openhands/sdk/context/condenser/llm_summarizing_condenser.py
  class Reason (line 29) | class Reason(Enum):
  class LLMSummarizingCondenser (line 37) | class LLMSummarizingCondenser(RollingCondenser):
    method validate_keep_first_vs_max_size (line 73) | def validate_keep_first_vs_max_size(self):
    method handles_condensation_requests (line 82) | def handles_condensation_requests(self) -> bool:
    method get_condensation_reasons (line 85) | def get_condensation_reasons(
    method condensation_requirement (line 116) | def condensation_requirement(
    method _generate_condensation (line 141) | def _generate_condensation(
    method _get_forgotten_events (line 197) | def _get_forgotten_events(
    method hard_context_reset (line 264) | def hard_context_reset(
    method get_condensation (line 309) | def get_condensation(

FILE: openhands-sdk/openhands/sdk/context/condenser/no_op_condenser.py
  class NoOpCondenser (line 7) | class NoOpCondenser(CondenserBase):
    method condense (line 13) | def condense(self, view: View, agent_llm: LLM | None = None) -> View |...

FILE: openhands-sdk/openhands/sdk/context/condenser/pipeline_condenser.py
  class PipelineCondenser (line 7) | class PipelineCondenser(CondenserBase):
    method condense (line 45) | def condense(self, view: View, agent_llm: LLM | None = None) -> View |...
    method handles_condensation_requests (line 53) | def handles_condensation_requests(self) -> bool:

FILE: openhands-sdk/openhands/sdk/context/condenser/utils.py
  function get_total_token_count (line 7) | def get_total_token_count(
  function get_shortest_prefix_above_token_count (line 41) | def get_shortest_prefix_above_token_count(
  function get_suffix_length_for_token_reduction (line 101) | def get_suffix_length_for_token_reduction(

FILE: openhands-sdk/openhands/sdk/context/prompts/prompt.py
  class FlexibleFileSystemLoader (line 16) | class FlexibleFileSystemLoader(BaseLoader):
    method __init__ (line 21) | def __init__(self, searchpath: str):
    method get_source (line 24) | def get_source(self, environment, template):  # noqa: ARG002
  function refine (line 48) | def refine(text: str) -> str:
  function _get_env (line 58) | def _get_env(prompt_dir: str) -> Environment:
  function _get_template (line 78) | def _get_template(prompt_dir: str, template_name: str) -> Template:
  function render_template (line 88) | def render_template(prompt_dir: str, template_name: str, **ctx) -> str:

FILE: openhands-sdk/openhands/sdk/context/view/manipulation_indices.py
  class ManipulationIndices (line 6) | class ManipulationIndices(set[int]):
    method find_next (line 18) | def find_next(self, threshold: int) -> int:
    method complete (line 41) | def complete(events: list[LLMConvertibleEvent]) -> ManipulationIndices:

FILE: openhands-sdk/openhands/sdk/context/view/properties/base.py
  class ViewPropertyBase (line 8) | class ViewPropertyBase(ABC):
    method enforce (line 29) | def enforce(
    method manipulation_indices (line 47) | def manipulation_indices(

FILE: openhands-sdk/openhands/sdk/context/view/properties/batch_atomicity.py
  class BatchAtomicityProperty (line 10) | class BatchAtomicityProperty(ViewPropertyBase):
    method enforce (line 21) | def enforce(
    method manipulation_indices (line 49) | def manipulation_indices(
    method _build_batches (line 78) | def _build_batches(self, events: Sequence[Event]) -> dict[EventID, set...

FILE: openhands-sdk/openhands/sdk/context/view/properties/observation_uniqueness.py
  class ObservationUniquenessProperty (line 18) | class ObservationUniquenessProperty(ViewPropertyBase):
    method enforce (line 33) | def enforce(
    method manipulation_indices (line 54) | def manipulation_indices(

FILE: openhands-sdk/openhands/sdk/context/view/properties/tool_call_matching.py
  class ToolCallMatchingProperty (line 15) | class ToolCallMatchingProperty(ViewPropertyBase):
    method enforce (line 25) | def enforce(
    method manipulation_indices (line 61) | def manipulation_indices(

FILE: openhands-sdk/openhands/sdk/context/view/properties/tool_loop_atomicity.py
  class ToolLoopAtomicityProperty (line 14) | class ToolLoopAtomicityProperty(ViewPropertyBase):
    method _tool_loops (line 24) | def _tool_loops(self, events: Sequence[Event]) -> list[set[EventID]]:
    method enforce (line 65) | def enforce(
    method manipulation_indices (line 95) | def manipulation_indices(

FILE: openhands-sdk/openhands/sdk/context/view/view.py
  class View (line 22) | class View(BaseModel):
    method __len__ (line 35) | def __len__(self) -> int:
    method manipulation_indices (line 39) | def manipulation_indices(self) -> ManipulationIndices:
    method __getitem__ (line 58) | def __getitem__(self, key: slice) -> list[LLMConvertibleEvent]: ...
    method __getitem__ (line 61) | def __getitem__(self, key: int) -> LLMConvertibleEvent: ...
    method __getitem__ (line 63) | def __getitem__(
    method enforce_properties (line 74) | def enforce_properties(
    method append_event (line 111) | def append_event(self, event: Event) -> None:
    method from_events (line 143) | def from_events(events: Sequence[Event]) -> View:

FILE: openhands-sdk/openhands/sdk/conversation/base.py
  class ConversationStateProtocol (line 44) | class ConversationStateProtocol(Protocol):
    method id (line 48) | def id(self) -> ConversationID:
    method events (line 53) | def events(self) -> EventsListBase:
    method execution_status (line 58) | def execution_status(self) -> "ConversationExecutionStatus":
    method confirmation_policy (line 63) | def confirmation_policy(self) -> ConfirmationPolicyBase:
    method security_analyzer (line 68) | def security_analyzer(self) -> SecurityAnalyzerBase | None:
    method activated_knowledge_skills (line 73) | def activated_knowledge_skills(self) -> list[str]:
    method invoked_skills (line 78) | def invoked_skills(self) -> list[str]:
    method workspace (line 83) | def workspace(self) -> BaseWorkspace:
    method persistence_dir (line 88) | def persistence_dir(self) -> str | None:
    method agent (line 96) | def agent(self) -> "AgentBase":
    method stats (line 101) | def stats(self) -> ConversationStats:
    method hook_config (line 106) | def hook_config(self) -> "HookConfig | None":
  class BaseConversation (line 111) | class BaseConversation(ABC):
    method __init__ (line 119) | def __init__(self) -> None:
    method _start_observability_span (line 130) | def _start_observability_span(self, session_id: str) -> None:
    method _end_observability_span (line 145) | def _end_observability_span(self) -> None:
    method id (line 155) | def id(self) -> ConversationID: ...
    method state (line 159) | def state(self) -> ConversationStateProtocol: ...
    method conversation_stats (line 163) | def conversation_stats(self) -> ConversationStats: ...
    method send_message (line 166) | def send_message(self, message: str | Message, sender: str | None = No...
    method run (line 180) | def run(self) -> None:
    method set_confirmation_policy (line 189) | def set_confirmation_policy(self, policy: ConfirmationPolicyBase) -> N...
    method set_security_analyzer (line 194) | def set_security_analyzer(self, analyzer: SecurityAnalyzerBase | None)...
    method confirmation_policy_active (line 199) | def confirmation_policy_active(self) -> bool:
    method is_confirmation_mode_active (line 203) | def is_confirmation_mode_active(self) -> bool:
    method reject_pending_actions (line 216) | def reject_pending_actions(
    method pause (line 221) | def pause(self) -> None: ...
    method update_secrets (line 224) | def update_secrets(self, secrets: Mapping[str, SecretValue]) -> None: ...
    method close (line 227) | def close(self) -> None: ...
    method generate_title (line 230) | def generate_title(self, llm: LLM | None = None, max_length: int = 50)...
    method get_persistence_dir (line 247) | def get_persistence_dir(
    method ask_agent (line 264) | def ask_agent(self, question: str) -> str:
    method condense (line 282) | def condense(self) -> None:
    method execute_tool (line 299) | def execute_tool(self, tool_name: str, action: Action) -> Observation:
    method fork (line 329) | def fork(
    method compose_callbacks (line 361) | def compose_callbacks(callbacks: Iterable[CallbackType]) -> CallbackType:

FILE: openhands-sdk/openhands/sdk/conversation/conversation.py
  class Conversation (line 31) | class Conversation:
    method __new__ (line 61) | def __new__(
    method __new__ (line 86) | def __new__(
    method __new__ (line 109) | def __new__(

FILE: openhands-sdk/openhands/sdk/conversation/conversation_stats.py
  class ConversationStats (line 13) | class ConversationStats(BaseModel):
    method _serialize_with_context (line 24) | def _serialize_with_context(self, serializer: Any, info: Any) -> dict[...
    method get_combined_metrics (line 58) | def get_combined_metrics(self) -> Metrics:
    method get_metrics_for_usage (line 64) | def get_metrics_for_usage(self, usage_id: str) -> Metrics:
    method register_llm (line 70) | def register_llm(self, event: RegistryEvent):

FILE: openhands-sdk/openhands/sdk/conversation/event_store.py
  class EventLog (line 25) | class EventLog(EventsListBase):
    method __init__ (line 44) | def __init__(self, fs: FileStore, dir_path: str = EVENTS_DIR) -> None:
    method set_write_guard (line 53) | def set_write_guard(
    method get_index (line 59) | def get_index(self, event_id: EventID) -> int:
    method get_id (line 66) | def get_id(self, idx: int) -> EventID:
    method __getitem__ (line 75) | def __getitem__(self, idx: int) -> Event: ...
    method __getitem__ (line 78) | def __getitem__(self, idx: slice) -> list[Event]: ...
    method __getitem__ (line 80) | def __getitem__(self, idx: SupportsIndex | slice) -> Event | list[Event]:
    method _get_single_item (line 86) | def _get_single_item(self, idx: SupportsIndex) -> Event:
    method __iter__ (line 107) | def __iter__(self) -> Iterator[Event]:
    method append (line 119) | def append(self, event: Event) -> None:
    method _count_events_on_disk (line 159) | def _count_events_on_disk(self) -> int:
    method _sync_from_disk (line 175) | def _sync_from_disk(self, disk_length: int) -> None:
    method __len__ (line 196) | def __len__(self) -> int:
    method _path (line 199) | def _path(self, idx: int, *, event_id: EventID | None = None) -> str:
    method _scan_and_build_index (line 206) | def _scan_and_build_index(self) -> int:

FILE: openhands-sdk/openhands/sdk/conversation/events_list_base.py
  class EventsListBase (line 7) | class EventsListBase(Sequence[Event], ABC):
    method append (line 15) | def append(self, event: Event) -> None:

FILE: openhands-sdk/openhands/sdk/conversation/exceptions.py
  class WebSocketConnectionError (line 7) | class WebSocketConnectionError(RuntimeError):
    method __init__ (line 10) | def __init__(
  class ConversationRunError (line 25) | class ConversationRunError(RuntimeError):
    method __init__ (line 36) | def __init__(
    method _build_error_message (line 52) | def _build_error_message(

FILE: openhands-sdk/openhands/sdk/conversation/fifo_lock.py
  class FIFOLock (line 14) | class FIFOLock:
    method __init__ (line 32) | def __init__(self) -> None:
    method acquire (line 40) | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool:
    method release (line 92) | def release(self) -> None:
    method __enter__ (line 112) | def __enter__(self: Self) -> Self:
    method __exit__ (line 117) | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    method locked (line 121) | def locked(self) -> bool:
    method owned (line 128) | def owned(self) -> bool:

FILE: openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
  class LocalConversation (line 71) | class LocalConversation(BaseConversation):
    method __init__ (line 90) | def __init__(
    method id (line 282) | def id(self) -> ConversationID:
    method state (line 287) | def state(self) -> ConversationState:
    method conversation_stats (line 298) | def conversation_stats(self):
    method stuck_detector (line 302) | def stuck_detector(self) -> StuckDetector | None:
    method resolved_plugins (line 307) | def resolved_plugins(self) -> list[ResolvedPluginSource] | None:
    method fork (line 316) | def fork(
    method _ensure_plugins_loaded (line 419) | def _ensure_plugins_loaded(self) -> None:
    method _register_file_based_agents (line 552) | def _register_file_based_agents(self) -> None:
    method _ensure_agent_ready (line 571) | def _ensure_agent_ready(self) -> None:
    method _should_initialize_agent_on_send_message (line 619) | def _should_initialize_agent_on_send_message(self) -> bool:
    method _pin_prompt_cache_key (line 629) | def _pin_prompt_cache_key(self) -> None:
    method switch_llm (line 637) | def switch_llm(self, llm: LLM) -> None:
    method switch_profile (line 658) | def switch_profile(self, profile_name: str) -> None:
    method send_message (line 681) | def send_message(self, message: str | Message, sender: str | None = No...
    method run (line 747) | def run(self) -> None:
    method set_confirmation_policy (line 892) | def set_confirmation_policy(self, policy: ConfirmationPolicyBase) -> N...
    method reject_pending_actions (line 898) | def reject_pending_actions(self, reason: str = "User rejected the acti...
    method pause (line 929) | def pause(self) -> None:
    method update_secrets (line 954) | def update_secrets(self, secrets: Mapping[str, SecretValue]) -> None:
    method set_security_analyzer (line 973) | def set_security_analyzer(self, analyzer: SecurityAnalyzerBase | None)...
    method close (line 978) | def close(self) -> None:
    method ask_agent (line 1016) | def ask_agent(self, question: str) -> str:
    method generate_title (line 1089) | def generate_title(self, llm: LLM | None = None, max_length: int = 50)...
    method condense (line 1112) | def condense(self) -> None:
    method rerun_actions (line 1161) | def rerun_actions(
    method execute_tool (line 1266) | def execute_tool(self, tool_name: str, action: Action) -> Observation:
    method __del__ (line 1309) | def __del__(self) -> None:

FILE: openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py
  function _agent_kind_mismatch_message (line 65) | def _agent_kind_mismatch_message(conversation_id: ConversationID) -> str:
  function _validate_remote_agent (line 72) | def _validate_remote_agent(agent_data: dict) -> AgentBase:
  function _send_request (line 80) | def _send_request(
  class WebSocketCallbackClient (line 108) | class WebSocketCallbackClient:
    method __init__ (line 119) | def __init__(
    method start (line 134) | def start(self) -> None:
    method stop (line 141) | def stop(self) -> None:
    method wait_until_ready (line 148) | def wait_until_ready(self, timeout: float | None = None) -> bool:
    method _run (line 180) | def _run(self) -> None:
    method _client_loop (line 190) | async def _client_loop(self) -> None:
  class RemoteEventsList (line 232) | class RemoteEventsList(EventsListBase):
    method __init__ (line 246) | def __init__(
    method _do_full_sync (line 262) | def _do_full_sync(self) -> None:
    method reconcile (line 292) | def reconcile(self) -> int:
    method _add_event_unsafe (line 347) | def _add_event_unsafe(self, event: Event) -> None:
    method add_event (line 392) | def add_event(self, event: Event) -> None:
    method append (line 403) | def append(self, event: Event) -> None:
    method create_default_callback (line 407) | def create_default_callback(self) -> ConversationCallbackType:
    method __len__ (line 415) | def __len__(self) -> int:
    method __getitem__ (line 419) | def __getitem__(self, index: int) -> Event: ...
    method __getitem__ (line 422) | def __getitem__(self, index: slice) -> list[Event]: ...
    method __getitem__ (line 424) | def __getitem__(self, index: SupportsIndex | slice) -> Event | list[Ev...
    method __iter__ (line 428) | def __iter__(self):
  class RemoteState (line 433) | class RemoteState(ConversationStateProtocol):
    method __init__ (line 443) | def __init__(
    method _get_conversation_info (line 459) | def _get_conversation_info(self) -> dict:
    method refresh_from_server (line 469) | def refresh_from_server(self) -> dict:
    method update_state_from_event (line 481) | def update_state_from_event(self, event: ConversationStateUpdateEvent)...
    method create_state_update_callback (line 496) | def create_state_update_callback(self) -> ConversationCallbackType:
    method events (line 506) | def events(self) -> RemoteEventsList:
    method id (line 511) | def id(self) -> ConversationID:
    method execution_status (line 516) | def execution_status(self) -> ConversationExecutionStatus:
    method execution_status (line 527) | def execution_status(self, value: ConversationExecutionStatus) -> None:
    method confirmation_policy (line 539) | def confirmation_policy(self) -> ConfirmationPolicyBase:
    method security_analyzer (line 550) | def security_analyzer(self) -> SecurityAnalyzerBase | None:
    method activated_knowledge_skills (line 560) | def activated_knowledge_skills(self) -> list[str]:
    method invoked_skills (line 566) | def invoked_skills(self) -> list[str]:
    method agent (line 572) | def agent(self):
    method workspace (line 581) | def workspace(self):
    method persistence_dir (line 590) | def persistence_dir(self):
    method stats (line 601) | def stats(self) -> ConversationStats:
    method hook_config (line 608) | def hook_config(self) -> HookConfig | None:
    method model_dump (line 616) | def model_dump(self, **_kwargs):
    method model_dump_json (line 621) | def model_dump_json(self, **kwargs):
    method __enter__ (line 626) | def __enter__(self):
    method __exit__ (line 629) | def __exit__(self, exc_type, exc_val, exc_tb):
  class RemoteConversation (line 633) | class RemoteConversation(BaseConversation):
    method __init__ (line 649) | def __init__(
    method _create_llm_completion_log_callback (line 890) | def _create_llm_completion_log_callback(self) -> ConversationCallbackT...
    method id (line 924) | def id(self) -> ConversationID:
    method state (line 928) | def state(self) -> RemoteState:
    method conversation_stats (line 933) | def conversation_stats(self):
    method stuck_detector (line 937) | def stuck_detector(self):
    method send_message (line 946) | def send_message(self, message: str | Message, sender: str | None = No...
    method run (line 967) | def run(
    method _wait_for_run_completion (line 1016) | def _wait_for_run_completion(
    method _poll_status_once (line 1120) | def _poll_status_once(self) -> str | None:
    method _handle_conversation_status (line 1131) | def _handle_conversation_status(self, status: str | None) -> bool:
    method _handle_poll_exception (line 1148) | def _handle_poll_exception(self, exc: Exception) -> None:
    method _get_last_error_detail (line 1177) | def _get_last_error_detail(self) -> str | None:
    method set_confirmation_policy (line 1189) | def set_confirmation_policy(self, policy: ConfirmationPolicyBase) -> N...
    method set_security_analyzer (line 1198) | def set_security_analyzer(self, analyzer: SecurityAnalyzerBase | None)...
    method reject_pending_actions (line 1212) | def reject_pending_actions(self, reason: str = "User rejected the acti...
    method pause (line 1224) | def pause(self) -> None:
    method update_secrets (line 1231) | def update_secrets(self, secrets: Mapping[str, SecretValue]) -> None:
    method ask_agent (line 1256) | def ask_agent(self, question: str) -> str:
    method generate_title (line 1284) | def generate_title(self, llm: LLM | None = None, max_length: int = 50)...
    method condense (line 1304) | def condense(self) -> None:
    method fork (line 1323) | def fork(
    method execute_tool (line 1396) | def execute_tool(self, tool_name: str, action: "Action") -> "Observati...
    method close (line 1418) | def close(self) -> None:
    method __del__ (line 1450) | def __del__(self) -> None:

FILE: openhands-sdk/openhands/sdk/conversation/request.py
  class SendMessageRequest (line 50) | class SendMessageRequest(BaseModel):
    method create_message (line 60) | def create_message(self) -> Message:
  class StartConversationRequest (line 64) | class StartConversationRequest(BaseModel):
    method _populate_agent_from_settings (line 207) | def _populate_agent_from_settings(cls, data: Any) -> Any:
    method _require_agent (line 228) | def _require_agent(self) -> StartConversationRequest:
  class StartACPConversationRequest (line 234) | class StartACPConversationRequest(StartConversationRequest):

FILE: openhands-sdk/openhands/sdk/conversation/resource_lock_manager.py
  class ResourceLockTimeout (line 31) | class ResourceLockTimeout(TimeoutError):
  class ResourceLockManager (line 35) | class ResourceLockManager:
    method __init__ (line 46) | def __init__(
    method _get_lock (line 55) | def _get_lock(self, key: str) -> FIFOLock:
    method _release_lock (line 67) | def _release_lock(self, key: str) -> None:
    method _get_timeout (line 79) | def _get_timeout(self, key: str) -> float:
    method lock (line 85) | def lock(self, *resource_keys: str) -> Generator[None]:

FILE: openhands-sdk/openhands/sdk/conversation/response_utils.py
  function get_agent_final_response (line 11) | def get_agent_final_response(events: Sequence[Event]) -> str:

FILE: openhands-sdk/openhands/sdk/conversation/secret_registry.py
  class SecretRegistry (line 15) | class SecretRegistry(OpenHandsModel):
    method update_secrets (line 36) | def update_secrets(
    method find_secrets_in_text (line 49) | def find_secrets_in_text(self, text: str) -> set[str]:
    method get_secrets_as_env_vars (line 64) | def get_secrets_as_env_vars(self, command: str) -> dict[str, str]:
    method mask_secrets_in_output (line 96) | def mask_secrets_in_output(self, text: str) -> str:
    method get_secret_infos (line 119) | def get_secret_infos(self) -> list[dict[str, str | None]]:
    method get_secret_value (line 135) | def get_secret_value(self, name: str) -> str | None:
  function _wrap_secret (line 185) | def _wrap_secret(value: SecretValue) -> SecretSource:

FILE: openhands-sdk/openhands/sdk/conversation/state.py
  class ConversationExecutionStatus (line 46) | class ConversationExecutionStatus(str, Enum):
    method is_terminal (line 60) | def is_terminal(self) -> bool:
  class ConversationState (line 80) | class ConversationState(OpenHandsModel):
    method events (line 225) | def events(self) -> EventLog:
    method env_observation_persistence_dir (line 229) | def env_observation_persistence_dir(self) -> str | None:
    method set_on_state_change (line 235) | def set_on_state_change(self, callback: ConversationCallbackType | Non...
    method set_write_guard (line 244) | def set_write_guard(
    method _save_base_state (line 252) | def _save_base_state(self, fs: FileStore) -> None:
    method create (line 277) | def create(
    method __setattr__ (line 399) | def __setattr__(self, name, value):
    method block_action (line 446) | def block_action(self, action_id: str, reason: str) -> None:
    method pop_blocked_action (line 450) | def pop_blocked_action(self, action_id: str) -> str | None:
    method block_message (line 459) | def block_message(self, message_id: str, reason: str) -> None:
    method pop_blocked_message (line 463) | def pop_blocked_message(self, message_id: str) -> str | None:
    method get_unmatched_actions (line 473) | def get_unmatched_actions(events: Sequence[Event]) -> list[ActionEvent]:
    method acquire (line 515) | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool:
    method release (line 530) | def release(self) -> None:
    method __enter__ (line 539) | def __enter__(self: Self) -> Self:
    method __exit__ (line 549) | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    method locked (line 562) | def locked(self) -> bool:
    method owned (line 568) | def owned(self) -> bool:

FILE: openhands-sdk/openhands/sdk/conversation/stuck_detector.py
  class StuckDetector (line 24) | class StuckDetector:
    method __init__ (line 38) | def __init__(
    method action_observation_threshold (line 47) | def action_observation_threshold(self) -> int:
    method action_error_threshold (line 51) | def action_error_threshold(self) -> int:
    method monologue_threshold (line 55) | def monologue_threshold(self) -> int:
    method alternating_pattern_threshold (line 59) | def alternating_pattern_threshold(self) -> int:
    method is_stuck (line 62) | def is_stuck(self) -> bool:
    method _is_stuck_repeating_action_observation (line 140) | def _is_stuck_repeating_action_observation(
    method _is_stuck_repeating_action_error (line 176) | def _is_stuck_repeating_action_error(
    method _is_stuck_monologue (line 200) | def _is_stuck_monologue(self, events: list[Event]) -> bool:
    method _is_stuck_alternating_action_observation (line 227) | def _is_stuck_alternating_action_observation(self, events: list[Event]...
    method _is_stuck_context_window_error (line 264) | def _is_stuck_context_window_error(self, _events: list[Event]) -> bool:
    method _event_eq (line 275) | def _event_eq(self, event1: Event, event2: Event) -> bool:

FILE: openhands-sdk/openhands/sdk/conversation/title_utils.py
  function extract_message_text (line 32) | def extract_message_text(event: MessageEvent) -> str | None:
  function extract_first_user_message (line 45) | def extract_first_user_message(events: Sequence[Event]) -> str | None:
  function generate_title_with_llm (line 62) | def generate_title_with_llm(message: str, llm: LLM, max_length: int = 50...
  function generate_fallback_title (line 144) | def generate_fallback_title(message: str, max_length: int = 50) -> str:
  function generate_title_from_message (line 160) | def generate_title_from_message(
  function generate_conversation_title (line 177) | def generate_conversation_title(

FILE: openhands-sdk/openhands/sdk/conversation/types.py
  function _validate_tags (line 25) | def _validate_tags(v: dict[str, str] | None) -> dict[str, str]:
  class StuckDetectionThresholds (line 48) | class StuckDetectionThresholds(BaseModel):

FILE: openhands-sdk/openhands/sdk/conversation/visualizer/base.py
  class ConversationVisualizerBase (line 12) | class ConversationVisualizerBase(ABC):
    method __init__ (line 33) | def __init__(self):
    method initialize (line 38) | def initialize(self, state: "ConversationStateProtocol") -> None:
    method conversation_stats (line 53) | def conversation_stats(self) -> "ConversationStats | None":
    method on_event (line 58) | def on_event(self, event: Event) -> None:
    method create_sub_visualizer (line 69) | def create_sub_visualizer(

FILE: openhands-sdk/openhands/sdk/conversation/visualizer/default.py
  class _EncodingSafeTextIO (line 61) | class _EncodingSafeTextIO:
    method encoding (line 67) | def encoding(self) -> str | None:
    method fileno (line 70) | def fileno(self) -> int:
    method flush (line 73) | def flush(self) -> None:
    method isatty (line 76) | def isatty(self) -> bool:
    method write (line 79) | def write(self, text: str) -> int:
  function _create_console (line 89) | def _create_console() -> Console:
  class EventVisualizationConfig (line 94) | class EventVisualizationConfig(BaseModel):
  function indent_content (line 115) | def indent_content(content: Text, spaces: int = 4) -> Text:
  function section_header (line 130) | def section_header(title: str, color: str) -> Rule:
  function build_event_block (line 140) | def build_event_block(
  function _get_action_title (line 172) | def _get_action_title(event: Event) -> str:
  function _get_message_title (line 179) | def _get_message_title(event: Event) -> str:
  function _get_message_color (line 190) | def _get_message_color(event: Event) -> str:
  class DefaultConversationVisualizer (line 256) | class DefaultConversationVisualizer(ConversationVisualizerBase):
    method __init__ (line 266) | def __init__(
    method on_event (line 286) | def on_event(self, event: Event) -> None:
    method _apply_highlighting (line 292) | def _apply_highlighting(self, text: Text) -> Text:
    method _create_event_block (line 314) | def _create_event_block(self, event: Event) -> Group | None:
    method _format_metrics_subtitle (line 367) | def _format_metrics_subtitle(self) -> str | None:

FILE: openhands-sdk/openhands/sdk/critic/base.py
  class IterativeRefinementConfig (line 20) | class IterativeRefinementConfig(BaseModel):
  class CriticBase (line 57) | class CriticBase(DiscriminatedUnionMixin, abc.ABC):
    method evaluate (line 83) | def evaluate(
    method get_followup_prompt (line 88) | def get_followup_prompt(self, critic_result: CriticResult, iteration: ...
    method should_refine (line 109) | def should_refine(self, critic_result: CriticResult) -> bool:

FILE: openhands-sdk/openhands/sdk/critic/impl/agent_finished.py
  class AgentFinishedCritic (line 24) | class AgentFinishedCritic(CriticBase):
    method evaluate (line 33) | def evaluate(
    method _has_finish_action (line 73) | def _has_finish_action(self, events: Sequence["LLMConvertibleEvent"]) ...

FILE: openhands-sdk/openhands/sdk/critic/impl/api/chat_template.py
  function _get_cache_path (line 34) | def _get_cache_path(tokenizer_name: str) -> Path:
  function _fetch_tokenizer_config (line 41) | def _fetch_tokenizer_config(
  function _compile_jinja_template (line 82) | def _compile_jinja_template(chat_template: str) -> jinja2.Template:
  class ChatTemplateRenderer (line 120) | class ChatTemplateRenderer:
    method __init__ (line 128) | def __init__(
    method chat_template (line 160) | def chat_template(self) -> str:
    method apply_chat_template (line 165) | def apply_chat_template(
  function apply_chat_template (line 193) | def apply_chat_template(

FILE: openhands-sdk/openhands/sdk/critic/impl/api/client.py
  class UsageTokens (line 33) | class UsageTokens(BaseModel):
  class ClassificationItem (line 41) | class ClassificationItem(BaseModel):
  class ClassificationResponse (line 51) | class ClassificationResponse(BaseModel):
  class LabelProbMap (line 61) | class LabelProbMap(BaseModel):
  class CriticClient (line 78) | class CriticClient(BaseModel):
    method _validate_and_convert_api_key (line 174) | def _validate_and_convert_api_key(
    method _serialize_api_key (line 181) | def _serialize_api_key(self, v: str | SecretStr | None, info):
    method all_labels (line 189) | def all_labels(self) -> tuple[str, ...]:
    method _get_template_renderer (line 203) | def _get_template_renderer(self) -> ChatTemplateRenderer:
    method normalize_messages (line 212) | def normalize_messages(messages: Sequence[dict]) -> Sequence[dict]:
    method apply_chat_template (line 229) | def apply_chat_template(
    method _get_api_key_value (line 250) | def _get_api_key_value(self) -> str:
    method classify_trace (line 262) | def classify_trace(
    method extract_prob_map (line 304) | def extract_prob_map(self, response: ClassificationResponse) -> LabelP...
    method predict_labels (line 334) | def predict_labels(self, probs: list[float], threshold: float = 0.5) -...

FILE: openhands-sdk/openhands/sdk/critic/impl/api/critic.py
  function _format_feature_list (line 18) | def _format_feature_list(features: list[dict[str, Any]]) -> str:
  function _get_high_probability_agent_issues (line 30) | def _get_high_probability_agent_issues(
  class APIBasedCritic (line 47) | class APIBasedCritic(CriticBase, CriticClient):
    method evaluate (line 58) | def evaluate(
    method should_refine (line 135) | def should_refine(self, critic_result: CriticResult) -> bool:
    method get_followup_prompt (line 146) | def get_followup_prompt(self, critic_result: CriticResult, iteration: ...

FILE: openhands-sdk/openhands/sdk/critic/impl/api/taxonomy.py
  function get_category (line 50) | def get_category(feature_name: str) -> str | None:
  function _softmax_normalize (line 62) | def _softmax_normalize(probs: dict[str, float]) -> dict[str, float]:
  function categorize_features (line 82) | def categorize_features(

FILE: openhands-sdk/openhands/sdk/critic/impl/empty_patch.py
  class EmptyPatchCritic (line 18) | class EmptyPatchCritic(CriticBase):
    method evaluate (line 29) | def evaluate(

FILE: openhands-sdk/openhands/sdk/critic/impl/pass_critic.py
  class PassCritic (line 18) | class PassCritic(CriticBase):
    method evaluate (line 26) | def evaluate(

FILE: openhands-sdk/openhands/sdk/critic/result.py
  class CriticResult (line 7) | class CriticResult(BaseModel):
    method success (line 28) | def success(self) -> bool:
    method _get_star_rating (line 33) | def _get_star_rating(score: float) -> str:
    method _get_star_style (line 43) | def _get_star_style(score: float) -> str:
    method visualize (line 53) | def visualize(self) -> Text:
    method _append_categorized_features (line 78) | def _append_categorized_features(
    method _append_feature_list_inline (line 121) | def _append_feature_list_inline(

FILE: openhands-sdk/openhands/sdk/event/acp_tool_call.py
  class ACPToolCallEvent (line 16) | class ACPToolCallEvent(Event):
    method visualize (line 38) | def visualize(self) -> Text:
    method __str__ (line 70) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/base.py
  class Event (line 20) | class Event(DiscriminatedUnionMixin, ABC):
    method visualize (line 35) | def visualize(self) -> Text:
    method __str__ (line 46) | def __str__(self) -> str:
    method __repr__ (line 50) | def __repr__(self) -> str:
  class LLMConvertibleEvent (line 58) | class LLMConvertibleEvent(Event, ABC):
    method to_llm_message (line 62) | def to_llm_message(self) -> Message:
    method __str__ (line 65) | def __str__(self) -> str:
    method events_to_messages (line 91) | def events_to_messages(events: list["LLMConvertibleEvent"]) -> list[Me...
  function _combine_action_events (line 129) | def _combine_action_events(events: list["ActionEvent"]) -> Message:

FILE: openhands-sdk/openhands/sdk/event/condenser.py
  class Condensation (line 11) | class Condensation(Event):
    method visualize (line 40) | def visualize(self) -> Text:
    method summary_event (line 52) | def summary_event(self) -> CondensationSummaryEvent:
    method has_summary_metadata (line 79) | def has_summary_metadata(self) -> bool:
    method apply (line 83) | def apply(self, events: list[LLMConvertibleEvent]) -> list[LLMConverti...
  class CondensationRequest (line 99) | class CondensationRequest(Event):
    method visualize (line 109) | def visualize(self) -> Text:
  class CondensationSummaryEvent (line 120) | class CondensationSummaryEvent(LLMConvertibleEvent):
    method to_llm_message (line 128) | def to_llm_message(self) -> Message:

FILE: openhands-sdk/openhands/sdk/event/conversation_error.py
  class ConversationErrorEvent (line 7) | class ConversationErrorEvent(Event):
    method visualize (line 29) | def visualize(self) -> Text:

FILE: openhands-sdk/openhands/sdk/event/conversation_state.py
  class ConversationStateUpdateEvent (line 18) | class ConversationStateUpdateEvent(Event):
    method validate_key (line 39) | def validate_key(cls, key):
    method validate_value (line 51) | def validate_value(cls, value, info):
    method from_conversation_state (line 82) | def from_conversation_state(
    method __str__ (line 103) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/hook_execution.py
  class HookExecutionEvent (line 22) | class HookExecutionEvent(Event):
    method visualize (line 85) | def visualize(self) -> Text:
    method __str__ (line 123) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/llm_completion_log.py
  class LLMCompletionLogEvent (line 9) | class LLMCompletionLogEvent(Event):
    method __str__ (line 35) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/llm_convertible/action.py
  class ActionEvent (line 21) | class ActionEvent(LLMConvertibleEvent):
    method visualize (line 89) | def visualize(self) -> Text:
    method to_llm_message (line 140) | def to_llm_message(self) -> Message:
    method __str__ (line 151) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/llm_convertible/message.py
  class MessageEvent (line 21) | class MessageEvent(LLMConvertibleEvent):
    method reasoning_content (line 61) | def reasoning_content(self) -> str:
    method thinking_blocks (line 65) | def thinking_blocks(self) -> Sequence[ThinkingBlock | RedactedThinking...
    method visualize (line 70) | def visualize(self) -> Text:
    method to_llm_message (line 116) | def to_llm_message(self) -> Message:
    method __str__ (line 121) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/llm_convertible/observation.py
  class ObservationBaseEvent (line 16) | class ObservationBaseEvent(LLMConvertibleEvent):
  class ObservationEvent (line 31) | class ObservationEvent(ObservationBaseEvent):
    method visualize (line 40) | def visualize(self) -> Text:
    method to_llm_message (line 51) | def to_llm_message(self) -> Message:
    method __str__ (line 59) | def __str__(self) -> str:
  class UserRejectObservation (line 71) | class UserRejectObservation(ObservationBaseEvent):
    method visualize (line 95) | def visualize(self) -> Text:
    method to_llm_message (line 104) | def to_llm_message(self) -> Message:
    method __str__ (line 112) | def __str__(self) -> str:
  class AgentErrorEvent (line 123) | class AgentErrorEvent(ObservationBaseEvent):
    method visualize (line 134) | def visualize(self) -> Text:
    method to_llm_message (line 141) | def to_llm_message(self) -> Message:
    method __str__ (line 151) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/llm_convertible/system.py
  class SystemPromptEvent (line 12) | class SystemPromptEvent(LLMConvertibleEvent):
    method visualize (line 44) | def visualize(self) -> Text:
    method to_llm_message (line 72) | def to_llm_message(self) -> Message:
    method __str__ (line 87) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/event/streaming_delta.py
  class StreamingDeltaEvent (line 5) | class StreamingDeltaEvent(Event):

FILE: openhands-sdk/openhands/sdk/event/token.py
  class TokenEvent (line 7) | class TokenEvent(Event):

FILE: openhands-sdk/openhands/sdk/event/user_action.py
  class PauseEvent (line 7) | class PauseEvent(Event):
    method visualize (line 13) | def visualize(self) -> Text:
    method __str__ (line 19) | def __str__(self) -> str:

FILE: openhands-sdk/openhands/sdk/extensions/fetch.py
  class ExtensionFetchError (line 16) | class ExtensionFetchError(Exception):
  class SourceType (line 20) | class SourceType(StrEnum):
  function parse_extension_source (line 33) | def parse_extension_source(source: str) -> tuple[SourceType, str]:
  function _resolve_local_source (line 92) | def _resolve_local_source(url: str) -> Path:
  function _apply_subpath (line 110) | def _apply_subpath(base_path: Path, subpath: str | None, context: str) -...
  function fetch (line 133) | def fetch(
  function fetch_with_resolution (line 165) | def fetch_with_resolution(
  function get_cache_path (line 209) | def get_cache_path(source: str, cache_dir: Path) -> Path:
  function _fetch_remote_source_with_resolution (line 231) | def _fetch_remote_source_with_resolution(

FILE: openhands-sdk/openhands/sdk/extensions/installation/info.py
  class InstallationInfo (line 11) | class InstallationInfo(BaseModel):
    method from_extension (line 40) | def from_extension(

FILE: openhands-sdk/openhands/sdk/extensions/installation/interface.py
  class ExtensionProtocol (line 6) | class ExtensionProtocol(Protocol):
    method name (line 15) | def name(self) -> str: ...
    method version (line 18) | def version(self) -> str: ...
    method description (line 21) | def description(self) -> str | None: ...
  class InstallationInterface (line 24) | class InstallationInterface[T: ExtensionProtocol](ABC):
    method load_from_dir (line 33) | def load_from_dir(extension_dir: Path) -> T: ...

FILE: openhands-sdk/openhands/sdk/extensions/installation/manager.py
  class InstallationManager (line 27) | class InstallationManager[T: ExtensionProtocol]:
    method __post_init__ (line 43) | def __post_init__(self) -> None:
    method metadata_session (line 47) | def metadata_session(self) -> MetadataSession:
    method install (line 53) | def install(
    method uninstall (line 132) | def uninstall(self, name: str) -> bool:
    method _set_enabled (line 170) | def _set_enabled(
    method enable (line 214) | def enable(self, name: str) -> bool:
    method disable (line 218) | def disable(self, name: str) -> bool:
    method list_installed (line 222) | def list_installed(self) -> list[InstallationInfo]:
    method load_installed (line 238) | def load_installed(self) -> list[T]:
    method get (line 264) | def get(self, name: str) -> InstallationInfo | None:
    method update (line 291) | def update(self, name: str) -> InstallationInfo | None:

FILE: openhands-sdk/openhands/sdk/extensions/installation/metadata.py
  class MetadataSession (line 20) | class MetadataSession:
    method __init__ (line 30) | def __init__(
    method extensions (line 41) | def extensions(self) -> dict[str, InstallationInfo]:
    method sync (line 44) | def sync(self) -> list[InstallationInfo]:
    method __enter__ (line 67) | def __enter__(self) -> MetadataSession:
    method __exit__ (line 70) | def __exit__(
  class InstallationMetadata (line 80) | class InstallationMetadata(BaseModel):
    method _migrate_legacy_keys (line 101) | def _migrate_legacy_keys(cls, data: Any) -> Any:
    method open (line 123) | def open(
    method get_metadata_path (line 141) | def get_metadata_path(cls, installed_dir: Path) -> Path:
    method load_from_dir (line 146) | def load_from_dir(cls, installed_dir: Path) -> InstallationMetadata:
    method save_to_dir (line 158) | def save_to_dir(self, installed_dir: Path) -> None:
    method validate_tracked (line 164) | def validate_tracked(self, installed_dir: Path) -> list[InstallationIn...
    method discover_untracked (line 197) | def discover_untracked(

FILE: openhands-sdk/openhands/sdk/extensions/installation/utils.py
  function validate_extension_name (line 8) | def validate_extension_name(name: str) -> None:

FILE: openhands-sdk/openhands/sdk/git/cached_repo.py
  class GitHelper (line 26) | class GitHelper:
    method clone (line 33) | def clone(
    method fetch (line 69) | def fetch(
    method checkout (line 93) | def checkout(self, repo_path: Path, ref: str, timeout: int = 30) -> None:
    method reset_hard (line 106) | def reset_hard(self, repo_path: Path, ref: str, timeout: int = 30) -> ...
    method get_current_branch (line 119) | def get_current_branch(self, repo_path: Path, timeout: int = 10) -> st...
    method get_default_branch (line 140) | def get_default_branch(self, repo_path: Path, timeout: int = 10) -> st...
    method get_head_commit (line 173) | def get_head_commit(self, repo_path: Path, timeout: int = 10) -> str:
  function try_cached_clone_or_update (line 193) | def try_cached_clone_or_update(
  function _do_clone_or_update (line 256) | def _do_clone_or_update(
  function _clone_repository (line 294) | def _clone_repository(
  function _update_repository (line 316) | def _update_repository(
  function _try_fetch (line 367) | def _try_fetch(repo_path: Path, git: GitHelper) -> bool:
  function _try_checkout_and_reset (line 377) | def _try_checkout_and_reset(repo_path: Path, ref: str, git: GitHelper) -...
  function _try_reset_to_origin (line 386) | def _try_reset_to_origin(repo_path: Path, branch: str, git: GitHelper) -...
  function _recover_from_detached_head (line 397) | def _recover_from_detached_head(repo_path: Path, git: GitHelper) -> None:
  function _checkout_ref (line 434) | def _checkout_ref(repo_path: Path, ref: str, git: GitHelper) -> None:

FILE: openhands-sdk/openhands/sdk/git/exceptions.py
  class GitError (line 4) | class GitError(Exception):
  class GitRepositoryError (line 10) | class GitRepositoryError(GitError):
    method __init__ (line 16) | def __init__(
  class GitCommandError (line 24) | class GitCommandError(GitError):
    method __init__ (line 31) | def __init__(
  class GitPathError (line 40) | class GitPathError(GitError):

FILE: openhands-sdk/openhands/sdk/git/git_changes.py
  function _map_git_status_to_enum (line 24) | def _map_git_status_to_enum(status: str) -> GitChangeStatus:
  function get_changes_in_repo (line 37) | def get_changes_in_repo(
  function get_git_changes (line 205) | def get_git_changes(cwd: str | Path, ref: str | None = None) -> list[Git...

FILE: openhands-sdk/openhands/sdk/git/git_diff.py
  function get_closest_git_repo (line 29) | def get_closest_git_repo(path: Path) -> Path | None:
  function get_git_diff (line 53) | def get_git_diff(relative_file_path: str | Path, ref: str | None = None)...

FILE: openhands-sdk/openhands/sdk/git/models.py
  class GitChangeStatus (line 9) | class GitChangeStatus(Enum):
  class GitChange (line 16) | class GitChange(BaseModel):
    method _serialize_path (line 21) | def _serialize_path(self, path: Path) -> str:
  class GitDiff (line 25) | class GitDiff(BaseModel):

FILE: openhands-sdk/openhands/sdk/git/utils.py
  function run_git_command (line 17) | def run_git_command(
  function _repo_has_commits (line 82) | def _repo_has_commits(repo_dir: str | Path) -> bool:
  function get_valid_ref (line 104) | def get_valid_ref(repo_dir: str | Path, override: str | None = None) -> ...
  function validate_git_repository (line 245) | def validate_git_repository(repo_dir: str | Path) -> Path:
  function is_git_url (line 282) | def is_git_url(source: str) -> bool:
  function normalize_git_url (line 321) | def normalize_git_url(url: str) -> str:
  function extract_repo_name (line 344) | def extract_repo_name(source: str) -> str:

FILE: openhands-sdk/openhands/sdk/hooks/config.py
  function _pascal_to_snake (line 18) | def _pascal_to_snake(name: str) -> str:
  class HookType (line 39) | class HookType(StrEnum):
  class HookDefinition (line 46) | class HookDefinition(BaseModel):
    method _set_command_for_prompt_hooks (line 61) | def _set_command_for_prompt_hooks(cls, data: Any) -> Any:
    method _check_required_fields (line 71) | def _check_required_fields(self) -> "HookDefinition":
  class HookMatcher (line 79) | class HookMatcher(BaseModel):
    method matches (line 91) | def matches(self, tool_name: str | None) -> bool:
  class HookConfig (line 125) | class HookConfig(BaseModel):
    method is_empty (line 174) | def is_empty(self) -> bool:
    method _normalize_hooks_input (line 189) | def _normalize_hooks_input(cls, data: Any) -> Any:
    method load (line 239) | def load(
    method from_dict (line 274) | def from_dict(cls, data: dict[str, Any]) -> "HookConfig":
    method _get_matchers_for_event (line 286) | def _get_matchers_for_event(self, event_type: HookEventType) -> list[H...
    method get_hooks_for_event (line 291) | def get_hooks_for_event(
    method has_hooks_for_event (line 304) | def has_hooks_for_event(self, event_type: HookEventType) -> bool:
    method save (line 309) | def save(self, path: str | Path) -> None:
    method merge (line 318) | def merge(cls, configs: list["HookConfig"]) -> "HookConfig | None":

FILE: openhands-sdk/openhands/sdk/hooks/conversation_hooks.py
  function _truncate_hook_log (line 32) | def _truncate_hook_log(value: str | None) -> str | None:
  class HookEventProcessor (line 46) | class HookEventProcessor:
    method __init__ (line 55) | def __init__(
    method set_conversation_state (line 66) | def set_conversation_state(self, state: "ConversationState") -> None:
    method _emit_hook_execution_event (line 70) | def _emit_hook_execution_event(
    method on_event (line 102) | def on_event(self, event: Event) -> None:
    method _handle_pre_tool_use (line 123) | def _handle_pre_tool_use(self, event: ActionEvent) -> None:
    method _handle_post_tool_use (line 175) | def _handle_post_tool_use(self, event: ObservationEvent) -> None:
    method _handle_user_prompt_submit (line 240) | def _handle_user_prompt_submit(self, event: MessageEvent) -> MessageEv...
    method is_action_blocked (line 309) | def is_action_blocked(self, action_id: str) -> bool:
    method is_message_blocked (line 315) | def is_message_blocked(self, message_id: str) -> bool:
    method run_session_start (line 321) | def run_session_start(self) -> None:
    method run_session_end (line 337) | def run_session_end(self) -> None:
    method run_stop (line 351) | def run_stop(self, reason: str | None = None) -> tuple[bool, str | None]:
  function create_hook_callback (line 386) | def create_hook_callback(

FILE: openhands-sdk/openhands/sdk/hooks/executor.py
  class HookResult (line 17) | class HookResult(BaseModel):
    method should_continue (line 44) | def should_continue(self) -> bool:
  class AsyncProcessManager (line 56) | class AsyncProcessManager:
    method __init__ (line 64) | def __init__(self):
    method add_process (line 67) | def add_process(self, process: subprocess.Popen, timeout: int) -> None:
    method _terminate_process (line 76) | def _terminate_process(self, process: subprocess.Popen) -> None:
    method cleanup_expired (line 118) | def cleanup_expired(self) -> None:
    method cleanup_all (line 132) | def cleanup_all(self) -> None:
  class HookExecutor (line 140) | class HookExecutor:
    method __init__ (line 143) | def __init__(
    method execute (line 151) | def execute(
    method execute_all (line 293) | def execute_all(

FILE: openhands-sdk/openhands/sdk/hooks/manager.py
  class HookManager (line 14) | class HookManager:
    method __init__ (line 17) | def __init__(
    method _create_event (line 28) | def _create_event(
    method run_pre_tool_use (line 49) | def run_pre_tool_use(
    method run_post_tool_use (line 80) | def run_post_tool_use(
    method run_user_prompt_submit (line 101) | def run_user_prompt_submit(
    method run_session_start (line 130) | def run_session_start(self) -> list[HookResult]:
    method run_session_end (line 139) | def run_session_end(self) -> list[HookResult]:
    method cleanup_async_processes (line 152) | def cleanup_async_processes(self) -> None:
    method run_stop (line 156) | def run_stop(
    method has_hooks (line 177) | def has_hooks(self, event_type: HookEventType) -> bool:
    method get_blocking_reason (line 181) | def get_blocking_reason(self, results: list[HookResult]) -> str | None:

FILE: openhands-sdk/openhands/sdk/hooks/types.py
  class HookEventType (line 9) | class HookEventType(str, Enum):
  class HookEvent (line 20) | class HookEvent(BaseModel):
  class HookDecision (line 35) | class HookDecision(str, Enum):

FILE: openhands-sdk/openhands/sdk/io/base.py
  class FileStore (line 6) | class FileStore(ABC):
    method write (line 17) | def write(self, path: str, contents: str | bytes) -> None:
    method read (line 26) | def read(self, path: str) -> str:
    method list (line 37) | def list(self, path: str) -> list[str]:
    method delete (line 48) | def delete(self, path: str) -> None:
    method exists (line 56) | def exists(self, path: str) -> bool:
    method get_absolute_path (line 67) | def get_absolute_path(self, path: str) -> str:
    method lock (line 79) | def lock(self, path: str, timeout: float = 30.0) -> Iterator[None]:

FILE: openhands-sdk/openhands/sdk/io/cache.py
  class MemoryLRUCache (line 11) | class MemoryLRUCache(LRUCache):
    method __init__ (line 24) | def __init__(self, max_memory: int, max_size: int, *args, **kwargs):
    method _get_size (line 31) | def _get_size(self, value: Any) -> int:
    method __setitem__ (line 55) | def __setitem__(self, key: Any, value: Any) -> None:
    method __delitem__ (line 80) | def __delitem__(self, key: Any) -> None:

FILE: openhands-sdk/openhands/sdk/io/local.py
  class LocalFileStore (line 18) | class LocalFileStore(FileStore):
    method __init__ (line 22) | def __init__(
    method get_full_path (line 46) | def get_full_path(self, path: str) -> str:
    method write (line 61) | def write(self, path: str, contents: str | bytes) -> None:
    method read (line 74) | def read(self, path: str) -> str:
    method list (line 89) | def list(self, path: str) -> list[str]:
    method delete (line 103) | def delete(self, path: str) -> None:
    method exists (line 122) | def exists(self, path: str) -> bool:
    method get_absolute_path (line 126) | def get_absolute_path(self, path: str) -> str:
    method lock (line 131) | def lock(self, path: str, timeout: float = 30.0) -> Iterator[None]:

FILE: openhands-sdk/openhands/sdk/io/memory.py
  class InMemoryFileStore (line 19) | class InMemoryFileStore(FileStore):
    method __init__ (line 24) | def __init__(
    method write (line 38) | def write(self, path: str, contents: str | bytes) -> None:
    method read (line 43) | def read(self, path: str) -> str:
    method list (line 48) | def list(self, path: str) -> list[str]:
    method delete (line 67) | def delete(self, path: str) -> None:
    method exists (line 76) | def exists(self, path: str) -> bool:
    method get_absolute_path (line 82) | def get_absolute_path(self, path: str) -> str:
    method lock (line 91) | def lock(self, path: str, timeout: float = 30.0) -> Iterator[None]:

FILE: openhands-sdk/openhands/sdk/llm/auth/credentials.py
  function get_credentials_dir (line 20) | def get_credentials_dir() -> Path:
  class OAuthCredentials (line 28) | class OAuthCredentials(BaseModel):
    method is_expired (line 39) | def is_expired(self) -> bool:
  class CredentialStore (line 46) | class CredentialStore:
    method __init__ (line 49) | def __init__(self, credentials_dir: Path | None = None):
    method credentials_dir (line 60) | def credentials_dir(self) -> Path:
    method _get_credentials_file (line 68) | def _get_credentials_file(self, vendor: str) -> Path:
    method get (line 72) | def get(self, vendor: str) -> OAuthCredentials | None:
    method save (line 94) | def save(self, credentials: OAuthCredentials) -> None:
    method delete (line 113) | def delete(self, vendor: str) -> bool:
    method update_tokens (line 128) | def update_tokens(

FILE: openhands-sdk/openhands/sdk/llm/auth/openai.py
  function _get_consent_marker_path (line 62) | def _get_consent_marker_path() -> Path:
  function _has_acknowledged_consent (line 67) | def _has_acknowledged_consent() -> bool:
  function _mark_consent_acknowledged (line 72) | def _mark_consent_acknowledged() -> None:
  function _display_consent_and_confirm (line 79) | def _display_consent_and_confirm() -> bool:
  class _JWKSCache (line 144) | class _JWKSCache:
    method __init__ (line 147) | def __init__(self) -> None:
    method get_key_set (line 152) | def get_key_set(self) -> jwk.KeySet:
    method _fetch_jwks (line 170) | def _fetch_jwks(self) -> None:
    method clear (line 184) | def clear(self) -> None:
  function _generate_pkce (line 194) | def _generate_pkce() -> tuple[str, str]:
  function _extract_chatgpt_account_id (line 201) | def _extract_chatgpt_account_id(access_token: str) -> str | None:
  function _build_authorize_url (line 246) | def _build_authorize_url(redirect_uri: str, code_challenge: str, state: ...
  function _exchange_code_for_tokens (line 263) | async def _exchange_code_for_tokens(
  class DeviceCode (line 285) | class DeviceCode:
  function _request_device_code (line 294) | async def _request_device_code() -> DeviceCode:
  function _poll_device_code (line 332) | async def _poll_device_code(device_code: DeviceCode) -> dict[str, Any]:
  function _refresh_access_token (line 361) | async def _refresh_access_token(refresh_token: str) -> dict[str, Any]:
  class OpenAISubscriptionAuth (line 427) | class OpenAISubscriptionAuth:
    method __init__ (line 430) | def __init__(
    method vendor (line 445) | def vendor(self) -> str:
    method get_credentials (line 449) | def get_credentials(self) -> OAuthCredentials | None:
    method has_valid_credentials (line 453) | def has_valid_credentials(self) -> bool:
    method refresh_if_needed (line 458) | async def refresh_if_needed(self) -> OAuthCredentials | None:
    method login (line 484) | async def login(
    method _login_with_device_code (line 626) | async def _login_with_device_code(self) -> OAuthCredentials:
    method logout (line 666) | def logout(self) -> bool:
    method create_llm (line 674) | def create_llm(
  function subscription_login_async (line 752) | async def subscription_login_async(
  function subscription_login (line 812) | def subscription_login(
  function inject_system_prefix (line 848) | def inject_system_prefix(
  function transform_for_subscription (line 872) | def transform_for_subscription(

FILE: openhands-sdk/openhands/sdk/llm/exceptions/classifier.py
  function is_context_window_exceeded (line 50) | def is_context_window_exceeded(exception: Exception) -> bool:
  function looks_like_malformed_conversation_history_error (line 64) | def looks_like_malformed_conversation_history_error(exception: Exception...
  function looks_like_auth_error (line 84) | def looks_like_auth_error(exception: Exception) -> bool:

FILE: openhands-sdk/openhands/sdk/llm/exceptions/mapping.py
  function map_provider_exception (line 28) | def map_provider_exception(exception: Exception) -> Exception:

FILE: openhands-sdk/openhands/sdk/llm/exceptions/types.py
  class LLMError (line 1) | class LLMError(Exception):
    method __init__ (line 4) | def __init__(self, message: str) -> None:
    method __str__ (line 8) | def __str__(self) -> str:
  class LLMMalformedActionError (line 13) | class LLMMalformedActionError(LLMError):
    method __init__ (line 14) | def __init__(self, message: str = "Malformed response") -> None:
  class LLMNoActionError (line 18) | class LLMNoActionError(LLMError):
    method __init__ (line 19) | def __init__(self, message: str = "Agent must return an action") -> None:
  class LLMResponseError (line 23) | class LLMResponseError(LLMError):
    method __init__ (line 24) | def __init__(
  class FunctionCallConversionError (line 31) | class FunctionCallConversionError(LLMError):
    method __init__ (line 32) | def __init__(self, message: str) -> None:
  class FunctionCallValidationError (line 36) | class FunctionCallValidationError(LLMError):
    method __init__ (line 37) | def __init__(self, message: str) -> None:
  class FunctionCallNotExistsError (line 41) | class FunctionCallNotExistsError(LLMError):
    method __init__ (line 42) | def __init__(self, message: str) -> None:
  class LLMNoResponseError (line 47) | class LLMNoResponseError(LLMError):
    method __init__ (line 48) | def __init__(
  class LLMContextWindowExceedError (line 57) | class LLMContextWindowExceedError(LLMError):
    method __init__ (line 58) | def __init__(
  class LLMMalformedConversationHistoryError (line 68) | class LLMMalformedConversationHistoryError(LLMError):
    method __init__ (line 69) | def __init__(
  class LLMContextWindowTooSmallError (line 80) | class LLMContextWindowTooSmallError(LLMError):
    method __init__ (line 83) | def __init__(
  class LLMAuthenticationError (line 109) | class LLMAuthenticationError(LLMError):
    method __init__ (line 110) | def __init__(self, message: str = "Invalid or missing API credentials"...
  class LLMRateLimitError (line 114) | class LLMRateLimitError(LLMError):
    method __init__ (line 115) | def __init__(self, message: str = "Rate limit exceeded") -> None:
  class LLMTimeoutError (line 119) | class LLMTimeoutError(LLMError):
    method __init__ (line 120) | def __init__(self, message: str = "LLM request timed out") -> None:
  class LLMServiceUnavailableError (line 124) | class LLMServiceUnavailableError(LLMError):
    method __init__ (line 125) | def __init__(self, message: str = "LLM service unavailable") -> None:
  class LLMBadRequestError (line 129) | class LLMBadRequestError(LLMError):
    method __init__ (line 130) | def __init__(self, message: str = "Bad request to LLM provider") -> None:
  class UserCancelledError (line 135) | class UserCancelledError(Exception):
    method __init__ (line 136) | def __init__(self, message: str = "User cancelled the request") -> None:
  class OperationCancelled (line 140) | class OperationCancelled(Exception):
    method __init__ (line 141) | def __init__(self, message: str = "Operation was cancelled") -> None:

FILE: openhands-sdk/openhands/sdk/llm/fallback_strategy.py
  class FallbackStrategy (line 39) | class FallbackStrategy(BaseModel):
    method should_fallback (line 59) | def should_fallback(self, error: Exception) -> bool:
    method try_fallback (line 63) | def try_fallback(
    method _profile_store (line 121) | def _profile_store(self) -> LLMProfileStore:
    method _iter_fallbacks (line 124) | def _iter_fallbacks(self) -> Generator[Any]:

FILE: openhands-sdk/openhands/sdk/llm/llm.py
  class LLM (line 148) | class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
    method _validate_secrets (line 475) | def _validate_secrets(cls, v: str | SecretStr | None, info) -> SecretS...
    method _coerce_inputs (line 480) | def _coerce_inputs(cls, data):
    method _post_init (line 511) | def _post_init(self):
    method _openrouter_headers (line 551) | def _openrouter_headers(self) -> dict[str, str]:
    method _aws_kwargs (line 566) | def _aws_kwargs(self) -> dict[str, str]:
    method _retry_listener_fn (line 590) | def _retry_listener_fn(
    method _serialize_secrets (line 605) | def _serialize_secrets(self, v: SecretStr | None, info):
    method metrics (line 612) | def metrics(self) -> Metrics:
    method telemetry (line 629) | def telemetry(self) -> Telemetry:
    method is_subscription (line 652) | def is_subscription(self) -> bool:
    method restore_metrics (line 664) | def restore_metrics(self, metrics: Metrics) -> None:
    method reset_metrics (line 672) | def reset_metrics(self) -> None:
    method _handle_error (line 688) | def _handle_error(
    method completion (line 714) | def completion(
    method responses (line 891) | def responses(
    method _infer_litellm_provider (line 1116) | def _infer_litellm_provider(self) -> str | None:
    method _infer_model_info_provider (line 1124) | def _infer_model_info_provider(self) -> str | None:
    method _get_litellm_api_key_value (line 1132) | def _get_litellm_api_key_value(self) -> str | None:
    method _transport_call (line 1147) | def _transport_call(
    method _litellm_modify_params_ctx (line 1215) | def _litellm_modify_params_ctx(self, flag: bool):
    method _model_name_for_capabilities (line 1227) | def _model_name_for_capabilities(self) -> str:
    method _init_model_info_and_caps (line 1231) | def _init_model_info_and_caps(self) -> None:
    method _validate_context_window_size (line 1332) | def _validate_context_window_size(self) -> None:
    method vision_is_active (line 1352) | def vision_is_active(self) -> bool:
    method _supports_vision (line 1357) | def _supports_vision(self) -> bool:
    method is_caching_prompt_active (line 1380) | def is_caching_prompt_active(self) -> bool:
    method uses_responses_api (line 1396) | def uses_responses_api(self) -> bool:
    method model_info (line 1403) | def model_info(self) -> dict | None:
    method effective_max_input_tokens (line 1408) | def effective_max_input_tokens(self) -> int | None:
    method effective_max_output_tokens (line 1417) | def effective_max_output_tokens(self) -> int | None:
    method _apply_prompt_caching (line 1428) | def _apply_prompt_caching(self, messages: list[Message]) -> None:
    method format_messages_for_llm (line 1457) | def format_messages_for_llm(self, messages: list[Message]) -> list[dict]:
    method format_messages_for_responses (line 1494) | def format_messages_for_responses(
    method get_token_count (line 1543) | def get_token_count(self, messages: list[Message]) -> int:
    method from_persisted (line 1569) | def from_persisted(cls, data: Any, *, context: dict[str, Any] | None =...
    method to_persisted (line 1588) | def to_persisted(self, *, context: dict[str, Any] | None = None) -> di...
    method load_from_json (line 1598) | def load_from_json(
    method load_from_env (line 1616) | def load_from_env(cls, prefix: str = "LLM_") -> LLM:
    method subscription_login (line 1671) | def subscription_login(

FILE: openhands-sdk/openhands/sdk/llm/llm_profile_store.py
  class ProfileLimitExceeded (line 35) | class ProfileLimitExceeded(Exception):
  class LLMProfileStore (line 39) | class LLMProfileStore:
    method __init__ (line 42) | def __init__(self, base_dir: Path | str | None = None) -> None:
    method _acquire_lock (line 56) | def _acquire_lock(self, timeout: float = _LOCK_TIMEOUT_SECONDS) -> Ite...
    method list (line 74) | def list(self) -> list[str]:
    method _get_profile_path (line 83) | def _get_profile_path(self, name: str) -> Path:
    method save (line 101) | def save(
    method load (line 172) | def load(self, name: str, *, cipher: Cipher | None = None) -> LLM:
    method delete (line 211) | def delete(self, name: str) -> None:
    method rename (line 232) | def rename(self, old_name: str, new_name: str) -> None:
    method list_summaries (line 252) | def list_summaries(self) -> list[dict[str, Any]]:

FILE: openhands-sdk/openhands/sdk/llm/llm_registry.py
  class RegistryEvent (line 15) | class RegistryEvent(BaseModel):
  class LLMRegistry (line 23) | class LLMRegistry:
    method __init__ (line 38) | def __init__(
    method subscribe (line 54) | def subscribe(self, callback: Callable[[RegistryEvent], None]) -> None:
    method notify (line 62) | def notify(self, event: RegistryEvent) -> None:
    method usage_to_llm (line 75) | def usage_to_llm(self) -> MappingProxyType[str, LLM]:
    method _ensure_independent_metrics (line 80) | def _ensure_independent_metrics(self, llm: LLM) -> None:
    method add (line 108) | def add(self, llm: LLM) -> None:
    method get (line 140) | def get(self, usage_id: str) -> LLM:
    method list_usage_ids (line 163) | def list_usage_ids(self) -> list[str]:

FILE: openhands-sdk/openhands/sdk/llm/llm_response.py
  class LLMResponse (line 28) | class LLMResponse(BaseModel):
    method id (line 49) | def id(self) -> str:

FILE: openhands-sdk/openhands/sdk/llm/message.py
  class MessageToolCall (line 24) | class MessageToolCall(BaseModel):
    method from_chat_tool_call (line 43) | def from_chat_tool_call(
    method from_responses_function_call (line 65) | def from_responses_function_call(
    method to_chat_dict (line 89) | def to_chat_dict(self) -> dict[str, Any]:
    method to_responses_dict (line 100) | def to_responses_dict(self) -> dict[str, Any]:
  class ThinkingBlock (line 122) | class ThinkingBlock(BaseModel):
  class RedactedThinkingBlock (line 137) | class RedactedThinkingBlock(BaseModel):
  class ReasoningItemModel (line 148) | class ReasoningItemModel(BaseModel):
  class BaseContent (line 161) | class BaseContent(BaseModel):
    method to_llm_dict (line 165) | def to_llm_dict(self) -> list[dict[str, str | dict[str, str]]]:
  class TextContent (line 173) | class TextContent(BaseContent):
    method _handle_deprecated_fields (line 189) | def _handle_deprecated_fields(cls, data: Any) -> Any:
    method to_llm_dict (line 193) | def to_llm_dict(self) -> list[dict[str, str | dict[str, str]]]:
  class ImageContent (line 204) | class ImageContent(BaseContent):
    method to_llm_dict (line 208) | def to_llm_dict(self) -> list[dict[str, str | dict[str, str]]]:
  class Message (line 218) | class Message(BaseModel):
    method _handle_deprecated_fields (line 259) | def _handle_deprecated_fields(cls, data: Any) -> Any:
    method contains_image (line 264) | def contains_image(self) -> bool:
    method _coerce_content (line 269) | def _coerce_content(cls, v: Any) -> Sequence[TextContent | ImageConten...
    method to_chat_dict (line 278) | def to_chat_dict(
    method _string_serializer (line 328) | def _string_serializer(self) -> dict[str, Any]:
    method _list_serializer (line 340) | def _list_serializer(self, *, vision_enabled: bool) -> dict[str, Any]:
    method _remove_content_if_empty (line 390) | def _remove_content_if_empty(self, message_dict: dict[str, Any]) -> None:
    method to_responses_value (line 436) | def to_responses_value(self, *, vision_enabled: bool) -> str | list[di...
    method to_responses_dict (line 448) | def to_responses_dict(self, *, vision_enabled: bool) -> list[dict[str,...
    method _maybe_truncate_tool_text (line 461) | def _maybe_truncate_tool_text(self, text: str) -> str:
    method from_llm_chat_message (line 472) | def from_llm_chat_message(cls, message: LiteLLMMessage) -> "Message":
    method from_llm_responses_output (line 531) | def from_llm_responses_output(
  function content_to_str (line 615) | def content_to_str(contents: Sequence[TextContent | ImageContent]) -> li...

FILE: openhands-sdk/openhands/sdk/llm/mixins/fn_call_converter.py
  class CacheControl (line 24) | class CacheControl(TypedDict):
  class TextPart (line 28) | class TextPart(TypedDict):
  function convert_tool_call_to_string (line 92) | def convert_tool_call_to_string(tool_call: dict) -> str:
  function _summarize_schema_type (line 122) | def _summarize_schema_type(schema: object | None) -> str:
  function _indent (line 151) | def _indent(indent: int) -> str:
  function _nested_indent (line 155) | def _nested_indent(indent: int, levels: int = 1) -> int:
  function _get_description (line 159) | def _get_description(schema: dict[str, object] | None) -> str:
  function _format_union_details (line 171) | def _format_union_details(schema: dict[str, object], indent: int) -> lis...
  function _format_array_details (line 189) | def _format_array_details(schema: dict[str, object], indent: int) -> lis...
  function _format_additional_properties (line 209) | def _format_additional_properties(
  function _format_object_details (line 227) | def _format_object_details(schema: dict[str, Any], indent: int) -> list[...
  function _format_schema_detail (line 248) | def _format_schema_detail(schema: object | None, indent: int = 4) -> lis...
  function convert_tools_to_description (line 271) | def convert_tools_to_description(tools: list[ChatCompletionToolParam]) -...
  function _build_system_message_suffix (line 315) | def _build_system_message_suffix(
  function _append_to_content (line 329) | def _append_to_content(content: Content, suffix: str) -> Content:
  function _prepend_to_content (line 344) | def _prepend_to_content(content: Content, prefix: str) -> Content:
  function _wrap_content_with_example (line 359) | def _wrap_content_with_example(
  function _convert_system_to_non_fncall (line 382) | def _convert_system_to_non_fncall(
  function _convert_user_to_non_fncall (line 391) | def _convert_user_to_non_fncall(
  function _convert_assistant_to_non_fncall (line 407) | def _convert_assistant_to_non_fncall(
  function _convert_tool_to_non_fncall (line 447) | def _convert_tool_to_non_fncall(message: dict, content: Content) -> dict:
  function convert_fncall_messages_to_non_fncall_messages (line 471) | def convert_fncall_messages_to_non_fncall_messages(
  function _extract_and_validate_params (line 516) | def _extract_and_validate_params(
  function _preprocess_model_output (line 577) | def _preprocess_model_output(content: str) -> str:
  function _fix_stopword (line 595) | def _fix_stopword(content: str) -> str:
  function _normalize_parameter_tags (line 606) | def _normalize_parameter_tags(fn_body: str) -> str:
  function _find_tool (line 635) | def _find_tool(
  function _resolve_tool_name (line 650) | def _resolve_tool_name(
  function _remove_suffix_from_content (line 673) | def _remove_suffix_from_content(content: Content, suffix: str) -> Content:
  function _strip_in_context_example (line 682) | def _strip_in_context_example(
  function _find_tool_result_match (line 702) | def _find_tool_result_match(content: Content) -> re.Match | None:
  function _convert_system_to_fncall (line 725) | def _convert_system_to_fncall(content: Content, system_message_suffix: s...
  function _convert_user_to_fncall (line 731) | def _convert_user_to_fncall(
  function _find_function_match (line 772) | def _find_function_match(content: Content) -> tuple[Content, re.Match | ...
  function _strip_function_call_from_content (line 804) | def _strip_function_call_from_content(content: Content) -> Content:
  function _convert_assistant_to_fncall (line 818) | def _convert_assistant_to_fncall(
  function convert_non_fncall_messages_to_fncall_messages (line 860) | def convert_non_fncall_messages_to_fncall_messages(
  function convert_from_multiple_tool_calls_to_single_tool_call_messages (line 906) | def convert_from_multiple_tool_calls_to_single_tool_call_messages(

FILE: openhands-sdk/openhands/sdk/llm/mixins/fn_call_examples.py
  function _refine_prompt (line 24) | def _refine_prompt(prompt: str) -> str:
  function get_example_for_tools (line 333) | def get_example_for_tools(tools: list[ChatCompletionToolParam]) -> str:

FILE: openhands-sdk/openhands/sdk/llm/mixins/non_native_fc.py
  class _HostSupports (line 18) | class _HostSupports(Protocol):
  class NonNativeToolCallingMixin (line 24) | class NonNativeToolCallingMixin:
    method should_mock_tool_calls (line 33) | def should_mock_tool_calls(
    method pre_request_prompt_mock (line 38) | def pre_request_prompt_mock(
    method post_response_prompt_mock (line 65) | def post_response_prompt_mock(

FILE: openhands-sdk/openhands/sdk/llm/options/chat_options.py
  function select_chat_options (line 9) | def select_chat_options(

FILE: openhands-sdk/openhands/sdk/llm/options/common.py
  function apply_defaults_if_absent (line 6) | def apply_defaults_if_absent(

FILE: openhands-sdk/openhands/sdk/llm/options/responses_options.py
  function select_responses_options (line 9) | def select_responses_options(

FILE: openhands-sdk/openhands/sdk/llm/router/base.py
  class RouterLLM (line 21) | class RouterLLM(LLM):
    method validate_llms_not_empty (line 43) | def validate_llms_not_empty(cls, v):
    method completion (line 50) | def completion(
    method select_llm (line 92) | def select_llm(self, messages: list[Message]) -> str:
    method __getattr__ (line 107) | def __getattr__(self, name):
    method __str__ (line 113) | def __str__(self) -> str:
    method set_placeholder_model (line 119) | def set_placeholder_model(cls, data):

FILE: openhands-sdk/openhands/sdk/llm/router/impl/multimodal.py
  class MultimodalRouter (line 13) | class MultimodalRouter(RouterLLM):
    method select_llm (line 29) | def select_llm(self, messages: list[Message]) -> str:
    method _validate_llms_for_routing (line 65) | def _validate_llms_for_routing(self) -> "MultimodalRouter":

FILE: openhands-sdk/openhands/sdk/llm/router/impl/random.py
  class RandomRouter (line 11) | class RandomRouter(RouterLLM):
    method select_llm (line 19) | def select_llm(self, messages: list[Message]) -> str:  # noqa: ARG002

FILE: openhands-sdk/openhands/sdk/llm/utils/image_resize.py
  function maybe_resize_messages_for_provider (line 24) | def maybe_resize_messages_for_provider(
  function _get_image_max_dimension (line 47) | def _get_image_max_dimension(
  function _resize_base64_data_url (line 67) | def _resize_base64_data_url(url: str, *, max_dimension: int) -> str:

FILE: openhands-sdk/openhands/sdk/llm/utils/litellm_provider.py
  function infer_litellm_provider (line 12) | def infer_litellm_provider(*, model: str, api_base: str | None) -> str |...

FILE: openhands-sdk/openhands/sdk/llm/utils/metrics.py
  class Cost (line 8) | class Cost(BaseModel):
    method validate_cost (line 15) | def validate_cost(cls, v: float) -> float:
  class ResponseLatency (line 21) | class ResponseLatency(BaseModel):
    method validate_latency (line 30) | def validate_latency(cls, v: float) -> float:
  class TokenUsage (line 34) | class TokenUsage(BaseModel):
    method __add__ (line 61) | def __add__(self, other: "TokenUsage") -> "TokenUsage":
  class MetricsSnapshot (line 76) | class MetricsSnapshot(BaseModel):
  class Metrics (line 95) | class Metrics(MetricsSnapshot):
    method validate_accumulated_cost (line 116) | def validate_accumulated_cost(cls, v: float) -> float:
    method initialize_accumulated_token_usage (line 122) | def initialize_accumulated_token_usage(self) -> "Metrics":
    method get_snapshot (line 136) | def get_snapshot(self) -> MetricsSnapshot:
    method add_cost (line 147) | def add_cost(self, value: float) -> None:
    method add_response_latency (line 153) | def add_response_latency(self, value: float, response_id: str) -> None:
    method add_token_usage (line 160) | def add_token_usage(
    method merge (line 204) | def merge(self, other: "Metrics") -> None:
    method get (line 224) | def get(self) -> dict:
    method log (line 239) | def log(self) -> str:
    method deep_copy (line 247) | def deep_copy(self) -> "Metrics":
    method diff (line 251) | def diff(self, baseline: "Metrics") -> "Metrics":
    method __repr__ (line 311) | def __repr__(self) -> str:

FILE: openhands-sdk/openhands/sdk/llm/utils/model_features.py
  function model_matches (line 7) | def model_matches(model: str, patterns: list[str]) -> bool:
  function apply_ordered_model_rules (line 21) | def apply_ordered_model_rules(model: str, rules: list[str]) -> bool:
  class ModelFeatures (line 44) | class ModelFeatures:
  function _normalized_supported_openai_params (line 62) | def _normalized_supported_openai_params(model: str | None) -> frozenset[...
  function _supports_reasoning_effort (line 84) | def _supports_reasoning_effort(model: str | None) -> bool:
  function get_features (line 194) | def get_features(model: str) -> ModelFeatures:

FILE: openhands-sdk/openhands/sdk/llm/utils/model_info.py
  function _get_model_info_from_litellm_proxy (line 15) | def _get_model_info_from_litellm_proxy(
  function get_litellm_model_info (line 51) | def get_litellm_model_info(

FILE: openhands-sdk/openhands/sdk/llm/utils/model_prompt_spec.py
  class ModelPromptSpec (line 12) | class ModelPromptSpec(BaseModel):
  function _normalize (line 54) | def _normalize(name: str | None) -> str:
  function _match_family (line 58) | def _match_family(model_name: str) -> str | None:
  function _match_variant (line 69) | def _match_variant(
  function get_model_prompt_spec (line 90) | def get_model_prompt_spec(

FILE: openhands-sdk/openhands/sdk/llm/utils/responses_serialization.py
  function message_to_responses_dict (line 16) | def message_to_responses_dict(
  function _user_to_responses_items (line 43) | def _user_to_responses_items(
  function _build_user_content_items (line 59) | def _build_user_content_items(
  function _assistant_to_responses_items (line 75) | def _assistant_to_responses_items(message: Message) -> list[dict[str, An...
  function _build_reasoning_item (line 93) | def _build_reasoning_item(
  function _build_assistant_content_items (line 120) | def _build_assistant_content_items(
  function _tool_to_responses_items (line 131) | def _tool_to_responses_items(

FILE: openhands-sdk/openhands/sdk/llm/utils/retry_mixin.py
  class RetryMixin (line 22) | class RetryMixin:
    method retry_decorator (line 25) | def retry_decorator(
    method log_retry_attempt (line 88) | def log_retry_attempt(self, retry_state: RetryCallState) -> None:

FILE: openhands-sdk/openhands/sdk/llm/utils/telemetry.py
  class Telemetry (line 22) | class Telemetry(BaseModel):
    method set_log_completions_callback (line 57) | def set_log_completions_callback(
    method set_stats_update_callback (line 68) | def set_stats_update_callback(self, callback: Callable[[], None] | Non...
    method on_request (line 77) | def on_request(self, telemetry_ctx: dict | None) -> None:
    method on_response (line 81) | def on_response(
    method on_error (line 124) | def on_error(self, _err: BaseException) -> None:
    method _has_meaningful_usage (line 168) | def _has_meaningful_usage(self, usage: Usage | ResponseAPIUsage | None...
    method _record_usage (line 189) | def _record_usage(
    method _compute_cost (line 248) | def _compute_cost(self, resp: ModelResponse | ResponsesAPIResponse) ->...
    method log_llm_call (line 288) | def log_llm_call(
  function _safe_json (line 386) | def _safe_json(obj: Any) -> Any:

FILE: openhands-sdk/openhands/sdk/llm/utils/unverified_models.py
  function _get_boto3 (line 10) | def _get_boto3():
  function _list_bedrock_foundation_models (line 21) | def _list_bedrock_foundation_models(
  function get_supported_llm_models (line 54) | def get_supported_llm_models(
  function _split_is_actually_version (line 82) | def _split_is_actually_version(split: list[str]) -> bool:
  function _get_litellm_provider_names (line 91) | def _get_litellm_provider_names() -> set[str]:
  function _extract_model_and_provider (line 111) | def _extract_model_and_provider(model: str) -> tuple[str, str, str]:
  function get_unverified_models (line 153) | def get_unverified_models(

FILE: openhands-sdk/openhands/sdk/logger/logger.py
  function disable_logger (line 77) | def disable_logger(name: str, level: int = logging.CRITICAL) -> None:
  function setup_logging (line 92) | def setup_logging(
  function get_logger (line 169) | def get_logger(name: str) -> logging.Logger:

FILE: openhands-sdk/openhands/sdk/logger/rolling.py
  class _RollingViewHandler (line 16) | class _RollingViewHandler(logging.Handler):
    method __init__ (line 17) | def __init__(self, max_lines: int, use_live: bool):
    method emit (line 24) | def emit(self, record: logging.LogRecord):
    method snapshot (line 45) | def snapshot(self) -> str:
  function rolling_log_view (line 50) | def rolling_log_view(

FILE: openhands-sdk/openhands/sdk/marketplace/types.py
  class MarketplaceOwner (line 25) | class MarketplaceOwner(BaseModel):
  class MarketplacePluginSource (line 37) | class MarketplacePluginSource(BaseModel):
    method validate_source_fields (line 58) | def validate_source_fields(self) -> MarketplacePluginSource:
  class MarketplaceEntry (line 67) | class MarketplaceEntry(BaseModel):
    method _parse_author (line 91) | def _parse_author(cls, v: Any) -> Any:
  class MarketplacePluginEntry (line 97) | class MarketplacePluginEntry(MarketplaceEntry):
    method _parse_source (line 143) | def _parse_source(cls, v: Any) -> Any:
    method to_plugin_manifest (line 148) | def to_plugin_manifest(self) -> PluginManifest:
  class MarketplaceMetadata (line 159) | class MarketplaceMetadata(BaseModel):
  class Marketplace (line 168) | class Marketplace(BaseModel):
    method load (line 218) | def load(cls, marketplace_path: str | Path) -> Marketplace:
    method get_plugin (line 263) | def get_plugin(self, name: str) -> MarketplacePluginEntry | None:
    method resolve_plugin_source (line 277) | def resolve_plugin_source(

FILE: openhands-sdk/openhands/sdk/mcp/client.py
  class MCPClient (line 18) | class MCPClient(AsyncMCPClient):
    method __init__ (line 39) | def __init__(self, *args, **kwargs):
    method tools (line 46) | def tools(self) -> "list[MCPToolDefinition]":
    method connect (line 50) | async def connect(self) -> None:
    method call_async_from_sync (line 57) | def call_async_from_sync(
    method call_sync_from_async (line 75) | async def call_sync_from_async(
    method sync_close (line 84) | def sync_close(self) -> None:
    method __del__ (line 105) | def __del__(self):
    method __enter__ (line 113) | def __enter__(self) -> "MCPClient":
    method __exit__ (line 116) | def __exit__(self, *args: object) -> None:
    method __iter__ (line 120) | def __iter__(self) -> "Iterator[MCPToolDefinition]":
    method __len__ (line 123) | def __len__(self) -> int:
    method __getitem__ (line 126) | def __getitem__(self, index: int) -> "MCPToolDefinition":

FILE: openhands-sdk/openhands/sdk/mcp/definition.py
  class MCPToolAction (line 26) | class MCPToolAction(Action):
    method to_mcp_arguments (line 41) | def to_mcp_arguments(self) -> dict:
  class MCPToolObservation (line 50) | class MCPToolObservation(Observation):
    method from_call_tool_result (line 56) | def from_call_tool_result(
    method visualize (line 86) | def visualize(self) -> Text:

FILE: openhands-sdk/openhands/sdk/mcp/exceptions.py
  class MCPError (line 4) | class MCPError(Exception):
  class MCPTimeoutError (line 10) | class MCPTimeoutError(MCPError):
    method __init__ (line 16) | def __init__(self, message: str, timeout: float, config: dict | None =...

FILE: openhands-sdk/openhands/sdk/mcp/tool.py
  function to_camel_case (line 41) | def to_camel_case(s: str) -> str:
  class MCPToolExecutor (line 46) | class MCPToolExecutor(ToolExecutor):
    method __init__ (line 53) | def __init__(
    method call_tool (line 64) | async def call_tool(self, action: MCPToolAction) -> MCPToolObservation:
    method __call__ (line 90) | def __call__(
    method close (line 113) | def close(self) -> None:
  function _create_mcp_action_type (line 120) | def _create_mcp_action_type(action_type: mcp.types.Tool) -> type[Schema]:
  class MCPToolDefinition (line 146) | class MCPToolDefinition(ToolDefinition[MCPToolAction, MCPToolObservation]):
    method name (line 152) | def name(self) -> str:  # type: ignore[override]
    method __call__ (line 156) | def __call__(
    method action_from_arguments (line 192) | def action_from_arguments(self, arguments: dict[str, Any]) -> MCPToolA...
    method create (line 225) | def create(
    method to_mcp_tool (line 258) | def to_mcp_tool(
    method to_openai_tool (line 273) | def to_openai_tool(

FILE: openhands-sdk/openhands/sdk/mcp/utils.py
  function log_handler (line 19) | async def log_handler(message: LogMessage):
  function _connect_and_list_tools (line 34) | async def _connect_and_list_tools(client: MCPClient) -> None:
  function create_mcp_tools (line 43) | def create_mcp_tools(

FILE: openhands-sdk/openhands/sdk/observability/laminar.py
  function _get_int_env (line 33) | def _get_int_env(key: str) -> int | None:
  function _get_bool_env (line 45) | def _get_bool_env(key: str) -> bool:
  function maybe_init_laminar (line 57) | def maybe_init_laminar():
  function observe (line 113) | def observe[**P, R](
  function should_enable_observability (line 197) | def should_enable_observability() -> bool:
  function _is_otel_backend_laminar (line 216) | def _is_otel_backend_laminar():
  class RootSpan (line 229) | class RootSpan:
    method __init__ (line 251) | def __init__(self, name: str, session_id: str | None = None) -> None:
    method end (line 265) | def end(self) -> None:
  function start_root_span (line 276) | def start_root_span(name: str, session_id: str | None = None) -> RootSpa...
  function end_root_span (line 290) | def end_root_span(root: RootSpan | None) -> None:
  function _maybe_use_root_span (line 298) | def _maybe_use_root_span(args: tuple[Any, ...]) -> Iterator[None]:
  function _root_span_from_args (line 333) | def _root_span_from_args(args: tuple[Any, ...]) -> RootSpan | None:
  class SpanManager (line 361) | class SpanManager:
    method __init__ (line 373) | def __init__(self) -> None:
    method start_active_span (line 376) | def start_active_span(self, name: str, session_id: str | None = None) ...
    method end_active_span (line 393) | def end_active_span(self) -> None:
  function _get_span_manager (line 411) | def _get_span_manager() -> SpanManager:
  function start_active_span (line 424) | def start_active_span(name: str, session_id: str | None = None) -> None:
  function end_active_span (line 452) | def end_active_span() -> None:
  function init_laminar_for_external (line 477) | def init_laminar_for_external():

FILE: openhands-sdk/openhands/sdk/observability/utils.py
  function get_env (line 8) | def get_env(key: str) -> str | None:
  function extract_action_name (line 13) | def extract_action_name(action_event: ActionEvent) -> str:

FILE: openhands-sdk/openhands/sdk/plugin/fetch.py
  class PluginFetchError (line 22) | class PluginFetchError(Exception):
  function fetch_plugin (line 26) | def fetch_plugin(
  function fetch_plugin_with_resolution (line 69) | def fetch_plugin_with_resolution(

FILE: openhands-sdk/openhands/sdk/plugin/installed.py
  function get_installed_plugins_dir (line 25) | def get_installed_plugins_dir() -> Path:
  class PluginInstallationInterface (line 35) | class PluginInstallationInterface(InstallationInterface[Plugin]):
    method load_from_dir (line 37) | def load_from_dir(extension_dir: Path) -> Plugin:
  function _resolve_installed_dir (line 41) | def _resolve_installed_dir(installed_dir: Path | None) -> Path:
  function _manager (line 45) | def _manager(installed_dir: Path) -> InstallationManager[Plugin]:
  function install_plugin (line 57) | def install_plugin(
  function uninstall_plugin (line 83) | def uninstall_plugin(
  function enable_plugin (line 95) | def enable_plugin(
  function disable_plugin (line 103) | def disable_plugin(
  function list_installed_plugins (line 111) | def list_installed_plugins(
  function load_installed_plugins (line 121) | def load_installed_plugins(
  function get_installed_plugin (line 128) | def get_installed_plugin(
  function update_plugin (line 136) | def update_plugin(

FILE: openhands-sdk/openhands/sdk/plugin/loader.py
  function load_plugins (line 28) | def load_plugins(

FILE: openhands-sdk/openhands/sdk/plugin/plugin.py
  class Plugin (line 39) | class Plugin(BaseModel):
    method name (line 77) | def name(self) -> str:
    method version (line 82) | def version(self) -> str:
    method description (line 87) | def description(self) -> str:
    method entry_slash_command (line 92) | def entry_slash_command(self) -> str | None:
    method get_all_skills (line 107) | def get_all_skills(self) -> list[Skill]:
    method add_skills_to (line 126) | def add_skills_to(
    method add_mcp_config_to (line 176) | def add_mcp_config_to(
    method fetch (line 237) | def fetch(
    method load (line 292) | def load(cls, plugin_path: str | Path) -> Plugin:
    method load_all (line 338) | def load_all(cls, plugins_dir: str | Path) -> list[Plugin]:
  function _load_manifest (line 365) | def _load_manifest(plugin_dir: Path) -> PluginManifest:
  function _load_skills (line 404) | def _load_skills(plugin_dir: Path) -> list[Skill]:
  function _load_hooks (line 439) | def _load_hooks(plugin_dir: Path) -> HookConfig | None:
  function _load_mcp_config (line 460) | def _load_mcp_config(plugin_dir: Path) -> dict[str, Any] | None:
  function _load_agents (line 491) | def _load_agents(plugin_dir: Path) -> list[AgentDefinition]:
  function _load_commands (line 510) | def _load_commands(plugin_dir: Path) -> list[CommandDefinition]:

FILE: openhands-sdk/openhands/sdk/plugin/source.py
  class GitHubURLComponents (line 27) | class GitHubURLComponents(NamedTuple):
  function parse_github_url (line 36) | def parse_github_url(url: str) -> GitHubURLComponents | None:
  function is_local_path (line 48) | def is_local_path(source: str) -> bool:
  function validate_source_path (line 53) | def validate_source_path(source: str) -> str:
  function resolve_source_path (line 62) | def resolve_source_path(

FILE: openhands-sdk/openhands/sdk/plugin/types.py
  class PluginSource (line 14) | class PluginSource(BaseModel):
    method validate_repo_path (line 52) | def validate_repo_path(cls, v: str | None) -> str | None:
    method source_url (line 67) | def source_url(self) -> str | None:
  class ResolvedPluginSource (line 109) | class ResolvedPluginSource(BaseModel):
    method from_plugin_source (line 142) | def from_plugin_source(
    method to_plugin_source (line 153) | def to_plugin_source(self) -> PluginSource:
  class PluginAuthor (line 190) | class PluginAuthor(BaseModel):
    method from_string (line 200) | def from_string(cls, author_str: str) -> PluginAuthor:
  class PluginManifest (line 209) | class PluginManifest(BaseModel):
  class CommandDefinition (line 228) | class CommandDefinition(BaseModel):
    method load (line 253) | def load(cls, command_path: Path) -> CommandDefinition:
    method to_skill (line 317) | def to_skill(self, plugin_name: str) -> Skill:

FILE: openhands-sdk/openhands/sdk/secret/secrets.py
  function _resolve_lookup_secret_url (line 26) | def _resolve_lookup_secret_url(url: str) -> str:
  class SecretSource (line 35) | class SecretSource(DiscriminatedUnionMixin, ABC):
    method get_value (line 44) | def get_value(self) -> str | None:
  class StaticSecret (line 48) | class StaticSecret(SecretSource):
    method get_value (line 53) | def get_value(self) -> str | None:
    method _validate_secrets (line 60) | def _validate_secrets(cls, v: SecretStr | None, info):
    method _serialize_secrets (line 64) | def _serialize_secrets(self, v: SecretStr | None, info):
  class LookupSecret (line 68) | class LookupSecret(SecretSource):
    method _normalize_url (line 76) | def _normalize_url(cls, url: str) -> str:
    method get_value (line 79) | def get_value(self) -> str:
    method _validate_secrets (line 86) | def _validate_secrets(cls, headers: dict[str, str], info):
    method _serialize_secrets (line 119) | def _serialize_secrets(self, headers: dict[str, str], info):

FILE: openhands-sdk/openhands/sdk/security/analyzer.py
  class SecurityAnalyzerBase (line 15) | class SecurityAnalyzerBase(DiscriminatedUnionMixin, ABC):
    method security_risk (line 26) | def security_risk(self, action: ActionEvent) -> SecurityRisk:
    method analyze_event (line 41) | def analyze_event(self, event: Event) -> SecurityRisk | None:
    method should_require_confirmation (line 57) | def should_require_confirmation(
    method analyze_pending_actions (line 85) | def analyze_pending_actions(

FILE: openhands-sdk/openhands/sdk/security/confirmation_policy.py
  class ConfirmationPolicyBase (line 9) | class ConfirmationPolicyBase(DiscriminatedUnionMixin, ABC):
    method should_confirm (line 11) | def should_confirm(self, risk: SecurityRisk = SecurityRisk.UNKNOWN) ->...
  class AlwaysConfirm (line 27) | class AlwaysConfirm(ConfirmationPolicyBase):
    method should_confirm (line 28) | def should_confirm(
  class NeverConfirm (line 35) | class NeverConfirm(ConfirmationPolicyBase):
    method should_confirm (line 36) | def should_confirm(
  class ConfirmRisky (line 43) | class ConfirmRisky(ConfirmationPolicyBase):
    method validate_threshold (line 48) | def validate_threshold(cls, v: SecurityRisk) -> SecurityRisk:
    method should_confirm (line 53) | def should_confirm(self, risk: SecurityRisk = SecurityRisk.UNKNOWN) ->...

FILE: openhands-sdk/openhands/sdk/security/defense_in_depth/pattern.py
  class PatternSecurityAnalyzer (line 140) | class PatternSecurityAnalyzer(SecurityAnalyzerBase):
    method model_post_init (line 193) | def model_post_init(self, __context: Any) -> None:
    method security_risk (line 212) | def security_risk(self, action: ActionEvent) -> SecurityRisk:

FILE: openhands-sdk/openhands/sdk/security/defense_in_depth/policy_rails.py
  class RailDecision (line 47) | class RailDecision:
  function _evaluate_rail_segments (line 68) | def _evaluate_rail_segments(segments: list[str]) -> RailDecision:
  function _evaluate_rail (line 133) | def _evaluate_rail(content: str) -> RailDecision:
  class PolicyRailSecurityAnalyzer (line 148) | class PolicyRailSecurityAnalyzer(SecurityAnalyzerBase):
    method security_risk (line 174) | def security_risk(self, action: ActionEvent) -> SecurityRisk:

FILE: openhands-sdk/openhands/sdk/security/defense_in_depth/utils.py
  class _BoundedSegments (line 49) | class _BoundedSegments:
    method __init__ (line 61) | def __init__(self, cap: int) -> None:
    method add (line 66) | def add(self, text: str) -> None:
  function _walk_json_strings (line 78) | def _walk_json_strings(obj: Any) -> list[str]:
  function _extract_exec_segments (line 104) | def _extract_exec_segments(action: ActionEvent) -> list[str]:
  function _extract_text_segments (line 138) | def _extract_text_segments(action: ActionEvent) -> list[str]:
  function _extract_segments (line 167) | def _extract_segments(action: ActionEvent) -> list[str]:
  function _extract_content (line 172) | def _extract_content(action: ActionEvent) -> str:
  function _extract_exec_content (line 186) | def _extract_exec_content(action: ActionEvent) -> str:
  function _normalize (line 346) | def _normalize(text: str) -> str:

FILE: openhands-sdk/openhands/sdk/security/ensemble.py
  class EnsembleSecurityAnalyzer (line 22) | class EnsembleSecurityAnalyzer(SecurityAnalyzerBase):
    method security_risk (line 78) | def security_risk(self, action: ActionEvent) -> SecurityRisk:

FILE: openhands-sdk/openhands/sdk/security/grayswan/analyzer.py
  class GraySwanAnalyzer (line 28) | class GraySwanAnalyzer(SecurityAnalyzerBase):
    method validate_thresholds (line 83) | def validate_thresholds(self) -> GraySwanAnalyzer:
    method model_post_init (line 92) | def model_post_init(self, __context: Any) -> None:
    method set_events (line 119) | def set_events(self, events: Sequence[LLMConvertibleEvent]) -> None:
    method _create_client (line 127) | def _create_client(self) -> httpx.Client:
    method _get_client (line 138) | def _get_client(self) -> httpx.Client:
    method _map_violation_to_risk (line 147) | def _map_violation_to_risk(self, violation_score: float) -> SecurityRisk:
    method _call_grayswan_api (line 163) | def _call_grayswan_api(self, messages: list[dict[str, Any]]) -> Securi...
    method security_risk (line 229) | def security_risk(self, action: ActionEvent) -> SecurityRisk:
    method close (line 276) | def close(self) -> None:

FILE: openhands-sdk/openhands/sdk/security/grayswan/utils.py
  function convert_events_to_openai_messages (line 28) | def convert_events_to_openai_messages(

FILE: openhands-sdk/openhands/sdk/security/llm_analyzer.py
  class LLMSecurityAnalyzer (line 10) | class LLMSecurityAnalyzer(SecurityAnalyzerBase):
    method security_risk (line 20) | def security_risk(self, action: ActionEvent) -> SecurityRisk:

FILE: openhands-sdk/openhands/sdk/security/risk.py
  class SecurityRisk (line 13) | class SecurityRisk(str, Enum):
    method description (line 26) | def description(self) -> str:
    method __str__ (line 42) | def __str__(self) -> str:
    method get_color (line 45) | def get_color(self) -> str:
    method visualize (line 56) | def visualize(self) -> Text:
    method is_riskier (line 69) | def is_riskier(self, other: SecurityRisk, reflexive: bool = True) -> b...
    method _check_comparable (line 102) | def _check_comparable(self, other: object) -> int | None:
    method __lt__ (line 114) | def __lt__(self, other: object) -> bool:
    method __gt__ (line 125) | def __gt__(self, other: object) -> bool:
    method __le__ (line 137) | def __le__(self, other: object) -> bool:
    method __ge__ (line 143) | def __ge__(self, other: object) -> bool:

FILE: openhands-sdk/openhands/sdk/settings/__init__.py
  function __getattr__ (line 119) | def __getattr__(name: str) -> Any:

FILE: openhands-sdk/openhands/sdk/settings/acp_providers.py
  class ACPProviderInfo (line 31) | class ACPProviderInfo:
  function get_acp_provider (line 135) | def get_acp_provider(key: str) -> ACPProviderInfo | None:
  function detect_acp_provider_by_agent_name (line 140) | def detect_acp_provider_by_agent_name(agent_name: str) -> ACPProviderInf...
  function build_session_model_meta (line 157) | def build_session_model_meta(agent_name: str, acp_model: str | None) -> ...

FILE: openhands-sdk/openhands/sdk/settings/api_models.py
  class SettingsResponse (line 41) | class SettingsResponse(BaseModel):
    method get_agent_settings (line 62) | def get_agent_settings(self) -> AgentSettingsConfig:
    method get_conversation_settings (line 73) | def get_conversation_settings(self) -> ConversationSettings:
  class SettingsUpdateRequest (line 84) | class SettingsUpdateRequest(BaseModel):
  class SecretItemResponse (line 98) | class SecretItemResponse(BaseModel):
  class SecretsListResponse (line 108) | class SecretsListResponse(BaseModel):
  class SecretCreateRequest (line 118) | class SecretCreateRequest(BaseModel):

FILE: openhands-sdk/openhands/sdk/settings/metadata.py
  class SettingProminence (line 13) | class SettingProminence(str, Enum):
  class SettingsSectionMetadata (line 19) | class SettingsSectionMetadata(BaseModel):
  class SettingsFieldMetadata (line 25) | class SettingsFieldMetadata(BaseModel):
  function field_meta (line 35) | def field_meta(

FILE: openhands-sdk/openhands/sdk/settings/model.py
  function _walk_mcp_secret_values (line 71) | def _walk_mcp_secret_values(
  function _decrypt_secret_value_or_keep (line 95) | def _decrypt_secret_value_or_keep(
  function _decrypt_mcp_value_or_keep (line 117) | def _decrypt_mcp_value_or_keep(cipher: Cipher, value: str) -> str:
  class SettingsChoice (line 134) | class SettingsChoice(BaseModel):
  class SettingsFieldSchema (line 139) | class SettingsFieldSchema(BaseModel):
  class SettingsSectionSchema (line 162) | class SettingsSectionSchema(BaseModel):
  class SettingsSchema (line 177) | class SettingsSchema(BaseModel):
  class CondenserSettings (line 186) | class CondenserSettings(BaseModel):
  class VerificationSettings (line 211) | class VerificationSettings(BaseModel):
  function _default_llm_settings (line 306) | def _default_llm_settings() -> LLM:
  class AgentSettingsBase (line 318) | class AgentSettingsBase(BaseModel):
    method export_schema (line 340) | def export_schema(cls) -> SettingsSchema:
    method create_agent (line 344) | def create_agent(self) -> AgentBase:
  function _copy_persisted_payload (line 360) | def _copy_persisted_payload(data: Any) -> dict[str, Any]:
  function _apply_persisted_migrations (line 371) | def _apply_persisted_migrations(
  function _migrate_agent_settings_v0_to_v1 (line 421) | def _migrate_agent_settings_v0_to_v1(payload: dict[str, Any]) -> dict[st...
  function _migrate_agent_settings_v1_to_v2 (line 428) | def _migrate_agent_settings_v1_to_v2(payload: dict[str, Any]) -> dict[st...
  function _migrate_agent_settings_v2_to_v3 (line 448) | def _migrate_agent_settings_v2_to_v3(payload: dict[str, Any]) -> dict[st...
  function _migrate_conversation_settings_v0_to_v1 (line 461) | def _migrate_conversation_settings_v0_to_v1(
  class ConversationSettings (line 479) | class ConversationSettings(BaseModel):
    method export_schema (line 579) | def export_schema(cls) -> SettingsSchema:
    method from_persisted (line 584) | def from_persisted(cls, data: Any) -> ConversationSettings:
    method _build_confirmation_policy (line 594) | def _build_confirmation_policy(self):
    method _build_security_analyzer (line 607) | def _build_security_analyzer(self):
    method _start_request_kwargs (line 617) | def _start_request_kwargs(self, **kwargs: Any) -> dict[str, Any]:
    method create_request (line 660) | def create_request(
  class OpenHandsAgentSettings (line 685) | class OpenHandsAgentSettings(AgentSettingsBase):
    method _normalize_empty_mcp_config (line 798) | def _normalize_empty_mcp_config(cls, value: Any) -> Any:
    method _decrypt_mcp_secret_values (line 805) | def _decrypt_mcp_secret_values(cls, value: Any, info: ValidationInfo) ...
    method _serialize_mcp_config (line 824) | def _serialize_mcp_config(
    method create_agent (line 850) | def create_agent(self) -> Agent:
    method build_condenser (line 885) | def build_condenser(self, llm: LLM) -> LLMSummarizingCondenser | None:
    method build_critic (line 894) | def build_critic(self) -> CriticBase | None:
  class ACPAgentSettings (line 936) | class ACPAgentSettings(AgentSettingsBase):
    method _decrypt_acp_env_values (line 1032) | def _decrypt_acp_env_values(cls, value: Any, info: ValidationInfo) -> ...
    method _serialize_acp_env (line 1053) | def _serialize_acp_env(self, value: dict[str, str], info):
    method provider_info (line 1135) | def provider_info(self) -> ACPProviderInfo | None:
    method api_key_env_var (line 1140) | def api_key_env_var(self) -> str | None:
    method base_url_env_var (line 1151) | def base_url_env_var(self) -> str | None:
    method resolve_provider_env (line 1160) | def resolve_provider_env(self) -> dict[str, str]:
    method resolve_acp_env (line 1189) | def resolve_acp_env(self) -> dict[str, str]:
    method resolve_acp_command (line 1203) | def resolve_acp_command(self) -> list[str]:
    method create_agent (line 1225) | def create_agent(self) -> ACPAgent:
  class LLMAgentSettings (line 1246) | class LLMAgentSettings(OpenHandsAgentSettings):
  function _agent_settings_discriminator (line 1274) | def _agent_settings_discriminator(value: Any) -> str:
  function validate_agent_settings (line 1314) | def validate_agent_settings(
  class AgentSettings (line 1336) | class AgentSettings(LLMAgentSettings):
    method from_persisted (line 1368) | def from_persisted(
    method __init__ (line 1377) | def __init__(self, *args: Any, **kwargs: Any) -> None:
  function default_agent_settings (line 1394) | def default_agent_settings() -> OpenHandsAgentSettings:
  function create_agent_from_settings (line 1403) | def create_agent_from_settings(
  function export_agent_settings_schema (line 1414) | def export_agent_settings_schema() -> SettingsSchema:
  function settings_section_metadata (line 1458) | def settings_section_metadata(field: FieldInfo) -> SettingsSectionMetada...
  function settings_metadata (line 1469) | def settings_metadata(field: FieldInfo) -> SettingsFieldMetadata | None:
  function export_settings_schema (line 1488) | def export_settings_schema(model: type[BaseModel]) -> SettingsSchema:
  function _nested_model_type (line 1597) | def _nested_model_type(annotation: Any) -> type[BaseModel] | None:
  function _annotation_options (line 1608) | def _annotation_options(annotation: Any) -> tuple[Any, ...]:
  function _contains_secret (line 1623) | def _contains_secret(annotation: Any) -> bool:
  function _infer_value_type (line 1627) | def _infer_value_type(annotation: Any) -> SettingsValueType:
  function _is_stringish (line 1648) | def _is_stringish(annotation: Any) -> bool:
  function _is_array_annotation (line 1652) | def _is_array_annotation(annotation: Any) -> bool:
  function _is_object_annotation (line 1656) | def _is_object_annotation(annotation: Any) -> bool:
  function _choice_values (line 1663) | def _choice_values(annotation: Any) -> list[SettingsChoiceValue]:
  function _value_type_for_values (line 1685) | def _value_type_for_values(values: list[SettingsChoiceValue]) -> Setting...
  function _extract_choices (line 1698) | def _extr
Copy disabled (too large) Download .json
Condensed preview — 1197 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (10,645K chars).
[
  {
    "path": ".agents/skills/cross-repo-testing/SKILL.md",
    "chars": 9883,
    "preview": "---\nname: cross-repo-testing\ndescription: This skill should be used when the user asks to \"test a saas cross-repo featur"
  },
  {
    "path": ".agents/skills/custom-codereview-guide.md",
    "chars": 17445,
    "preview": "---\nname: custom-codereview-guide\ndescription: Repo-specific code review guidelines for OpenHands/software-agent-sdk. Pr"
  },
  {
    "path": ".agents/skills/debug-test-examples-workflow/SKILL.md",
    "chars": 2688,
    "preview": "---\nname: debug-test-examples-workflow\ndescription: Guide for debugging failing example tests in the `test-examples` lab"
  },
  {
    "path": ".agents/skills/design-principles.md",
    "chars": 2146,
    "preview": "---\nname: design-principles\ndescription: Core architectural design principles of the OpenHands Software Agent SDK. Refer"
  },
  {
    "path": ".agents/skills/feature-release-rollout/SKILL.md",
    "chars": 7516,
    "preview": "---\nname: feature-release-rollout\ndescription: This skill should be used when the user asks to \"rollout a feature\", \"com"
  },
  {
    "path": ".agents/skills/manage-evals/SKILL.md",
    "chars": 8076,
    "preview": "---\nname: manage-evals\ndescription: This skill should be used when the user asks to \"trigger an eval\", \"run evaluation\","
  },
  {
    "path": ".agents/skills/manage-evals/references/eval-infrastructure.md",
    "chars": 4978,
    "preview": "# Evaluation Infrastructure Reference\n\n## Architecture Overview\n\nThe evaluation pipeline spans three repositories:\n\n1. *"
  },
  {
    "path": ".agents/skills/manage-evals/scripts/manage_evals.py",
    "chars": 20561,
    "preview": "#!/usr/bin/env python3\n\"\"\"Trigger, compare, and report on OpenHands evaluation runs.\n\nSubcommands:\n    trigger   Dispatc"
  },
  {
    "path": ".agents/skills/run-eval.md",
    "chars": 2491,
    "preview": "---\nname: run-eval\ndescription: Trigger and monitor evaluation runs for benchmarks like SWE-bench, GAIA, and others. Use"
  },
  {
    "path": ".agents/skills/sdk-release/SKILL.md",
    "chars": 6926,
    "preview": "---\nname: sdk-release\ndescription: >-\n  This skill should be used when the user asks to \"release the SDK\",\n  \"prepare a "
  },
  {
    "path": ".agents/skills/sdk-release/references/post-release-checklist.md",
    "chars": 3014,
    "preview": "# Post-Release Checklist\n\nAfter the GitHub release is published and PyPI packages are available,\nseveral automated and m"
  },
  {
    "path": ".agents/skills/write-behavior-test.md",
    "chars": 5910,
    "preview": "---\nname: write-behavior-test\ndescription: Guide for writing behavior tests that verify agents follow system message gui"
  },
  {
    "path": ".dockerignore",
    "chars": 4229,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_template.yml",
    "chars": 5756,
    "preview": "---\nname: Bug\ndescription: Report a problem with OpenHands SDK\ntitle: '[Bug]: '\nlabels: [bug]\nbody:\n    - type: markdown"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 5238,
    "preview": "---\nname: Feature Request or Enhancement\ndescription: Suggest a new feature or improvement for OpenHands SDK\ntitle: '[Fe"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 1342,
    "preview": "<!-- Keep this PR as draft until it is ready for review. -->\n\n<!-- AI/LLM agents: \n\nProvide evidence that the code runs "
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 544,
    "preview": "---\n# Dependabot configuration for automated dependency updates\n# See: https://docs.github.com/en/code-security/dependab"
  },
  {
    "path": ".github/prompts/update-documentation.md",
    "chars": 4317,
    "preview": "# Documentation Update Prompt\n\nYou are a world-class documentation writer tasked with keeping the OpenHands Agent SDK do"
  },
  {
    "path": ".github/run-eval/ADDINGMODEL.md",
    "chars": 13743,
    "preview": "# Adding Models to resolve_model_config.py\n\n## Overview\n\nThis file (`resolve_model_config.py`) defines models available "
  },
  {
    "path": ".github/run-eval/AGENTS.md",
    "chars": 2234,
    "preview": "# Model Configuration for OpenHands SDK\n\nSee the [project root AGENTS.md](../../AGENTS.md) for repository-wide policies "
  },
  {
    "path": ".github/run-eval/resolve_model_config.py",
    "chars": 18389,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nResolve model IDs to full model configurations and verify model availability.\n\nReads:\n- MODEL"
  },
  {
    "path": ".github/run-eval/validate_sdk_ref.py",
    "chars": 4053,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nValidate SDK reference for semantic versioning.\n\nThis script validates that the SDK reference"
  },
  {
    "path": ".github/scripts/check_agent_server_rest_api_breakage.py",
    "chars": 30645,
    "preview": "#!/usr/bin/env python3\n\"\"\"REST API breakage detection for openhands-agent-server using oasdiff.\n\nThis script compares th"
  },
  {
    "path": ".github/scripts/check_deprecations.py",
    "chars": 18748,
    "preview": "#!/usr/bin/env python3\n\"\"\"Static analysis for deprecation deadlines.\n\nThis script scans Python deprecation metadata (`de"
  },
  {
    "path": ".github/scripts/check_docstrings.py",
    "chars": 9256,
    "preview": "#!/usr/bin/env python3\n\"\"\"Validate docstrings conform to MDX-compatible formatting guidelines.\n\nThis script checks that "
  },
  {
    "path": ".github/scripts/check_documented_examples.py",
    "chars": 7103,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nCheck if all examples in agent-sdk are documented in the docs repository.\n\nThis script:\n1. Sc"
  },
  {
    "path": ".github/scripts/check_duplicate_example_numbers.py",
    "chars": 3121,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nCheck for duplicate example numbers in the examples directory.\n\nThis script ensures that with"
  },
  {
    "path": ".github/scripts/check_sdk_api_breakage.py",
    "chars": 34983,
    "preview": "#!/usr/bin/env python3\n\"\"\"API breakage detection for published OpenHands packages using Griffe.\n\nThis script compares cu"
  },
  {
    "path": ".github/scripts/check_version_bumps.py",
    "chars": 6210,
    "preview": "\"\"\"Guard package version changes so they only happen in release PRs.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\ni"
  },
  {
    "path": ".github/scripts/update_sdk_ref_default.py",
    "chars": 3742,
    "preview": "#!/usr/bin/env python3\n\"\"\"Update the sdk_ref default value in run-eval.yml.\n\nThis script updates the default SDK referen"
  },
  {
    "path": ".github/workflows/README-RELEASE.md",
    "chars": 8041,
    "preview": "# Release Automation Workflows\n\nThis document describes the automated release workflows for the OpenHands Software Agent"
  },
  {
    "path": ".github/workflows/agent-server-rest-api-breakage.yml",
    "chars": 6502,
    "preview": "---\nname: REST API breakage checks\n\non:\n    push:\n        branches: [main]\n    pull_request:\n        branches: [main]\n\nj"
  },
  {
    "path": ".github/workflows/api-breakage.yml",
    "chars": 6493,
    "preview": "---\nname: Python API breakage checks\n\non:\n    push:\n        branches: [main]\n    pull_request:\n        branches: [main]\n"
  },
  {
    "path": ".github/workflows/api-compliance-runner.yml",
    "chars": 4856,
    "preview": "---\nname: API Compliance Tests\n\non:\n    pull_request:\n        types: [labeled]\n    workflow_dispatch:\n        inputs:\n  "
  },
  {
    "path": ".github/workflows/assign-reviews.yml",
    "chars": 12770,
    "preview": "---\n# To set this up:\n#  1. Change the name below to something relevant to your task\n#  2. Modify the \"env\" section belo"
  },
  {
    "path": ".github/workflows/auto-label-issues.yml",
    "chars": 1257,
    "preview": "---\nname: Auto-label New Issues\n\non:\n    issues:\n        types: [opened]\n\npermissions:\n    issues: write\n\njobs:\n    add-"
  },
  {
    "path": ".github/workflows/cancel-eval.yml",
    "chars": 2077,
    "preview": "---\nname: Cancel Eval\n\nrun-name: Cancel Eval (${{ inputs.run_id }})\n\non:\n    workflow_dispatch:\n        inputs:\n        "
  },
  {
    "path": ".github/workflows/check-docstrings.yml",
    "chars": 550,
    "preview": "---\n# .github/workflows/check-docstrings.yml\nname: Check Docstrings\n\non:\n    push:\n        branches: [main]\n    pull_req"
  },
  {
    "path": ".github/workflows/check-documented-examples.yml",
    "chars": 1770,
    "preview": "---\nname: '[Optional] Docs example'\n\non:\n    pull_request:\n        branches:\n            - '**'\n        paths:\n         "
  },
  {
    "path": ".github/workflows/check-duplicate-examples.yml",
    "chars": 842,
    "preview": "---\nname: Check duplicate example numbers\n\non:\n    pull_request:\n        branches:\n            - '**'\n        paths:\n   "
  },
  {
    "path": ".github/workflows/condenser-runner.yml",
    "chars": 10648,
    "preview": "---\nname: Run Condenser Tests\n\non:\n    # Use pull_request_target to access secrets even on fork PRs\n    # This is safe b"
  },
  {
    "path": ".github/workflows/create-release.yml",
    "chars": 6143,
    "preview": "---\nname: Create GitHub Release\n\n# Automatically create a GitHub release when a release PR is merged into main.\n# This b"
  },
  {
    "path": ".github/workflows/deploy-docs.yml",
    "chars": 641,
    "preview": "---\nname: Dispatch to docs repo\n\non:\n    push:\n        branches:\n            - main\n        paths:\n            - openhan"
  },
  {
    "path": ".github/workflows/deprecation-check.yml",
    "chars": 556,
    "preview": "---\nname: Deprecation deadlines\n\non:\n    push:\n        branches: [main]\n    pull_request:\n        branches: ['**']\n\njobs"
  },
  {
    "path": ".github/workflows/integration-runner.yml",
    "chars": 22483,
    "preview": "---\nname: Run Integration Tests\nrun-name: >-\n    Run Integration Tests ${{ inputs.reason || github.event.label.name || '"
  },
  {
    "path": ".github/workflows/issue-duplicate-checker.yml",
    "chars": 22068,
    "preview": "---\nname: Issue Duplicate Check via OpenHands Cloud\n\non:\n    issues:\n        types: [opened]\n    schedule:\n        - cro"
  },
  {
    "path": ".github/workflows/oh-update-documentation.yml.back",
    "chars": 863,
    "preview": "name: Update Documentation (by OpenHands)\n\non:\n  schedule:\n    # Run every 7 days at 2 AM UTC on Sundays\n    - cron: '0 "
  },
  {
    "path": ".github/workflows/pr-artifacts.yml",
    "chars": 5928,
    "preview": "---\nname: PR Artifacts\n\non:\n    workflow_dispatch: # Manual trigger for testing\n    pull_request:\n        types: [opened"
  },
  {
    "path": ".github/workflows/pr-review-by-openhands.yml",
    "chars": 3224,
    "preview": "---\nname: PR Review by OpenHands\n\non:\n    # Use pull_request for same-repo PRs so workflow changes can self-verify in PR"
  },
  {
    "path": ".github/workflows/pr-review-evaluation.yml",
    "chars": 3326,
    "preview": "---\nname: PR Review Evaluation\n\n# This workflow evaluates how well PR review comments were addressed.\n# It runs when a P"
  },
  {
    "path": ".github/workflows/precommit.yml",
    "chars": 716,
    "preview": "---\n# .github/workflows/precommit.yml\nname: Pre-commit checks\n\non:\n    push:\n        branches: [main]\n    pull_request:\n"
  },
  {
    "path": ".github/workflows/prepare-release.yml",
    "chars": 5255,
    "preview": "---\nname: Prepare Release\n\non:\n    workflow_dispatch:\n        inputs:\n            version:\n                description: "
  },
  {
    "path": ".github/workflows/pypi-release.yml",
    "chars": 3485,
    "preview": "---\nname: Publish all OpenHands packages (uv)\n\non:\n  # Run manually\n    workflow_dispatch:\n  # Run automatically when a "
  },
  {
    "path": ".github/workflows/qa-changes-by-openhands.yml",
    "chars": 2251,
    "preview": "---\n# Automated QA validation of PR changes using OpenHands.\n#\n# Unlike pr-review (which reads diffs and posts code-revi"
  },
  {
    "path": ".github/workflows/qa-changes-evaluation.yml",
    "chars": 3214,
    "preview": "---\nname: QA Changes Evaluation\n\n# This workflow evaluates how well QA validation performed.\n# It runs when a PR is clos"
  },
  {
    "path": ".github/workflows/release-binaries.yml",
    "chars": 12559,
    "preview": "---\nname: Publish agent-server release artifacts\n\n# On release published or push to main:\n#   1. Build the agent-server "
  },
  {
    "path": ".github/workflows/remove-duplicate-candidate-label.yml",
    "chars": 2564,
    "preview": "---\nname: Remove duplicate candidate label on activity\n\non:\n    issue_comment:\n        types: [created]\n\npermissions:\n  "
  },
  {
    "path": ".github/workflows/review-thread-gate.yml",
    "chars": 4769,
    "preview": "---\nname: Review Thread Gate\n\non:\n    pull_request:\n        branches: [main]\n        types: [opened, synchronize, reopen"
  },
  {
    "path": ".github/workflows/run-eval.yml",
    "chars": 21754,
    "preview": "---\nname: Run Eval\nrun-name: Run Eval (${{ inputs.benchmark || 'swebench' }}) ${{ inputs.reason || github.event.label.na"
  },
  {
    "path": ".github/workflows/run-examples.yml",
    "chars": 8719,
    "preview": "---\nname: Run Examples Scripts\n\non:\n    pull_request:\n        types: [labeled]\n    workflow_dispatch:\n        inputs:\n  "
  },
  {
    "path": ".github/workflows/server.yml",
    "chars": 34789,
    "preview": "---\nname: Agent Server\n\non:\n    push:\n        branches: [main]\n        tags:\n            - '*'  # Trigger on any tag (e."
  },
  {
    "path": ".github/workflows/stale.yml",
    "chars": 1648,
    "preview": "---\n# Workflow that marks issues and PRs with no activity for 30 days with \"Stale\" and closes them after 7 more days of "
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 20227,
    "preview": "---\nname: Run tests\n\non:\n    push:\n        branches: [main]\n    pull_request:\n        branches: ['**']\n\npermissions:\n   "
  },
  {
    "path": ".github/workflows/todo-management.yml",
    "chars": 12840,
    "preview": "---\n# Automated TODO Management Workflow\n#\n# This workflow automatically scans for TODO(openhands) comments and creates\n"
  },
  {
    "path": ".github/workflows/version-bump-guard.yml",
    "chars": 704,
    "preview": "---\nname: Version bump guard\n\non:\n    pull_request:\n        branches: [main]\n\njobs:\n    version-bump-guard:\n        name"
  },
  {
    "path": ".github/workflows/version-bump-prs.yml",
    "chars": 20488,
    "preview": "---\nname: Create Version Bump PRs\n\non:\n    # Dispatched by pypi-release.yml after a successful publish.\n    # Also suppo"
  },
  {
    "path": ".gitignore",
    "chars": 3977,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".openhands/hooks/on_stop.sh",
    "chars": 14336,
    "preview": "#!/bin/bash\n# Stop hook: runs pre-commit, pytest, and checks CI status before allowing agent to finish\n#\n# This hook run"
  },
  {
    "path": ".openhands/hooks.json",
    "chars": 205,
    "preview": "{\n  \"stop\": [\n    {\n      \"matcher\": \"*\",\n      \"hooks\": [\n        {\n          \"type\": \"command\",\n          \"command\": \""
  },
  {
    "path": ".openhands/setup.sh",
    "chars": 263,
    "preview": "#!/bin/bash\n\nif ! command -v uv &> /dev/null; then\n    echo \"uv is not installed. Installing...\"\n    curl -LsSf https://"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1831,
    "preview": "---\nrepos:\n    - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt\n      rev: 0.2.1 # or other specific tag\n"
  },
  {
    "path": ".python-version",
    "chars": 5,
    "preview": "3.13\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 25647,
    "preview": "<ROLE>\nYou are a collaborative software engineering partner with a strong focus on code quality and simplicity. Your app"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3630,
    "preview": "# Contributing\n\nThank you for helping improve the OpenHands Software Agent SDK.\n\nThis repo is a foundation. We want the "
  },
  {
    "path": "DEVELOPMENT.md",
    "chars": 1102,
    "preview": "# Development Guide\n\n## Setup\n\n```bash\ngit clone https://github.com/OpenHands/agent-sdk.git\ncd agent-sdk\nmake build\n```\n"
  },
  {
    "path": "LICENSE",
    "chars": 1079,
    "preview": "MIT License\n\nCopyright (c) 2026 OpenHands contributors\n\nPermission is hereby granted, free of charge, to any person obta"
  },
  {
    "path": "MAINTAINERS",
    "chars": 311,
    "preview": "# Repository Maintainers\n#\n# Format: Each maintainer on a new line starting with \"- @username\"\n# This file is read by .g"
  },
  {
    "path": "MANIFEST.in",
    "chars": 2049,
    "preview": "# This MANIFEST.in file tells setuptools which files to include \n# in the sdist package distribution used for building d"
  },
  {
    "path": "Makefile",
    "chars": 4171,
    "preview": "SHELL := /usr/bin/env bash\n.SHELLFLAGS := -eu -o pipefail -c\n\n# Colors for output\nECHO := printf '%b\\n'\nGREEN := \\033[32"
  },
  {
    "path": "README.md",
    "chars": 9774,
    "preview": "<a name=\"readme-top\"></a>\n\n<div align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/OpenHands/docs/main/openha"
  },
  {
    "path": "examples/01_standalone_sdk/01_hello_world.py",
    "chars": 771,
    "preview": "import os\n\nfrom openhands.sdk import LLM, Agent, Conversation, Tool\nfrom openhands.tools.file_editor import FileEditorTo"
  },
  {
    "path": "examples/01_standalone_sdk/02_custom_tools.py",
    "chars": 7090,
    "preview": "\"\"\"Advanced example showing explicit executor usage and custom grep tool.\"\"\"\n\nimport os\nimport shlex\nfrom collections.ab"
  },
  {
    "path": "examples/01_standalone_sdk/03_activate_skill.py",
    "chars": 4091,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    AgentContext,\n    Convers"
  },
  {
    "path": "examples/01_standalone_sdk/04_confirmation_mode_example.py",
    "chars": 5237,
    "preview": "\"\"\"OpenHands Agent SDK — Confirmation Mode Example\"\"\"\n\nimport os\nimport signal\nfrom collections.abc import Callable\n\nfro"
  },
  {
    "path": "examples/01_standalone_sdk/05_use_llm_registry.py",
    "chars": 2223,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/06_interactive_terminal_w_reasoning.py",
    "chars": 1453,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/07_mcp_integration.py",
    "chars": 2197,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/08_mcp_with_oauth.py",
    "chars": 1590,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/09_pause_example.py",
    "chars": 2332,
    "preview": "import os\nimport threading\nimport time\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n"
  },
  {
    "path": "examples/01_standalone_sdk/10_persistence.py",
    "chars": 2420,
    "preview": "import os\nimport uuid\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,"
  },
  {
    "path": "examples/01_standalone_sdk/11_async.py",
    "chars": 2586,
    "preview": "\"\"\"\nThis example demonstrates usage of a Conversation in an async context\n(e.g.: From a fastapi server). The conversatio"
  },
  {
    "path": "examples/01_standalone_sdk/12_custom_secrets.py",
    "chars": 1282,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n)\nfrom open"
  },
  {
    "path": "examples/01_standalone_sdk/13_get_llm_metrics.py",
    "chars": 1947,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/14_context_condenser.py",
    "chars": 4171,
    "preview": "\"\"\"\nTo manage context in long-running conversations, the agent can use a context condenser\nthat keeps the conversation h"
  },
  {
    "path": "examples/01_standalone_sdk/15_browser_use.py",
    "chars": 1783,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/16_llm_security_analyzer.py",
    "chars": 4545,
    "preview": "\"\"\"OpenHands Agent SDK — LLM Security Analyzer Example (Simplified)\n\nThis example shows how to use the LLMSecurityAnalyz"
  },
  {
    "path": "examples/01_standalone_sdk/17_image_input.py",
    "chars": 4341,
    "preview": "\"\"\"OpenHands Agent SDK — Image Input Example.\n\nThis script mirrors the basic setup from ``examples/01_hello_world.py`` b"
  },
  {
    "path": "examples/01_standalone_sdk/18_send_message_while_processing.py",
    "chars": 4779,
    "preview": "\"\"\"\nExample demonstrating that user messages can be sent and processed while\nan agent is busy.\n\nThis example demonstrate"
  },
  {
    "path": "examples/01_standalone_sdk/19_llm_routing.py",
    "chars": 2403,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,\n    Conversation,\n    Event,\n"
  },
  {
    "path": "examples/01_standalone_sdk/20_stuck_detector.py",
    "chars": 1749,
    "preview": "import os\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Conversation,\n    Event,\n    LLMConv"
  },
  {
    "path": "examples/01_standalone_sdk/21_generate_extraneous_conversation_costs.py",
    "chars": 3043,
    "preview": "import os\n\nfrom pydantic import SecretStr\nfrom tabulate import tabulate\n\nfrom openhands.sdk import (\n    LLM,\n    Agent,"
  },
  {
    "path": "examples/01_standalone_sdk/22_anthropic_thinking.py",
    "chars": 1929,
    "preview": "\"\"\"Example demonstrating Anthropic's extended thinking feature with thinking blocks.\"\"\"\n\nimport os\n\nfrom pydantic import"
  },
  {
    "path": "examples/01_standalone_sdk/23_responses_reasoning.py",
    "chars": 2027,
    "preview": "\"\"\"\nExample: Responses API path via LiteLLM in a Real Agent Conversation\n\n- Runs a real Agent/Conversation to verify /re"
  },
  {
    "path": "examples/01_standalone_sdk/24_planning_agent_workflow.py",
    "chars": 3977,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nPlanning Agent Workflow Example\n\nThis example demonstrates a two-stage workflow:\n1. Planning "
  },
  {
    "path": "examples/01_standalone_sdk/25_agent_delegation.py",
    "chars": 4232,
    "preview": "\"\"\"\nAgent Delegation Example\n\nThis example demonstrates the agent delegation feature where a main agent\ndelegates tasks "
  },
  {
    "path": "examples/01_standalone_sdk/26_custom_visualizer.py",
    "chars": 2235,
    "preview": "\"\"\"Custom Visualizer Example\n\nThis example demonstrates how to create and use a custom visualizer by subclassing\nConvers"
  },
  {
    "path": "examples/01_standalone_sdk/27_observability_laminar.py",
    "chars": 1221,
    "preview": "\"\"\"\nObservability & Laminar example\n\nThis example demonstrates enabling OpenTelemetry tracing with Laminar in the\nOpenHa"
  },
  {
    "path": "examples/01_standalone_sdk/28_ask_agent_example.py",
    "chars": 4359,
    "preview": "\"\"\"\nExample demonstrating the ask_agent functionality for getting sidebar replies\nfrom the agent for a running conversat"
  },
  {
    "path": "examples/01_standalone_sdk/29_llm_streaming.py",
    "chars": 4347,
    "preview": "import os\nimport sys\nfrom typing import Literal\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    Convers"
  },
  {
    "path": "examples/01_standalone_sdk/30_tom_agent.py",
    "chars": 3890,
    "preview": "\"\"\"Example demonstrating Tom agent with Theory of Mind capabilities.\n\nThis example shows how to set up an agent with Tom"
  },
  {
    "path": "examples/01_standalone_sdk/31_iterative_refinement.py",
    "chars": 15608,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nIterative Refinement Example: COBOL to Java Refactoring\n\nThis example demonstrates an iterati"
  },
  {
    "path": "examples/01_standalone_sdk/32_configurable_security_policy.py",
    "chars": 5022,
    "preview": "\"\"\"OpenHands Agent SDK — Configurable Security Policy Example\n\nThis example demonstrates how to use a custom security po"
  },
  {
    "path": "examples/01_standalone_sdk/33_hooks/README.md",
    "chars": 1880,
    "preview": "# Hooks Examples\n\nThis folder demonstrates the OpenHands hooks system.\n\n## Example\n\n- **main.py** - Complete hooks demo "
  },
  {
    "path": "examples/01_standalone_sdk/33_hooks/hook_scripts/block_dangerous.sh",
    "chars": 415,
    "preview": "#!/bin/bash\n# PreToolUse hook: Block dangerous rm -rf commands\n# Uses grep on raw JSON input (no jq needed)\n\ninput=$(cat"
  },
  {
    "path": "examples/01_standalone_sdk/33_hooks/hook_scripts/inject_git_context.sh",
    "chars": 621,
    "preview": "#!/bin/bash\n# UserPromptSubmit hook: Inject git status when user asks about code changes\n\ninput=$(cat)\n\n# Check if user "
  },
  {
    "path": "examples/01_standalone_sdk/33_hooks/hook_scripts/log_tools.sh",
    "chars": 273,
    "preview": "#!/bin/bash\n# PostToolUse hook: Log all tool usage\n# Uses OPENHANDS_TOOL_NAME env var (no jq/python needed!)\n\n# LOG_FILE"
  },
  {
    "path": "examples/01_standalone_sdk/33_hooks/hook_scripts/require_summary.sh",
    "chars": 320,
    "preview": "#!/bin/bash\n# Stop hook: Require a summary.txt file before allowing agent to finish\n# SUMMARY_FILE should be set by the "
  },
  {
    "path": "examples/01_standalone_sdk/33_hooks/main.py",
    "chars": 4646,
    "preview": "\"\"\"OpenHands Agent SDK — Hooks Example\n\nDemonstrates the OpenHands hooks system.\nHooks are shell scripts that run at key"
  },
  {
    "path": "examples/01_standalone_sdk/34_critic_example.py",
    "chars": 7933,
    "preview": "\"\"\"Iterative Refinement with Critic Model Example.\n\nThis is EXPERIMENTAL.\n\nThis example demonstrates how to use a critic"
  },
  {
    "path": "examples/01_standalone_sdk/35_subscription_login.py",
    "chars": 3157,
    "preview": "\"\"\"Example: Using ChatGPT subscription for Codex models.\n\nThis example demonstrates how to use your ChatGPT Plus/Pro sub"
  },
  {
    "path": "examples/01_standalone_sdk/36_event_json_to_openai_messages.py",
    "chars": 2993,
    "preview": "\"\"\"Load persisted events and convert them into LLM-ready messages.\"\"\"\n\nimport json\nimport os\nimport uuid\nfrom pathlib im"
  },
  {
    "path": "examples/01_standalone_sdk/37_llm_profile_store/main.py",
    "chars": 2047,
    "preview": "\"\"\"Example: Using LLMProfileStore to save and reuse LLM configurations.\n\nThis example ships with one pre-generated profi"
  },
  {
    "path": "examples/01_standalone_sdk/37_llm_profile_store/profiles/fast.json",
    "chars": 820,
    "preview": "{\n  \"model\": \"anthropic/claude-sonnet-4-5-20250929\",\n  \"api_key\": \"**********\",\n  \"openrouter_site_url\": \"https://docs.a"
  },
  {
    "path": "examples/01_standalone_sdk/38_browser_session_recording.py",
    "chars": 6093,
    "preview": "\"\"\"Browser Session Recording Example\n\nThis example demonstrates how to use the browser session recording feature\nto capt"
  },
  {
    "path": "examples/01_standalone_sdk/39_llm_fallback.py",
    "chars": 3201,
    "preview": "\"\"\"Example: Using FallbackStrategy for LLM resilience.\n\nWhen the primary LLM fails with a transient error (rate limit, t"
  },
  {
    "path": "examples/01_standalone_sdk/40_acp_agent_example.py",
    "chars": 2353,
    "preview": "\"\"\"Example: Using ACPAgent with Claude Code ACP server.\n\nThis example shows how to use an ACP-compatible server (claude-"
  },
  {
    "path": "examples/01_standalone_sdk/41_task_tool_set.py",
    "chars": 3622,
    "preview": "\"\"\"\nAnimal Quiz with Task Tool Set\n\nDemonstrates the TaskToolSet with a main agent delegating to an\nanimal-expert sub-ag"
  },
  {
    "path": "examples/01_standalone_sdk/42_file_based_subagents.py",
    "chars": 2037,
    "preview": "\"\"\"Example: Defining a sub-agent inline with AgentDefinition.\n\nDefines a grammar-checker sub-agent using AgentDefinition"
  },
  {
    "path": "examples/01_standalone_sdk/43_mixed_marketplace_skills/.plugin/marketplace.json",
    "chars": 681,
    "preview": "{\n    \"name\": \"mixed-skills-marketplace\",\n    \"owner\": {\n        \"name\": \"OpenHands Team\",\n        \"email\": \"contact@all"
  },
  {
    "path": "examples/01_standalone_sdk/43_mixed_marketplace_skills/README.md",
    "chars": 3318,
    "preview": "# Mixed Marketplace Skills Example\n\nThis example demonstrates how to create a marketplace that includes both local and r"
  },
  {
    "path": "examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py",
    "chars": 3933,
    "preview": "\"\"\"Example: Mixed Marketplace with Local and Remote Skills\n\nThis example demonstrates how to create a marketplace that i"
  },
  {
    "path": "examples/01_standalone_sdk/43_mixed_marketplace_skills/skills/greeting-helper/SKILL.md",
    "chars": 896,
    "preview": "# greeting-helper\n\nA local skill that helps generate creative greetings for different occasions.\n\n## Description\n\nThis s"
  },
  {
    "path": "examples/01_standalone_sdk/44_model_switching_in_convo.py",
    "chars": 1434,
    "preview": "\"\"\"Mid-conversation model switching.\n\nUsage:\n    uv run examples/01_standalone_sdk/44_model_switching_in_convo.py\n\"\"\"\n\ni"
  },
  {
    "path": "examples/01_standalone_sdk/45_parallel_tool_execution.py",
    "chars": 7381,
    "preview": "\"\"\"Example: Parallel tool execution with tool_concurrency_limit.\n\nDemonstrates how setting tool_concurrency_limit on an "
  },
  {
    "path": "examples/01_standalone_sdk/46_agent_settings.py",
    "chars": 3434,
    "preview": "\"\"\"Create, serialize, and deserialize OpenHandsAgentSettings, then build an agent.\n\nDemonstrates:\n1. Configuring an agen"
  },
  {
    "path": "examples/01_standalone_sdk/47_defense_in_depth_security.py",
    "chars": 1551,
    "preview": "\"\"\"Defense-in-Depth Security: composing local analyzers with ConfirmRisky.\n\nThis example demonstrates how to wire the de"
  },
  {
    "path": "examples/01_standalone_sdk/48_conversation_fork.py",
    "chars": 3720,
    "preview": "\"\"\"Fork a conversation to branch off for follow-up exploration.\n\n``Conversation.fork()`` deep-copies a conversation — ev"
  },
  {
    "path": "examples/01_standalone_sdk/49_switch_llm_tool.py",
    "chars": 2505,
    "preview": "\"\"\"Switch LLM profiles with the built-in switch_llm tool.\n\nThis example creates two temporary LLM profiles, starts the c"
  },
  {
    "path": "examples/02_remote_agent_server/01_convo_with_local_agent_server.py",
    "chars": 14020,
    "preview": "import os\nimport subprocess\nimport sys\nimport tempfile\nimport threading\nimport time\nfrom pathlib import Path\n\nfrom pydan"
  },
  {
    "path": "examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py",
    "chars": 4209,
    "preview": "import os\nimport platform\nimport time\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Conversa"
  },
  {
    "path": "examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py",
    "chars": 4078,
    "preview": "import os\nimport platform\nimport time\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import LLM, Conversation, get_"
  },
  {
    "path": "examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py",
    "chars": 2769,
    "preview": "\"\"\"Example: APIRemoteWorkspace with Dynamic Build.\n\nThis example demonstrates building an agent-server image on-the-fly "
  },
  {
    "path": "examples/02_remote_agent_server/05_vscode_with_docker_sandboxed_server.py",
    "chars": 4089,
    "preview": "import os\nimport platform\nimport time\n\nimport httpx\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import LLM, Conve"
  },
  {
    "path": "examples/02_remote_agent_server/06_custom_tool/Dockerfile",
    "chars": 638,
    "preview": "# Dockerfile for custom base image with custom tools\n#\n# This Dockerfile creates a base image that includes custom tools"
  },
  {
    "path": "examples/02_remote_agent_server/06_custom_tool/README.md",
    "chars": 8457,
    "preview": "# Custom Tools with Remote Agent Server\n\nThis example demonstrates how to use custom tools with a remote agent server by"
  },
  {
    "path": "examples/02_remote_agent_server/06_custom_tool/build_custom_image.sh",
    "chars": 1541,
    "preview": "#!/bin/bash\n# Build script for custom base image with custom tools\n#\n# This script builds a custom base image that inclu"
  },
  {
    "path": "examples/02_remote_agent_server/06_custom_tool/custom_tools/__init__.py",
    "chars": 52,
    "preview": "\"\"\"Custom tools for remote agent server example.\"\"\"\n"
  },
  {
    "path": "examples/02_remote_agent_server/06_custom_tool/custom_tools/log_data.py",
    "chars": 5559,
    "preview": "\"\"\"Log Data Tool - Example custom tool for logging structured data to JSON.\n\nThis tool demonstrates how to create a cust"
  },
  {
    "path": "examples/02_remote_agent_server/06_custom_tool/main.py",
    "chars": 9641,
    "preview": "\"\"\"Example: Using custom tools with remote agent server.\n\nThis example demonstrates how to use custom tools with a remot"
  },
  {
    "path": "examples/02_remote_agent_server/07_convo_with_cloud_workspace.py",
    "chars": 3112,
    "preview": "\"\"\"Example: OpenHandsCloudWorkspace for OpenHands Cloud API.\n\nThis example demonstrates using OpenHandsCloudWorkspace to"
  },
  {
    "path": "examples/02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py",
    "chars": 4123,
    "preview": "import os\nimport platform\nimport time\n\nfrom pydantic import SecretStr\n\nfrom openhands.sdk import (\n    LLM,\n    Conversa"
  },
  {
    "path": "examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py",
    "chars": 2872,
    "preview": "\"\"\"Example: ACPAgent with Remote Runtime via API.\n\nThis example demonstrates running an ACPAgent (Claude Code via ACP pr"
  },
  {
    "path": "examples/02_remote_agent_server/10_cloud_workspace_share_credentials.py",
    "chars": 4135,
    "preview": "\"\"\"Example: Inherit SaaS credentials via OpenHandsCloudWorkspace.\n\nThis example shows the simplified flow where your Ope"
  },
  {
    "path": "examples/02_remote_agent_server/11_conversation_fork.py",
    "chars": 6915,
    "preview": "\"\"\"Fork a conversation through the agent server REST API.\n\nDemonstrates ``RemoteConversation.fork()`` which delegates to"
  },
  {
    "path": "examples/02_remote_agent_server/12_settings_and_secrets_api.py",
    "chars": 14270,
    "preview": "\"\"\"Example demonstrating the Settings and Secrets API.\n\nThis example shows the recommended workflow for managing secrets"
  },
  {
    "path": "examples/02_remote_agent_server/13_workspace_get_llm.py",
    "chars": 13205,
    "preview": "\"\"\"Example demonstrating workspace.get_llm() for settings-driven conversations.\n\nThis example shows how to use the new R"
  },
  {
    "path": "examples/02_remote_agent_server/hook_scripts/pycompile_check.sh",
    "chars": 932,
    "preview": "#!/bin/bash\n# Stop hook: Run Python syntax check on all .py files in the workspace\n# Returns deny if any Python file has"
  },
  {
    "path": "examples/03_github_workflows/01_basic_action/README.md",
    "chars": 4933,
    "preview": "# Routine Maintenance Workflow\n\nThis example demonstrates how to set up a GitHub Actions workflow for automated routine "
  },
  {
    "path": "examples/03_github_workflows/01_basic_action/agent_script.py",
    "chars": 4701,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nExample: Task Runner\n\nThis script runs OpenHands agent for an arbitrary task. It accepts a\npr"
  },
  {
    "path": "examples/03_github_workflows/01_basic_action/assign-reviews.yml",
    "chars": 9449,
    "preview": "---\n# To set this up:\n#  1. Change the name below to something relevant to your task\n#  2. Modify the \"env\" section belo"
  },
  {
    "path": "examples/03_github_workflows/01_basic_action/workflow.yml",
    "chars": 4977,
    "preview": "---\n# To set this up:\n#  1. Change the name below to something relevant to your task\n#  2. Modify the \"env\" section belo"
  },
  {
    "path": "examples/03_github_workflows/02_pr_review/README.md",
    "chars": 10764,
    "preview": "# PR Review Workflow\n\nThis example demonstrates how to set up a GitHub Actions workflow for automated pull request revie"
  },
  {
    "path": "examples/03_github_workflows/02_pr_review/workflow.yml",
    "chars": 1572,
    "preview": "---\n# OpenHands PR Review Workflow\n#\n# To set this up:\n#  1. Copy this file to .github/workflows/pr-review.yml in your r"
  },
  {
    "path": "examples/03_github_workflows/03_todo_management/README.md",
    "chars": 4648,
    "preview": "# Automated TODO Management with GitHub Actions\n\nThis example demonstrates how to use the OpenHands SDK to automatically"
  },
  {
    "path": "examples/03_github_workflows/03_todo_management/agent_script.py",
    "chars": 3289,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nTODO Agent for OpenHands Automated TODO Management\n\nThis script processes individual TODO(ope"
  },
  {
    "path": "examples/03_github_workflows/03_todo_management/prompt.py",
    "chars": 1218,
    "preview": "\"\"\"Prompt template for TODO implementation.\"\"\"\n\nPROMPT = \"\"\"Please implement a TODO comment in a codebase.\n\nIMPORTANT - "
  },
  {
    "path": "examples/03_github_workflows/03_todo_management/scanner.py",
    "chars": 6326,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nTODO Scanner for OpenHands Automated TODO Management\n\nScans for configurable TODO comments in"
  },
  {
    "path": "examples/03_github_workflows/03_todo_management/workflow.yml",
    "chars": 12881,
    "preview": "---\n# Automated TODO Management Workflow\n# Make sure to replace <YOUR_LLM_MODEL> and <YOUR_LLM_BASE_URL> with \n# appropr"
  },
  {
    "path": "examples/03_github_workflows/04_datadog_debugging/README.md",
    "chars": 9966,
    "preview": "# Datadog Error Debugging Workflow\n\nThis example demonstrates how to use OpenHands agents to automatically debug errors "
  },
  {
    "path": "examples/03_github_workflows/04_datadog_debugging/datadog_debugging.py",
    "chars": 23084,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nDatadog Debugging Example\n\nThis example demonstrates how to use the OpenHands agent to debug "
  },
  {
    "path": "examples/03_github_workflows/04_datadog_debugging/debug_prompt.jinja",
    "chars": 4866,
    "preview": "Your task is to debug an error from Datadog Error Tracking to find out why it is happening.\n\n## GitHub Issue for Trackin"
  },
  {
    "path": "examples/03_github_workflows/04_datadog_debugging/workflow.yml",
    "chars": 4729,
    "preview": "---\nname: Datadog Error Debugging\n\non:\n    workflow_dispatch:\n        inputs:\n            query_type:\n                de"
  },
  {
    "path": "examples/03_github_workflows/05_posthog_debugging/README.md",
    "chars": 12560,
    "preview": "# PostHog Error Debugging Workflow\n\nThis example demonstrates how to use OpenHands agents to automatically debug errors "
  },
  {
    "path": "examples/03_github_workflows/05_posthog_debugging/debug_prompt.jinja",
    "chars": 5462,
    "preview": "Your task is to debug an error from PostHog event tracking to find out why it is happening.\n\n## GitHub Issue for Trackin"
  },
  {
    "path": "examples/03_github_workflows/05_posthog_debugging/posthog_debugging.py",
    "chars": 35966,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nPostHog Debugging Example\n\nThis example demonstrates how to use the OpenHands agent to debug "
  },
  {
    "path": "examples/03_github_workflows/05_posthog_debugging/workflow.yml",
    "chars": 3933,
    "preview": "---\nname: PostHog Error Debugging\n\non:\n    workflow_dispatch:\n        inputs:\n            query_type:\n                de"
  },
  {
    "path": "examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py",
    "chars": 1483,
    "preview": "\"\"\"Example: Using GPT-5 preset with ApplyPatchTool for file editing.\n\nThis example demonstrates how to enable the GPT-5 "
  },
  {
    "path": "examples/04_llm_specific_tools/02_gemini_file_tools.py",
    "chars": 1583,
    "preview": "\"\"\"Example: Using Gemini-style file editing tools.\n\nThis example demonstrates how to use gemini-style file editing tools"
  },
  {
    "path": "examples/05_skills_and_plugins/01_loading_agentskills/example_skills/code-style-guide/SKILL.md",
    "chars": 758,
    "preview": "---\nname: code-style-guide\ndescription: >\n  Project coding standards and style guidelines. Always follow these\n  convent"
  },
  {
    "path": "examples/05_skills_and_plugins/01_loading_agentskills/example_skills/rot13-encryption/SKILL.md",
    "chars": 620,
    "preview": "---\nname: rot13-encryption\ndescription: >\n  This skill helps encrypt and decrypt messages using ROT13 cipher.\n  Use when"
  },
  {
    "path": "examples/05_skills_and_plugins/01_loading_agentskills/example_skills/rot13-encryption/references/examples.md",
    "chars": 205,
    "preview": "# ROT13 Examples\n\n## Encrypt \"hello world\"\n```bash\n./scripts/encrypt.sh \"hello world\"\n# Output: uryyb jbeyq\n```\n\n## Decr"
  },
  {
    "path": "examples/05_skills_and_plugins/01_loading_agentskills/example_skills/rot13-encryption/scripts/encrypt.sh",
    "chars": 108,
    "preview": "#!/bin/bash\n# ROT13 encryption - encrypts/decrypts the input message\necho \"$1\" | tr 'A-Za-z' 'N-ZA-Mn-za-m'\n"
  },
  {
    "path": "examples/05_skills_and_plugins/01_loading_agentskills/main.py",
    "chars": 5111,
    "preview": "\"\"\"Example: Loading Skills from Disk (AgentSkills Standard)\n\nThis example demonstrates how to load skills following the "
  },
  {
    "path": "examples/05_skills_and_plugins/02_loading_plugins/example_plugins/code-quality/.mcp.json",
    "chars": 106,
    "preview": "{\n  \"mcpServers\": {\n    \"fetch\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mcp-server-fetch\"]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/05_skills_and_plugins/02_loading_plugins/example_plugins/code-quality/.plugin/plugin.json",
    "chars": 307,
    "preview": "{\n  \"name\": \"code-quality\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A plugin for code quality checks including linting a"
  },
  {
    "path": "examples/05_skills_and_plugins/02_loading_plugins/example_plugins/code-quality/hooks/hooks.json",
    "chars": 375,
    "preview": "{\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"*\",\n        \"hooks\": [\n          {\n            \"type\": \""
  },
  {
    "path": "examples/05_skills_and_plugins/02_loading_plugins/example_plugins/code-quality/skills/linting/SKILL.md",
    "chars": 899,
    "preview": "---\nname: python-linting\ndescription: >\n  This skill helps lint Python code using ruff.\n  Use when the user asks to \"lin"
  },
  {
    "path": "examples/05_skills_and_plugins/02_loading_plugins/main.py",
    "chars": 8985,
    "preview": "\"\"\"Example: Loading and Managing Plugins\n\nThis example demonstrates plugin loading and lifecycle management in the SDK:\n"
  },
  {
    "path": "examples/05_skills_and_plugins/03_managing_installed_skills/main.py",
    "chars": 5486,
    "preview": "\"\"\"Example: Installing and Managing Skills\n\nThis example demonstrates installed skill lifecycle operations in the SDK:\n\n"
  },
  {
    "path": "openhands-agent-server/AGENTS.md",
    "chars": 9990,
    "preview": "# openhands-agent-server\n\nSee the [project root AGENTS.md](../AGENTS.md) for repository-wide policies and workflows.\n\n##"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/README.md",
    "chars": 8965,
    "preview": "# OpenHands Agent Server\n\nThe OpenHands Agent Server is a minimal REST API and WebSocket server that provides a programm"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/__main__.py",
    "chars": 9348,
    "preview": "import argparse\nimport atexit\nimport faulthandler\nimport importlib\nimport os\nimport signal\nimport sys\nfrom types import "
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/_secrets_exposure.py",
    "chars": 4320,
    "preview": "\"\"\"Shared helpers for the ``X-Expose-Secrets`` flow used by settings and profiles.\"\"\"\n\nfrom collections.abc import Itera"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/agent-server.spec",
    "chars": 6425,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\n\"\"\"\nPyInstaller spec for OpenHands Agent Server with PEP 420 (implicit namespace)"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/api.py",
    "chars": 20157,
    "preview": "import asyncio\nimport os\nimport tempfile\nimport traceback\nfrom collections.abc import AsyncIterator, Sequence\nfrom conte"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/auth_router.py",
    "chars": 6708,
    "preview": "\"\"\"Workspace static-server cookie auth endpoints.\n\nBrowsers cannot attach custom headers to ``<iframe src>``, ``<img src"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/bash_router.py",
    "chars": 3704,
    "preview": "\"\"\"Bash router for OpenHands SDK.\"\"\"\n\nimport logging\nfrom datetime import datetime\nfrom typing import Annotated, Literal"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/bash_service.py",
    "chars": 15299,
    "preview": "import asyncio\nimport glob\nimport json\nimport os\nimport signal\nfrom dataclasses import dataclass, field\nfrom datetime im"
  },
  {
    "path": "openhands-agent-server/openhands/agent_server/cloud_proxy_router.py",
    "chars": 7177,
    "preview": "\"\"\"Cloud proxy router.\n\nForwards browser-originated requests to a configured cloud SaaS host so the\nGUI never has to mak"
  }
]

// ... and 997 more files (download for full content)

About this extraction

This page contains the full source code of the OpenHands/software-agent-sdk GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1197 files (9.6 MB), approximately 2.6M tokens, and a symbol index with 10864 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!