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,320 @@
|
|
|
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
|
+
"""Contract layer between actuator backends and the agent-facing tool surface.
|
|
16
|
+
|
|
17
|
+
This module is the single place that declares *what a backend must provide*. It sits
|
|
18
|
+
between the actuator implementations (``artemis/mcp/actuators/``) and the agents that
|
|
19
|
+
consume tools, so that a backend -- ADB today, a robot arm tomorrow -- can omit tools
|
|
20
|
+
it cannot perform, and add tools nobody anticipated, without any agent knowing.
|
|
21
|
+
|
|
22
|
+
Two rules govern the split between required and optional:
|
|
23
|
+
|
|
24
|
+
1. A tool is REQUIRED only when a prompt depends on it *structurally*, i.e. the prompt
|
|
25
|
+
cannot be assembled around its absence. Today that is ``click_sequence`` alone: the
|
|
26
|
+
Flash runner prompt's "Time-Sensitive Tasks" teaching is built around "atomic
|
|
27
|
+
chained execution to defeat turn latency", which *is* ``click_sequence``. (The Pro
|
|
28
|
+
Operator needs no such tool: a multi-action turn is executed by the Validator as an
|
|
29
|
+
unvetted fast-action burst.)
|
|
30
|
+
2. Everything else is OPTIONAL, because every other prompt reference lives in a tool
|
|
31
|
+
enumeration slot or a self-contained instruction block that the assembly layer
|
|
32
|
+
(``artemis/agents/prompt_assembly.py``) renders conditionally. An optional tool that
|
|
33
|
+
a backend does not implement disappears from the declarations *and* from the prompt,
|
|
34
|
+
so it costs the model nothing.
|
|
35
|
+
|
|
36
|
+
``tests/unit/mcp/test_action_manifest.py`` re-derives rule 2 by scanning the prompt
|
|
37
|
+
sources, so adding a tool name to a prompt without classifying it here fails CI.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from collections.abc import Awaitable, Callable
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"ActuatorContractError",
|
|
46
|
+
"AGENT_ROLES",
|
|
47
|
+
"ALL_KNOWN_TOOLS",
|
|
48
|
+
"BACKEND_INDEPENDENT_TOOLS",
|
|
49
|
+
"DEVICE_ACTIONS",
|
|
50
|
+
"ExtensionTool",
|
|
51
|
+
"INTERNAL_ACTIONS",
|
|
52
|
+
"MIN_EXTENSION_DESCRIPTION_LENGTH",
|
|
53
|
+
"OPTIONAL_ACTIONS",
|
|
54
|
+
"REQUIRED_ACTIONS",
|
|
55
|
+
"available_device_actions",
|
|
56
|
+
"filter_declarations",
|
|
57
|
+
"validate_actuator",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- Tool classification -------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
#: Device actions a backend MUST implement. See rule 1 in the module docstring.
|
|
64
|
+
#:
|
|
65
|
+
#: ``click_sequence`` implies the ability to tap, so a backend providing it can
|
|
66
|
+
#: trivially provide ``click`` as well; ``click`` is nonetheless optional because no
|
|
67
|
+
#: prompt depends on it structurally once the enumeration slots are assembled.
|
|
68
|
+
REQUIRED_ACTIONS: frozenset[str] = frozenset(
|
|
69
|
+
{
|
|
70
|
+
"click_sequence",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
#: Device actions a backend MAY implement. Absent ones vanish from both the tool
|
|
75
|
+
#: declarations and the rendered prompts.
|
|
76
|
+
OPTIONAL_ACTIONS: frozenset[str] = frozenset(
|
|
77
|
+
{
|
|
78
|
+
"click",
|
|
79
|
+
"long_press",
|
|
80
|
+
"input_text",
|
|
81
|
+
"swipe",
|
|
82
|
+
"press_key",
|
|
83
|
+
"manage_app",
|
|
84
|
+
"wait_for_delay",
|
|
85
|
+
# The four below appear in no prompt at all. `wait_for_text` corroborates the
|
|
86
|
+
# classification: the former legacy executor dispatched it, but it has never had
|
|
87
|
+
# a ToolDeclaration, i.e. it has always been unreachable by any LLM.
|
|
88
|
+
"wait_for_text",
|
|
89
|
+
"open_link",
|
|
90
|
+
"erase_one_char",
|
|
91
|
+
"focus_and_clear_text",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
#: Used by the client-side adapter only; never declared to any LLM, so the
|
|
96
|
+
#: "no cognitive load" rule does not apply. The adapter cannot function without them,
|
|
97
|
+
#: so they are validated as strictly as REQUIRED_ACTIONS.
|
|
98
|
+
INTERNAL_ACTIONS: frozenset[str] = frozenset(
|
|
99
|
+
{
|
|
100
|
+
"observe_screen",
|
|
101
|
+
"take_screenshot",
|
|
102
|
+
"get_ui_hierarchy",
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
#: Tools that are not device control at all -- notes, sub-agents, diagnostics, and the
|
|
107
|
+
#: loop-termination sentinels. They are unaffected by which actuator is installed.
|
|
108
|
+
#:
|
|
109
|
+
#: ``run_adb_command`` and ``manage_app`` ride the *system channel* rather than physical
|
|
110
|
+
#: actuation: a robot arm driving a handset still has ADB attached, so ``am start``
|
|
111
|
+
#: suffices and no icon-hunting is required. (``manage_app`` is nonetheless classified
|
|
112
|
+
#: as an optional device action above, since it is declared through the same path.)
|
|
113
|
+
BACKEND_INDEPENDENT_TOOLS: frozenset[str] = frozenset(
|
|
114
|
+
{
|
|
115
|
+
"read_note",
|
|
116
|
+
"list_notes",
|
|
117
|
+
"save_note",
|
|
118
|
+
"update_note",
|
|
119
|
+
"append_note",
|
|
120
|
+
"ask_explorer",
|
|
121
|
+
"ask_diagnoser",
|
|
122
|
+
"ask_committee",
|
|
123
|
+
"video_analyzer",
|
|
124
|
+
"run_adb_command",
|
|
125
|
+
"manage_task",
|
|
126
|
+
"analyze_task_output",
|
|
127
|
+
"search_history",
|
|
128
|
+
"replay_steps",
|
|
129
|
+
"get_step_screenshot",
|
|
130
|
+
"report_task_status",
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
#: Everything an actuator may legitimately claim in ``capabilities()``.
|
|
135
|
+
DEVICE_ACTIONS: frozenset[str] = REQUIRED_ACTIONS | OPTIONAL_ACTIONS | INTERNAL_ACTIONS
|
|
136
|
+
|
|
137
|
+
#: Every name the manifest knows about. Extension tools must not collide with these.
|
|
138
|
+
ALL_KNOWN_TOOLS: frozenset[str] = DEVICE_ACTIONS | BACKEND_INDEPENDENT_TOOLS
|
|
139
|
+
|
|
140
|
+
#: Agent roles an extension tool may target.
|
|
141
|
+
AGENT_ROLES: frozenset[str] = frozenset(
|
|
142
|
+
{
|
|
143
|
+
"operator",
|
|
144
|
+
"flash",
|
|
145
|
+
"validator",
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
#: Extension descriptions carry the entire usage contract (see ``ExtensionTool``), so a
|
|
150
|
+
#: one-word description is a bug rather than a terse style choice.
|
|
151
|
+
MIN_EXTENSION_DESCRIPTION_LENGTH = 40
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ActuatorContractError(RuntimeError):
|
|
155
|
+
"""Raised when an actuator does not satisfy the manifest contract.
|
|
156
|
+
|
|
157
|
+
Always raised at session start rather than mid-run, so a misconfigured backend
|
|
158
|
+
fails before the agent has taken any action on the device.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class ExtensionTool:
|
|
164
|
+
"""A tool contributed by a backend that the manifest does not know about.
|
|
165
|
+
|
|
166
|
+
Extension tools are invisible to the prompt layer by construction: prompts are
|
|
167
|
+
static text that cannot anticipate future backends, so an extension gets **no**
|
|
168
|
+
prompt-level teaching. Its ``description`` must therefore be self-contained --
|
|
169
|
+
stating when to reach for it and what it does -- or the model will never use it
|
|
170
|
+
correctly. :func:`validate_actuator` enforces a length floor as a crude guard.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
name: str
|
|
174
|
+
description: str
|
|
175
|
+
input_schema: dict[str, Any]
|
|
176
|
+
handler: Callable[..., Awaitable[Any]]
|
|
177
|
+
targets: frozenset[str] = field(default=AGENT_ROLES)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --- Contract validation -------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_actuator(actuator: Any) -> None:
|
|
184
|
+
"""Checks an actuator against the manifest, raising on any violation.
|
|
185
|
+
|
|
186
|
+
Called once from ``get_action_session`` before the first device interaction.
|
|
187
|
+
"""
|
|
188
|
+
caps = frozenset(actuator.capabilities())
|
|
189
|
+
|
|
190
|
+
missing_required = REQUIRED_ACTIONS - caps
|
|
191
|
+
if missing_required:
|
|
192
|
+
raise ActuatorContractError(
|
|
193
|
+
f"{type(actuator).__name__} does not implement required action(s): "
|
|
194
|
+
f"{sorted(missing_required)}. These are structurally depended on by prompts "
|
|
195
|
+
f"and cannot be assembled away."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
missing_internal = INTERNAL_ACTIONS - caps
|
|
199
|
+
if missing_internal:
|
|
200
|
+
raise ActuatorContractError(
|
|
201
|
+
f"{type(actuator).__name__} does not implement internal action(s): "
|
|
202
|
+
f"{sorted(missing_internal)}. The client-side adapter cannot observe the "
|
|
203
|
+
f"device without them."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
unknown = caps - DEVICE_ACTIONS
|
|
207
|
+
if unknown:
|
|
208
|
+
raise ActuatorContractError(
|
|
209
|
+
f"{type(actuator).__name__} claims capabilities unknown to the manifest: "
|
|
210
|
+
f"{sorted(unknown)}. Backend-specific tools belong in extensions(), not "
|
|
211
|
+
f"capabilities()."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
_validate_extensions(actuator)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _validate_extensions(actuator: Any) -> None:
|
|
218
|
+
"""Validates extension tools for name collisions and self-sufficient descriptions."""
|
|
219
|
+
extensions = list(actuator.extensions())
|
|
220
|
+
seen: set[str] = set()
|
|
221
|
+
|
|
222
|
+
for ext in extensions:
|
|
223
|
+
if ext.name in ALL_KNOWN_TOOLS:
|
|
224
|
+
raise ActuatorContractError(
|
|
225
|
+
f"Extension tool '{ext.name}' collides with a manifest tool of the same "
|
|
226
|
+
f"name. Extension names must be unique across the whole tool surface."
|
|
227
|
+
)
|
|
228
|
+
if ext.name in seen:
|
|
229
|
+
raise ActuatorContractError(
|
|
230
|
+
f"{type(actuator).__name__} registers extension '{ext.name}' more than once."
|
|
231
|
+
)
|
|
232
|
+
seen.add(ext.name)
|
|
233
|
+
|
|
234
|
+
unknown_targets = frozenset(ext.targets) - AGENT_ROLES
|
|
235
|
+
if unknown_targets:
|
|
236
|
+
raise ActuatorContractError(
|
|
237
|
+
f"Extension tool '{ext.name}' targets unknown agent role(s): "
|
|
238
|
+
f"{sorted(unknown_targets)}. Valid roles: {sorted(AGENT_ROLES)}."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if len(ext.description.strip()) < MIN_EXTENSION_DESCRIPTION_LENGTH:
|
|
242
|
+
raise ActuatorContractError(
|
|
243
|
+
f"Extension tool '{ext.name}' has a description of "
|
|
244
|
+
f"{len(ext.description.strip())} characters. Extensions receive no "
|
|
245
|
+
f"prompt-level guidance, so the description must state when to use the "
|
|
246
|
+
f"tool and what it does (minimum "
|
|
247
|
+
f"{MIN_EXTENSION_DESCRIPTION_LENGTH} characters)."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# --- Surface derivation --------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def available_device_actions(actuator: Any) -> frozenset[str]:
|
|
255
|
+
"""Returns the LLM-facing device actions this actuator supports.
|
|
256
|
+
|
|
257
|
+
Internal actions are excluded: they are adapter plumbing and never reach a model.
|
|
258
|
+
"""
|
|
259
|
+
caps = frozenset(actuator.capabilities())
|
|
260
|
+
return (REQUIRED_ACTIONS | OPTIONAL_ACTIONS) & caps
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def filter_declarations(
|
|
264
|
+
declarations: list[Any],
|
|
265
|
+
actuator: Any,
|
|
266
|
+
agent: str,
|
|
267
|
+
) -> list[Any]:
|
|
268
|
+
"""Filters a declaration list down to what ``actuator`` actually provides.
|
|
269
|
+
|
|
270
|
+
Declarations for known tools keep coming from the hand-tuned ``ToolDeclaration``
|
|
271
|
+
constants -- this layer only removes entries, it never rewrites descriptions.
|
|
272
|
+
Extension declarations are appended from the actuator itself, since the manifest
|
|
273
|
+
cannot know them in advance.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
declarations: Declarations the agent would use if every tool existed.
|
|
277
|
+
actuator: The installed backend.
|
|
278
|
+
agent: One of :data:`AGENT_ROLES`; selects which extensions apply.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Declarations in their original order, with unavailable device actions dropped
|
|
282
|
+
and this agent's extension tools appended.
|
|
283
|
+
"""
|
|
284
|
+
if agent not in AGENT_ROLES:
|
|
285
|
+
raise ValueError(f"Unknown agent role '{agent}'. Valid roles: {sorted(AGENT_ROLES)}.")
|
|
286
|
+
|
|
287
|
+
available = available_device_actions(actuator)
|
|
288
|
+
|
|
289
|
+
kept = [
|
|
290
|
+
decl
|
|
291
|
+
for decl in declarations
|
|
292
|
+
# Only device actions are gated. Backend-independent tools and anything the
|
|
293
|
+
# manifest does not classify (e.g. an agent's own bespoke tool) pass through.
|
|
294
|
+
if _declaration_name(decl) not in DEVICE_ACTIONS or _declaration_name(decl) in available
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
for ext in actuator.extensions():
|
|
298
|
+
if agent in ext.targets:
|
|
299
|
+
kept.append(_extension_declaration(ext))
|
|
300
|
+
|
|
301
|
+
return kept
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _declaration_name(declaration: Any) -> str:
|
|
305
|
+
"""Reads a tool name off either a ToolDeclaration mapping or a LangChain tool."""
|
|
306
|
+
name = getattr(declaration, "name", None)
|
|
307
|
+
if name is None and isinstance(declaration, dict):
|
|
308
|
+
name = declaration.get("name")
|
|
309
|
+
return name or ""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _extension_declaration(ext: ExtensionTool) -> Any:
|
|
313
|
+
"""Renders an ExtensionTool into the same ToolDeclaration shape as known tools."""
|
|
314
|
+
from artemis.core.tool_declaration import ToolDeclaration
|
|
315
|
+
|
|
316
|
+
return ToolDeclaration(
|
|
317
|
+
name=ext.name,
|
|
318
|
+
description=ext.description,
|
|
319
|
+
parameters=ext.input_schema,
|
|
320
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
"""The one place where the historical verb split is resolved.
|
|
16
|
+
|
|
17
|
+
Canonical tool names are the agent-facing ones (``click``, ``long_press``,
|
|
18
|
+
``input_text``, ``manage_app``, ...) because those are what the prompts teach and the
|
|
19
|
+
prompts are the contract. The Operator/Validator-internal vocabulary
|
|
20
|
+
(``tap``, ``long_press_on``, ``focus_and_input_text``, ``launch_app``/``stop_app``,
|
|
21
|
+
``back``) is translated here and nowhere else.
|
|
22
|
+
|
|
23
|
+
``to_canonical_call`` converts a Validator action item (the dicts Operator writes into
|
|
24
|
+
``state.structured_decisions``) into a canonical ``(name, kwargs)`` pair for the
|
|
25
|
+
actuator/session layer, using the ``normalized_coordinates`` the Operator already
|
|
26
|
+
computed so the canonical surface never needs a pixel escape hatch.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
__all__ = ["OPERATOR_ACTION_TO_CANONICAL", "to_canonical_call"]
|
|
32
|
+
|
|
33
|
+
#: Operator-internal action verb -> canonical tool name.
|
|
34
|
+
OPERATOR_ACTION_TO_CANONICAL: dict[str, str] = {
|
|
35
|
+
"tap": "click",
|
|
36
|
+
"long_press_on": "long_press",
|
|
37
|
+
"swipe": "swipe",
|
|
38
|
+
"focus_and_input_text": "input_text",
|
|
39
|
+
"focus_and_clear_text": "focus_and_clear_text",
|
|
40
|
+
"erase_one_char": "erase_one_char",
|
|
41
|
+
"press_key": "press_key",
|
|
42
|
+
"back": "press_key",
|
|
43
|
+
"launch_app": "manage_app",
|
|
44
|
+
"stop_app": "manage_app",
|
|
45
|
+
"open_link": "open_link",
|
|
46
|
+
"wait_for_delay": "wait_for_delay",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _norm_pair(
|
|
51
|
+
action_item: dict[str, Any], dims: tuple[int, int] | None = None
|
|
52
|
+
) -> tuple[int, int] | None:
|
|
53
|
+
"""Reads the Operator-computed normalized [nx, ny] off an action item.
|
|
54
|
+
|
|
55
|
+
Falls back to converting the pixel ``coordinates`` when the item predates the
|
|
56
|
+
``normalized_coordinates`` field and screen ``dims`` are provided.
|
|
57
|
+
"""
|
|
58
|
+
coords = action_item.get("normalized_coordinates")
|
|
59
|
+
if isinstance(coords, (list, tuple)) and len(coords) >= 2:
|
|
60
|
+
return int(coords[0]), int(coords[1])
|
|
61
|
+
|
|
62
|
+
pixel = action_item.get("coordinates")
|
|
63
|
+
if dims and isinstance(pixel, (list, tuple)) and len(pixel) >= 2:
|
|
64
|
+
width, height = dims
|
|
65
|
+
nx = int(max(0, min(1000, round(int(pixel[0]) * 1000 / max(1, width)))))
|
|
66
|
+
ny = int(max(0, min(1000, round(int(pixel[1]) * 1000 / max(1, height)))))
|
|
67
|
+
return nx, ny
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def to_canonical_call(
|
|
72
|
+
action_item: dict[str, Any], dims: tuple[int, int] | None = None
|
|
73
|
+
) -> tuple[str, dict[str, Any]]:
|
|
74
|
+
"""Translates a Validator action item into a canonical (tool, kwargs) call.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
action_item: One entry of the Operator's structured decisions.
|
|
78
|
+
dims: Optional (width, height) for pixel->normalized fallback on legacy items
|
|
79
|
+
lacking ``normalized_coordinates``.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: for an unknown action verb or an item missing the coordinates the
|
|
83
|
+
canonical call requires.
|
|
84
|
+
"""
|
|
85
|
+
verb = str(action_item.get("action", ""))
|
|
86
|
+
canonical = OPERATOR_ACTION_TO_CANONICAL.get(verb)
|
|
87
|
+
if canonical is None:
|
|
88
|
+
raise ValueError(f"Unsupported action: {verb}")
|
|
89
|
+
|
|
90
|
+
if verb == "tap":
|
|
91
|
+
pair = _norm_pair(action_item, dims)
|
|
92
|
+
if pair is None:
|
|
93
|
+
raise ValueError("Invalid coords")
|
|
94
|
+
return "click", {
|
|
95
|
+
"target": [pair[0], pair[1]],
|
|
96
|
+
"times": action_item.get("times") or action_item.get("click_times") or 1,
|
|
97
|
+
"delay_ms": action_item.get("delay_ms") or action_item.get("delay") or 100,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if verb == "long_press_on":
|
|
101
|
+
pair = _norm_pair(action_item, dims)
|
|
102
|
+
if pair is None:
|
|
103
|
+
raise ValueError("Invalid coords")
|
|
104
|
+
return "long_press", {
|
|
105
|
+
"target": [pair[0], pair[1]],
|
|
106
|
+
"duration_ms": action_item.get("duration", 1000),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if verb == "swipe":
|
|
110
|
+
coords = action_item.get("normalized_coordinates")
|
|
111
|
+
if not (isinstance(coords, (list, tuple)) and len(coords) == 4):
|
|
112
|
+
pixel = action_item.get("coordinates")
|
|
113
|
+
if dims and isinstance(pixel, (list, tuple)) and len(pixel) == 4:
|
|
114
|
+
width, height = dims
|
|
115
|
+
coords = [
|
|
116
|
+
int(max(0, min(1000, round(int(pixel[0]) * 1000 / max(1, width))))),
|
|
117
|
+
int(max(0, min(1000, round(int(pixel[1]) * 1000 / max(1, height))))),
|
|
118
|
+
int(max(0, min(1000, round(int(pixel[2]) * 1000 / max(1, width))))),
|
|
119
|
+
int(max(0, min(1000, round(int(pixel[3]) * 1000 / max(1, height))))),
|
|
120
|
+
]
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError("Invalid coords")
|
|
123
|
+
return "swipe", {
|
|
124
|
+
"start": [int(coords[0]), int(coords[1])],
|
|
125
|
+
"end": [int(coords[2]), int(coords[3])],
|
|
126
|
+
"duration_ms": action_item.get("duration", 400),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if verb == "focus_and_input_text":
|
|
130
|
+
pair = _norm_pair(action_item, dims)
|
|
131
|
+
if pair is None:
|
|
132
|
+
raise ValueError("Invalid coords")
|
|
133
|
+
return "input_text", {
|
|
134
|
+
"text": action_item.get("text", ""),
|
|
135
|
+
"target": [pair[0], pair[1]],
|
|
136
|
+
"clear_exist": bool(action_item.get("clear_before_input", False)),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if verb == "focus_and_clear_text":
|
|
140
|
+
pair = _norm_pair(action_item, dims)
|
|
141
|
+
if pair is None:
|
|
142
|
+
raise ValueError("Invalid coords")
|
|
143
|
+
return "focus_and_clear_text", {"target": [pair[0], pair[1]]}
|
|
144
|
+
|
|
145
|
+
if verb == "erase_one_char":
|
|
146
|
+
return "erase_one_char", {}
|
|
147
|
+
|
|
148
|
+
if verb == "press_key":
|
|
149
|
+
keycode = str(action_item.get("keycode", ""))
|
|
150
|
+
# Operator emits Android-style KEYCODE_* names; the canonical tool takes the
|
|
151
|
+
# bare key word for the common keys. Anything else keeps its original spelling
|
|
152
|
+
# so the driver can forward it verbatim (arbitrary KEYCODE_* / numeric codes).
|
|
153
|
+
bare = keycode.removeprefix("KEYCODE_").lower() if keycode else ""
|
|
154
|
+
known = {"home", "back", "enter", "delete", "tab", "search", "menu", "app_switch"}
|
|
155
|
+
return "press_key", {"key": bare if bare in known else keycode}
|
|
156
|
+
|
|
157
|
+
if verb == "back":
|
|
158
|
+
return "press_key", {"key": "back"}
|
|
159
|
+
|
|
160
|
+
if verb in ("launch_app", "stop_app"):
|
|
161
|
+
return "manage_app", {
|
|
162
|
+
"action": "launch" if verb == "launch_app" else "stop",
|
|
163
|
+
"app_name": action_item.get("app_name", ""),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if verb == "open_link":
|
|
167
|
+
return "open_link", {"url": action_item.get("url", "")}
|
|
168
|
+
|
|
169
|
+
if verb == "wait_for_delay":
|
|
170
|
+
return "wait_for_delay", {"time_in_ms": action_item.get("time_in_ms", 0)}
|
|
171
|
+
|
|
172
|
+
raise ValueError(f"Unsupported action: {verb}")
|
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
"""The unified device action MCP server.
|
|
16
|
+
|
|
17
|
+
``build_action_server`` turns an actuator into a ``FastMCP`` server that registers
|
|
18
|
+
**only** the tools the actuator implements, plus its extension tools -- so
|
|
19
|
+
``list_tools()`` is clean by construction and an absent optional tool is invisible to
|
|
20
|
+
every consumer.
|
|
21
|
+
|
|
22
|
+
Return protocol: every device action returns a ``CallToolResult`` whose
|
|
23
|
+
``structuredContent`` is an ``ActionResult`` -- ``ok``/``code`` carry the real status
|
|
24
|
+
(killing the historical ``"Error" in text`` sniffing) while ``content`` keeps the
|
|
25
|
+
historical human-readable message. ``isError`` is reserved for protocol-level
|
|
26
|
+
failures; a device refusing an action is ``isError=False, ok=False`` so it reaches the
|
|
27
|
+
model as an observation, not an exception.
|
|
28
|
+
|
|
29
|
+
Exceptions raised by the actuator are wrapped here with the historical
|
|
30
|
+
``"Error during X: {e}"`` wording the agents' transcripts already contain.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
from typing import Annotated, Any
|
|
35
|
+
|
|
36
|
+
from mcp.server.fastmcp import FastMCP
|
|
37
|
+
from mcp.types import CallToolResult, ImageContent, TextContent
|
|
38
|
+
|
|
39
|
+
from artemis.mcp.action_manifest import validate_actuator
|
|
40
|
+
from artemis.mcp.action_specs import (
|
|
41
|
+
exception_prefix,
|
|
42
|
+
make_wire_handler,
|
|
43
|
+
wire_dialects,
|
|
44
|
+
)
|
|
45
|
+
from artemis.mcp.action_types import ActionCode, ActionResult, ObserveResult
|
|
46
|
+
from artemis.mcp.actuators.base import Actuator
|
|
47
|
+
from artemis.mcp.observation import observe as observe_impl
|
|
48
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
49
|
+
|
|
50
|
+
logger = get_logger(__name__)
|
|
51
|
+
|
|
52
|
+
__all__ = ["build_action_server"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _wrap(res: ActionResult) -> CallToolResult:
|
|
56
|
+
return CallToolResult(
|
|
57
|
+
content=[TextContent(type="text", text=res.message)],
|
|
58
|
+
structuredContent=res.model_dump(mode="json"),
|
|
59
|
+
isError=False,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _wrap_exception(action: str, exc: Exception) -> CallToolResult:
|
|
64
|
+
return _wrap(
|
|
65
|
+
ActionResult.failure(action, f"{exception_prefix(action)}: {exc}", detail=repr(exc))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_action_server(actuator: Actuator, name: str = "artemis_actions") -> FastMCP:
|
|
70
|
+
"""Builds a FastMCP server exposing exactly what ``actuator`` implements.
|
|
71
|
+
|
|
72
|
+
Raises ``ActuatorContractError`` when the actuator violates the manifest, so a
|
|
73
|
+
misconfigured backend fails before serving anything.
|
|
74
|
+
"""
|
|
75
|
+
validate_actuator(actuator)
|
|
76
|
+
caps = actuator.capabilities()
|
|
77
|
+
mcp = FastMCP(name)
|
|
78
|
+
|
|
79
|
+
# --- Device actions (registered only when implemented) ---------------------------
|
|
80
|
+
# Declarations and argument conversion both come from the canonical manifest
|
|
81
|
+
# (artemis/mcp/action_specs.py); this server contributes only the transport
|
|
82
|
+
# wrapping, so the served schema cannot drift from the manifest.
|
|
83
|
+
|
|
84
|
+
for spec in wire_dialects():
|
|
85
|
+
if spec.name in caps:
|
|
86
|
+
mcp.add_tool(
|
|
87
|
+
make_wire_handler(spec, actuator, _wrap, _wrap_exception),
|
|
88
|
+
name=spec.name,
|
|
89
|
+
description=spec.wire.description,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# --- Internal observation tools (never declared to an LLM) -----------------------
|
|
93
|
+
|
|
94
|
+
if "observe_screen" in caps:
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
async def observe_screen(
|
|
98
|
+
settle_ms: int = 400, include_image: bool = False
|
|
99
|
+
) -> Annotated[CallToolResult, ObserveResult]:
|
|
100
|
+
"""Capture and index the current screen (adapter-internal)."""
|
|
101
|
+
try:
|
|
102
|
+
obs, img_bytes = await observe_impl(
|
|
103
|
+
getattr(actuator, "ctx", None),
|
|
104
|
+
actuator.controller,
|
|
105
|
+
settle_ms=settle_ms,
|
|
106
|
+
)
|
|
107
|
+
content: list[Any] = [TextContent(type="text", text=obs.message)]
|
|
108
|
+
if include_image and img_bytes:
|
|
109
|
+
import base64 as _b64
|
|
110
|
+
|
|
111
|
+
content.append(
|
|
112
|
+
ImageContent(
|
|
113
|
+
type="image",
|
|
114
|
+
data=_b64.b64encode(img_bytes).decode("utf-8"),
|
|
115
|
+
mimeType="image/jpeg",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return CallToolResult(
|
|
119
|
+
content=content,
|
|
120
|
+
structuredContent=obs.model_dump(mode="json"),
|
|
121
|
+
isError=False,
|
|
122
|
+
)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
obs = ObserveResult(
|
|
125
|
+
ok=False,
|
|
126
|
+
code=ActionCode.DEVICE_ERROR,
|
|
127
|
+
message=f"Error during observe_screen: {e}",
|
|
128
|
+
hierarchy_ok=False,
|
|
129
|
+
)
|
|
130
|
+
return CallToolResult(
|
|
131
|
+
content=[TextContent(type="text", text=obs.message)],
|
|
132
|
+
structuredContent=obs.model_dump(mode="json"),
|
|
133
|
+
isError=False,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Timeouts on the two observation tools run INSIDE the tool, around the actuator
|
|
137
|
+
# call only. They must never be expressed as a transport-level read timeout
|
|
138
|
+
# (``read_timeout_seconds``) or a caller-side ``asyncio.wait_for``: both produce a
|
|
139
|
+
# late response to an abandoned request, which crashes the mcp-1.29 client
|
|
140
|
+
# receive loop (``_handle_response`` sends into the request's already-closed
|
|
141
|
+
# response stream) and bricks the in-memory session for every later caller.
|
|
142
|
+
# A server-side timeout yields a normal error payload and no late response.
|
|
143
|
+
|
|
144
|
+
if "take_screenshot" in caps:
|
|
145
|
+
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
async def take_screenshot(timeout_ms: int | None = None) -> CallToolResult:
|
|
148
|
+
"""Return the current screen as base64 (adapter-internal, no persistence)."""
|
|
149
|
+
try:
|
|
150
|
+
coro = actuator.take_screenshot()
|
|
151
|
+
if timeout_ms:
|
|
152
|
+
coro = asyncio.wait_for(coro, timeout=timeout_ms / 1000.0)
|
|
153
|
+
b64 = await coro
|
|
154
|
+
payload: dict[str, Any] = {"ok": True, "image_b64": b64}
|
|
155
|
+
except TimeoutError:
|
|
156
|
+
payload = {"ok": False, "error": f"screenshot timed out after {timeout_ms}ms"}
|
|
157
|
+
except Exception as e:
|
|
158
|
+
payload = {"ok": False, "error": str(e)}
|
|
159
|
+
return CallToolResult(
|
|
160
|
+
content=[
|
|
161
|
+
TextContent(type="text", text="screenshot" if payload["ok"] else str(payload))
|
|
162
|
+
],
|
|
163
|
+
structuredContent=payload,
|
|
164
|
+
isError=False,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if "get_ui_hierarchy" in caps:
|
|
168
|
+
|
|
169
|
+
@mcp.tool()
|
|
170
|
+
async def get_ui_hierarchy(timeout_ms: int | None = None) -> CallToolResult:
|
|
171
|
+
"""Return the raw UI element hierarchy (adapter-internal)."""
|
|
172
|
+
try:
|
|
173
|
+
coro = actuator.get_ui_elements()
|
|
174
|
+
if timeout_ms:
|
|
175
|
+
coro = asyncio.wait_for(coro, timeout=timeout_ms / 1000.0)
|
|
176
|
+
elements = await coro
|
|
177
|
+
payload: dict[str, Any] = {"ok": True, "elements": elements}
|
|
178
|
+
except TimeoutError:
|
|
179
|
+
payload = {
|
|
180
|
+
"ok": False,
|
|
181
|
+
"error": f"ui hierarchy fetch timed out after {timeout_ms}ms",
|
|
182
|
+
}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
payload = {"ok": False, "error": str(e)}
|
|
185
|
+
return CallToolResult(
|
|
186
|
+
content=[
|
|
187
|
+
TextContent(type="text", text="ui_hierarchy" if payload["ok"] else str(payload))
|
|
188
|
+
],
|
|
189
|
+
structuredContent=payload,
|
|
190
|
+
isError=False,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# --- Backend extension tools -----------------------------------------------------
|
|
194
|
+
|
|
195
|
+
for ext in actuator.extensions():
|
|
196
|
+
mcp.add_tool(ext.handler, name=ext.name, description=ext.description)
|
|
197
|
+
|
|
198
|
+
logger.info(
|
|
199
|
+
f"Action server '{name}' built with {len(caps)} device actions and"
|
|
200
|
+
f" {len(actuator.extensions())} extension tools."
|
|
201
|
+
)
|
|
202
|
+
return mcp
|