android-midscene-automation 0.2.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""MCP Tool: mobile_diagnose.
|
|
16
|
+
|
|
17
|
+
Runs the same readiness probes as the web console's device wizard and the
|
|
18
|
+
``artemis doctor`` CLI, adds the MCP-host probe, and renders the outcome as an
|
|
19
|
+
action list an AI coding assistant can execute without reading Artemis source.
|
|
20
|
+
On request it also boots an installed emulator in the background, verifies the
|
|
21
|
+
configured API keys against the providers, and drives the attached device end
|
|
22
|
+
to end (screenshot + UI hierarchy) to prove a task could actually start.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import subprocess
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
import re
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from mcp_server.base import mcp
|
|
36
|
+
from mcp_server.utils import env_utils
|
|
37
|
+
from artemis.core.diagnostics import readiness_engine
|
|
38
|
+
from artemis.core.diagnostics.device_smoke import smoke_test_device
|
|
39
|
+
from artemis.core.diagnostics.readiness import (
|
|
40
|
+
adb_keys_corrupted,
|
|
41
|
+
base_verdict,
|
|
42
|
+
collect_readiness,
|
|
43
|
+
sort_by_fix_order,
|
|
44
|
+
)
|
|
45
|
+
from artemis.core.diagnostics.schema import ProbeResult, ProbeStatus, SystemReadinessReport
|
|
46
|
+
from artemis.runtime import DeviceExecutionLock, trace_store
|
|
47
|
+
from artemis.runtime.helper_manager import helper_manager
|
|
48
|
+
from artemis.utils.credentials_validator import validate_api_key
|
|
49
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
50
|
+
|
|
51
|
+
logger = get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
#: Upper bound for one diagnosis phase. The ADB probe shells out without its
|
|
54
|
+
#: own deadline, so a wedged ADB server must surface as a verdict, not a hung
|
|
55
|
+
#: tool. The optional extras (credential verification, device smoke test) run
|
|
56
|
+
#: concurrently under a second budget of the same size.
|
|
57
|
+
DIAGNOSIS_TIMEOUT_SECONDS = 40.0
|
|
58
|
+
|
|
59
|
+
#: Per-provider budget for a live API key verification request.
|
|
60
|
+
CREDENTIAL_CHECK_TIMEOUT_SECONDS = 12.0
|
|
61
|
+
|
|
62
|
+
#: Providers the credentials probe lists by their *endpoint URL* rather than a
|
|
63
|
+
#: key (``OPENAI_BASE_URL`` / ``OLLAMA_BASE_URL`` / ``VLLM_BASE_URL``). Their
|
|
64
|
+
#: "raw_key" is that URL: verification must call the endpoint, not treat the
|
|
65
|
+
#: URL as an API key.
|
|
66
|
+
_ENDPOINT_PROVIDERS = frozenset({"custom", "ollama", "vllm"})
|
|
67
|
+
|
|
68
|
+
#: Metadata keys that carry credential material. The console UI needs them to
|
|
69
|
+
#: prefill its settings form; an MCP caller is an LLM context and must never
|
|
70
|
+
#: see them.
|
|
71
|
+
_SECRET_METADATA_KEYS = frozenset(
|
|
72
|
+
{"raw_key", "key", "api_keys", "current_key", "current_gemini_key"}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
_ERROR_MARKERS = ("traceback", "error", "exception", "failed", "critical")
|
|
76
|
+
|
|
77
|
+
#: rich/typer write colour escapes into the tee'd stderr log; the model must not see them.
|
|
78
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[A-Za-z]")
|
|
79
|
+
|
|
80
|
+
#: Emulator lifecycle stages during which a second launch must not be issued.
|
|
81
|
+
_LAUNCH_IN_PROGRESS = frozenset({"starting", "waiting_for_adb", "booting"})
|
|
82
|
+
|
|
83
|
+
#: ``emulator -avd X`` / ``<sdk>/emulator/emulator.exe -avd X``: a foreground
|
|
84
|
+
#: process that never returns; it must not be handed to the assistant as a
|
|
85
|
+
#: shell command.
|
|
86
|
+
_EMULATOR_LAUNCH_RE = re.compile(r"emulator(?:\.exe)?\s+-avd\s+(\S+)", re.IGNORECASE)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------------- #
|
|
90
|
+
# Rendering helpers
|
|
91
|
+
# --------------------------------------------------------------------------- #
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _scrub(value: Any) -> Any:
|
|
95
|
+
"""Drop secrets and bulky inventories from probe metadata before it reaches the model."""
|
|
96
|
+
if isinstance(value, dict):
|
|
97
|
+
cleaned: dict[str, Any] = {}
|
|
98
|
+
for key, item in value.items():
|
|
99
|
+
if key in _SECRET_METADATA_KEYS:
|
|
100
|
+
continue
|
|
101
|
+
if key == "installed_packages" and isinstance(item, list):
|
|
102
|
+
cleaned["installed_package_count"] = len(item)
|
|
103
|
+
continue
|
|
104
|
+
cleaned[key] = _scrub(item)
|
|
105
|
+
return cleaned
|
|
106
|
+
if isinstance(value, list):
|
|
107
|
+
return [_scrub(item) for item in value]
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _find(results: list[ProbeResult], probe_id: str) -> ProbeResult | None:
|
|
112
|
+
return next((r for r in results if r.id == probe_id), None)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _render_check(result: ProbeResult) -> dict[str, Any]:
|
|
116
|
+
"""Passing checks are one line each; only problems carry detail, fixes and facts."""
|
|
117
|
+
check: dict[str, Any] = {
|
|
118
|
+
"id": result.id,
|
|
119
|
+
"title": result.title,
|
|
120
|
+
"status": result.status.value,
|
|
121
|
+
"required": result.is_blocker,
|
|
122
|
+
"summary": result.summary,
|
|
123
|
+
}
|
|
124
|
+
if result.status is ProbeStatus.PASS:
|
|
125
|
+
return check
|
|
126
|
+
check["category"] = result.category.value
|
|
127
|
+
check["detail"] = result.description
|
|
128
|
+
check["fix"] = [
|
|
129
|
+
{"type": action.action_type, "label": action.label, "payload": action.payload}
|
|
130
|
+
for action in result.actions
|
|
131
|
+
]
|
|
132
|
+
check["facts"] = _scrub(result.metadata)
|
|
133
|
+
return check
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _compact_host(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
137
|
+
daemon = metadata.get("daemon") or {}
|
|
138
|
+
host: dict[str, Any] = {
|
|
139
|
+
key: metadata.get(key)
|
|
140
|
+
for key in (
|
|
141
|
+
"server_python",
|
|
142
|
+
"runner_python",
|
|
143
|
+
"interpreter_matches_venv",
|
|
144
|
+
"env_file",
|
|
145
|
+
"env_file_exists",
|
|
146
|
+
"traces_dir",
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
if "mcp_client" in metadata:
|
|
150
|
+
host["mcp_client"] = _scrub(metadata["mcp_client"])
|
|
151
|
+
host["daemon"] = {
|
|
152
|
+
key: daemon.get(key)
|
|
153
|
+
for key in ("port", "reachable", "port_held_by_other_process", "log_path")
|
|
154
|
+
}
|
|
155
|
+
return host
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _compact_device(report: SystemReadinessReport) -> dict[str, Any] | None:
|
|
159
|
+
device = report.active_device
|
|
160
|
+
if device is None:
|
|
161
|
+
return None
|
|
162
|
+
return {
|
|
163
|
+
"serial": device.serial,
|
|
164
|
+
"state": device.state,
|
|
165
|
+
"model": device.model,
|
|
166
|
+
"android_version": device.android_version,
|
|
167
|
+
"is_locked": device.is_locked,
|
|
168
|
+
"is_emulator": device.is_emulator,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _compact_emulator(raw: dict[str, Any]) -> dict[str, Any]:
|
|
173
|
+
status = raw.get("status")
|
|
174
|
+
return {
|
|
175
|
+
"avd_name": raw.get("avd_name"),
|
|
176
|
+
"status": getattr(status, "value", status),
|
|
177
|
+
"stage_message": raw.get("stage_message"),
|
|
178
|
+
"error": raw.get("error"),
|
|
179
|
+
"serial": raw.get("serial"),
|
|
180
|
+
"elapsed_seconds": raw.get("elapsed_seconds"),
|
|
181
|
+
"progress_percent": raw.get("progress_percent"),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _launch_in_progress(emulator: dict[str, Any] | None) -> bool:
|
|
186
|
+
return emulator is not None and emulator.get("status") in _LAUNCH_IN_PROGRESS
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _launch_guidance(avd: str) -> str:
|
|
190
|
+
return f' Guidance: Call mobile_diagnose(launch_avd="{avd}") to start it in the background.'
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _render_action_lines(action: Any, *, installed_avds: list[str]) -> list[str]:
|
|
194
|
+
"""Turn one ProbeAction into next_steps lines the assistant can act on verbatim.
|
|
195
|
+
|
|
196
|
+
Commands chained with ``&&`` become consecutive ``Run:`` lines (PowerShell
|
|
197
|
+
5.1 cannot parse ``&&``); ``emulator -avd`` never becomes a ``Run:`` line
|
|
198
|
+
because it would block the assistant's shell forever.
|
|
199
|
+
"""
|
|
200
|
+
payload = action.payload.strip()
|
|
201
|
+
if action.action_type == "link":
|
|
202
|
+
return [f" Docs: {payload}"]
|
|
203
|
+
if action.action_type != "command":
|
|
204
|
+
return [f" Guidance: {payload}"]
|
|
205
|
+
|
|
206
|
+
lines: list[str] = []
|
|
207
|
+
for part in payload.split("&&"):
|
|
208
|
+
command = part.strip()
|
|
209
|
+
if not command or command == "artemis init":
|
|
210
|
+
continue # interactive; covered by the credential / config guidance
|
|
211
|
+
match = _EMULATOR_LAUNCH_RE.search(command)
|
|
212
|
+
if match is None:
|
|
213
|
+
lines.append(f" Run: {command}")
|
|
214
|
+
continue
|
|
215
|
+
avd = match.group(1)
|
|
216
|
+
if avd in installed_avds:
|
|
217
|
+
lines.append(_launch_guidance(avd))
|
|
218
|
+
else:
|
|
219
|
+
lines.append(
|
|
220
|
+
f" Guidance: No AVD named '{avd}' is installed. Create one in Android Studio's "
|
|
221
|
+
"Device Manager (or ask the user to connect a phone), then call "
|
|
222
|
+
'mobile_diagnose(launch_avd="<name>").'
|
|
223
|
+
)
|
|
224
|
+
return lines
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _credential_steps(env_file: str | None) -> list[str]:
|
|
228
|
+
location = env_file or "the project's .env file"
|
|
229
|
+
return [
|
|
230
|
+
" Ask the user to add a provider key (for example GEMINI_API_KEY=...) to "
|
|
231
|
+
f"{location}, or to the MCP server's env block in the IDE's MCP config. "
|
|
232
|
+
"Never ask them to paste the key into the chat.",
|
|
233
|
+
" `artemis init` is interactive and cannot run from a tool call; edit the env file instead.",
|
|
234
|
+
" Keys are read when the server starts: restart the MCP server (reload MCP servers in "
|
|
235
|
+
"the IDE) after adding one.",
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# --------------------------------------------------------------------------- #
|
|
240
|
+
# Device lock / task state
|
|
241
|
+
# --------------------------------------------------------------------------- #
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _normalize_serial(value: Any) -> str:
|
|
245
|
+
return re.sub(r"[^a-zA-Z0-9_-]", "_", str(value or "").strip())
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _task_state() -> dict[str, list[dict[str, Any]]]:
|
|
249
|
+
"""Compact view of who holds or waits for a device, from the cross-process lock files."""
|
|
250
|
+
active: list[dict[str, Any]] = []
|
|
251
|
+
queued: list[dict[str, Any]] = []
|
|
252
|
+
try:
|
|
253
|
+
seen: set[Any] = set()
|
|
254
|
+
for owner in DeviceExecutionLock.get_active_owners().values():
|
|
255
|
+
token = getattr(owner, "token", id(owner))
|
|
256
|
+
if token in seen:
|
|
257
|
+
continue
|
|
258
|
+
seen.add(token)
|
|
259
|
+
active.append(
|
|
260
|
+
{
|
|
261
|
+
"device": getattr(owner, "device_id", None),
|
|
262
|
+
"session_id": getattr(owner, "session_id", None),
|
|
263
|
+
"pid": getattr(owner, "pid", None),
|
|
264
|
+
"description": getattr(owner, "description", None),
|
|
265
|
+
"ingress": getattr(owner, "ingress", None),
|
|
266
|
+
"started_at": getattr(owner, "acquired_at", None),
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
logger.debug(f"Could not read active device locks: {exc}")
|
|
271
|
+
try:
|
|
272
|
+
for ticket in DeviceExecutionLock.get_queued_tasks():
|
|
273
|
+
queued.append(
|
|
274
|
+
{
|
|
275
|
+
"device": ticket.get("device_id"),
|
|
276
|
+
"session_id": ticket.get("session_id"),
|
|
277
|
+
"pid": ticket.get("pid"),
|
|
278
|
+
"description": ticket.get("goal"),
|
|
279
|
+
"ingress": ticket.get("ingress"),
|
|
280
|
+
"created_at": ticket.get("created_at"),
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
except Exception as exc:
|
|
284
|
+
logger.debug(f"Could not read queued device tasks: {exc}")
|
|
285
|
+
return {"active": active, "queued": queued}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _busy_step(tasks: dict[str, list[dict[str, Any]]], serial: str | None) -> str | None:
|
|
289
|
+
if not serial:
|
|
290
|
+
return None
|
|
291
|
+
wanted = _normalize_serial(serial)
|
|
292
|
+
for entry in tasks.get("active", []):
|
|
293
|
+
if _normalize_serial(entry.get("device")) != wanted:
|
|
294
|
+
continue
|
|
295
|
+
task_id = entry.get("session_id") or "unknown"
|
|
296
|
+
description = entry.get("description") or "no description"
|
|
297
|
+
return (
|
|
298
|
+
f"Device '{serial}' is busy with task {task_id} (pid {entry.get('pid')}): "
|
|
299
|
+
f"{description}. Wait for it to finish, or stop it with "
|
|
300
|
+
f'mobile_manage_task(action="stop", trace_id="{task_id}"). A new task on this '
|
|
301
|
+
"device queues behind it."
|
|
302
|
+
)
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# --------------------------------------------------------------------------- #
|
|
307
|
+
# Emulator launch
|
|
308
|
+
# --------------------------------------------------------------------------- #
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _emulator_status() -> dict[str, Any] | None:
|
|
312
|
+
"""Current background launch state, or None when nothing was ever launched."""
|
|
313
|
+
try:
|
|
314
|
+
state = _compact_emulator(readiness_engine.get_emulator_status())
|
|
315
|
+
except Exception:
|
|
316
|
+
return None
|
|
317
|
+
if state.get("status") in (None, "idle"):
|
|
318
|
+
return None
|
|
319
|
+
return state
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def _handle_launch_avd(
|
|
323
|
+
name: str, adb_result: ProbeResult | None
|
|
324
|
+
) -> tuple[dict[str, Any] | None, list[str]]:
|
|
325
|
+
"""Validate the AVD name and start it once; returns (emulator state, next_steps lines)."""
|
|
326
|
+
installed = list((adb_result.metadata.get("installed_avds") if adb_result else None) or [])
|
|
327
|
+
current = _emulator_status()
|
|
328
|
+
|
|
329
|
+
if name not in installed:
|
|
330
|
+
listing = ", ".join(installed) or "none"
|
|
331
|
+
advice = (
|
|
332
|
+
"Pass one of them as launch_avd."
|
|
333
|
+
if installed
|
|
334
|
+
else "Create one in Android Studio's Device Manager or ask the user to connect a phone."
|
|
335
|
+
)
|
|
336
|
+
return current, [
|
|
337
|
+
f"[REQUIRED] AVD '{name}' is not installed, so nothing was launched. Installed AVDs: "
|
|
338
|
+
f"{listing}. {advice}"
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
if _launch_in_progress(current):
|
|
342
|
+
assert current is not None
|
|
343
|
+
return current, [
|
|
344
|
+
f"Emulator '{current.get('avd_name')}' is already {current.get('status')} "
|
|
345
|
+
f"({current.get('stage_message')}; {current.get('elapsed_seconds') or 0}s elapsed); "
|
|
346
|
+
"a second launch was not started. Call mobile_diagnose again in about 60 seconds "
|
|
347
|
+
"without launch_avd."
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
state = _compact_emulator(await readiness_engine.launch_emulator(name))
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
return current, [
|
|
354
|
+
f"[REQUIRED] Launching AVD '{name}' raised {exc.__class__.__name__}: {exc}"
|
|
355
|
+
]
|
|
356
|
+
if state.get("status") == "failed":
|
|
357
|
+
return state, [
|
|
358
|
+
f"[REQUIRED] Emulator '{name}' failed to launch: "
|
|
359
|
+
f"{state.get('error') or state.get('stage_message')}."
|
|
360
|
+
]
|
|
361
|
+
return state, [
|
|
362
|
+
f"Emulator '{name}' is starting in the background ({state.get('stage_message')}). Boot "
|
|
363
|
+
"takes 1-3 minutes: call mobile_diagnose again in about 60 seconds, and do not pass "
|
|
364
|
+
"launch_avd again while the emulator status is starting, waiting_for_adb or booting."
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# --------------------------------------------------------------------------- #
|
|
369
|
+
# Credential verification
|
|
370
|
+
# --------------------------------------------------------------------------- #
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def _verify_credentials(cred_result: ProbeResult | None) -> list[dict[str, Any]]:
|
|
374
|
+
"""Check every configured key against its provider; the keys never leave this function."""
|
|
375
|
+
if cred_result is None:
|
|
376
|
+
return []
|
|
377
|
+
metadata = cred_result.metadata
|
|
378
|
+
api_keys = metadata.get("api_keys") or {}
|
|
379
|
+
targets: list[tuple[str, str, str]] = []
|
|
380
|
+
for entry in metadata.get("providers") or []:
|
|
381
|
+
provider = str(entry.get("provider") or "").strip()
|
|
382
|
+
raw_key = entry.get("raw_key") or api_keys.get(provider) or ""
|
|
383
|
+
if not provider or not raw_key:
|
|
384
|
+
continue
|
|
385
|
+
targets.append((provider, str(entry.get("label") or provider), str(raw_key)))
|
|
386
|
+
if api_keys.get("ocr"):
|
|
387
|
+
targets.append(("ocr", "Vision OCR", str(api_keys["ocr"])))
|
|
388
|
+
if not targets:
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
outcomes = await asyncio.gather(
|
|
392
|
+
*(
|
|
393
|
+
validate_api_key(
|
|
394
|
+
provider,
|
|
395
|
+
"EMPTY",
|
|
396
|
+
base_url=key,
|
|
397
|
+
timeout=CREDENTIAL_CHECK_TIMEOUT_SECONDS,
|
|
398
|
+
)
|
|
399
|
+
if provider in _ENDPOINT_PROVIDERS
|
|
400
|
+
else validate_api_key(provider, key, timeout=CREDENTIAL_CHECK_TIMEOUT_SECONDS)
|
|
401
|
+
for provider, _label, key in targets
|
|
402
|
+
),
|
|
403
|
+
return_exceptions=True,
|
|
404
|
+
)
|
|
405
|
+
verified: list[dict[str, Any]] = []
|
|
406
|
+
for (provider, label, key), outcome in zip(targets, outcomes):
|
|
407
|
+
if isinstance(outcome, BaseException):
|
|
408
|
+
valid, message = False, f"verification raised {outcome.__class__.__name__}: {outcome}"
|
|
409
|
+
else:
|
|
410
|
+
valid, message = bool(outcome[0]), str(outcome[1])
|
|
411
|
+
verified.append(
|
|
412
|
+
{
|
|
413
|
+
"provider": provider,
|
|
414
|
+
"label": label,
|
|
415
|
+
"valid": valid,
|
|
416
|
+
"message": message.replace(key, "***"),
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
return verified
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _primary_credential(credentials: list[dict[str, Any]] | None) -> dict[str, Any] | None:
|
|
423
|
+
"""The provider the task runner will use: first configured LLM provider (OCR is auxiliary)."""
|
|
424
|
+
for entry in credentials or []:
|
|
425
|
+
if entry.get("provider") != "ocr":
|
|
426
|
+
return entry
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _credential_verification_steps(
|
|
431
|
+
credentials: list[dict[str, Any]] | None, env_file: str | None
|
|
432
|
+
) -> tuple[list[str], bool]:
|
|
433
|
+
"""Lines for failed verifications; second item says whether a restart is needed."""
|
|
434
|
+
if not credentials:
|
|
435
|
+
return [], False
|
|
436
|
+
steps: list[str] = []
|
|
437
|
+
primary = _primary_credential(credentials)
|
|
438
|
+
needs_restart = False
|
|
439
|
+
for entry in credentials:
|
|
440
|
+
if entry.get("valid"):
|
|
441
|
+
continue
|
|
442
|
+
tag = "REQUIRED" if entry is primary else "OPTIONAL"
|
|
443
|
+
steps.append(
|
|
444
|
+
f"[{tag}] {entry.get('label')} API key ({entry.get('provider')}) failed live "
|
|
445
|
+
f"verification: {entry.get('message')}"
|
|
446
|
+
)
|
|
447
|
+
if entry is primary:
|
|
448
|
+
steps.extend(_credential_steps(env_file))
|
|
449
|
+
needs_restart = True
|
|
450
|
+
return steps, needs_restart
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# --------------------------------------------------------------------------- #
|
|
454
|
+
# Device smoke test
|
|
455
|
+
# --------------------------------------------------------------------------- #
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _probe_unavailable(serial: str | None, error: str) -> dict[str, Any]:
|
|
459
|
+
return {
|
|
460
|
+
"ok": False,
|
|
461
|
+
"serial": serial,
|
|
462
|
+
"elapsed_seconds": 0.0,
|
|
463
|
+
"screenshot_bytes": None,
|
|
464
|
+
"element_count": None,
|
|
465
|
+
"error": error,
|
|
466
|
+
"fix": [],
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
async def _device_smoke_test(device_serial: str | None) -> dict[str, Any]:
|
|
471
|
+
"""Drive the device end to end (screenshot + UI hierarchy) through the shared smoke test."""
|
|
472
|
+
return await smoke_test_device(device_serial)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
async def _run_device_probe(
|
|
476
|
+
adb_result: ProbeResult | None, requested_device: str | None
|
|
477
|
+
) -> dict[str, Any]:
|
|
478
|
+
devices = (adb_result.metadata.get("devices") if adb_result else None) or []
|
|
479
|
+
ready = [str(d.get("serial")) for d in devices if d.get("state") == "device"]
|
|
480
|
+
if requested_device and requested_device not in ready:
|
|
481
|
+
return _probe_unavailable(
|
|
482
|
+
requested_device,
|
|
483
|
+
f"requested device '{requested_device}' is not attached and authorized; nothing to probe",
|
|
484
|
+
)
|
|
485
|
+
if not ready:
|
|
486
|
+
return _probe_unavailable(None, "no authorized device is attached; nothing to probe")
|
|
487
|
+
serial = requested_device or (ready[0] if len(ready) == 1 else None)
|
|
488
|
+
try:
|
|
489
|
+
return await _device_smoke_test(serial)
|
|
490
|
+
except Exception as exc:
|
|
491
|
+
return _probe_unavailable(
|
|
492
|
+
serial, f"device smoke test raised {exc.__class__.__name__}: {exc}"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _device_probe_steps(device_probe: dict[str, Any] | None) -> list[str]:
|
|
497
|
+
if device_probe is None or device_probe.get("ok"):
|
|
498
|
+
return []
|
|
499
|
+
serial = device_probe.get("serial") or "the attached device"
|
|
500
|
+
steps = [f"[REQUIRED] Device probe failed on {serial}: {device_probe.get('error')}"]
|
|
501
|
+
steps.extend(f" Guidance: {fix}" for fix in device_probe.get("fix") or [])
|
|
502
|
+
return steps
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# --------------------------------------------------------------------------- #
|
|
506
|
+
# Accessibility helper (UI hierarchy backend)
|
|
507
|
+
# --------------------------------------------------------------------------- #
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _hierarchy_backend() -> str:
|
|
511
|
+
try:
|
|
512
|
+
from artemis.clients.screen_client_factory import resolve_backend
|
|
513
|
+
|
|
514
|
+
return resolve_backend().value
|
|
515
|
+
except (ImportError, ValueError):
|
|
516
|
+
return "auto"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _helper_target(adb_result: ProbeResult | None, requested_device: str | None) -> str | None:
|
|
520
|
+
"""The one ready device whose helper state is worth reporting, or None."""
|
|
521
|
+
devices = (adb_result.metadata.get("devices") if adb_result else None) or []
|
|
522
|
+
ready = [str(d.get("serial")) for d in devices if d.get("state") == "device"]
|
|
523
|
+
if requested_device:
|
|
524
|
+
return requested_device if requested_device in ready else None
|
|
525
|
+
return ready[0] if len(ready) == 1 else None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _helper_status(serial: str) -> dict[str, Any]:
|
|
529
|
+
try:
|
|
530
|
+
status = helper_manager.status(serial)
|
|
531
|
+
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
|
|
532
|
+
return {"serial": serial, "error": f"{exc.__class__.__name__}: {exc}"}
|
|
533
|
+
status["serial"] = serial
|
|
534
|
+
status["backend"] = _hierarchy_backend()
|
|
535
|
+
return status
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _helper_needs_provision(status: dict[str, Any] | None) -> bool:
|
|
539
|
+
if not status or status.get("error"):
|
|
540
|
+
return False
|
|
541
|
+
return not status.get("installed") or bool(status.get("outdated")) or not status.get("enabled")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _helper_fix(
|
|
545
|
+
adb_result: ProbeResult | None, requested_device: str | None
|
|
546
|
+
) -> dict[str, Any] | None:
|
|
547
|
+
"""Install / upgrade / enable the helper on the idle target device (attempt_fix only)."""
|
|
548
|
+
serial = _helper_target(adb_result, requested_device)
|
|
549
|
+
if serial is None or _hierarchy_backend() == "uiautomator":
|
|
550
|
+
return None
|
|
551
|
+
status = _helper_status(serial)
|
|
552
|
+
if not _helper_needs_provision(status):
|
|
553
|
+
return None
|
|
554
|
+
if not status.get("bundled_apk_present"):
|
|
555
|
+
return {
|
|
556
|
+
"fix": "install_accessibility_helper",
|
|
557
|
+
"success": False,
|
|
558
|
+
"skipped": True,
|
|
559
|
+
"message": "Skipped: the bundled ArtemisAccessibilityHelper.apk is missing from the checkout.",
|
|
560
|
+
}
|
|
561
|
+
wanted = _normalize_serial(serial)
|
|
562
|
+
if any(_normalize_serial(entry.get("device")) == wanted for entry in _task_state()["active"]):
|
|
563
|
+
return {
|
|
564
|
+
"fix": "install_accessibility_helper",
|
|
565
|
+
"success": False,
|
|
566
|
+
"skipped": True,
|
|
567
|
+
"message": f"Skipped: a task is running on {serial}; install it after the task finishes.",
|
|
568
|
+
}
|
|
569
|
+
try:
|
|
570
|
+
result = helper_manager.provision(serial)
|
|
571
|
+
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
|
|
572
|
+
return {
|
|
573
|
+
"fix": "install_accessibility_helper",
|
|
574
|
+
"success": False,
|
|
575
|
+
"skipped": False,
|
|
576
|
+
"message": f"provisioning raised {exc.__class__.__name__}: {exc}",
|
|
577
|
+
}
|
|
578
|
+
message = (
|
|
579
|
+
f"{result.action} (installed version {result.installed_version}, "
|
|
580
|
+
f"bundled {result.bundled_version}, service enabled={result.enabled})"
|
|
581
|
+
)
|
|
582
|
+
if result.error:
|
|
583
|
+
message += f": {result.error}"
|
|
584
|
+
return {
|
|
585
|
+
"fix": "install_accessibility_helper",
|
|
586
|
+
"success": result.ok,
|
|
587
|
+
"skipped": False,
|
|
588
|
+
"message": message,
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _helper_steps(status: dict[str, Any] | None, *, attempt_fix: bool) -> list[str]:
|
|
593
|
+
if not status:
|
|
594
|
+
return []
|
|
595
|
+
serial = status.get("serial") or "the device"
|
|
596
|
+
backend = status.get("backend") or "auto"
|
|
597
|
+
if backend == "uiautomator":
|
|
598
|
+
return []
|
|
599
|
+
if status.get("error"):
|
|
600
|
+
return [
|
|
601
|
+
f"[OPTIONAL] Accessibility helper state on {serial} could not be read: {status['error']}"
|
|
602
|
+
]
|
|
603
|
+
healthy = status.get("installed") and not status.get("outdated") and status.get("enabled")
|
|
604
|
+
if healthy and status.get("reachable"):
|
|
605
|
+
return []
|
|
606
|
+
if not status.get("installed"):
|
|
607
|
+
problem = "is not installed"
|
|
608
|
+
elif status.get("outdated"):
|
|
609
|
+
problem = (
|
|
610
|
+
f"is outdated (device has version {status.get('installed_version')}, "
|
|
611
|
+
f"bundled is {status.get('bundled_version')})"
|
|
612
|
+
)
|
|
613
|
+
elif not status.get("enabled"):
|
|
614
|
+
problem = "is installed but its accessibility service is disabled"
|
|
615
|
+
elif status.get("newer_than_bundled") and status.get("reachable"):
|
|
616
|
+
return [] # a newer dev build is fine; nothing to do
|
|
617
|
+
else:
|
|
618
|
+
problem = "is installed and enabled but not answering on its loopback port"
|
|
619
|
+
tag = "REQUIRED" if backend == "helper" else "OPTIONAL"
|
|
620
|
+
consequence = (
|
|
621
|
+
"tasks cannot read the UI hierarchy until it is fixed"
|
|
622
|
+
if backend == "helper"
|
|
623
|
+
else "tasks fall back to UIAutomator2 (slower dumps, conflicts with Appium/Mobly)"
|
|
624
|
+
)
|
|
625
|
+
steps = [f"[{tag}] Accessibility helper on {serial} {problem}; {consequence}."]
|
|
626
|
+
if not status.get("bundled_apk_present"):
|
|
627
|
+
steps.append(
|
|
628
|
+
" Guidance: the bundled APK is missing; build it with "
|
|
629
|
+
"packages/artemis-accessibility-helper/build_apk.sh."
|
|
630
|
+
)
|
|
631
|
+
return steps
|
|
632
|
+
if problem.startswith("is installed and enabled"):
|
|
633
|
+
steps.append(
|
|
634
|
+
" Guidance: unlock the phone; if the helper still does not answer, reinstall it with "
|
|
635
|
+
"the command below (add --force)."
|
|
636
|
+
)
|
|
637
|
+
steps.append(f" Run: uv run artemis helper install --serial {serial} --force")
|
|
638
|
+
return steps
|
|
639
|
+
if not attempt_fix:
|
|
640
|
+
steps.append(
|
|
641
|
+
" Guidance: call mobile_diagnose(attempt_fix=true) to install and enable it "
|
|
642
|
+
"without touching the phone, or run the command below."
|
|
643
|
+
)
|
|
644
|
+
steps.append(f" Run: uv run artemis helper install --serial {serial}")
|
|
645
|
+
if problem.startswith("is installed but"):
|
|
646
|
+
# Enabling from adb was already attempted once by whoever installed it;
|
|
647
|
+
# on ROMs that reject it only a person can flip the switch.
|
|
648
|
+
from artemis.runtime.helper_manager import MANUAL_ENABLE_PATH
|
|
649
|
+
|
|
650
|
+
steps.append(
|
|
651
|
+
" Guidance: if the command reports that this device rejected enabling the "
|
|
652
|
+
f"service, ask the user to turn it on by hand: {MANUAL_ENABLE_PATH} "
|
|
653
|
+
"(the command opens that settings screen on the phone)."
|
|
654
|
+
)
|
|
655
|
+
return steps
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# --------------------------------------------------------------------------- #
|
|
659
|
+
# next_steps
|
|
660
|
+
# --------------------------------------------------------------------------- #
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _device_steps(
|
|
664
|
+
adb_result: ProbeResult | None,
|
|
665
|
+
*,
|
|
666
|
+
attempt_fix: bool,
|
|
667
|
+
requested_device: str | None,
|
|
668
|
+
emulator: dict[str, Any] | None,
|
|
669
|
+
launch_requested: bool,
|
|
670
|
+
tasks: dict[str, list[dict[str, Any]]],
|
|
671
|
+
) -> list[str]:
|
|
672
|
+
steps: list[str] = []
|
|
673
|
+
if adb_result is None or not adb_result.metadata.get("installed"):
|
|
674
|
+
return steps
|
|
675
|
+
devices = adb_result.metadata.get("devices") or []
|
|
676
|
+
ready = [d for d in devices if d.get("state") == "device"]
|
|
677
|
+
keys = adb_result.metadata.get("adb_keys") or {}
|
|
678
|
+
installed_avds = [str(a) for a in adb_result.metadata.get("installed_avds") or []]
|
|
679
|
+
in_progress = _launch_in_progress(emulator)
|
|
680
|
+
|
|
681
|
+
if requested_device and requested_device not in {d.get("serial") for d in devices}:
|
|
682
|
+
steps.append(
|
|
683
|
+
f"[REQUIRED] Requested device '{requested_device}' is not attached. Attached: "
|
|
684
|
+
+ (", ".join(f"{d.get('serial')} ({d.get('state')})" for d in devices) or "none")
|
|
685
|
+
+ ". Ask the user to connect it or pick another serial."
|
|
686
|
+
)
|
|
687
|
+
elif requested_device and not any(d.get("serial") == requested_device for d in ready):
|
|
688
|
+
steps.append(
|
|
689
|
+
f"[REQUIRED] Requested device '{requested_device}' is attached but not authorized "
|
|
690
|
+
"(state is not 'device'). Ask the user to accept the USB debugging prompt on it."
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
target = requested_device or (str(ready[0].get("serial")) if len(ready) == 1 else None)
|
|
694
|
+
busy = _busy_step(tasks, target)
|
|
695
|
+
if busy:
|
|
696
|
+
steps.append(busy)
|
|
697
|
+
|
|
698
|
+
if not ready and not in_progress and not launch_requested and installed_avds:
|
|
699
|
+
steps.append(
|
|
700
|
+
f'No device is ready. Call mobile_diagnose(launch_avd="{installed_avds[0]}") to boot '
|
|
701
|
+
f"an installed emulator in the background (installed: {', '.join(installed_avds)}), "
|
|
702
|
+
"or ask the user to connect a phone."
|
|
703
|
+
)
|
|
704
|
+
if len(ready) > 1 and not requested_device:
|
|
705
|
+
steps.append(
|
|
706
|
+
"Several devices are ready: ask the user which serial to use and pass it as "
|
|
707
|
+
"device_serial. Ready: " + ", ".join(str(d.get("serial")) for d in ready) + "."
|
|
708
|
+
)
|
|
709
|
+
if (
|
|
710
|
+
not attempt_fix
|
|
711
|
+
and not in_progress
|
|
712
|
+
and not launch_requested
|
|
713
|
+
and (keys.get("is_corrupted") or not ready)
|
|
714
|
+
):
|
|
715
|
+
steps.append(
|
|
716
|
+
"Call mobile_diagnose(attempt_fix=true) to let Artemis heal corrupted ADB keys and "
|
|
717
|
+
"restart the ADB server before asking the user to do anything manually."
|
|
718
|
+
)
|
|
719
|
+
return steps
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _next_steps(
|
|
723
|
+
results: list[ProbeResult],
|
|
724
|
+
*,
|
|
725
|
+
attempt_fix: bool,
|
|
726
|
+
fixes_applied: list[dict[str, Any]],
|
|
727
|
+
requested_device: str | None,
|
|
728
|
+
env_file: str | None,
|
|
729
|
+
emulator: dict[str, Any] | None,
|
|
730
|
+
launch_steps: list[str],
|
|
731
|
+
credentials: list[dict[str, Any]] | None,
|
|
732
|
+
tasks: dict[str, list[dict[str, Any]]],
|
|
733
|
+
device_probe: dict[str, Any] | None,
|
|
734
|
+
accessibility_helper: dict[str, Any] | None = None,
|
|
735
|
+
) -> list[str]:
|
|
736
|
+
steps: list[str] = []
|
|
737
|
+
needs_restart = False
|
|
738
|
+
adb_result = _find(results, "android_adb")
|
|
739
|
+
installed_avds = [
|
|
740
|
+
str(a) for a in (adb_result.metadata.get("installed_avds") if adb_result else None) or []
|
|
741
|
+
]
|
|
742
|
+
in_progress = _launch_in_progress(emulator)
|
|
743
|
+
credential_lines, credential_restart = _credential_verification_steps(credentials, env_file)
|
|
744
|
+
|
|
745
|
+
for result in sort_by_fix_order(results):
|
|
746
|
+
if result.status is not ProbeStatus.PASS:
|
|
747
|
+
tag = "REQUIRED" if result.is_blocker else "OPTIONAL"
|
|
748
|
+
steps.append(f"[{tag}] {result.title}: {result.description}")
|
|
749
|
+
|
|
750
|
+
if result.id == "gemini_api_key" and result.status is ProbeStatus.FAIL:
|
|
751
|
+
steps.extend(_credential_steps(env_file))
|
|
752
|
+
needs_restart = True
|
|
753
|
+
elif result.id == "android_adb" and in_progress and emulator is not None:
|
|
754
|
+
devices = result.metadata.get("devices") or []
|
|
755
|
+
if not any(d.get("state") == "device" for d in devices):
|
|
756
|
+
steps.append(
|
|
757
|
+
f" Guidance: Emulator '{emulator.get('avd_name')}' is "
|
|
758
|
+
f"{emulator.get('status')} ({emulator.get('stage_message')}; "
|
|
759
|
+
f"{emulator.get('elapsed_seconds') or 0}s elapsed). Wait about 60 "
|
|
760
|
+
"seconds and call mobile_diagnose again instead of asking the user to "
|
|
761
|
+
"connect a device."
|
|
762
|
+
)
|
|
763
|
+
else:
|
|
764
|
+
for action in result.actions:
|
|
765
|
+
steps.extend(_render_action_lines(action, installed_avds=installed_avds))
|
|
766
|
+
else:
|
|
767
|
+
for action in result.actions:
|
|
768
|
+
steps.extend(_render_action_lines(action, installed_avds=installed_avds))
|
|
769
|
+
if result.id in ("system_config", "integration_host", "python_runtime"):
|
|
770
|
+
needs_restart = True
|
|
771
|
+
|
|
772
|
+
if result.id == "gemini_api_key":
|
|
773
|
+
steps.extend(credential_lines)
|
|
774
|
+
needs_restart = needs_restart or credential_restart
|
|
775
|
+
|
|
776
|
+
steps.extend(
|
|
777
|
+
_device_steps(
|
|
778
|
+
adb_result,
|
|
779
|
+
attempt_fix=attempt_fix,
|
|
780
|
+
requested_device=requested_device,
|
|
781
|
+
emulator=emulator,
|
|
782
|
+
launch_requested=bool(launch_steps),
|
|
783
|
+
tasks=tasks,
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
steps.extend(launch_steps)
|
|
787
|
+
steps.extend(_device_probe_steps(device_probe))
|
|
788
|
+
steps.extend(_helper_steps(accessibility_helper, attempt_fix=attempt_fix))
|
|
789
|
+
|
|
790
|
+
for fix in fixes_applied:
|
|
791
|
+
outcome = "applied" if fix.get("success") else "did not help"
|
|
792
|
+
steps.append(f"Auto-fix {fix['fix']} {outcome}: {fix.get('message', '')}".rstrip())
|
|
793
|
+
|
|
794
|
+
if not steps:
|
|
795
|
+
steps.append(
|
|
796
|
+
"Environment is ready. Use mobile_run_task to delegate a task; pass device_serial "
|
|
797
|
+
"when more than one device is attached."
|
|
798
|
+
)
|
|
799
|
+
return steps
|
|
800
|
+
|
|
801
|
+
if needs_restart:
|
|
802
|
+
steps.append(
|
|
803
|
+
"After changing the env file, config, or MCP server config: restart the MCP server "
|
|
804
|
+
"(reload MCP servers in the IDE), then call mobile_diagnose again."
|
|
805
|
+
)
|
|
806
|
+
else:
|
|
807
|
+
steps.append("After each fix, call mobile_diagnose again until verdict is 'ready'.")
|
|
808
|
+
return steps
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# --------------------------------------------------------------------------- #
|
|
812
|
+
# Logs
|
|
813
|
+
# --------------------------------------------------------------------------- #
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _tail_errors(path: str, *, max_lines: int = 12, max_chars: int = 300) -> list[str]:
|
|
817
|
+
"""Return the last error-looking lines of a log without loading the whole file."""
|
|
818
|
+
try:
|
|
819
|
+
size = os.path.getsize(path)
|
|
820
|
+
with open(path, "rb") as handle:
|
|
821
|
+
handle.seek(max(0, size - 64 * 1024))
|
|
822
|
+
text = handle.read().decode("utf-8", errors="replace")
|
|
823
|
+
except OSError:
|
|
824
|
+
return []
|
|
825
|
+
matches = [
|
|
826
|
+
_ANSI_RE.sub("", line).strip()[:max_chars]
|
|
827
|
+
for line in text.splitlines()
|
|
828
|
+
if any(marker in line.lower() for marker in _ERROR_MARKERS)
|
|
829
|
+
]
|
|
830
|
+
return matches[-max_lines:]
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _last_failed_task(limit: int = 30) -> dict[str, Any] | None:
|
|
834
|
+
"""Locate the most recent failed trace and surface its error tail inline."""
|
|
835
|
+
traces_dir = Path(trace_store.TRACES_DIR)
|
|
836
|
+
try:
|
|
837
|
+
candidates = [p for p in traces_dir.iterdir() if p.is_dir()]
|
|
838
|
+
except OSError:
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
def _mtime(path: Path) -> float:
|
|
842
|
+
try:
|
|
843
|
+
return path.stat().st_mtime
|
|
844
|
+
except OSError:
|
|
845
|
+
return 0.0
|
|
846
|
+
|
|
847
|
+
candidates.sort(key=_mtime, reverse=True)
|
|
848
|
+
for trace_dir in candidates[:limit]:
|
|
849
|
+
status = trace_store.read_status(trace_dir.name)
|
|
850
|
+
if not status or status.get("status") != "failed":
|
|
851
|
+
continue
|
|
852
|
+
stderr_log = trace_store.get_trace_stderr_log_path(trace_dir.name)
|
|
853
|
+
return {
|
|
854
|
+
"trace_id": trace_dir.name,
|
|
855
|
+
"error": (status.get("error") or "")[:600],
|
|
856
|
+
"device_serial": status.get("device_serial"),
|
|
857
|
+
"stderr_log": stderr_log,
|
|
858
|
+
"stdout_log": trace_store.get_trace_stdout_log_path(trace_dir.name),
|
|
859
|
+
"recent_errors": _tail_errors(stderr_log),
|
|
860
|
+
}
|
|
861
|
+
return None
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _collect_logs(host_metadata: dict[str, Any]) -> dict[str, Any]:
|
|
865
|
+
root = env_utils.get_project_root()
|
|
866
|
+
stderr_log = os.path.join(root, "scratch", "mcp_stderr.log")
|
|
867
|
+
return {
|
|
868
|
+
"mcp_stderr_log": stderr_log,
|
|
869
|
+
"mcp_launch_log": os.path.join(root, "scratch", "mcp_launch_debug.log"),
|
|
870
|
+
"daemon_log": (host_metadata.get("daemon") or {}).get("log_path"),
|
|
871
|
+
"recent_mcp_errors": _tail_errors(stderr_log),
|
|
872
|
+
"last_failed_task": _last_failed_task(),
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
# --------------------------------------------------------------------------- #
|
|
877
|
+
# Self-heal
|
|
878
|
+
# --------------------------------------------------------------------------- #
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _public_fix_result(name: str, result: dict[str, Any]) -> dict[str, Any]:
|
|
882
|
+
return {
|
|
883
|
+
"fix": name,
|
|
884
|
+
"success": bool(result.get("success")),
|
|
885
|
+
"message": str(result.get("message") or ""),
|
|
886
|
+
"skipped": bool(result.get("skipped")),
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _cleanup_stale_locks() -> dict[str, Any] | None:
|
|
891
|
+
"""Drop lock files / queue tickets of dead processes; reported only when something changed."""
|
|
892
|
+
try:
|
|
893
|
+
removed = int(DeviceExecutionLock.cleanup_stale_locks())
|
|
894
|
+
except Exception as exc:
|
|
895
|
+
return {
|
|
896
|
+
"fix": "cleanup_stale_locks",
|
|
897
|
+
"success": False,
|
|
898
|
+
"skipped": False,
|
|
899
|
+
"message": f"cleanup raised {exc.__class__.__name__}: {exc}",
|
|
900
|
+
}
|
|
901
|
+
if removed <= 0:
|
|
902
|
+
return None
|
|
903
|
+
return {
|
|
904
|
+
"fix": "cleanup_stale_locks",
|
|
905
|
+
"success": True,
|
|
906
|
+
"skipped": False,
|
|
907
|
+
"message": f"removed {removed} stale device lock(s) / queue ticket(s)",
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
async def _apply_safe_fixes(
|
|
912
|
+
report: SystemReadinessReport, requested_device: str | None = None
|
|
913
|
+
) -> list[dict[str, Any]]:
|
|
914
|
+
"""Run the self-heals the console exposes as buttons, only when they cannot disrupt a task."""
|
|
915
|
+
applied: list[dict[str, Any]] = []
|
|
916
|
+
cleanup = _cleanup_stale_locks()
|
|
917
|
+
if cleanup is not None:
|
|
918
|
+
applied.append(cleanup)
|
|
919
|
+
|
|
920
|
+
adb = next((p for p in report.probes if p.id == "android_adb"), None)
|
|
921
|
+
if adb is None or not adb.metadata.get("installed"):
|
|
922
|
+
return applied
|
|
923
|
+
|
|
924
|
+
if adb_keys_corrupted(report.probes):
|
|
925
|
+
result = await readiness_engine.heal_adb_keys()
|
|
926
|
+
applied.append(_public_fix_result("heal_adb_keys", result))
|
|
927
|
+
return applied # healing already restarted the ADB server
|
|
928
|
+
|
|
929
|
+
devices = adb.metadata.get("devices") or []
|
|
930
|
+
if any(d.get("state") == "device" for d in devices):
|
|
931
|
+
# A ready device exists; nothing to restart. Provision the accessibility
|
|
932
|
+
# helper on it when it is idle and missing, outdated or disabled.
|
|
933
|
+
helper_fix = await asyncio.to_thread(_helper_fix, adb, requested_device)
|
|
934
|
+
if helper_fix is not None:
|
|
935
|
+
applied.append(helper_fix)
|
|
936
|
+
return applied
|
|
937
|
+
if DeviceExecutionLock.get_active_owners():
|
|
938
|
+
applied.append(
|
|
939
|
+
{
|
|
940
|
+
"fix": "restart_adb_server",
|
|
941
|
+
"success": False,
|
|
942
|
+
"skipped": True,
|
|
943
|
+
"message": "Skipped: an Artemis task currently holds a device lock.",
|
|
944
|
+
}
|
|
945
|
+
)
|
|
946
|
+
return applied
|
|
947
|
+
result = await readiness_engine.restart_adb_server()
|
|
948
|
+
applied.append(_public_fix_result("restart_adb_server", result))
|
|
949
|
+
return applied
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _probes_changed(fixes_applied: list[dict[str, Any]]) -> bool:
|
|
953
|
+
"""Only ADB-side fixes can change probe results; lock cleanup does not warrant a re-run."""
|
|
954
|
+
return any(
|
|
955
|
+
fix.get("success")
|
|
956
|
+
and not fix.get("skipped")
|
|
957
|
+
and fix.get("fix") not in ("cleanup_stale_locks", "install_accessibility_helper")
|
|
958
|
+
for fix in fixes_applied
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
# --------------------------------------------------------------------------- #
|
|
963
|
+
# Collection / verdict
|
|
964
|
+
# --------------------------------------------------------------------------- #
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
async def _none() -> None:
|
|
968
|
+
return None
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
async def _run_extras(
|
|
972
|
+
results: list[ProbeResult],
|
|
973
|
+
*,
|
|
974
|
+
requested_device: str | None,
|
|
975
|
+
launch_avd: str | None,
|
|
976
|
+
verify_credentials: bool,
|
|
977
|
+
probe_device: bool,
|
|
978
|
+
) -> dict[str, Any]:
|
|
979
|
+
"""Optional, slower work: emulator launch, live key checks, device smoke test, lock state."""
|
|
980
|
+
adb_result = _find(results, "android_adb")
|
|
981
|
+
launch_steps: list[str] = []
|
|
982
|
+
if launch_avd:
|
|
983
|
+
emulator, launch_steps = await _handle_launch_avd(launch_avd, adb_result)
|
|
984
|
+
else:
|
|
985
|
+
emulator = _emulator_status()
|
|
986
|
+
|
|
987
|
+
helper_serial = _helper_target(adb_result, requested_device)
|
|
988
|
+
credentials, device_probe, accessibility_helper = await asyncio.gather(
|
|
989
|
+
_verify_credentials(_find(results, "gemini_api_key")) if verify_credentials else _none(),
|
|
990
|
+
_run_device_probe(adb_result, requested_device) if probe_device else _none(),
|
|
991
|
+
asyncio.to_thread(_helper_status, helper_serial) if helper_serial else _none(),
|
|
992
|
+
)
|
|
993
|
+
return {
|
|
994
|
+
"emulator": emulator,
|
|
995
|
+
"launch_steps": launch_steps,
|
|
996
|
+
"credentials": credentials,
|
|
997
|
+
"device_probe": device_probe,
|
|
998
|
+
"accessibility_helper": accessibility_helper,
|
|
999
|
+
"tasks": _task_state(),
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _requested_device_ready(results: list[ProbeResult], requested_device: str | None) -> bool:
|
|
1004
|
+
"""A caller-named device counts as a blocker: attached and authorized, or not ready."""
|
|
1005
|
+
if not requested_device:
|
|
1006
|
+
return True
|
|
1007
|
+
adb_result = _find(results, "android_adb")
|
|
1008
|
+
if adb_result is None:
|
|
1009
|
+
return False
|
|
1010
|
+
devices = adb_result.metadata.get("devices") or []
|
|
1011
|
+
return any(d.get("serial") == requested_device and d.get("state") == "device" for d in devices)
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _verdict(
|
|
1015
|
+
results: list[ProbeResult],
|
|
1016
|
+
*,
|
|
1017
|
+
requested_device: str | None,
|
|
1018
|
+
credentials: list[dict[str, Any]] | None,
|
|
1019
|
+
device_probe: dict[str, Any] | None,
|
|
1020
|
+
accessibility_helper: dict[str, Any] | None = None,
|
|
1021
|
+
) -> str:
|
|
1022
|
+
verdict = base_verdict(results)
|
|
1023
|
+
if verdict == "blocked" or not _requested_device_ready(results, requested_device):
|
|
1024
|
+
return "blocked"
|
|
1025
|
+
primary = _primary_credential(credentials)
|
|
1026
|
+
if primary is not None and not primary.get("valid"):
|
|
1027
|
+
return "blocked"
|
|
1028
|
+
if device_probe is not None and not device_probe.get("ok"):
|
|
1029
|
+
return "blocked"
|
|
1030
|
+
helper_backend = (accessibility_helper or {}).get("backend")
|
|
1031
|
+
if helper_backend == "helper" and not accessibility_helper.get("reachable"):
|
|
1032
|
+
return "blocked"
|
|
1033
|
+
if any(not entry.get("valid") for entry in credentials or []):
|
|
1034
|
+
return "degraded"
|
|
1035
|
+
if helper_backend == "auto" and _helper_needs_provision(accessibility_helper):
|
|
1036
|
+
return "degraded" if verdict == "ready" else verdict
|
|
1037
|
+
return verdict
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _summary(
|
|
1041
|
+
results: list[ProbeResult],
|
|
1042
|
+
verdict: str,
|
|
1043
|
+
*,
|
|
1044
|
+
credentials: list[dict[str, Any]] | None,
|
|
1045
|
+
device_probe: dict[str, Any] | None,
|
|
1046
|
+
) -> str:
|
|
1047
|
+
blockers = [r for r in results if r.is_blocker]
|
|
1048
|
+
passed = [r for r in blockers if r.status is ProbeStatus.PASS]
|
|
1049
|
+
line = f"{len(passed)}/{len(blockers)} required checks pass ({verdict})."
|
|
1050
|
+
failing = [
|
|
1051
|
+
f"{r.title}: {r.summary}"
|
|
1052
|
+
for r in sort_by_fix_order(results)
|
|
1053
|
+
if r.status is not ProbeStatus.PASS
|
|
1054
|
+
]
|
|
1055
|
+
failing.extend(
|
|
1056
|
+
f"{entry.get('label')} key verification: {entry.get('message')}"
|
|
1057
|
+
for entry in credentials or []
|
|
1058
|
+
if not entry.get("valid")
|
|
1059
|
+
)
|
|
1060
|
+
if device_probe is not None and not device_probe.get("ok"):
|
|
1061
|
+
failing.append(f"Device probe: {device_probe.get('error')}")
|
|
1062
|
+
if failing:
|
|
1063
|
+
line += " Attention: " + "; ".join(failing) + "."
|
|
1064
|
+
return line
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _timeout_response(fixes_applied: list[dict[str, Any]]) -> dict[str, Any]:
|
|
1068
|
+
return {
|
|
1069
|
+
"verdict": "blocked",
|
|
1070
|
+
"summary": (
|
|
1071
|
+
f"Diagnosis did not finish within {DIAGNOSIS_TIMEOUT_SECONDS:.0f}s; the ADB "
|
|
1072
|
+
"server, the device, or the toolchain scan is hung."
|
|
1073
|
+
),
|
|
1074
|
+
"next_steps": [
|
|
1075
|
+
"Run: adb kill-server",
|
|
1076
|
+
"Run: adb start-server",
|
|
1077
|
+
"Then call mobile_diagnose again. If it hangs again, ask the user to unplug and "
|
|
1078
|
+
"replug the device or restart the emulator.",
|
|
1079
|
+
],
|
|
1080
|
+
"checks": [],
|
|
1081
|
+
"host": {},
|
|
1082
|
+
"device": None,
|
|
1083
|
+
"emulator": _emulator_status(),
|
|
1084
|
+
"tasks": _task_state(),
|
|
1085
|
+
"credentials": None,
|
|
1086
|
+
"device_probe": None,
|
|
1087
|
+
"fixes_applied": fixes_applied,
|
|
1088
|
+
"logs": _collect_logs({}),
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
@mcp.tool()
|
|
1093
|
+
async def mobile_diagnose(
|
|
1094
|
+
attempt_fix: bool = False,
|
|
1095
|
+
device_serial: str | None = None,
|
|
1096
|
+
launch_avd: str | None = None,
|
|
1097
|
+
verify_credentials: bool = False,
|
|
1098
|
+
probe_device: bool = False,
|
|
1099
|
+
) -> dict[str, Any]:
|
|
1100
|
+
"""Diagnoses why ARTEMIS cannot run tasks from this IDE and returns the fixes.
|
|
1101
|
+
|
|
1102
|
+
Call this FIRST whenever another ARTEMIS tool errors, a task fails to
|
|
1103
|
+
start, no device is found, or the user says ARTEMIS "does not work".
|
|
1104
|
+
It checks, in fix order: Python runtime, config file, the MCP host
|
|
1105
|
+
(interpreter vs project venv, .env location, traces directory, daemon
|
|
1106
|
+
port), LLM credentials, ADB + devices (authorization, lock screen, RSA
|
|
1107
|
+
keys, emulators), and the optional video toolchain. A plain call takes a
|
|
1108
|
+
few seconds; the optional extras cost more (see Args).
|
|
1109
|
+
|
|
1110
|
+
Returns a dict:
|
|
1111
|
+
- `verdict`: "ready" | "degraded" (optional pieces missing) | "blocked".
|
|
1112
|
+
- `summary`: one line with the pass count and what needs attention.
|
|
1113
|
+
- `next_steps`: ordered instructions. `[REQUIRED]`/`[OPTIONAL]` lines
|
|
1114
|
+
name a problem; the indented lines under them are the fix. `Run:`
|
|
1115
|
+
lines are single, local, non-destructive shell commands you may
|
|
1116
|
+
execute yourself, one per line (ask before installing software).
|
|
1117
|
+
`Guidance:` lines need the user, explain a setting, or tell you
|
|
1118
|
+
which mobile_diagnose call to make next. Follow them top to bottom,
|
|
1119
|
+
then call this tool again until `verdict` is "ready".
|
|
1120
|
+
- `checks`: per-check status. Passing checks are one line (id, title,
|
|
1121
|
+
status, required, summary); failing ones add detail, fix, facts.
|
|
1122
|
+
Secrets are never included.
|
|
1123
|
+
- `host`: how the MCP server was launched: server_python,
|
|
1124
|
+
runner_python, interpreter_matches_venv, env_file, env_file_exists,
|
|
1125
|
+
traces_dir, mcp_client (when known), daemon {port, reachable,
|
|
1126
|
+
port_held_by_other_process, log_path}.
|
|
1127
|
+
- `device`: the device a task would use ({serial, state, model,
|
|
1128
|
+
android_version, is_locked, is_emulator, accessibility_helper}) or
|
|
1129
|
+
null. `accessibility_helper` describes the Artemis UI-hierarchy
|
|
1130
|
+
helper APK on that device ({installed, installed_version,
|
|
1131
|
+
bundled_version, outdated, enabled, forward_port, reachable,
|
|
1132
|
+
backend}); with backend "auto" a missing helper only degrades
|
|
1133
|
+
(UIAutomator2 fallback), with backend "helper" it blocks.
|
|
1134
|
+
- `emulator`: background emulator launch state ({avd_name, status,
|
|
1135
|
+
stage_message, error, serial, elapsed_seconds, progress_percent}) or
|
|
1136
|
+
null when nothing was launched. `status` is starting /
|
|
1137
|
+
waiting_for_adb / booting while it boots, then ready or failed.
|
|
1138
|
+
- `tasks`: {"active": [...], "queued": [...]} ARTEMIS tasks holding or
|
|
1139
|
+
waiting for a device (device, session_id, pid, description, ingress,
|
|
1140
|
+
started_at/created_at). Stop a stuck one with
|
|
1141
|
+
mobile_manage_task(action="stop", trace_id=<session_id>).
|
|
1142
|
+
- `credentials`: null unless verify_credentials; else
|
|
1143
|
+
[{provider, label, valid, message}] per configured key.
|
|
1144
|
+
- `device_probe`: null unless probe_device; else {ok, serial,
|
|
1145
|
+
elapsed_seconds, screenshot_bytes, element_count, error, fix}.
|
|
1146
|
+
- `fixes_applied`: what `attempt_fix` did ({fix, success, skipped, message}).
|
|
1147
|
+
- `logs`: paths to the MCP server logs and the daemon log, recent
|
|
1148
|
+
error lines, and the most recent failed task with its error, log
|
|
1149
|
+
paths and `recent_errors` (tail of its stderr log).
|
|
1150
|
+
|
|
1151
|
+
Args:
|
|
1152
|
+
attempt_fix: When true, applies the safe self-heals the ARTEMIS
|
|
1153
|
+
console offers: remove device locks left by dead processes,
|
|
1154
|
+
regenerate corrupted ADB RSA keys, restart the ADB server (only
|
|
1155
|
+
when no device is ready and no task holds a device), and install,
|
|
1156
|
+
upgrade or enable the Artemis accessibility helper APK on the idle
|
|
1157
|
+
target device when it is missing, outdated or disabled. Then
|
|
1158
|
+
re-runs the checks. Nothing else is changed.
|
|
1159
|
+
device_serial: Optional serial the user wants to use; the report
|
|
1160
|
+
then states explicitly whether that device is attached, authorized
|
|
1161
|
+
and idle.
|
|
1162
|
+
launch_avd: Name of an installed Android Virtual Device to boot in
|
|
1163
|
+
the background (pick it from the `next_steps` guidance or the
|
|
1164
|
+
android_adb facts `installed_avds`). Returns immediately; boot
|
|
1165
|
+
takes 1-3 minutes, so call mobile_diagnose again (without
|
|
1166
|
+
launch_avd) after about 60 seconds and watch `emulator.status`.
|
|
1167
|
+
Never pass it while the status is starting/waiting_for_adb/booting.
|
|
1168
|
+
Use it when no device is attached and an AVD exists instead of
|
|
1169
|
+
asking the user to start an emulator.
|
|
1170
|
+
verify_credentials: When true, checks every configured API key
|
|
1171
|
+
against its provider over the network (about 12 seconds worst
|
|
1172
|
+
case, keys stay on the server). Use it when tasks fail with
|
|
1173
|
+
authentication, quota or model errors although the key check
|
|
1174
|
+
passes. An invalid primary key makes the verdict "blocked".
|
|
1175
|
+
probe_device: When true, drives the attached device end to end
|
|
1176
|
+
(screenshot + UI hierarchy, about 20 seconds) to prove a task can
|
|
1177
|
+
really start. Use it when the checks pass but tasks still fail on
|
|
1178
|
+
the device, or the screen stays black. A failed probe makes the
|
|
1179
|
+
verdict "blocked" and lists the fix.
|
|
1180
|
+
"""
|
|
1181
|
+
fixes_applied: list[dict[str, Any]] = []
|
|
1182
|
+
requested_device = device_serial.strip() if device_serial and device_serial.strip() else None
|
|
1183
|
+
avd_name = launch_avd.strip() if launch_avd and launch_avd.strip() else None
|
|
1184
|
+
try:
|
|
1185
|
+
report, host = await asyncio.wait_for(
|
|
1186
|
+
collect_readiness(), timeout=DIAGNOSIS_TIMEOUT_SECONDS
|
|
1187
|
+
)
|
|
1188
|
+
if attempt_fix:
|
|
1189
|
+
fixes_applied = await asyncio.wait_for(
|
|
1190
|
+
_apply_safe_fixes(report, requested_device), timeout=DIAGNOSIS_TIMEOUT_SECONDS
|
|
1191
|
+
)
|
|
1192
|
+
if _probes_changed(fixes_applied):
|
|
1193
|
+
report, host = await asyncio.wait_for(
|
|
1194
|
+
collect_readiness(), timeout=DIAGNOSIS_TIMEOUT_SECONDS
|
|
1195
|
+
)
|
|
1196
|
+
results = [*report.probes, host]
|
|
1197
|
+
extras = await asyncio.wait_for(
|
|
1198
|
+
_run_extras(
|
|
1199
|
+
results,
|
|
1200
|
+
requested_device=requested_device,
|
|
1201
|
+
launch_avd=avd_name,
|
|
1202
|
+
verify_credentials=verify_credentials,
|
|
1203
|
+
probe_device=probe_device,
|
|
1204
|
+
),
|
|
1205
|
+
timeout=DIAGNOSIS_TIMEOUT_SECONDS,
|
|
1206
|
+
)
|
|
1207
|
+
except TimeoutError:
|
|
1208
|
+
return _timeout_response(fixes_applied)
|
|
1209
|
+
|
|
1210
|
+
credentials: list[dict[str, Any]] | None = extras["credentials"]
|
|
1211
|
+
device_probe: dict[str, Any] | None = extras["device_probe"]
|
|
1212
|
+
accessibility_helper: dict[str, Any] | None = extras.get("accessibility_helper")
|
|
1213
|
+
verdict = _verdict(
|
|
1214
|
+
results,
|
|
1215
|
+
requested_device=requested_device,
|
|
1216
|
+
credentials=credentials,
|
|
1217
|
+
device_probe=device_probe,
|
|
1218
|
+
accessibility_helper=accessibility_helper,
|
|
1219
|
+
)
|
|
1220
|
+
device = _compact_device(report)
|
|
1221
|
+
if device is not None and accessibility_helper is not None:
|
|
1222
|
+
device["accessibility_helper"] = accessibility_helper
|
|
1223
|
+
env_file = host.metadata.get("env_file")
|
|
1224
|
+
return {
|
|
1225
|
+
"verdict": verdict,
|
|
1226
|
+
"summary": _summary(results, verdict, credentials=credentials, device_probe=device_probe),
|
|
1227
|
+
"next_steps": _next_steps(
|
|
1228
|
+
results,
|
|
1229
|
+
attempt_fix=attempt_fix,
|
|
1230
|
+
fixes_applied=fixes_applied,
|
|
1231
|
+
requested_device=requested_device,
|
|
1232
|
+
env_file=env_file,
|
|
1233
|
+
emulator=extras["emulator"],
|
|
1234
|
+
launch_steps=extras["launch_steps"],
|
|
1235
|
+
credentials=credentials,
|
|
1236
|
+
tasks=extras["tasks"],
|
|
1237
|
+
device_probe=device_probe,
|
|
1238
|
+
accessibility_helper=accessibility_helper,
|
|
1239
|
+
),
|
|
1240
|
+
"checks": [_render_check(r) for r in sort_by_fix_order(results)],
|
|
1241
|
+
"host": _compact_host(host.metadata),
|
|
1242
|
+
"device": device,
|
|
1243
|
+
"emulator": extras["emulator"],
|
|
1244
|
+
"tasks": extras["tasks"],
|
|
1245
|
+
"credentials": credentials,
|
|
1246
|
+
"device_probe": device_probe,
|
|
1247
|
+
"fixes_applied": fixes_applied,
|
|
1248
|
+
"logs": _collect_logs(host.metadata),
|
|
1249
|
+
}
|