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,1240 @@
|
|
|
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
|
+
"""Universal ADB command and background task management tools for ARTEMIS.
|
|
16
|
+
|
|
17
|
+
State model
|
|
18
|
+
-----------
|
|
19
|
+
Every automation task owns one :class:`AdbTaskRegistry` (stored on its
|
|
20
|
+
:class:`~artemis.context.ArtemisContext`). The registry holds the running
|
|
21
|
+
background processes, the recently finished task logs, and the persistent
|
|
22
|
+
terminal environments. Keeping this per context matters because the MCP
|
|
23
|
+
daemon runs concurrent tasks for different devices inside one process: a
|
|
24
|
+
module-level registry would leak device A's tasks and completion notices into
|
|
25
|
+
device B's Operator prompt. A module-level default registry still exists for
|
|
26
|
+
callers that have no context (tests, ad-hoc scripts).
|
|
27
|
+
|
|
28
|
+
Hang protection
|
|
29
|
+
---------------
|
|
30
|
+
Commands are passed to ``adb shell`` as an argument, never through stdin, so
|
|
31
|
+
the remote shell exits as soon as the script does. ``stdin`` is closed
|
|
32
|
+
(``DEVNULL``) unless the caller asks for ``Interactive=True``; a command that
|
|
33
|
+
reads stdin therefore sees EOF instead of blocking forever. Anything that
|
|
34
|
+
outlives ``WaitMsBeforeAsync`` is moved to a background task with a bounded
|
|
35
|
+
log buffer, and :func:`shutdown_adb_background_tasks` kills whatever is still
|
|
36
|
+
running when the automation task ends.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import asyncio
|
|
40
|
+
from asyncio.subprocess import Process as AsyncProcess
|
|
41
|
+
from collections import deque
|
|
42
|
+
import os
|
|
43
|
+
import shlex
|
|
44
|
+
from typing import Any, Literal
|
|
45
|
+
import uuid
|
|
46
|
+
|
|
47
|
+
from langchain_core.tools import BaseTool
|
|
48
|
+
from pydantic import BaseModel, Field
|
|
49
|
+
|
|
50
|
+
from artemis.core.tool_failure import ToolFailure
|
|
51
|
+
from artemis.agents.log_analyzer.output_analyzer import TaskOutputAnalyzerNode
|
|
52
|
+
from artemis.context import ArtemisContext
|
|
53
|
+
from artemis.data_engine.trace import trace_langchain_tool
|
|
54
|
+
from artemis.drivers.base import BaseDeviceDriver
|
|
55
|
+
from artemis.tools.base import ArtemisTool
|
|
56
|
+
from artemis.tools.tool_wrapper import ToolWrapper
|
|
57
|
+
from third_party.mobile_use.controllers.platform_specific_commands_controller import (
|
|
58
|
+
get_adb_device,
|
|
59
|
+
)
|
|
60
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
61
|
+
|
|
62
|
+
logger = get_logger(__name__)
|
|
63
|
+
|
|
64
|
+
#: Maximum number of finished task records kept per registry.
|
|
65
|
+
_FINISHED_TASKS_CAP = 50
|
|
66
|
+
#: Maximum number of output lines retained for a background task.
|
|
67
|
+
_BACKGROUND_LOG_MAX_LINES = 20000
|
|
68
|
+
#: Seconds to wait for a terminated process before killing it.
|
|
69
|
+
_TERMINATE_GRACE_SECONDS = 3.0
|
|
70
|
+
|
|
71
|
+
_EXIT_CODE_MARKER = "===EXIT_CODE==="
|
|
72
|
+
_ENV_START_MARKER = "===ENV_START==="
|
|
73
|
+
|
|
74
|
+
#: Variables that every Android shell already defines. They are never
|
|
75
|
+
#: re-exported into a persistent terminal: re-exporting PATH/HOME etc. is
|
|
76
|
+
#: pointless and some of them (BOOTCLASSPATH, LD_*) are long and fragile.
|
|
77
|
+
_PERSISTENT_ENV_EXCLUDE = frozenset(
|
|
78
|
+
{
|
|
79
|
+
"PATH",
|
|
80
|
+
"HOME",
|
|
81
|
+
"HOSTNAME",
|
|
82
|
+
"USER",
|
|
83
|
+
"LOGNAME",
|
|
84
|
+
"SHELL",
|
|
85
|
+
"TERM",
|
|
86
|
+
"PWD",
|
|
87
|
+
"OLDPWD",
|
|
88
|
+
"SHLVL",
|
|
89
|
+
"_",
|
|
90
|
+
"TMPDIR",
|
|
91
|
+
"HISTFILE",
|
|
92
|
+
"LD_LIBRARY_PATH",
|
|
93
|
+
"LD_PRELOAD",
|
|
94
|
+
"EXTERNAL_STORAGE",
|
|
95
|
+
"ASEC_MOUNTPOINT",
|
|
96
|
+
"DOWNLOAD_CACHE",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
_PERSISTENT_ENV_EXCLUDE_PREFIXES = (
|
|
100
|
+
"ANDROID_",
|
|
101
|
+
"BOOTCLASSPATH",
|
|
102
|
+
"DEX2OAT",
|
|
103
|
+
"SYSTEMSERVER",
|
|
104
|
+
"STANDALONE_",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
#: Guidance shared by the tool descriptions: the commands that hang an
|
|
108
|
+
#: ``adb shell`` and how to bound them.
|
|
109
|
+
HANG_GUIDANCE = (
|
|
110
|
+
"Streaming or open-ended commands (logcat, top, getevent, screenrecord,"
|
|
111
|
+
" tail -f, an interactive shell) never return on their own: bound them"
|
|
112
|
+
" (logcat -d, top -n 1, --time-limit) or expect them to be moved to a"
|
|
113
|
+
" background task. Detach daemons explicitly with"
|
|
114
|
+
" `nohup CMD >/dev/null 2>&1 &`, otherwise the shell waits for them."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _adb_binary() -> str:
|
|
119
|
+
"""Resolves the adb executable through the shared toolchain resolver."""
|
|
120
|
+
try:
|
|
121
|
+
from artemis.toolchain import toolchain
|
|
122
|
+
|
|
123
|
+
return toolchain.find_adb()
|
|
124
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
125
|
+
return "adb"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
#: Inline display cap for command output, in characters. Beyond it the full
|
|
129
|
+
#: text is cached under a TaskId and only the tail is shown to the model.
|
|
130
|
+
_LONG_OUTPUT_CHARS = 50000
|
|
131
|
+
|
|
132
|
+
#: How many trailing lines of an over-long output stay visible inline.
|
|
133
|
+
_LONG_OUTPUT_TAIL_LINES = 200
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _is_output_long(output: str) -> bool:
|
|
137
|
+
"""Whether command output is too long to show inline.
|
|
138
|
+
|
|
139
|
+
The trigger is size alone: more than ``_LONG_OUTPUT_CHARS`` characters.
|
|
140
|
+
A line count by itself never trips it, so a 1000-line / 4 KB listing
|
|
141
|
+
(``seq 1 1000``) is shown in full instead of being cut to its tail.
|
|
142
|
+
"""
|
|
143
|
+
return len(output) > _LONG_OUTPUT_CHARS
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _format_long_output_response(task_id: str, output: str, intro: str) -> str:
|
|
147
|
+
"""Formats truncated output with guidance to use the analyzer tool."""
|
|
148
|
+
lines = output.splitlines()
|
|
149
|
+
tail = "\n".join(lines[-_LONG_OUTPUT_TAIL_LINES:])
|
|
150
|
+
return (
|
|
151
|
+
f"{intro}\nWarning: Output is too long ({len(lines)} lines,"
|
|
152
|
+
f" {len(output)} chars) and has been truncated.\n--- Last"
|
|
153
|
+
f" {_LONG_OUTPUT_TAIL_LINES} lines of output ---\n{tail}\n"
|
|
154
|
+
f"------------------------\nThe full output is kept under TaskId"
|
|
155
|
+
f" {task_id}: use analyze_task_output with TaskId {task_id} to query it."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _render_output_body(output: str) -> str:
|
|
160
|
+
"""Renders command output for the model; whitespace-only output reads as empty."""
|
|
161
|
+
return output if output.strip() else "(empty output)"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _abbreviate(text: str, limit: int = 40) -> str:
|
|
165
|
+
"""Cuts ``text`` to ``limit`` characters, marking the cut only when one happened."""
|
|
166
|
+
return text if len(text) <= limit else f"{text[:limit]}..."
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _terminal_label(terminal_id: str | None) -> str:
|
|
170
|
+
"""Renders a terminal id for the model; a task without one shows as ``-``."""
|
|
171
|
+
return terminal_id or "-"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _filter_persistent_env(env: dict[str, str]) -> dict[str, str]:
|
|
175
|
+
"""Drops the platform-provided variables from a captured shell environment."""
|
|
176
|
+
kept: dict[str, str] = {}
|
|
177
|
+
for key, value in env.items():
|
|
178
|
+
if not key or key in _PERSISTENT_ENV_EXCLUDE:
|
|
179
|
+
continue
|
|
180
|
+
if key.startswith(_PERSISTENT_ENV_EXCLUDE_PREFIXES):
|
|
181
|
+
continue
|
|
182
|
+
if not key.replace("_", "a").isalnum():
|
|
183
|
+
continue
|
|
184
|
+
kept[key] = value
|
|
185
|
+
return kept
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Registry
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class AdbTaskRegistry:
|
|
194
|
+
"""Per-task bookkeeping for background processes, finished logs and envs."""
|
|
195
|
+
|
|
196
|
+
def __init__(self) -> None:
|
|
197
|
+
self.background: dict[str, BackgroundTask] = {}
|
|
198
|
+
self.finished: dict[str, dict[str, Any]] = {}
|
|
199
|
+
self.persistent_envs: dict[str, dict[str, str]] = {}
|
|
200
|
+
|
|
201
|
+
# -- finished task cache -------------------------------------------------
|
|
202
|
+
|
|
203
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
204
|
+
def register_finished(
|
|
205
|
+
self,
|
|
206
|
+
task_id: str,
|
|
207
|
+
command: str,
|
|
208
|
+
cwd: str | None,
|
|
209
|
+
terminal_id: str | None,
|
|
210
|
+
status: str,
|
|
211
|
+
exit_code: int | None,
|
|
212
|
+
output: str,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Caches execution details for a finished task."""
|
|
215
|
+
self.finished[task_id] = {
|
|
216
|
+
"task_id": task_id,
|
|
217
|
+
"command": command,
|
|
218
|
+
"cwd": cwd,
|
|
219
|
+
"terminal_id": terminal_id,
|
|
220
|
+
"status": status,
|
|
221
|
+
"exit_code": exit_code,
|
|
222
|
+
"output": output,
|
|
223
|
+
"notified": False,
|
|
224
|
+
}
|
|
225
|
+
while len(self.finished) > _FINISHED_TASKS_CAP:
|
|
226
|
+
oldest_key = next(iter(self.finished))
|
|
227
|
+
del self.finished[oldest_key]
|
|
228
|
+
|
|
229
|
+
def get_task_info(self, task_id: str) -> dict[str, Any] | None:
|
|
230
|
+
"""Returns metadata and logs for a running or finished task, if known."""
|
|
231
|
+
if task_id in self.background:
|
|
232
|
+
t = self.background[task_id]
|
|
233
|
+
return {
|
|
234
|
+
"command": t.command,
|
|
235
|
+
"cwd": t.cwd,
|
|
236
|
+
"terminal_id": t.terminal_id,
|
|
237
|
+
"status": t.status,
|
|
238
|
+
"output": "".join(t.stdout_log),
|
|
239
|
+
}
|
|
240
|
+
return self.finished.get(task_id)
|
|
241
|
+
|
|
242
|
+
# -- prompt helpers ------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def active_task_summaries(self) -> list[dict[str, Any]]:
|
|
245
|
+
"""Compact view of running tasks for the Operator prompt."""
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
"task_id": tid,
|
|
249
|
+
"command": t.command,
|
|
250
|
+
"cwd": t.cwd,
|
|
251
|
+
"terminal_id": t.terminal_id,
|
|
252
|
+
"output_line_count": len(t.stdout_log),
|
|
253
|
+
}
|
|
254
|
+
for tid, t in self.background.items()
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
def pop_unnotified_finished(self) -> list[dict[str, Any]]:
|
|
258
|
+
"""Returns finished tasks not yet shown to the model and marks them shown."""
|
|
259
|
+
newly_finished: list[dict[str, Any]] = []
|
|
260
|
+
for tid, tinfo in self.finished.items():
|
|
261
|
+
if tinfo.get("notified", False):
|
|
262
|
+
continue
|
|
263
|
+
newly_finished.append(
|
|
264
|
+
{
|
|
265
|
+
"task_id": tid,
|
|
266
|
+
"command": tinfo.get("command", ""),
|
|
267
|
+
"status": tinfo.get("status", "completed"),
|
|
268
|
+
"output_text": tinfo.get("output", ""),
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
tinfo["notified"] = True
|
|
272
|
+
return newly_finished
|
|
273
|
+
|
|
274
|
+
# -- lifecycle -----------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
async def shutdown(self, ctx: ArtemisContext | None = None) -> int:
|
|
277
|
+
"""Terminates every running background task. Returns the number killed."""
|
|
278
|
+
tasks = list(self.background.values())
|
|
279
|
+
for task in tasks:
|
|
280
|
+
try:
|
|
281
|
+
await task.stop(ctx, status="killed")
|
|
282
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
283
|
+
logger.warning(f"Failed to stop background ADB task {task.task_id}: {e}")
|
|
284
|
+
return len(tasks)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
#: Registry used when no context is available.
|
|
288
|
+
_DEFAULT_REGISTRY = AdbTaskRegistry()
|
|
289
|
+
|
|
290
|
+
# Backward-compatibility aliases onto the default registry's dictionaries.
|
|
291
|
+
_BACKGROUND_TASKS = _DEFAULT_REGISTRY.background
|
|
292
|
+
_FINISHED_TASKS_LOGS = _DEFAULT_REGISTRY.finished
|
|
293
|
+
_PERSISTENT_ENVIRONMENTS = _DEFAULT_REGISTRY.persistent_envs
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_adb_task_registry(ctx: ArtemisContext | None) -> AdbTaskRegistry:
|
|
297
|
+
"""Returns the registry bound to ``ctx``, creating it on first use."""
|
|
298
|
+
if ctx is None:
|
|
299
|
+
return _DEFAULT_REGISTRY
|
|
300
|
+
existing = getattr(ctx, "adb_task_registry", None)
|
|
301
|
+
if isinstance(existing, AdbTaskRegistry):
|
|
302
|
+
return existing
|
|
303
|
+
registry = AdbTaskRegistry()
|
|
304
|
+
try:
|
|
305
|
+
setattr(ctx, "adb_task_registry", registry)
|
|
306
|
+
except (AttributeError, TypeError):
|
|
307
|
+
return _DEFAULT_REGISTRY
|
|
308
|
+
return registry
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def shutdown_adb_background_tasks(ctx: ArtemisContext | None) -> int:
|
|
312
|
+
"""Kills the background ADB tasks of ``ctx``. Safe to call when none exist."""
|
|
313
|
+
existing = getattr(ctx, "adb_task_registry", None) if ctx is not None else None
|
|
314
|
+
if not isinstance(existing, AdbTaskRegistry):
|
|
315
|
+
return 0
|
|
316
|
+
killed = await existing.shutdown(ctx)
|
|
317
|
+
if killed:
|
|
318
|
+
logger.info(f"Terminated {killed} background ADB task(s) at task end.")
|
|
319
|
+
return killed
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
323
|
+
def _register_finished_task(
|
|
324
|
+
task_id: str,
|
|
325
|
+
command: str,
|
|
326
|
+
cwd: str | None,
|
|
327
|
+
terminal_id: str | None,
|
|
328
|
+
status: str,
|
|
329
|
+
exit_code: int | None,
|
|
330
|
+
output: str,
|
|
331
|
+
ctx: ArtemisContext | None = None,
|
|
332
|
+
):
|
|
333
|
+
"""Caches execution details for finished tasks (registry of ``ctx``)."""
|
|
334
|
+
get_adb_task_registry(ctx).register_finished(
|
|
335
|
+
task_id, command, cwd, terminal_id, status, exit_code, output
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _get_task_info(task_id: str, ctx: ArtemisContext | None) -> dict[str, Any] | None:
|
|
340
|
+
"""Retrieves metadata and logs for a running or finished task.
|
|
341
|
+
|
|
342
|
+
Looks in the context registry, then the default registry, then the
|
|
343
|
+
persisted background task table.
|
|
344
|
+
"""
|
|
345
|
+
registry = get_adb_task_registry(ctx)
|
|
346
|
+
info = registry.get_task_info(task_id)
|
|
347
|
+
if info is None and registry is not _DEFAULT_REGISTRY:
|
|
348
|
+
info = _DEFAULT_REGISTRY.get_task_info(task_id)
|
|
349
|
+
if info is not None:
|
|
350
|
+
return info
|
|
351
|
+
if ctx and ctx.data_engine:
|
|
352
|
+
db_tasks = ctx.data_engine.get_all_background_tasks()
|
|
353
|
+
for db_t in db_tasks:
|
|
354
|
+
if db_t.get("task_id") == task_id:
|
|
355
|
+
return {
|
|
356
|
+
"command": db_t.get("summary") or "",
|
|
357
|
+
"cwd": None,
|
|
358
|
+
"terminal_id": None,
|
|
359
|
+
"status": db_t.get("status") or "completed",
|
|
360
|
+
"output": db_t.get("logs") or "",
|
|
361
|
+
}
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
# Background task
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# pylint: disable=too-many-instance-attributes
|
|
371
|
+
class BackgroundTask:
|
|
372
|
+
"""Represents an active or completing background ADB shell task."""
|
|
373
|
+
|
|
374
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
375
|
+
def __init__(
|
|
376
|
+
self,
|
|
377
|
+
task_id: str,
|
|
378
|
+
command: str,
|
|
379
|
+
process: AsyncProcess,
|
|
380
|
+
terminal_id: str | None = None,
|
|
381
|
+
cwd: str | None = None,
|
|
382
|
+
interactive: bool = False,
|
|
383
|
+
registry: AdbTaskRegistry | None = None,
|
|
384
|
+
):
|
|
385
|
+
self.task_id = task_id
|
|
386
|
+
self.command = command
|
|
387
|
+
self.process = process
|
|
388
|
+
self.terminal_id = terminal_id
|
|
389
|
+
self.cwd = cwd
|
|
390
|
+
self.interactive = interactive
|
|
391
|
+
self.registry = registry
|
|
392
|
+
self.stdout_log: deque[str] = deque(maxlen=_BACKGROUND_LOG_MAX_LINES)
|
|
393
|
+
self.status = "running"
|
|
394
|
+
self.exit_code: int | None = None
|
|
395
|
+
self.trace_id: uuid.UUID | None = None
|
|
396
|
+
self.listener_task: asyncio.Task | None = None
|
|
397
|
+
self._stop_requested = False
|
|
398
|
+
self._finalized = False
|
|
399
|
+
|
|
400
|
+
async def start(self, ctx: ArtemisContext):
|
|
401
|
+
"""Starts the background output listener task."""
|
|
402
|
+
self.listener_task = asyncio.create_task(self._listen(ctx))
|
|
403
|
+
|
|
404
|
+
async def stop(self, ctx: ArtemisContext | None, status: str = "killed") -> None:
|
|
405
|
+
"""Terminates the process (kill after a grace period) and records the result."""
|
|
406
|
+
self._stop_requested = True
|
|
407
|
+
self.status = status
|
|
408
|
+
proc = self.process
|
|
409
|
+
if getattr(proc, "returncode", None) is None:
|
|
410
|
+
try:
|
|
411
|
+
proc.terminate()
|
|
412
|
+
except (ProcessLookupError, OSError):
|
|
413
|
+
pass
|
|
414
|
+
try:
|
|
415
|
+
await asyncio.wait_for(proc.wait(), timeout=_TERMINATE_GRACE_SECONDS)
|
|
416
|
+
except TimeoutError:
|
|
417
|
+
try:
|
|
418
|
+
proc.kill()
|
|
419
|
+
except (ProcessLookupError, OSError):
|
|
420
|
+
pass
|
|
421
|
+
try:
|
|
422
|
+
await asyncio.wait_for(proc.wait(), timeout=_TERMINATE_GRACE_SECONDS)
|
|
423
|
+
except TimeoutError:
|
|
424
|
+
pass
|
|
425
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
426
|
+
# Shutdown must never raise; the exit code is read back below.
|
|
427
|
+
logger.debug(
|
|
428
|
+
f"Waiting for background ADB task {self.task_id} to exit failed: {exc}",
|
|
429
|
+
exc_info=True,
|
|
430
|
+
)
|
|
431
|
+
if self.listener_task is not None and not self.listener_task.done():
|
|
432
|
+
self.listener_task.cancel()
|
|
433
|
+
try:
|
|
434
|
+
await self.listener_task
|
|
435
|
+
except asyncio.CancelledError:
|
|
436
|
+
pass
|
|
437
|
+
self.exit_code = getattr(proc, "returncode", None)
|
|
438
|
+
self._finalize(ctx)
|
|
439
|
+
|
|
440
|
+
def _finalize(self, ctx: ArtemisContext | None) -> None:
|
|
441
|
+
"""Moves the task from the active map to the finished cache exactly once."""
|
|
442
|
+
if self._finalized:
|
|
443
|
+
return
|
|
444
|
+
self._finalized = True
|
|
445
|
+
registry = self.registry or get_adb_task_registry(ctx)
|
|
446
|
+
|
|
447
|
+
output_text = "".join(self.stdout_log)
|
|
448
|
+
_, parsed_env, _ = self.parse_output(output_text)
|
|
449
|
+
if self.terminal_id and parsed_env is not None:
|
|
450
|
+
registry.persistent_envs[self.terminal_id] = _filter_persistent_env(parsed_env)
|
|
451
|
+
logger.info(
|
|
452
|
+
"Updated persistent env for Android terminal"
|
|
453
|
+
f" '{self.terminal_id}' from background task."
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
if ctx and getattr(ctx, "data_engine", None):
|
|
457
|
+
try:
|
|
458
|
+
ctx.data_engine.unregister_background_task(self.task_id, self.status)
|
|
459
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
460
|
+
logger.debug(f"Failed to unregister background task {self.task_id}: {e}")
|
|
461
|
+
|
|
462
|
+
registry.register_finished(
|
|
463
|
+
task_id=self.task_id,
|
|
464
|
+
command=self.command,
|
|
465
|
+
cwd=self.cwd,
|
|
466
|
+
terminal_id=self.terminal_id,
|
|
467
|
+
status=self.status,
|
|
468
|
+
exit_code=self.exit_code,
|
|
469
|
+
output=output_text,
|
|
470
|
+
)
|
|
471
|
+
registry.background.pop(self.task_id, None)
|
|
472
|
+
|
|
473
|
+
async def _listen(self, ctx: ArtemisContext):
|
|
474
|
+
"""Reads process output streams and updates lifecycle status."""
|
|
475
|
+
try:
|
|
476
|
+
while True:
|
|
477
|
+
line = await self.process.stdout.readline()
|
|
478
|
+
if not line:
|
|
479
|
+
break
|
|
480
|
+
decoded = line.decode(errors="replace")
|
|
481
|
+
self.stdout_log.append(decoded)
|
|
482
|
+
if ctx and getattr(ctx, "data_engine", None) and self.trace_id:
|
|
483
|
+
ctx.data_engine.stream_output(self.trace_id, decoded)
|
|
484
|
+
|
|
485
|
+
self.exit_code = await self.process.wait()
|
|
486
|
+
if self._stop_requested:
|
|
487
|
+
return
|
|
488
|
+
_, _, script_exit = self.parse_output("".join(self.stdout_log))
|
|
489
|
+
if self.terminal_id and script_exit is not None:
|
|
490
|
+
self.exit_code = script_exit
|
|
491
|
+
self.status = "completed" if self.exit_code == 0 else "failed"
|
|
492
|
+
logger.info(
|
|
493
|
+
f"Background ADB task {self.task_id} completed with exit code {self.exit_code}"
|
|
494
|
+
)
|
|
495
|
+
except asyncio.CancelledError:
|
|
496
|
+
return
|
|
497
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
498
|
+
logger.error(f"Error in background task {self.task_id} listener: {e}")
|
|
499
|
+
self.status = "error"
|
|
500
|
+
finally:
|
|
501
|
+
if not self._stop_requested:
|
|
502
|
+
self._finalize(ctx)
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def parse_output(output: str) -> tuple[str, dict[str, str] | None, int | None]:
|
|
506
|
+
"""Separates stdout lines from the exit-code marker and exported env.
|
|
507
|
+
|
|
508
|
+
Returns ``(clean_output, env_or_None, script_exit_code_or_None)``.
|
|
509
|
+
"""
|
|
510
|
+
clean_lines: list[str] = []
|
|
511
|
+
env_lines: list[str] = []
|
|
512
|
+
exit_code: int | None = None
|
|
513
|
+
in_env = False
|
|
514
|
+
|
|
515
|
+
for line in output.splitlines():
|
|
516
|
+
if _EXIT_CODE_MARKER in line:
|
|
517
|
+
raw = line.split(_EXIT_CODE_MARKER, 1)[1].strip()
|
|
518
|
+
try:
|
|
519
|
+
exit_code = int(raw)
|
|
520
|
+
except ValueError:
|
|
521
|
+
exit_code = None
|
|
522
|
+
continue
|
|
523
|
+
if _ENV_START_MARKER in line:
|
|
524
|
+
in_env = True
|
|
525
|
+
continue
|
|
526
|
+
if in_env:
|
|
527
|
+
env_lines.append(line)
|
|
528
|
+
else:
|
|
529
|
+
clean_lines.append(line)
|
|
530
|
+
|
|
531
|
+
clean_output = "\n".join(clean_lines)
|
|
532
|
+
|
|
533
|
+
parsed_env: dict[str, str] | None = None
|
|
534
|
+
if in_env:
|
|
535
|
+
parsed_env = {}
|
|
536
|
+
for el in env_lines:
|
|
537
|
+
if "=" in el:
|
|
538
|
+
k, v = el.split("=", 1)
|
|
539
|
+
parsed_env[k] = v
|
|
540
|
+
|
|
541
|
+
return clean_output, parsed_env, exit_code
|
|
542
|
+
|
|
543
|
+
# Backwards compatibility alias
|
|
544
|
+
_parse_output = parse_output
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# ---------------------------------------------------------------------------
|
|
548
|
+
# run_adb_command
|
|
549
|
+
# ---------------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class RunAdbCommandArgs(BaseModel):
|
|
553
|
+
"""Arguments schema for executing ADB shell commands."""
|
|
554
|
+
|
|
555
|
+
CommandLine: str = Field(
|
|
556
|
+
...,
|
|
557
|
+
description=(
|
|
558
|
+
"The exact shell command line string to execute inside the Android device shell."
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
Cwd: str = Field(
|
|
562
|
+
default="/data/local/tmp",
|
|
563
|
+
description="The directory on the Android device to run the command in.",
|
|
564
|
+
)
|
|
565
|
+
RunPersistent: bool = Field(
|
|
566
|
+
default=False,
|
|
567
|
+
description=(
|
|
568
|
+
"Set to true to run this command in a persistent terminal session"
|
|
569
|
+
" that preserves environment variables between invocations on the"
|
|
570
|
+
" phone."
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
RequestedTerminalID: str | None = Field(
|
|
574
|
+
default=None,
|
|
575
|
+
description=(
|
|
576
|
+
"Optional ID of a persistent terminal to reuse. Reuses environment"
|
|
577
|
+
" variables if provided."
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
WaitMsBeforeAsync: int = Field(
|
|
581
|
+
default=5000,
|
|
582
|
+
description=(
|
|
583
|
+
"Number of milliseconds to wait for the command to finish"
|
|
584
|
+
" synchronously before sending it to the background."
|
|
585
|
+
),
|
|
586
|
+
)
|
|
587
|
+
Interactive: bool = Field(
|
|
588
|
+
default=False,
|
|
589
|
+
description=(
|
|
590
|
+
"Keep stdin open so 'manage_task send_input' can feed the command"
|
|
591
|
+
" later. Default false: stdin is closed and commands that read it"
|
|
592
|
+
" see EOF instead of hanging."
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _build_phone_script(
|
|
598
|
+
cmd_line: str,
|
|
599
|
+
cwd: str,
|
|
600
|
+
env_vars: dict[str, str],
|
|
601
|
+
run_persistent: bool,
|
|
602
|
+
) -> str:
|
|
603
|
+
"""Builds the script passed as the single ``adb shell`` argument."""
|
|
604
|
+
script = f"cd {shlex.quote(cwd)}\n"
|
|
605
|
+
for k, v in env_vars.items():
|
|
606
|
+
script += f"export {k}={shlex.quote(v)}\n"
|
|
607
|
+
script += f"{cmd_line}\n"
|
|
608
|
+
if run_persistent:
|
|
609
|
+
script += (
|
|
610
|
+
"_artemis_ec=$?\n"
|
|
611
|
+
f'echo "{_EXIT_CODE_MARKER}$_artemis_ec"\n'
|
|
612
|
+
f'echo "{_ENV_START_MARKER}"\n'
|
|
613
|
+
"env\n"
|
|
614
|
+
)
|
|
615
|
+
return script
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class RunAdbCommandTool(ArtemisTool):
|
|
619
|
+
"""Universal tool for executing ADB shell commands on an Android device."""
|
|
620
|
+
|
|
621
|
+
def __init__(self):
|
|
622
|
+
super().__init__(
|
|
623
|
+
name="run_adb_command",
|
|
624
|
+
description=(
|
|
625
|
+
"[SHELL] Executes a shell command directly on the Android mobile "
|
|
626
|
+
"device via ADB shell. Runs synchronously and transitions to a "
|
|
627
|
+
"background task if execution takes longer than WaitMsBeforeAsync. "
|
|
628
|
+
"Supports persistent environments (environment variables) on the "
|
|
629
|
+
f"phone across invocations. {HANG_GUIDANCE}"
|
|
630
|
+
),
|
|
631
|
+
args_schema=RunAdbCommandArgs,
|
|
632
|
+
category="system",
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements,too-many-return-statements,too-many-positional-arguments,too-many-boolean-expressions
|
|
636
|
+
async def execute(
|
|
637
|
+
self,
|
|
638
|
+
driver: BaseDeviceDriver | None = None,
|
|
639
|
+
ctx: ArtemisContext | None = None,
|
|
640
|
+
CommandLine: str | None = None,
|
|
641
|
+
Cwd: str = "/data/local/tmp",
|
|
642
|
+
RunPersistent: bool = False,
|
|
643
|
+
RequestedTerminalID: str | None = None,
|
|
644
|
+
WaitMsBeforeAsync: int = 5000,
|
|
645
|
+
Interactive: bool = False,
|
|
646
|
+
**kwargs: Any,
|
|
647
|
+
) -> str:
|
|
648
|
+
# Extract command line supporting alternative key formats
|
|
649
|
+
cmd_line = (
|
|
650
|
+
CommandLine
|
|
651
|
+
if CommandLine is not None
|
|
652
|
+
else (kwargs.get("command_line") or kwargs.get("command") or kwargs.get("cmd") or "")
|
|
653
|
+
)
|
|
654
|
+
cwd = Cwd or kwargs.get("cwd") or "/data/local/tmp"
|
|
655
|
+
run_persistent = (
|
|
656
|
+
RunPersistent if RunPersistent is not False else kwargs.get("run_persistent", False)
|
|
657
|
+
)
|
|
658
|
+
requested_terminal_id = (
|
|
659
|
+
RequestedTerminalID
|
|
660
|
+
if RequestedTerminalID is not None
|
|
661
|
+
else kwargs.get("requested_terminal_id")
|
|
662
|
+
)
|
|
663
|
+
wait_ms = (
|
|
664
|
+
WaitMsBeforeAsync
|
|
665
|
+
if WaitMsBeforeAsync != 5000
|
|
666
|
+
else kwargs.get("wait_ms_before_async", WaitMsBeforeAsync)
|
|
667
|
+
)
|
|
668
|
+
interactive = bool(Interactive or kwargs.get("interactive", False))
|
|
669
|
+
wait_seconds = max(float(wait_ms) / 1000.0, 0.05)
|
|
670
|
+
|
|
671
|
+
# Fallback to driver's execute_shell if no full ArtemisContext is available
|
|
672
|
+
if (
|
|
673
|
+
(ctx is None or not hasattr(ctx, "device") or not ctx.device)
|
|
674
|
+
and driver is not None
|
|
675
|
+
and hasattr(driver, "execute_shell")
|
|
676
|
+
and not run_persistent
|
|
677
|
+
):
|
|
678
|
+
return await driver.execute_shell(cmd_line, timeout_seconds=wait_seconds)
|
|
679
|
+
|
|
680
|
+
registry = get_adb_task_registry(ctx)
|
|
681
|
+
|
|
682
|
+
device_id = None
|
|
683
|
+
if ctx and hasattr(ctx, "device") and ctx.device:
|
|
684
|
+
device_id = getattr(ctx.device, "device_id", None)
|
|
685
|
+
elif driver and hasattr(driver, "device_id"):
|
|
686
|
+
device_id = getattr(driver, "device_id", None)
|
|
687
|
+
if not device_id:
|
|
688
|
+
device_id = "default_device"
|
|
689
|
+
|
|
690
|
+
# Prepare environment
|
|
691
|
+
terminal_id = None
|
|
692
|
+
run_env = os.environ.copy() # Local host env to run adb command
|
|
693
|
+
android_env_vars: dict[str, str] = {}
|
|
694
|
+
|
|
695
|
+
if run_persistent:
|
|
696
|
+
terminal_id = requested_terminal_id or f"term_{uuid.uuid4().hex[:8]}"
|
|
697
|
+
android_env_vars = registry.persistent_envs.setdefault(terminal_id, {})
|
|
698
|
+
|
|
699
|
+
phone_script = _build_phone_script(cmd_line, cwd, android_env_vars, run_persistent)
|
|
700
|
+
|
|
701
|
+
# In Cloud Mode or when adb client is virtualized: no host subprocess,
|
|
702
|
+
# therefore no background handoff. The wait window is a hard timeout.
|
|
703
|
+
adb_client = getattr(ctx, "adb_client", None) if ctx else None
|
|
704
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") == "1" or (
|
|
705
|
+
adb_client is not None and hasattr(adb_client, "_bridge")
|
|
706
|
+
):
|
|
707
|
+
try:
|
|
708
|
+
device = get_adb_device(ctx)
|
|
709
|
+
output = await asyncio.wait_for(
|
|
710
|
+
asyncio.to_thread(device.shell, phone_script),
|
|
711
|
+
timeout=wait_seconds,
|
|
712
|
+
)
|
|
713
|
+
except TimeoutError:
|
|
714
|
+
return ToolFailure(
|
|
715
|
+
f"Error: Command did not finish within {wait_seconds:.1f}s."
|
|
716
|
+
" Background tasks are not available on this device"
|
|
717
|
+
" connection; bound the command (logcat -d, top -n 1,"
|
|
718
|
+
" --time-limit) and retry."
|
|
719
|
+
)
|
|
720
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
721
|
+
logger.error(f"Failed to execute virtual adb shell for '{cmd_line}': {e}")
|
|
722
|
+
return ToolFailure(f"Failed to execute adb command: {e}")
|
|
723
|
+
clean_output, new_env, script_exit = BackgroundTask.parse_output(str(output))
|
|
724
|
+
if run_persistent and new_env is not None:
|
|
725
|
+
registry.persistent_envs[terminal_id] = _filter_persistent_env(new_env)
|
|
726
|
+
intro = "ADB command completed"
|
|
727
|
+
if script_exit is not None:
|
|
728
|
+
intro += f" with exit code {script_exit}"
|
|
729
|
+
intro += "."
|
|
730
|
+
if run_persistent:
|
|
731
|
+
intro += f" TerminalID: {terminal_id}."
|
|
732
|
+
return f"{intro}\nOutput:\n{_render_output_body(clean_output)}"
|
|
733
|
+
|
|
734
|
+
# We spawn the host subprocess: adb -s <serial> shell <script>.
|
|
735
|
+
# The script travels as an argument so the remote shell exits with it;
|
|
736
|
+
# stdin is closed unless the caller wants to feed input later.
|
|
737
|
+
try:
|
|
738
|
+
process = await asyncio.create_subprocess_exec(
|
|
739
|
+
_adb_binary(),
|
|
740
|
+
"-s",
|
|
741
|
+
device_id,
|
|
742
|
+
"shell",
|
|
743
|
+
phone_script,
|
|
744
|
+
cwd=os.getcwd(),
|
|
745
|
+
env=run_env,
|
|
746
|
+
stdout=asyncio.subprocess.PIPE,
|
|
747
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
748
|
+
stdin=asyncio.subprocess.PIPE if interactive else asyncio.subprocess.DEVNULL,
|
|
749
|
+
)
|
|
750
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
751
|
+
logger.error(f"Failed to spawn adb shell process for command '{cmd_line}': {e}")
|
|
752
|
+
return ToolFailure(f"Failed to spawn adb process: {e}")
|
|
753
|
+
|
|
754
|
+
stdout_data: list[str] = []
|
|
755
|
+
try:
|
|
756
|
+
|
|
757
|
+
async def read_limit():
|
|
758
|
+
while True:
|
|
759
|
+
line = await process.stdout.readline()
|
|
760
|
+
if not line:
|
|
761
|
+
break
|
|
762
|
+
stdout_data.append(line.decode(errors="replace"))
|
|
763
|
+
await process.wait()
|
|
764
|
+
|
|
765
|
+
await asyncio.wait_for(read_limit(), timeout=wait_seconds)
|
|
766
|
+
except TimeoutError:
|
|
767
|
+
return await self._hand_off_to_background(
|
|
768
|
+
ctx,
|
|
769
|
+
registry,
|
|
770
|
+
process,
|
|
771
|
+
cmd_line,
|
|
772
|
+
cwd,
|
|
773
|
+
terminal_id,
|
|
774
|
+
interactive,
|
|
775
|
+
stdout_data,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# The process completed within the wait window.
|
|
779
|
+
exit_code = process.returncode
|
|
780
|
+
output_text = "".join(stdout_data)
|
|
781
|
+
|
|
782
|
+
clean_output = output_text
|
|
783
|
+
if run_persistent:
|
|
784
|
+
clean_output, parsed_env, script_exit = BackgroundTask.parse_output(output_text)
|
|
785
|
+
if parsed_env is not None:
|
|
786
|
+
registry.persistent_envs[terminal_id] = _filter_persistent_env(parsed_env)
|
|
787
|
+
if script_exit is not None:
|
|
788
|
+
exit_code = script_exit
|
|
789
|
+
|
|
790
|
+
intro = f"ADB command completed with exit code {exit_code}."
|
|
791
|
+
if run_persistent:
|
|
792
|
+
intro += f" TerminalID: {terminal_id}."
|
|
793
|
+
|
|
794
|
+
if _is_output_long(clean_output):
|
|
795
|
+
task_id = f"task_sync_{uuid.uuid4().hex[:8]}"
|
|
796
|
+
intro += f" TaskId: {task_id}."
|
|
797
|
+
registry.register_finished(
|
|
798
|
+
task_id=task_id,
|
|
799
|
+
command=cmd_line,
|
|
800
|
+
cwd=cwd,
|
|
801
|
+
terminal_id=terminal_id,
|
|
802
|
+
status="completed" if exit_code == 0 else "failed",
|
|
803
|
+
exit_code=exit_code,
|
|
804
|
+
output=clean_output,
|
|
805
|
+
)
|
|
806
|
+
# Already shown inline; do not re-announce it as newly finished.
|
|
807
|
+
registry.finished[task_id]["notified"] = True
|
|
808
|
+
return _format_long_output_response(task_id, clean_output, intro)
|
|
809
|
+
|
|
810
|
+
return f"{intro}\nOutput:\n{_render_output_body(clean_output)}"
|
|
811
|
+
|
|
812
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
813
|
+
async def _hand_off_to_background(
|
|
814
|
+
self,
|
|
815
|
+
ctx: ArtemisContext | None,
|
|
816
|
+
registry: AdbTaskRegistry,
|
|
817
|
+
process: AsyncProcess,
|
|
818
|
+
cmd_line: str,
|
|
819
|
+
cwd: str,
|
|
820
|
+
terminal_id: str | None,
|
|
821
|
+
interactive: bool,
|
|
822
|
+
stdout_so_far: list[str],
|
|
823
|
+
) -> str:
|
|
824
|
+
"""Registers a still-running process as a background task."""
|
|
825
|
+
task_id = f"task_{process.pid}_{uuid.uuid4().hex[:6]}"
|
|
826
|
+
trace_id = uuid.uuid4()
|
|
827
|
+
|
|
828
|
+
bg_task = BackgroundTask(
|
|
829
|
+
task_id=task_id,
|
|
830
|
+
command=cmd_line,
|
|
831
|
+
process=process,
|
|
832
|
+
terminal_id=terminal_id,
|
|
833
|
+
cwd=cwd,
|
|
834
|
+
interactive=interactive,
|
|
835
|
+
registry=registry,
|
|
836
|
+
)
|
|
837
|
+
bg_task.stdout_log.extend(stdout_so_far)
|
|
838
|
+
bg_task.trace_id = trace_id
|
|
839
|
+
registry.background[task_id] = bg_task
|
|
840
|
+
|
|
841
|
+
if ctx and getattr(ctx, "data_engine", None):
|
|
842
|
+
ctx.data_engine.register_background_task(
|
|
843
|
+
task_id=task_id,
|
|
844
|
+
summary=f"ADB Command: {cmd_line[:50]}...",
|
|
845
|
+
trace_id=trace_id,
|
|
846
|
+
)
|
|
847
|
+
if stdout_so_far:
|
|
848
|
+
ctx.data_engine.stream_output(trace_id, "".join(stdout_so_far))
|
|
849
|
+
|
|
850
|
+
await bg_task.start(ctx)
|
|
851
|
+
|
|
852
|
+
response_msg = f"ADB command was sent to the background as a task.\nTaskId: {task_id}\n"
|
|
853
|
+
if terminal_id:
|
|
854
|
+
response_msg += f"TerminalID: {terminal_id}\n"
|
|
855
|
+
response_msg += "Use the 'manage_task' tool to check its status or terminate it."
|
|
856
|
+
if not interactive:
|
|
857
|
+
response_msg += " (stdin is closed; relaunch with Interactive=true to send input.)"
|
|
858
|
+
return response_msg
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
# Universal tool instance & aliases
|
|
862
|
+
run_adb_command = RunAdbCommandTool()
|
|
863
|
+
RunAdbCommand = RunAdbCommandTool
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def get_run_adb_command_tool(ctx: ArtemisContext) -> BaseTool:
|
|
867
|
+
"""Exports run_adb_command as a LangChain BaseTool."""
|
|
868
|
+
return trace_langchain_tool(run_adb_command.to_langchain_tool(ctx), ctx)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
# ---------------------------------------------------------------------------
|
|
872
|
+
# manage_task
|
|
873
|
+
# ---------------------------------------------------------------------------
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
class ManageTaskArgs(BaseModel):
|
|
877
|
+
"""Arguments schema for managing background ADB tasks."""
|
|
878
|
+
|
|
879
|
+
Action: Literal["list", "list_finished", "kill", "status", "send_input"] = Field(
|
|
880
|
+
...,
|
|
881
|
+
description=(
|
|
882
|
+
"The action to perform: 'list' (list running tasks),"
|
|
883
|
+
" 'list_finished' (list completed tasks), 'kill', 'status',"
|
|
884
|
+
" 'send_input'."
|
|
885
|
+
),
|
|
886
|
+
)
|
|
887
|
+
TaskId: str | None = Field(
|
|
888
|
+
default=None,
|
|
889
|
+
description=("The task ID to manage (required for kill, status, and send_input)."),
|
|
890
|
+
)
|
|
891
|
+
Input: str | None = Field(
|
|
892
|
+
default=None,
|
|
893
|
+
description=("The input to send to the task (required when Action is 'send_input')."),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
class ManageTaskTool(ArtemisTool):
|
|
898
|
+
"""Universal tool for managing background ADB shell tasks."""
|
|
899
|
+
|
|
900
|
+
def __init__(self):
|
|
901
|
+
super().__init__(
|
|
902
|
+
name="manage_task",
|
|
903
|
+
description=("[SHELL] Manage background ADB shell tasks launched via run_adb_command."),
|
|
904
|
+
args_schema=ManageTaskArgs,
|
|
905
|
+
category="system",
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
# pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements,too-many-return-statements,too-many-positional-arguments
|
|
909
|
+
async def execute(
|
|
910
|
+
self,
|
|
911
|
+
driver: BaseDeviceDriver | None = None, # pylint: disable=unused-argument
|
|
912
|
+
ctx: ArtemisContext | None = None,
|
|
913
|
+
Action: Literal["list", "list_finished", "kill", "status", "send_input"] = "list",
|
|
914
|
+
TaskId: str | None = None,
|
|
915
|
+
Input: str | None = None,
|
|
916
|
+
**kwargs: Any,
|
|
917
|
+
) -> str:
|
|
918
|
+
action = Action or kwargs.get("action", "list")
|
|
919
|
+
task_id = TaskId if TaskId is not None else kwargs.get("task_id")
|
|
920
|
+
input_str = Input if Input is not None else kwargs.get("input")
|
|
921
|
+
registry = get_adb_task_registry(ctx)
|
|
922
|
+
|
|
923
|
+
if action == "list":
|
|
924
|
+
active_tasks = []
|
|
925
|
+
for tid, t in registry.background.items():
|
|
926
|
+
active_tasks.append(
|
|
927
|
+
f"- {tid}: Command='{_abbreviate(t.command)}', Phone"
|
|
928
|
+
f" Cwd='{t.cwd}', TerminalID={_terminal_label(t.terminal_id)},"
|
|
929
|
+
f" Interactive={t.interactive}"
|
|
930
|
+
)
|
|
931
|
+
if not active_tasks:
|
|
932
|
+
return "No active background tasks running."
|
|
933
|
+
return "Active background tasks:\n" + "\n".join(active_tasks)
|
|
934
|
+
|
|
935
|
+
if action == "list_finished":
|
|
936
|
+
finished_tasks = []
|
|
937
|
+
for tid, t in registry.finished.items():
|
|
938
|
+
finished_tasks.append(
|
|
939
|
+
f"- {tid}: Command='{_abbreviate(t['command'])}', Status='{t['status']}'"
|
|
940
|
+
)
|
|
941
|
+
if not finished_tasks:
|
|
942
|
+
return "No recently finished tasks."
|
|
943
|
+
return f"Recently finished tasks (up to {_FINISHED_TASKS_CAP}):\n" + "\n".join(
|
|
944
|
+
finished_tasks
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
if not task_id:
|
|
948
|
+
return ToolFailure("Error: TaskId is required for status, kill, or send_input actions.")
|
|
949
|
+
|
|
950
|
+
task = registry.background.get(task_id)
|
|
951
|
+
|
|
952
|
+
if action == "status":
|
|
953
|
+
task_info = _get_task_info(task_id, ctx)
|
|
954
|
+
if not task_info:
|
|
955
|
+
return ToolFailure(f"Error: Task {task_id} not found.")
|
|
956
|
+
|
|
957
|
+
output_text = task_info.get("output", "")
|
|
958
|
+
if _ENV_START_MARKER in output_text:
|
|
959
|
+
output_text = output_text.split(_ENV_START_MARKER)[0]
|
|
960
|
+
|
|
961
|
+
intro = (
|
|
962
|
+
f"Task: {task_id}\n"
|
|
963
|
+
f"Status: {task_info.get('status')}\n"
|
|
964
|
+
f"Command: {task_info.get('command')}\n"
|
|
965
|
+
f"Cwd: {task_info.get('cwd')}\n"
|
|
966
|
+
f"TerminalID: {_terminal_label(task_info.get('terminal_id'))}"
|
|
967
|
+
)
|
|
968
|
+
exit_code = task_info.get("exit_code")
|
|
969
|
+
if exit_code is not None:
|
|
970
|
+
intro += f"\nExitCode: {exit_code}"
|
|
971
|
+
|
|
972
|
+
if _is_output_long(output_text):
|
|
973
|
+
return _format_long_output_response(task_id, output_text, intro)
|
|
974
|
+
|
|
975
|
+
return f"{intro}\nAccumulated output:\n{output_text}"
|
|
976
|
+
|
|
977
|
+
if action == "kill":
|
|
978
|
+
if not task:
|
|
979
|
+
return ToolFailure(f"Error: Task {task_id} is not active or already finished.")
|
|
980
|
+
try:
|
|
981
|
+
await task.stop(ctx, status="killed")
|
|
982
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
983
|
+
return ToolFailure(f"Failed to terminate task {task_id}: {e}")
|
|
984
|
+
if task.exit_code is not None:
|
|
985
|
+
return f"Terminated task {task_id} (exit code {task.exit_code})."
|
|
986
|
+
return f"Terminated task {task_id}."
|
|
987
|
+
|
|
988
|
+
if action == "send_input":
|
|
989
|
+
if not task:
|
|
990
|
+
return ToolFailure(f"Error: Task {task_id} is not active.")
|
|
991
|
+
if not input_str:
|
|
992
|
+
return ToolFailure("Error: Input is required for send_input action.")
|
|
993
|
+
if not task.interactive or task.process.stdin is None:
|
|
994
|
+
return ToolFailure(
|
|
995
|
+
f"Error: Task {task_id} was launched without Interactive=true, so its"
|
|
996
|
+
" stdin is closed. Relaunch the command with Interactive=true."
|
|
997
|
+
)
|
|
998
|
+
try:
|
|
999
|
+
task.process.stdin.write(input_str.encode())
|
|
1000
|
+
await task.process.stdin.drain()
|
|
1001
|
+
return f"Sent input to task {task_id}."
|
|
1002
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1003
|
+
return ToolFailure(f"Failed to send input: {e}")
|
|
1004
|
+
|
|
1005
|
+
return "Unsupported action."
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
# Universal tool instance & aliases
|
|
1009
|
+
manage_task = ManageTaskTool()
|
|
1010
|
+
ManageTask = ManageTaskTool
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def get_manage_task_tool(ctx: ArtemisContext) -> BaseTool:
|
|
1014
|
+
"""Exports manage_task as a LangChain BaseTool."""
|
|
1015
|
+
return trace_langchain_tool(manage_task.to_langchain_tool(ctx), ctx)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
# ---------------------------------------------------------------------------
|
|
1019
|
+
# run_short_adb_command (Diagnoser)
|
|
1020
|
+
# ---------------------------------------------------------------------------
|
|
1021
|
+
|
|
1022
|
+
_SHORT_COMMAND_TIMEOUT_SECONDS = 3.0
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
class RunShortAdbCommandArgs(BaseModel):
|
|
1026
|
+
"""Arguments schema for short ADB shell commands."""
|
|
1027
|
+
|
|
1028
|
+
CommandLine: str = Field(
|
|
1029
|
+
...,
|
|
1030
|
+
description=(
|
|
1031
|
+
"The exact shell command line string to execute inside the Android device shell."
|
|
1032
|
+
),
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
class RunShortAdbCommandTool(ArtemisTool):
|
|
1037
|
+
"""Universal tool for executing short, synchronous ADB shell commands."""
|
|
1038
|
+
|
|
1039
|
+
def __init__(self, name: str = "run_short_adb_command"):
|
|
1040
|
+
super().__init__(
|
|
1041
|
+
name=name,
|
|
1042
|
+
description=(
|
|
1043
|
+
"[SHELL] Executes a short, synchronous shell command directly on the Android "
|
|
1044
|
+
"mobile device via ADB shell. Returns the command output. Times out after 3 "
|
|
1045
|
+
"seconds, so bound streaming commands (logcat -d, top -n 1). Do not use this "
|
|
1046
|
+
"tool to execute actions that change the phone state such as clicking, "
|
|
1047
|
+
"swiping, or navigating back."
|
|
1048
|
+
),
|
|
1049
|
+
args_schema=RunShortAdbCommandArgs,
|
|
1050
|
+
category="system",
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
# pylint: disable=too-many-locals,too-many-return-statements
|
|
1054
|
+
async def execute(
|
|
1055
|
+
self,
|
|
1056
|
+
driver: BaseDeviceDriver | None = None,
|
|
1057
|
+
ctx: ArtemisContext | None = None,
|
|
1058
|
+
CommandLine: str | None = None,
|
|
1059
|
+
**kwargs: Any,
|
|
1060
|
+
) -> str:
|
|
1061
|
+
cmd_line = (
|
|
1062
|
+
CommandLine
|
|
1063
|
+
if CommandLine is not None
|
|
1064
|
+
else (kwargs.get("command_line") or kwargs.get("command") or kwargs.get("cmd") or "")
|
|
1065
|
+
)
|
|
1066
|
+
timeout = _SHORT_COMMAND_TIMEOUT_SECONDS
|
|
1067
|
+
|
|
1068
|
+
# Fallback to driver's execute_shell if no full ArtemisContext is available
|
|
1069
|
+
if (
|
|
1070
|
+
(ctx is None or not hasattr(ctx, "device") or not ctx.device)
|
|
1071
|
+
and driver is not None
|
|
1072
|
+
and hasattr(driver, "execute_shell")
|
|
1073
|
+
):
|
|
1074
|
+
return await driver.execute_shell(cmd_line, timeout_seconds=timeout)
|
|
1075
|
+
|
|
1076
|
+
device_id = None
|
|
1077
|
+
if ctx and hasattr(ctx, "device") and ctx.device:
|
|
1078
|
+
device_id = getattr(ctx.device, "device_id", None)
|
|
1079
|
+
elif driver and hasattr(driver, "device_id"):
|
|
1080
|
+
device_id = getattr(driver, "device_id", None)
|
|
1081
|
+
if not device_id:
|
|
1082
|
+
device_id = "default_device"
|
|
1083
|
+
|
|
1084
|
+
run_env = os.environ.copy()
|
|
1085
|
+
phone_script = f"cd /data/local/tmp\n{cmd_line}\n"
|
|
1086
|
+
|
|
1087
|
+
adb_client = getattr(ctx, "adb_client", None) if ctx else None
|
|
1088
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") == "1" or (
|
|
1089
|
+
adb_client is not None and hasattr(adb_client, "_bridge")
|
|
1090
|
+
):
|
|
1091
|
+
try:
|
|
1092
|
+
device = get_adb_device(ctx)
|
|
1093
|
+
output = await asyncio.wait_for(
|
|
1094
|
+
asyncio.to_thread(device.shell, phone_script),
|
|
1095
|
+
timeout=timeout,
|
|
1096
|
+
)
|
|
1097
|
+
return f"ADB command completed.\nOutput:\n{output}"
|
|
1098
|
+
except TimeoutError:
|
|
1099
|
+
return ToolFailure(f"Error: Command timed out after {timeout:.0f} seconds.")
|
|
1100
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1101
|
+
return ToolFailure(f"Error running command: {e}")
|
|
1102
|
+
|
|
1103
|
+
try:
|
|
1104
|
+
process = await asyncio.create_subprocess_exec(
|
|
1105
|
+
_adb_binary(),
|
|
1106
|
+
"-s",
|
|
1107
|
+
device_id,
|
|
1108
|
+
"shell",
|
|
1109
|
+
phone_script,
|
|
1110
|
+
cwd=os.getcwd(),
|
|
1111
|
+
env=run_env,
|
|
1112
|
+
stdout=asyncio.subprocess.PIPE,
|
|
1113
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
1114
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
1115
|
+
)
|
|
1116
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1117
|
+
logger.error(f"Failed to spawn adb shell process for command '{cmd_line}': {e}")
|
|
1118
|
+
return ToolFailure(f"Failed to spawn adb process: {e}")
|
|
1119
|
+
|
|
1120
|
+
try:
|
|
1121
|
+
stdout_data, _ = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
1122
|
+
exit_code = process.returncode
|
|
1123
|
+
output_text = stdout_data.decode(errors="replace")
|
|
1124
|
+
|
|
1125
|
+
return f"ADB command completed with exit code {exit_code}.\nOutput:\n{output_text}"
|
|
1126
|
+
except TimeoutError:
|
|
1127
|
+
try:
|
|
1128
|
+
process.terminate()
|
|
1129
|
+
await asyncio.wait_for(process.wait(), timeout=_TERMINATE_GRACE_SECONDS)
|
|
1130
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
1131
|
+
try:
|
|
1132
|
+
process.kill()
|
|
1133
|
+
except (ProcessLookupError, OSError):
|
|
1134
|
+
pass
|
|
1135
|
+
return ToolFailure(f"Error: Command timed out after {timeout:.0f} seconds.")
|
|
1136
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1137
|
+
return ToolFailure(f"Error running command: {e}")
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
# Universal tool instance & aliases
|
|
1141
|
+
run_short_adb_command = RunShortAdbCommandTool()
|
|
1142
|
+
RunShortAdbCommand = RunShortAdbCommandTool
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def get_run_short_adb_command_tool(ctx: ArtemisContext) -> BaseTool:
|
|
1146
|
+
"""Exports run_short_adb_command as a LangChain BaseTool."""
|
|
1147
|
+
return trace_langchain_tool(
|
|
1148
|
+
run_short_adb_command.to_langchain_tool(ctx, name="run_adb_command"),
|
|
1149
|
+
ctx,
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# Expose Wrappers
|
|
1154
|
+
run_adb_command_wrapper = ToolWrapper(
|
|
1155
|
+
tool_fn_getter=get_run_adb_command_tool,
|
|
1156
|
+
on_success_fn=lambda output: output,
|
|
1157
|
+
on_failure_fn=lambda err: f"ADB command failed: {err}",
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
manage_task_wrapper = ToolWrapper(
|
|
1161
|
+
tool_fn_getter=get_manage_task_tool,
|
|
1162
|
+
on_success_fn=lambda output: output,
|
|
1163
|
+
on_failure_fn=lambda err: f"Task management failed: {err}",
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
# ---------------------------------------------------------------------------
|
|
1168
|
+
# analyze_task_output
|
|
1169
|
+
# ---------------------------------------------------------------------------
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
class AnalyzeTaskOutputArgs(BaseModel):
|
|
1173
|
+
"""Arguments schema for output analysis requests."""
|
|
1174
|
+
|
|
1175
|
+
TaskId: str = Field(
|
|
1176
|
+
...,
|
|
1177
|
+
description="The task ID whose output you want to analyze.",
|
|
1178
|
+
)
|
|
1179
|
+
Query: str = Field(
|
|
1180
|
+
...,
|
|
1181
|
+
description=(
|
|
1182
|
+
"The question or query to ask the output analyzer about the task's full output log."
|
|
1183
|
+
),
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
class AnalyzeTaskOutputTool(ArtemisTool):
|
|
1188
|
+
"""Universal tool for analyzing the full, long output of an ADB task."""
|
|
1189
|
+
|
|
1190
|
+
def __init__(self):
|
|
1191
|
+
super().__init__(
|
|
1192
|
+
name="analyze_task_output",
|
|
1193
|
+
description=(
|
|
1194
|
+
"[ANALYSIS] Analyzes the full, long output of a completed or running ADB task. "
|
|
1195
|
+
"Use this tool when you receive a message indicating that a task's output "
|
|
1196
|
+
"is too long and was truncated."
|
|
1197
|
+
),
|
|
1198
|
+
args_schema=AnalyzeTaskOutputArgs,
|
|
1199
|
+
category="system",
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
async def execute(
|
|
1203
|
+
self,
|
|
1204
|
+
driver: BaseDeviceDriver | None = None, # pylint: disable=unused-argument
|
|
1205
|
+
ctx: ArtemisContext | None = None,
|
|
1206
|
+
TaskId: str | None = None,
|
|
1207
|
+
Query: str | None = None,
|
|
1208
|
+
**kwargs: Any,
|
|
1209
|
+
) -> str:
|
|
1210
|
+
task_id = TaskId or kwargs.get("task_id") or ""
|
|
1211
|
+
query = Query or kwargs.get("query") or ""
|
|
1212
|
+
|
|
1213
|
+
task_info = _get_task_info(task_id, ctx)
|
|
1214
|
+
if not task_info:
|
|
1215
|
+
return ToolFailure(f"Error: Task {task_id} not found.")
|
|
1216
|
+
|
|
1217
|
+
analyzer = TaskOutputAnalyzerNode(ctx)
|
|
1218
|
+
result = await analyzer.run(
|
|
1219
|
+
command=task_info.get("command", ""),
|
|
1220
|
+
output_text=task_info.get("output", ""),
|
|
1221
|
+
query=query,
|
|
1222
|
+
)
|
|
1223
|
+
return f"Analysis result for Task {task_id}:\n{result}"
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
# Universal tool instance & aliases
|
|
1227
|
+
analyze_task_output = AnalyzeTaskOutputTool()
|
|
1228
|
+
AnalyzeTaskOutput = AnalyzeTaskOutputTool
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def get_analyze_task_output_tool(ctx: ArtemisContext) -> BaseTool:
|
|
1232
|
+
"""Exports analyze_task_output as a LangChain BaseTool."""
|
|
1233
|
+
return trace_langchain_tool(analyze_task_output.to_langchain_tool(ctx), ctx)
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
analyze_task_output_wrapper = ToolWrapper(
|
|
1237
|
+
tool_fn_getter=get_analyze_task_output_tool,
|
|
1238
|
+
on_success_fn=lambda output: output,
|
|
1239
|
+
on_failure_fn=lambda err: f"Task output analysis failed: {err}",
|
|
1240
|
+
)
|