android-midscene-automation 0.2.4 → 1.1.0
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 +7 -0
- package/README.md +35 -0
- 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,254 @@
|
|
|
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
|
+
"""Cross-process cooperative cancellation for task workers.
|
|
16
|
+
|
|
17
|
+
Workers use ``CREATE_NO_WINDOW`` on Windows, so console signals cannot reach
|
|
18
|
+
them reliably. The daemon writes a cancel marker in the shared temp directory;
|
|
19
|
+
the worker polls it and cancels its asyncio task, allowing recording, trace
|
|
20
|
+
and device-lease cleanup to finish. The daemon kills unresponsive workers
|
|
21
|
+
after a deadline.
|
|
22
|
+
|
|
23
|
+
Markers use the session id, falling back to the PID and process creation time
|
|
24
|
+
to avoid cancelling a different process after PID reuse.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
import time
|
|
34
|
+
from typing import Any
|
|
35
|
+
import uuid
|
|
36
|
+
|
|
37
|
+
from artemis.config.paths import get_temp_dir
|
|
38
|
+
|
|
39
|
+
CANCEL_DIR_NAME = "cancel-requests"
|
|
40
|
+
MARKER_TTL_SECONDS = 3600.0
|
|
41
|
+
DEFAULT_POLL_SECONDS = 0.5
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cancel_request_dir() -> Path:
|
|
45
|
+
"""Directory holding cancel markers (created on demand)."""
|
|
46
|
+
directory = get_temp_dir(CANCEL_DIR_NAME)
|
|
47
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
return directory
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _session_marker(session_id: str) -> Path:
|
|
52
|
+
safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in str(session_id).strip())
|
|
53
|
+
return cancel_request_dir() / f"session-{safe}.cancel"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _pid_marker(pid: int) -> Path:
|
|
57
|
+
return cancel_request_dir() / f"pid-{int(pid)}.cancel"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_marker(path: Path, payload: dict[str, Any]) -> None:
|
|
61
|
+
tmp = path.with_name(f"{path.name}.{uuid.uuid4().hex}.tmp")
|
|
62
|
+
tmp.write_text(json.dumps(payload), encoding="utf-8")
|
|
63
|
+
os.replace(tmp, path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_marker(path: Path) -> dict[str, Any] | None:
|
|
67
|
+
try:
|
|
68
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
69
|
+
except (OSError, ValueError):
|
|
70
|
+
return None
|
|
71
|
+
return payload if isinstance(payload, dict) else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_expired(payload: dict[str, Any] | None, path: Path, now: float) -> bool:
|
|
75
|
+
requested_at = None
|
|
76
|
+
if payload is not None:
|
|
77
|
+
try:
|
|
78
|
+
requested_at = float(payload.get("requested_at"))
|
|
79
|
+
except (TypeError, ValueError):
|
|
80
|
+
requested_at = None
|
|
81
|
+
if requested_at is None:
|
|
82
|
+
try:
|
|
83
|
+
requested_at = path.stat().st_mtime
|
|
84
|
+
except OSError:
|
|
85
|
+
return True
|
|
86
|
+
return (now - requested_at) > MARKER_TTL_SECONDS
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def request_cancel(
|
|
90
|
+
*,
|
|
91
|
+
session_id: str | None = None,
|
|
92
|
+
pid: int | None = None,
|
|
93
|
+
process_created_at: float = 0.0,
|
|
94
|
+
reason: str = "",
|
|
95
|
+
) -> list[Path]:
|
|
96
|
+
"""Ask the worker identified by ``session_id`` and/or ``pid`` to cancel.
|
|
97
|
+
|
|
98
|
+
Returns the marker paths that were written. Writing is best-effort: an
|
|
99
|
+
unwritable temp directory yields an empty list and the caller falls back
|
|
100
|
+
to a hard kill.
|
|
101
|
+
"""
|
|
102
|
+
written: list[Path] = []
|
|
103
|
+
now = time.time()
|
|
104
|
+
payload = {
|
|
105
|
+
"requested_at": now,
|
|
106
|
+
"reason": reason,
|
|
107
|
+
"session_id": str(session_id) if session_id else None,
|
|
108
|
+
"pid": int(pid) if pid else None,
|
|
109
|
+
"process_created_at": float(process_created_at or 0.0),
|
|
110
|
+
}
|
|
111
|
+
targets: list[Path] = []
|
|
112
|
+
if session_id:
|
|
113
|
+
targets.append(_session_marker(str(session_id)))
|
|
114
|
+
if pid:
|
|
115
|
+
targets.append(_pid_marker(int(pid)))
|
|
116
|
+
for target in targets:
|
|
117
|
+
try:
|
|
118
|
+
_write_marker(target, payload)
|
|
119
|
+
written.append(target)
|
|
120
|
+
except OSError:
|
|
121
|
+
continue
|
|
122
|
+
purge_expired(now)
|
|
123
|
+
return written
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def is_cancel_requested(
|
|
127
|
+
*,
|
|
128
|
+
session_id: str | None = None,
|
|
129
|
+
pid: int | None = None,
|
|
130
|
+
process_created_at: float | None = None,
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Return True when a live cancel marker targets this worker.
|
|
133
|
+
|
|
134
|
+
A PID marker is honoured only when its recorded process creation time is
|
|
135
|
+
absent or matches ``process_created_at`` (within one second), which
|
|
136
|
+
protects a fresh worker from a marker left behind for a dead one.
|
|
137
|
+
"""
|
|
138
|
+
now = time.time()
|
|
139
|
+
if session_id:
|
|
140
|
+
path = _session_marker(str(session_id))
|
|
141
|
+
if path.exists():
|
|
142
|
+
payload = _read_marker(path)
|
|
143
|
+
if not _is_expired(payload, path, now):
|
|
144
|
+
return True
|
|
145
|
+
if pid:
|
|
146
|
+
path = _pid_marker(int(pid))
|
|
147
|
+
if path.exists():
|
|
148
|
+
payload = _read_marker(path)
|
|
149
|
+
if payload is None:
|
|
150
|
+
# Partially written or unreadable marker: retry on the next poll.
|
|
151
|
+
return False
|
|
152
|
+
if _is_expired(payload, path, now):
|
|
153
|
+
return False
|
|
154
|
+
try:
|
|
155
|
+
recorded = float(payload.get("process_created_at") or 0.0)
|
|
156
|
+
except (TypeError, ValueError):
|
|
157
|
+
recorded = 0.0
|
|
158
|
+
if (
|
|
159
|
+
recorded > 0
|
|
160
|
+
and process_created_at is not None
|
|
161
|
+
and process_created_at > 0
|
|
162
|
+
and abs(recorded - process_created_at) >= 1.0
|
|
163
|
+
):
|
|
164
|
+
return False
|
|
165
|
+
return True
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def clear_cancel_request(*, session_id: str | None = None, pid: int | None = None) -> None:
|
|
170
|
+
"""Remove the markers for a worker (best-effort)."""
|
|
171
|
+
targets: list[Path] = []
|
|
172
|
+
if session_id:
|
|
173
|
+
targets.append(_session_marker(str(session_id)))
|
|
174
|
+
if pid:
|
|
175
|
+
targets.append(_pid_marker(int(pid)))
|
|
176
|
+
for target in targets:
|
|
177
|
+
try:
|
|
178
|
+
target.unlink(missing_ok=True)
|
|
179
|
+
except OSError:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def purge_expired(now: float | None = None) -> int:
|
|
184
|
+
"""Delete markers older than the TTL. Returns the number removed."""
|
|
185
|
+
now = time.time() if now is None else now
|
|
186
|
+
removed = 0
|
|
187
|
+
try:
|
|
188
|
+
entries = list(cancel_request_dir().glob("*.cancel"))
|
|
189
|
+
except OSError:
|
|
190
|
+
return 0
|
|
191
|
+
for path in entries:
|
|
192
|
+
payload = _read_marker(path)
|
|
193
|
+
if _is_expired(payload, path, now):
|
|
194
|
+
try:
|
|
195
|
+
path.unlink(missing_ok=True)
|
|
196
|
+
removed += 1
|
|
197
|
+
except OSError:
|
|
198
|
+
continue
|
|
199
|
+
return removed
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def current_process_created_at() -> float:
|
|
203
|
+
"""Creation time of the current process, or 0.0 when unavailable."""
|
|
204
|
+
try:
|
|
205
|
+
import psutil
|
|
206
|
+
|
|
207
|
+
return float(psutil.Process(os.getpid()).create_time())
|
|
208
|
+
except Exception:
|
|
209
|
+
return 0.0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def watch_for_cancel_request(
|
|
213
|
+
on_cancel: Callable[[], Any],
|
|
214
|
+
*,
|
|
215
|
+
session_id: str | None = None,
|
|
216
|
+
pid: int | None = None,
|
|
217
|
+
process_created_at: float | None = None,
|
|
218
|
+
poll_seconds: float | None = None,
|
|
219
|
+
) -> bool:
|
|
220
|
+
"""Poll for a cancel marker and invoke ``on_cancel`` once it appears.
|
|
221
|
+
|
|
222
|
+
Returns True when a cancellation was delivered. The coroutine runs until
|
|
223
|
+
it delivers one or is itself cancelled, so callers should run it as a
|
|
224
|
+
background task alongside the work it guards.
|
|
225
|
+
"""
|
|
226
|
+
import asyncio
|
|
227
|
+
|
|
228
|
+
if poll_seconds is None:
|
|
229
|
+
try:
|
|
230
|
+
poll_seconds = float(os.getenv("ARTEMIS_CANCEL_POLL_SECONDS", DEFAULT_POLL_SECONDS))
|
|
231
|
+
except ValueError:
|
|
232
|
+
poll_seconds = DEFAULT_POLL_SECONDS
|
|
233
|
+
poll_seconds = max(0.05, poll_seconds)
|
|
234
|
+
if pid is None:
|
|
235
|
+
pid = os.getpid()
|
|
236
|
+
if process_created_at is None:
|
|
237
|
+
process_created_at = current_process_created_at()
|
|
238
|
+
|
|
239
|
+
while True:
|
|
240
|
+
try:
|
|
241
|
+
requested = is_cancel_requested(
|
|
242
|
+
session_id=session_id,
|
|
243
|
+
pid=pid,
|
|
244
|
+
process_created_at=process_created_at,
|
|
245
|
+
)
|
|
246
|
+
except Exception:
|
|
247
|
+
requested = False
|
|
248
|
+
if requested:
|
|
249
|
+
clear_cancel_request(session_id=session_id, pid=pid)
|
|
250
|
+
result = on_cancel()
|
|
251
|
+
if asyncio.iscoroutine(result):
|
|
252
|
+
await result
|
|
253
|
+
return True
|
|
254
|
+
await asyncio.sleep(poll_seconds)
|
|
@@ -0,0 +1,422 @@
|
|
|
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
|
+
"""Client utilities for communicating with and auto-spawning the Artemis Daemon."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from http.client import HTTPException
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any
|
|
27
|
+
import urllib.error
|
|
28
|
+
import urllib.request
|
|
29
|
+
|
|
30
|
+
from artemis.config.paths import ROOT_DIR
|
|
31
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_DAEMON_HOST = os.environ.get("ARTEMIS_DAEMON_HOST", "127.0.0.1")
|
|
36
|
+
DEFAULT_DAEMON_PORT = int(os.environ.get("ARTEMIS_DAEMON_PORT", "8000"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_standalone_forced() -> bool:
|
|
40
|
+
"""Return True if standalone/embedded execution is explicitly configured."""
|
|
41
|
+
return os.environ.get("ARTEMIS_STANDALONE", "").lower() in ("1", "true", "yes")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_daemon_running(
|
|
45
|
+
host: str = DEFAULT_DAEMON_HOST,
|
|
46
|
+
port: int = DEFAULT_DAEMON_PORT,
|
|
47
|
+
timeout: float = 0.3,
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""Fast probe to check if the Artemis Daemon HTTP server is responding."""
|
|
50
|
+
url = f"http://{host}:{port}/api/system/readiness"
|
|
51
|
+
try:
|
|
52
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Artemis-Daemon-Probe"})
|
|
53
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
54
|
+
return response.status in (200, 400, 404, 503)
|
|
55
|
+
except Exception:
|
|
56
|
+
# Fallback probe to root endpoint
|
|
57
|
+
try:
|
|
58
|
+
root_url = f"http://{host}:{port}/"
|
|
59
|
+
req = urllib.request.Request(root_url, headers={"User-Agent": "Artemis-Daemon-Probe"})
|
|
60
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
61
|
+
return response.status in (200, 301, 302, 404)
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_artemis_daemon(
|
|
67
|
+
host: str = DEFAULT_DAEMON_HOST,
|
|
68
|
+
port: int = DEFAULT_DAEMON_PORT,
|
|
69
|
+
timeout: float = 2.0,
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""Identity probe: is the process on ``host:port`` the Artemis daemon?
|
|
72
|
+
|
|
73
|
+
:func:`is_daemon_running` is a liveness probe that accepts any HTTP
|
|
74
|
+
listener (a dev server on port 8000 answers ``200`` on ``/``). Diagnostics
|
|
75
|
+
need to tell the daemon apart from a port squatter, so this asks the
|
|
76
|
+
daemon's lightweight ``/api/system/server-status`` route and requires the
|
|
77
|
+
JSON shape only Artemis serves.
|
|
78
|
+
"""
|
|
79
|
+
url = f"http://{host}:{port}/api/system/server-status"
|
|
80
|
+
try:
|
|
81
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Artemis-Daemon-Probe"})
|
|
82
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
83
|
+
if response.status != 200:
|
|
84
|
+
return False
|
|
85
|
+
payload = json.loads(response.read().decode("utf-8", errors="replace") or "null")
|
|
86
|
+
except (OSError, ValueError, HTTPException):
|
|
87
|
+
return False
|
|
88
|
+
return isinstance(payload, dict) and "current_pid" in payload and "port" in payload
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def daemon_log_path(port: int = DEFAULT_DAEMON_PORT) -> Path:
|
|
92
|
+
"""Return the log file path used by a background-spawned Daemon on the given port."""
|
|
93
|
+
from artemis.config.paths import get_app_dir
|
|
94
|
+
|
|
95
|
+
return get_app_dir() / "logs" / f"daemon-{port}.log"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def spawn_daemon(
|
|
99
|
+
host: str = DEFAULT_DAEMON_HOST,
|
|
100
|
+
port: int = DEFAULT_DAEMON_PORT,
|
|
101
|
+
) -> subprocess.Popen | None:
|
|
102
|
+
"""Silently spawn the Artemis Daemon server in the background as a detached process.
|
|
103
|
+
|
|
104
|
+
The server is always launched via ``sys.executable -m`` (never through the
|
|
105
|
+
``artemis``/``artemis-admin`` console-script shims) so the long-lived process
|
|
106
|
+
chain never holds a lock on ``Scripts/artemis.exe`` — on Windows, a resident
|
|
107
|
+
shim blocks ``uv sync``/reinstalls with "os error 32" until it exits.
|
|
108
|
+
"""
|
|
109
|
+
env = os.environ.copy()
|
|
110
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
111
|
+
env["PYTHONUTF8"] = "1"
|
|
112
|
+
|
|
113
|
+
cmd = [
|
|
114
|
+
sys.executable,
|
|
115
|
+
"-m",
|
|
116
|
+
"apps.admin_console.server",
|
|
117
|
+
"--host",
|
|
118
|
+
host,
|
|
119
|
+
"--port",
|
|
120
|
+
str(port),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Persist daemon output for post-mortem debugging (daemon mode is the
|
|
124
|
+
# default for `artemis restart`, so silent DEVNULL would hide crashes).
|
|
125
|
+
log_handle: Any = subprocess.DEVNULL
|
|
126
|
+
try:
|
|
127
|
+
log_file = daemon_log_path(port)
|
|
128
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
log_handle = open(log_file, "ab")
|
|
130
|
+
except Exception:
|
|
131
|
+
log_handle = subprocess.DEVNULL
|
|
132
|
+
|
|
133
|
+
kwargs: dict[str, Any] = {
|
|
134
|
+
"cwd": str(ROOT_DIR),
|
|
135
|
+
"env": env,
|
|
136
|
+
"stdout": log_handle,
|
|
137
|
+
"stderr": subprocess.STDOUT if log_handle is not subprocess.DEVNULL else subprocess.DEVNULL,
|
|
138
|
+
"stdin": subprocess.DEVNULL,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if sys.platform == "win32":
|
|
142
|
+
kwargs["creationflags"] = (
|
|
143
|
+
subprocess.CREATE_NEW_PROCESS_GROUP
|
|
144
|
+
| getattr(subprocess, "DETACHED_PROCESS", 0x00000008)
|
|
145
|
+
| getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
kwargs["start_new_session"] = True
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
proc = subprocess.Popen(cmd, **kwargs)
|
|
152
|
+
logger.info(
|
|
153
|
+
f"Spawned Artemis Daemon in background (PID {proc.pid}) at http://{host}:{port}"
|
|
154
|
+
)
|
|
155
|
+
return proc
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
logger.warning(f"Could not auto-spawn Artemis Daemon: {exc}")
|
|
158
|
+
return None
|
|
159
|
+
finally:
|
|
160
|
+
# The child inherited the handle; the parent's copy is no longer needed.
|
|
161
|
+
if log_handle is not subprocess.DEVNULL:
|
|
162
|
+
try:
|
|
163
|
+
log_handle.close()
|
|
164
|
+
except OSError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def ensure_daemon_running(
|
|
169
|
+
host: str = DEFAULT_DAEMON_HOST,
|
|
170
|
+
port: int = DEFAULT_DAEMON_PORT,
|
|
171
|
+
timeout: float = 2.0,
|
|
172
|
+
wait_ready: bool = False,
|
|
173
|
+
) -> tuple[bool, str | None]:
|
|
174
|
+
"""Ensure the Artemis Daemon is running, auto-spawning it if necessary.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
host: Daemon host to probe.
|
|
178
|
+
port: Daemon port to probe.
|
|
179
|
+
timeout: Maximum seconds to wait if wait_ready is True.
|
|
180
|
+
wait_ready: If True, blocks until Daemon is healthy or timeout reached.
|
|
181
|
+
If False, spawns asynchronously and returns immediately.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
(True, base_url) if Daemon is accessible.
|
|
185
|
+
(False, None) if standalone mode is forced or Daemon could not be reached.
|
|
186
|
+
"""
|
|
187
|
+
if is_standalone_forced():
|
|
188
|
+
return False, None
|
|
189
|
+
|
|
190
|
+
if is_daemon_running(host, port):
|
|
191
|
+
return True, f"http://{host}:{port}"
|
|
192
|
+
|
|
193
|
+
# Auto-spawn Daemon in background
|
|
194
|
+
spawn_daemon(host, port)
|
|
195
|
+
|
|
196
|
+
if not wait_ready:
|
|
197
|
+
return True, f"http://{host}:{port}"
|
|
198
|
+
|
|
199
|
+
# Poll until ready or timeout
|
|
200
|
+
started = time.monotonic()
|
|
201
|
+
while time.monotonic() - started < timeout:
|
|
202
|
+
time.sleep(0.1)
|
|
203
|
+
if is_daemon_running(host, port):
|
|
204
|
+
return True, f"http://{host}:{port}"
|
|
205
|
+
|
|
206
|
+
logger.warning(f"Daemon did not respond within {timeout}s. Falling back to standalone mode.")
|
|
207
|
+
return False, None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def submit_task_to_daemon(
|
|
211
|
+
goal: str,
|
|
212
|
+
*,
|
|
213
|
+
profile: str = "flash",
|
|
214
|
+
device_serial: str | None = None,
|
|
215
|
+
expected_output: str | None = None,
|
|
216
|
+
enable_outputter: bool | None = None,
|
|
217
|
+
locked_app_package: str | None = None,
|
|
218
|
+
app_path: str | None = None,
|
|
219
|
+
session_id: str | None = None,
|
|
220
|
+
ingress: str = "client",
|
|
221
|
+
conversation_id: str | None = None,
|
|
222
|
+
verification_level: str | None = None,
|
|
223
|
+
explorer_mode: str | None = None,
|
|
224
|
+
base_url: str | None = None,
|
|
225
|
+
timeout: float = 15.0,
|
|
226
|
+
) -> dict[str, Any] | None:
|
|
227
|
+
"""Submit a task to the running Daemon via REST API.
|
|
228
|
+
|
|
229
|
+
``verification_level`` ('off' | 'final' | 'checkpoints' | 'strict') and
|
|
230
|
+
``explorer_mode`` ('flash' | 'pro' | 'ultra') are the Pro-profile tuning
|
|
231
|
+
knobs of ``/api/run``; they are forwarded verbatim and ignored by Flash.
|
|
232
|
+
|
|
233
|
+
Returns the response JSON dict if successfully enqueued, or None on error.
|
|
234
|
+
"""
|
|
235
|
+
url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/run"
|
|
236
|
+
payload = {
|
|
237
|
+
"goal": goal,
|
|
238
|
+
"profile": profile,
|
|
239
|
+
"device_serial": device_serial,
|
|
240
|
+
"expected_output": expected_output,
|
|
241
|
+
"enable_outputter": enable_outputter,
|
|
242
|
+
"verification_level": verification_level,
|
|
243
|
+
"explorer_mode": explorer_mode,
|
|
244
|
+
"locked_app_package": locked_app_package,
|
|
245
|
+
"app_path": app_path,
|
|
246
|
+
"session_id": session_id,
|
|
247
|
+
"ingress": ingress,
|
|
248
|
+
"conversation_id": conversation_id,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
data = json.dumps(payload).encode("utf-8")
|
|
253
|
+
req = urllib.request.Request(
|
|
254
|
+
url,
|
|
255
|
+
data=data,
|
|
256
|
+
headers={"Content-Type": "application/json", "User-Agent": "Artemis-Client"},
|
|
257
|
+
method="POST",
|
|
258
|
+
)
|
|
259
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
260
|
+
if response.status in (200, 201):
|
|
261
|
+
return json.loads(response.read().decode("utf-8"))
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
logger.debug(f"Failed to submit task to Daemon at {url}: {exc}")
|
|
264
|
+
return None
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def stop_task_on_daemon(
|
|
269
|
+
session_id: str,
|
|
270
|
+
*,
|
|
271
|
+
base_url: str | None = None,
|
|
272
|
+
timeout: float = 5.0,
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""Request the running Artemis Daemon to stop a task by session ID."""
|
|
275
|
+
url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/stop"
|
|
276
|
+
payload = {"session_id": session_id}
|
|
277
|
+
try:
|
|
278
|
+
data = json.dumps(payload).encode("utf-8")
|
|
279
|
+
req = urllib.request.Request(
|
|
280
|
+
url,
|
|
281
|
+
data=data,
|
|
282
|
+
headers={"Content-Type": "application/json", "User-Agent": "Artemis-Client"},
|
|
283
|
+
method="POST",
|
|
284
|
+
)
|
|
285
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
286
|
+
if response.status == 200:
|
|
287
|
+
resp_data = json.loads(response.read().decode("utf-8"))
|
|
288
|
+
return resp_data.get("status") == "stopped"
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
logger.debug(f"Failed to request stop from Daemon at {url}: {exc}")
|
|
291
|
+
return False
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_daemon_session(
|
|
296
|
+
session_id: str,
|
|
297
|
+
*,
|
|
298
|
+
base_url: str | None = None,
|
|
299
|
+
timeout: float = 5.0,
|
|
300
|
+
) -> dict[str, Any] | None:
|
|
301
|
+
"""Fetch details of a single session from the Artemis Daemon."""
|
|
302
|
+
url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/sessions/{session_id}"
|
|
303
|
+
try:
|
|
304
|
+
req = urllib.request.Request(
|
|
305
|
+
url,
|
|
306
|
+
headers={"User-Agent": "Artemis-Client"},
|
|
307
|
+
method="GET",
|
|
308
|
+
)
|
|
309
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
310
|
+
if response.status == 200:
|
|
311
|
+
return json.loads(response.read().decode("utf-8"))
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
logger.debug(f"Failed to get session {session_id} from Daemon at {url}: {exc}")
|
|
314
|
+
return None
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_daemon_status(
|
|
319
|
+
*,
|
|
320
|
+
base_url: str | None = None,
|
|
321
|
+
timeout: float = 5.0,
|
|
322
|
+
) -> dict[str, Any] | None:
|
|
323
|
+
"""Fetch global scheduler and device status from the Artemis Daemon."""
|
|
324
|
+
url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/status"
|
|
325
|
+
try:
|
|
326
|
+
req = urllib.request.Request(
|
|
327
|
+
url,
|
|
328
|
+
headers={"User-Agent": "Artemis-Client"},
|
|
329
|
+
method="GET",
|
|
330
|
+
)
|
|
331
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
332
|
+
if response.status == 200:
|
|
333
|
+
return json.loads(response.read().decode("utf-8"))
|
|
334
|
+
except Exception as exc:
|
|
335
|
+
logger.debug(f"Failed to get status from Daemon at {url}: {exc}")
|
|
336
|
+
return None
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def submit_batch_to_daemon(
|
|
341
|
+
goals: list[str],
|
|
342
|
+
*,
|
|
343
|
+
profile: str = "flash",
|
|
344
|
+
device_serial: str | None = None,
|
|
345
|
+
ingress: str = "cli",
|
|
346
|
+
verification_level: str | None = None,
|
|
347
|
+
explorer_mode: str | None = None,
|
|
348
|
+
base_url: str | None = None,
|
|
349
|
+
timeout: float = 15.0,
|
|
350
|
+
) -> dict[str, Any] | None:
|
|
351
|
+
"""Submit a batch of goals to the running Daemon.
|
|
352
|
+
|
|
353
|
+
``verification_level`` / ``explorer_mode`` apply to every goal of the batch
|
|
354
|
+
(see :func:`submit_task_to_daemon`).
|
|
355
|
+
"""
|
|
356
|
+
url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/run"
|
|
357
|
+
payload = {
|
|
358
|
+
"goals": goals,
|
|
359
|
+
"profile": profile,
|
|
360
|
+
"device_serial": device_serial,
|
|
361
|
+
"ingress": ingress,
|
|
362
|
+
"verification_level": verification_level,
|
|
363
|
+
"explorer_mode": explorer_mode,
|
|
364
|
+
}
|
|
365
|
+
try:
|
|
366
|
+
data = json.dumps(payload).encode("utf-8")
|
|
367
|
+
req = urllib.request.Request(
|
|
368
|
+
url,
|
|
369
|
+
data=data,
|
|
370
|
+
headers={"Content-Type": "application/json", "User-Agent": "Artemis-Client"},
|
|
371
|
+
method="POST",
|
|
372
|
+
)
|
|
373
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
374
|
+
if response.status in (200, 201):
|
|
375
|
+
return json.loads(response.read().decode("utf-8"))
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
logger.debug(f"Failed to submit batch to Daemon at {url}: {exc}")
|
|
378
|
+
return None
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def wait_for_daemon_task(
|
|
383
|
+
session_id: str,
|
|
384
|
+
*,
|
|
385
|
+
base_url: str | None = None,
|
|
386
|
+
timeout: float = 600.0,
|
|
387
|
+
poll_interval: float = 1.0,
|
|
388
|
+
on_status_update: Any = None,
|
|
389
|
+
) -> dict[str, Any]:
|
|
390
|
+
"""Poll the Daemon until the task reaches a terminal status (completed, failed, cancelled)."""
|
|
391
|
+
started = time.monotonic()
|
|
392
|
+
last_status = None
|
|
393
|
+
|
|
394
|
+
while time.monotonic() - started < timeout:
|
|
395
|
+
sess = get_daemon_session(session_id, base_url=base_url)
|
|
396
|
+
if sess:
|
|
397
|
+
status = sess.get("status")
|
|
398
|
+
if status != last_status:
|
|
399
|
+
last_status = status
|
|
400
|
+
if callable(on_status_update):
|
|
401
|
+
on_status_update(sess)
|
|
402
|
+
|
|
403
|
+
if status in ("completed", "success", "failed", "cancelled"):
|
|
404
|
+
return sess
|
|
405
|
+
else:
|
|
406
|
+
# Check if task is queued in daemon status
|
|
407
|
+
st = get_daemon_status(base_url=base_url)
|
|
408
|
+
if st:
|
|
409
|
+
queue = st.get("queue") or []
|
|
410
|
+
is_queued = any(
|
|
411
|
+
isinstance(item, dict) and str(item.get("session_id")) == str(session_id)
|
|
412
|
+
for item in queue
|
|
413
|
+
)
|
|
414
|
+
current_st = "queued" if is_queued else "launching"
|
|
415
|
+
if current_st != last_status:
|
|
416
|
+
last_status = current_st
|
|
417
|
+
if callable(on_status_update):
|
|
418
|
+
on_status_update({"status": current_st, "session_id": session_id})
|
|
419
|
+
|
|
420
|
+
time.sleep(poll_interval)
|
|
421
|
+
|
|
422
|
+
return {"session_id": session_id, "status": "timeout", "error": f"Timed out after {timeout}s"}
|