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,1167 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Universal Multi-Model FlashRunner for Artemis.
|
|
16
|
+
|
|
17
|
+
Executes autonomous reactive mobile workflows across Google Gemini,
|
|
18
|
+
OpenAI GPT-4o/o3, Anthropic Claude 3.5/3.7, and OpenRouter endpoints.
|
|
19
|
+
|
|
20
|
+
Conversation shape (shared with the Pro operator, history redesign §3.2):
|
|
21
|
+
the prompt is built every turn from a session :class:`TranscriptLedger` —
|
|
22
|
+
a byte-stable system prefix, the committed earlier turns (append-only; old UI
|
|
23
|
+
lists stripped at depth 1, screenshots resolved to visual summaries at depth
|
|
24
|
+
K, long spans chunk-compressed), and a fresh tail carrying the current
|
|
25
|
+
observation under a ``# CURRENT OBSERVATION [T+mm:ss]`` header. Actions run
|
|
26
|
+
synchronously and their tool messages already carry the outcome; only a turn
|
|
27
|
+
whose action failed ends with an ``--- Action Execution Result (T+mm:ss) ---``
|
|
28
|
+
message (it carries the error). Every timestamp the model sees is a
|
|
29
|
+
session-relative offset — the same clock the video analyzer uses for the
|
|
30
|
+
session recording.
|
|
31
|
+
|
|
32
|
+
Per-turn notices on the tail (the reasoning reminder after a silent turn, the
|
|
33
|
+
no-tool-call nudge, the final-turn warning, the user-guidance wrapper) are
|
|
34
|
+
marked ephemeral: the ledger's scrub edge deletes them once the message leaves
|
|
35
|
+
the recent window, so they never reach the frozen history or a chunk capsule.
|
|
36
|
+
The verbatim body of a user instruction is the exception: it stays in the
|
|
37
|
+
active window until its turn is chunked, whose user lines then carry it on.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import asyncio
|
|
41
|
+
import base64
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
import json
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
import re
|
|
46
|
+
import uuid
|
|
47
|
+
|
|
48
|
+
from jinja2 import Template
|
|
49
|
+
from langchain_core.messages import (
|
|
50
|
+
BaseMessage,
|
|
51
|
+
HumanMessage,
|
|
52
|
+
SystemMessage,
|
|
53
|
+
ToolMessage,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from artemis.agents.flash.summarizer import VisualStepSummarizer, build_focus_context
|
|
57
|
+
from artemis.agents.operator.prompts import REASONING_REMINDER, UserGuidance, render_user_guidance
|
|
58
|
+
from artemis.agents.validator.tool_declarations import (
|
|
59
|
+
ASK_EXPLORER_TOOL,
|
|
60
|
+
CLICK_SEQUENCE_TOOL,
|
|
61
|
+
LIST_NOTES_TOOL,
|
|
62
|
+
READ_NOTE_TOOL,
|
|
63
|
+
REPORT_TASK_STATUS_TOOL,
|
|
64
|
+
VALIDATOR_TOOLS_DECLARATION,
|
|
65
|
+
capture_screenshot_and_parse_ui,
|
|
66
|
+
prune_intermediate_screenshots,
|
|
67
|
+
)
|
|
68
|
+
from artemis.config import (
|
|
69
|
+
MemoryRuntimeConfig,
|
|
70
|
+
MemoryTranscriptConfig,
|
|
71
|
+
StepSummarizerConfig,
|
|
72
|
+
load_agent_config,
|
|
73
|
+
)
|
|
74
|
+
from artemis.context import ArtemisContext
|
|
75
|
+
from artemis.controllers.unified_controller import UnifiedMobileController
|
|
76
|
+
from artemis.data_engine.trace import trace
|
|
77
|
+
from artemis.graph.perception import _check_injected_instruction_file
|
|
78
|
+
from artemis.graph.state import State
|
|
79
|
+
from artemis.llm.structured import ParseFailure, parse_structured
|
|
80
|
+
from artemis.mcp.action_executor import McpActionExecutor
|
|
81
|
+
from artemis.mcp.observation import observe
|
|
82
|
+
from artemis.memory.transcript import PRO_UI_LIST_MARKER, TranscriptLedger, mark_ephemeral
|
|
83
|
+
from artemis.services.llm import (
|
|
84
|
+
acomplete,
|
|
85
|
+
get_llm,
|
|
86
|
+
invoke_llm_with_timeout_message,
|
|
87
|
+
)
|
|
88
|
+
from artemis.tools.history import history_tool_declarations
|
|
89
|
+
from artemis.tools.tool_wrapper import tool_result_messages
|
|
90
|
+
from artemis.utils.coordinates import (
|
|
91
|
+
COORDINATE_SPACE_KEY,
|
|
92
|
+
COORDINATE_SPACE_NORMALIZED,
|
|
93
|
+
parse_swipe_parameters,
|
|
94
|
+
)
|
|
95
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
96
|
+
|
|
97
|
+
logger = get_logger(__name__)
|
|
98
|
+
|
|
99
|
+
_NO_TOOL_CALL_NOTICE = (
|
|
100
|
+
"You did not call any tools last turn. Please make progress by calling an"
|
|
101
|
+
" action tool or 'report_task_status'."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
_FINAL_TURN_WARNING = "[WARNING] This is your final turn; only 'report_task_status' is available."
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class _TurnRecord:
|
|
109
|
+
"""What one reactive turn produced, for committing it into the ledger.
|
|
110
|
+
|
|
111
|
+
``step_keys`` are the DataEngine step ids recorded this turn (one per
|
|
112
|
+
executed action; the tool_call_id stands in without a DataEngine) — the
|
|
113
|
+
first keys the observation screenshot to its visual-transition summary,
|
|
114
|
+
all of them feed the chunk ledger. ``actions`` collects the outcome of
|
|
115
|
+
every device action so the turn's execution result can be rendered.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
step_keys: list[str] = field(default_factory=list)
|
|
119
|
+
actions: list[tuple[str, str, str]] = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
def result(self) -> dict | None:
|
|
122
|
+
"""The turn's execution result in the validator-report shape.
|
|
123
|
+
|
|
124
|
+
``None`` unless an action failed: Flash executes synchronously and each
|
|
125
|
+
action's tool message already carries its outcome, so a result message
|
|
126
|
+
would only repeat it. A failure is worth its own message because it
|
|
127
|
+
carries the error the turn ended on.
|
|
128
|
+
"""
|
|
129
|
+
for name, status, text in self.actions:
|
|
130
|
+
if status != "success":
|
|
131
|
+
# Executor messages name the action themselves ("Error
|
|
132
|
+
# executing click: ..."); prefix only the ones that don't.
|
|
133
|
+
return {"status": "failed", "error": text if name in text else f"{name}: {text}"}
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class FlashRunner:
|
|
138
|
+
"""Reactive, ultra-fast agent execution loop supporting all VLM providers."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, ctx: ArtemisContext, goal: str, max_turns: int | None = None):
|
|
141
|
+
self.ctx = ctx
|
|
142
|
+
self.goal = goal
|
|
143
|
+
try:
|
|
144
|
+
cfg = load_agent_config()
|
|
145
|
+
self.max_turns = max_turns if max_turns is not None else cfg.flash.max_turns
|
|
146
|
+
self.step_summarizer_cfg = cfg.flash.step_summarizer
|
|
147
|
+
self.memory_runtime_cfg = cfg.memory.runtime
|
|
148
|
+
self.transcript_cfg = cfg.memory.transcript
|
|
149
|
+
self.chunking_cfg = cfg.memory.chunking
|
|
150
|
+
except Exception:
|
|
151
|
+
self.max_turns = max_turns if max_turns is not None else 0
|
|
152
|
+
self.step_summarizer_cfg = StepSummarizerConfig()
|
|
153
|
+
self.memory_runtime_cfg = MemoryRuntimeConfig()
|
|
154
|
+
self.transcript_cfg = MemoryTranscriptConfig()
|
|
155
|
+
self.chunking_cfg = None
|
|
156
|
+
|
|
157
|
+
self.controller = UnifiedMobileController(ctx)
|
|
158
|
+
# ``agent_name="flash"`` makes ask_explorer follow ``explorer.flash_mode``
|
|
159
|
+
# (the Flash profile knob) instead of the Pro profile's tier.
|
|
160
|
+
self.executor = McpActionExecutor(ctx, self.controller, agent_name="flash")
|
|
161
|
+
self.summarizer = (
|
|
162
|
+
VisualStepSummarizer(
|
|
163
|
+
ctx,
|
|
164
|
+
model_name=self.step_summarizer_cfg.model,
|
|
165
|
+
retry_limit=self.memory_runtime_cfg.retry_limit,
|
|
166
|
+
max_concurrency=self.memory_runtime_cfg.max_concurrency,
|
|
167
|
+
flush_timeout_s=self.memory_runtime_cfg.flush_timeout_s,
|
|
168
|
+
)
|
|
169
|
+
if self.step_summarizer_cfg.enabled
|
|
170
|
+
else None
|
|
171
|
+
)
|
|
172
|
+
# Publish the service on the composition root (ctx.step_memory) so
|
|
173
|
+
# any co-resident consumer shares this run's summary runtime.
|
|
174
|
+
if self.summarizer is not None and getattr(ctx, "step_memory", None) is None:
|
|
175
|
+
try:
|
|
176
|
+
ctx.step_memory = self.summarizer
|
|
177
|
+
except (AttributeError, TypeError, ValueError):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def turn_limit(self) -> int | None:
|
|
182
|
+
"""The reactive turn cap, or ``None`` when the loop is unbounded."""
|
|
183
|
+
try:
|
|
184
|
+
limit = int(self.max_turns) if self.max_turns is not None else 0
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
return None
|
|
187
|
+
return limit if limit > 0 else None
|
|
188
|
+
|
|
189
|
+
def _video_tools_enabled(self) -> bool:
|
|
190
|
+
"""Same gate the Pro graph applies before binding ``video_analyzer``."""
|
|
191
|
+
setup = getattr(self.ctx, "execution_setup", None)
|
|
192
|
+
return getattr(setup, "video_recording_tools_enabled", False) is True
|
|
193
|
+
|
|
194
|
+
def _get_tools(self) -> list:
|
|
195
|
+
# Flash has no tool that writes notes and its prompt never teaches
|
|
196
|
+
# them, so the Validator set's note readers are left out here.
|
|
197
|
+
tools = [
|
|
198
|
+
t
|
|
199
|
+
for t in VALIDATOR_TOOLS_DECLARATION
|
|
200
|
+
if t is not READ_NOTE_TOOL and t is not LIST_NOTES_TOOL
|
|
201
|
+
]
|
|
202
|
+
tools.insert(1, CLICK_SEQUENCE_TOOL)
|
|
203
|
+
tools.append(ASK_EXPLORER_TOOL)
|
|
204
|
+
# Helper tools shared with the Pro operator: the history tools are
|
|
205
|
+
# declared from the very same args schemas the LangChain tools bind
|
|
206
|
+
# (same availability gates: a DataEngine session, recall config).
|
|
207
|
+
tools.extend(history_tool_declarations(self.ctx))
|
|
208
|
+
if self._video_tools_enabled():
|
|
209
|
+
from artemis.tools.video_tool import VIDEO_ANALYZER_TOOL
|
|
210
|
+
|
|
211
|
+
tools.append(VIDEO_ANALYZER_TOOL)
|
|
212
|
+
tools.append(REPORT_TASK_STATUS_TOOL)
|
|
213
|
+
# With an actuator installed, drop declarations for device actions the
|
|
214
|
+
# backend does not implement (and append its extension tools); without
|
|
215
|
+
# one, the full historical declaration set is kept.
|
|
216
|
+
actuator = getattr(self.ctx, "actuator", None)
|
|
217
|
+
if actuator is not None:
|
|
218
|
+
from artemis.mcp.action_manifest import filter_declarations
|
|
219
|
+
|
|
220
|
+
tools = filter_declarations(tools, actuator, "flash")
|
|
221
|
+
return tools
|
|
222
|
+
|
|
223
|
+
def _prune_intermediate_screenshots(self, messages: list[BaseMessage]) -> None:
|
|
224
|
+
"""Prunes binary screenshot blocks from intermediate observation messages."""
|
|
225
|
+
prune_intermediate_screenshots(messages)
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# run() setup helpers
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def _build_ledger(self) -> TranscriptLedger:
|
|
232
|
+
"""Builds the session transcript ledger with the shared Pro history policy.
|
|
233
|
+
|
|
234
|
+
The ``T+mm:ss`` clock is anchored to the DataEngine session start so
|
|
235
|
+
the observation headers, the chunk ledger lines and the video
|
|
236
|
+
analyzer's action timeline share one origin. L2/L3 chunk compression
|
|
237
|
+
needs DataEngine step records; without an engine the ledger runs
|
|
238
|
+
scrub-edge-only.
|
|
239
|
+
"""
|
|
240
|
+
engine = getattr(self.ctx, "data_engine", None)
|
|
241
|
+
session_start = getattr(engine, "session_start_time", None) if engine else None
|
|
242
|
+
cfg = self.transcript_cfg
|
|
243
|
+
ledger = TranscriptLedger(
|
|
244
|
+
step_memory=self.summarizer,
|
|
245
|
+
prune_history_xml=self.step_summarizer_cfg.prune_history_xml,
|
|
246
|
+
image_scrub_depth=getattr(cfg, "image_scrub_depth", 3),
|
|
247
|
+
pending_grace_steps=getattr(cfg, "pending_grace_steps", 3),
|
|
248
|
+
xml_scrub_depth=getattr(cfg, "xml_scrub_depth", 1),
|
|
249
|
+
image_scrub_depth_relaxed=getattr(cfg, "image_scrub_depth_relaxed", None),
|
|
250
|
+
context_budget_tokens=getattr(cfg, "context_budget_tokens", None),
|
|
251
|
+
start_ratio=getattr(cfg, "start_ratio", None),
|
|
252
|
+
session_start=session_start if isinstance(session_start, (int, float)) else None,
|
|
253
|
+
)
|
|
254
|
+
if engine is not None:
|
|
255
|
+
try:
|
|
256
|
+
from artemis.memory import HistoryChunkManager
|
|
257
|
+
|
|
258
|
+
ledger.attach_chunker(
|
|
259
|
+
HistoryChunkManager(
|
|
260
|
+
engine=engine,
|
|
261
|
+
ctx=self.ctx,
|
|
262
|
+
chunking_config=self.chunking_cfg,
|
|
263
|
+
transcript_config=cfg,
|
|
264
|
+
goal=self.goal,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"History chunk manager unavailable for FlashRunner: {e}")
|
|
269
|
+
try:
|
|
270
|
+
self.ctx.transcript_ledger = ledger
|
|
271
|
+
except (AttributeError, TypeError, ValueError):
|
|
272
|
+
pass
|
|
273
|
+
return ledger
|
|
274
|
+
|
|
275
|
+
#: The model of the current run; decides how screenshot tool results travel.
|
|
276
|
+
_llm = None
|
|
277
|
+
|
|
278
|
+
def _init_llm(self):
|
|
279
|
+
"""Initializes the Universal LLM via the Service Layer."""
|
|
280
|
+
return get_llm(self.ctx, name="operator")
|
|
281
|
+
|
|
282
|
+
def _render_system_prompt(self, tools_declaration: list) -> str:
|
|
283
|
+
"""Renders the system prompt from the flash_runner.md template.
|
|
284
|
+
|
|
285
|
+
Tool-teaching segments are gated on the available tool set so an absent
|
|
286
|
+
tool leaves no trace in the prompt; until an actuator is wired in, the
|
|
287
|
+
full manifest set reproduces the historical prompt.
|
|
288
|
+
"""
|
|
289
|
+
prompt_path = Path(__file__).parent / "flash_runner.md"
|
|
290
|
+
prompt_template = prompt_path.read_text(encoding="utf-8")
|
|
291
|
+
available_tools = frozenset(t.name for t in tools_declaration)
|
|
292
|
+
return Template(prompt_template).render(goal=self.goal, available_tools=available_tools)
|
|
293
|
+
|
|
294
|
+
# ------------------------------------------------------------------
|
|
295
|
+
# Per-turn helpers (observe / think)
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
async def _read_injected_instruction(
|
|
299
|
+
self, offset_label: str | None = None
|
|
300
|
+
) -> tuple[str | None, UserGuidance | None]:
|
|
301
|
+
"""Consumes this turn's injected instruction: ``(instruction, guidance)``.
|
|
302
|
+
|
|
303
|
+
``instruction`` is the user's text verbatim — what the step record,
|
|
304
|
+
the chunk ledger and the visual-transition lens receive (the same
|
|
305
|
+
shape Pro stamps). ``guidance`` is the shared user-guidance pair
|
|
306
|
+
(:func:`render_user_guidance`, the Pro rendering without a plan to
|
|
307
|
+
edit): the ephemeral ``--- User Guidance ---`` wrapper and the
|
|
308
|
+
persistent verbatim body, both riding on the observation tail.
|
|
309
|
+
``offset_label`` stamps the body with the session offset. Both are
|
|
310
|
+
None when nothing was injected.
|
|
311
|
+
"""
|
|
312
|
+
if not (self.ctx.data_engine and self.ctx.data_engine.base_dir):
|
|
313
|
+
return None, None
|
|
314
|
+
try:
|
|
315
|
+
injected_payload = await asyncio.to_thread(
|
|
316
|
+
_check_injected_instruction_file,
|
|
317
|
+
str(self.ctx.data_engine.base_dir),
|
|
318
|
+
)
|
|
319
|
+
except (OSError, ValueError, AttributeError) as e:
|
|
320
|
+
logger.warning(f"Failed to check injected instruction in FlashRunner: {e}")
|
|
321
|
+
return None, None
|
|
322
|
+
if not (injected_payload and injected_payload.get("instruction")):
|
|
323
|
+
return None, None
|
|
324
|
+
instruction = str(injected_payload["instruction"])
|
|
325
|
+
guidance = render_user_guidance(
|
|
326
|
+
instruction,
|
|
327
|
+
release_loop=bool(injected_payload.get("release_loop")),
|
|
328
|
+
has_plan=False,
|
|
329
|
+
offset_label=offset_label,
|
|
330
|
+
)
|
|
331
|
+
return instruction, guidance
|
|
332
|
+
|
|
333
|
+
def _build_tail(
|
|
334
|
+
self,
|
|
335
|
+
ledger: TranscriptLedger,
|
|
336
|
+
turns: int,
|
|
337
|
+
img_bytes,
|
|
338
|
+
xml_list,
|
|
339
|
+
*,
|
|
340
|
+
injected: UserGuidance | None = None,
|
|
341
|
+
notices: list[str] | None = None,
|
|
342
|
+
is_final: bool = False,
|
|
343
|
+
previous_turn_silent: bool = False,
|
|
344
|
+
) -> HumanMessage:
|
|
345
|
+
"""Builds this turn's observation tail (Pro observation shape).
|
|
346
|
+
|
|
347
|
+
Header, screenshot and UI list carry the same markers as the Pro
|
|
348
|
+
operator's tail so the ledger's scrub edge treats both alike. The
|
|
349
|
+
objective is stated once, in the system prompt; the tail never repeats
|
|
350
|
+
it. Every per-turn notice (nudges, the user-guidance wrapper, the
|
|
351
|
+
final-turn warning, the reasoning reminder after a silent turn) is
|
|
352
|
+
marked ephemeral so it leaves the transcript with the recent window.
|
|
353
|
+
The verbatim body of an injected instruction is the one exception: it
|
|
354
|
+
is a regular block, so a standing instruction stays in the active
|
|
355
|
+
window until the turn is chunked (the chunk's user lines carry it on).
|
|
356
|
+
"""
|
|
357
|
+
# ``turns`` is kept for call-site symmetry: the tail has the same shape
|
|
358
|
+
# on every turn.
|
|
359
|
+
blocks: list[dict] = [
|
|
360
|
+
{"type": "text", "text": f"# CURRENT OBSERVATION [{ledger.elapsed_label()}]"}
|
|
361
|
+
]
|
|
362
|
+
if img_bytes:
|
|
363
|
+
img_b64 = base64.b64encode(img_bytes).decode("utf-8")
|
|
364
|
+
blocks.append({"type": "text", "text": "--- Current Screenshot ---"})
|
|
365
|
+
blocks.append(
|
|
366
|
+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}
|
|
367
|
+
)
|
|
368
|
+
if xml_list:
|
|
369
|
+
blocks.append({"type": "text", "text": f"{PRO_UI_LIST_MARKER}\n{xml_list}"})
|
|
370
|
+
ephemeral: list[int] = []
|
|
371
|
+
# (text, ephemeral) in tail order.
|
|
372
|
+
per_turn: list[tuple[str, bool]] = [(text, True) for text in (notices or [])]
|
|
373
|
+
if injected:
|
|
374
|
+
# The wrapper is per-turn; the verbatim body right after it is a
|
|
375
|
+
# regular block that stays until the turn is chunked.
|
|
376
|
+
per_turn.append((injected.wrapper, True))
|
|
377
|
+
per_turn.append((injected.body, False))
|
|
378
|
+
if is_final:
|
|
379
|
+
per_turn.append((_FINAL_TURN_WARNING, True))
|
|
380
|
+
if previous_turn_silent:
|
|
381
|
+
per_turn.append((REASONING_REMINDER, True))
|
|
382
|
+
for text, is_ephemeral in per_turn:
|
|
383
|
+
if is_ephemeral:
|
|
384
|
+
ephemeral.append(len(blocks))
|
|
385
|
+
blocks.append({"type": "text", "text": text})
|
|
386
|
+
tail = HumanMessage(content=blocks)
|
|
387
|
+
if ephemeral:
|
|
388
|
+
mark_ephemeral(tail, ephemeral)
|
|
389
|
+
return tail
|
|
390
|
+
|
|
391
|
+
def _extract_response_text(self, response) -> str:
|
|
392
|
+
"""Extracts the natural-language thought text from the model response."""
|
|
393
|
+
raw_text = response.content if isinstance(response.content, str) else ""
|
|
394
|
+
if isinstance(response.content, list):
|
|
395
|
+
raw_text = "".join(
|
|
396
|
+
b.get("text", "") for b in response.content if isinstance(b, dict) and "text" in b
|
|
397
|
+
)
|
|
398
|
+
return raw_text
|
|
399
|
+
|
|
400
|
+
def _extract_response_thinking(self, response) -> str:
|
|
401
|
+
"""Extracts the model's native thinking blocks (thought summaries), if any.
|
|
402
|
+
|
|
403
|
+
Providers running with ``include_thoughts`` return them as ``thinking``
|
|
404
|
+
content blocks next to the ``text`` blocks; they are recorded on the step
|
|
405
|
+
as ``operator_native_thinking`` and never fed back as reply text.
|
|
406
|
+
"""
|
|
407
|
+
if not isinstance(response.content, list):
|
|
408
|
+
return ""
|
|
409
|
+
parts = [
|
|
410
|
+
str(b.get("thinking") or "")
|
|
411
|
+
for b in response.content
|
|
412
|
+
if isinstance(b, dict) and b.get("type") == "thinking"
|
|
413
|
+
]
|
|
414
|
+
return "\n".join(p for p in parts if p.strip())
|
|
415
|
+
|
|
416
|
+
def _token_usage_from_response(self, response) -> dict | None:
|
|
417
|
+
"""Extracts token usage metadata from the model response, if present."""
|
|
418
|
+
step_token_usage = None
|
|
419
|
+
if hasattr(response, "usage_metadata") and response.usage_metadata:
|
|
420
|
+
u = response.usage_metadata
|
|
421
|
+
pr = u.get("input_tokens") or u.get("prompt_tokens") or 0
|
|
422
|
+
co = u.get("output_tokens") or u.get("completion_tokens") or 0
|
|
423
|
+
to = u.get("total_tokens") or (pr + co)
|
|
424
|
+
if to > 0:
|
|
425
|
+
step_token_usage = {
|
|
426
|
+
"prompt_tokens": int(pr),
|
|
427
|
+
"completion_tokens": int(co),
|
|
428
|
+
"total_tokens": int(to),
|
|
429
|
+
}
|
|
430
|
+
elif hasattr(response, "response_metadata") and isinstance(
|
|
431
|
+
response.response_metadata, dict
|
|
432
|
+
):
|
|
433
|
+
u = response.response_metadata.get("usage_metadata") or response.response_metadata.get(
|
|
434
|
+
"token_usage"
|
|
435
|
+
)
|
|
436
|
+
if isinstance(u, dict):
|
|
437
|
+
pr = (
|
|
438
|
+
u.get("input_tokens")
|
|
439
|
+
or u.get("prompt_tokens")
|
|
440
|
+
or u.get("prompt_token_count")
|
|
441
|
+
or 0
|
|
442
|
+
)
|
|
443
|
+
co = (
|
|
444
|
+
u.get("output_tokens")
|
|
445
|
+
or u.get("completion_tokens")
|
|
446
|
+
or u.get("candidates_token_count")
|
|
447
|
+
or 0
|
|
448
|
+
)
|
|
449
|
+
to = u.get("total_tokens") or u.get("total_token_count") or (pr + co)
|
|
450
|
+
if to > 0:
|
|
451
|
+
step_token_usage = {
|
|
452
|
+
"prompt_tokens": int(pr),
|
|
453
|
+
"completion_tokens": int(co),
|
|
454
|
+
"total_tokens": int(to),
|
|
455
|
+
}
|
|
456
|
+
return step_token_usage
|
|
457
|
+
|
|
458
|
+
def _estimate_token_usage(self, messages: list[BaseMessage], raw_text: str) -> dict:
|
|
459
|
+
"""Approximates token usage from message sizes when metadata is absent."""
|
|
460
|
+
calc_prompt_chars = 0
|
|
461
|
+
calc_images = 0
|
|
462
|
+
for msg in messages:
|
|
463
|
+
c = getattr(msg, "content", "")
|
|
464
|
+
if isinstance(c, str):
|
|
465
|
+
calc_prompt_chars += len(c)
|
|
466
|
+
elif isinstance(c, list):
|
|
467
|
+
for block in c:
|
|
468
|
+
if isinstance(block, dict):
|
|
469
|
+
if block.get("type") == "image_url" or "image_url" in block:
|
|
470
|
+
calc_images += 1
|
|
471
|
+
else:
|
|
472
|
+
calc_prompt_chars += len(str(block.get("text", "")))
|
|
473
|
+
else:
|
|
474
|
+
calc_prompt_chars += len(str(block))
|
|
475
|
+
prompt_tokens = (calc_prompt_chars // 4) + (calc_images * 258)
|
|
476
|
+
completion_tokens = len(raw_text) // 4
|
|
477
|
+
return {
|
|
478
|
+
"prompt_tokens": max(1, prompt_tokens),
|
|
479
|
+
"completion_tokens": max(1, completion_tokens),
|
|
480
|
+
"total_tokens": max(1, prompt_tokens + completion_tokens),
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
def _resolve_token_usage(
|
|
484
|
+
self, response, messages: list[BaseMessage], raw_text: str
|
|
485
|
+
) -> tuple[dict, bool]:
|
|
486
|
+
"""Token usage for the call and whether the provider measured it.
|
|
487
|
+
|
|
488
|
+
Falls back to an estimate when the response carries no usage
|
|
489
|
+
metadata; the flag lets the caller keep an estimate out of the
|
|
490
|
+
ledger's chars-per-token calibration (an estimate is itself derived
|
|
491
|
+
from character counts, so calibrating on it would be circular).
|
|
492
|
+
"""
|
|
493
|
+
step_token_usage = self._token_usage_from_response(response)
|
|
494
|
+
if step_token_usage and step_token_usage.get("total_tokens", 0) > 0:
|
|
495
|
+
return step_token_usage, True
|
|
496
|
+
return self._estimate_token_usage(messages, raw_text), False
|
|
497
|
+
|
|
498
|
+
def _record_llm_trace(self, step_token_usage: dict, raw_text: str) -> None:
|
|
499
|
+
"""Records the llm_call trace for this turn in the DataEngine."""
|
|
500
|
+
if self.ctx.data_engine:
|
|
501
|
+
current_step_id = getattr(self.ctx.data_engine, "current_step_id", None)
|
|
502
|
+
self.ctx.data_engine.record_trace(
|
|
503
|
+
type="llm_call",
|
|
504
|
+
name="FlashRunner",
|
|
505
|
+
payload={"token_usage": step_token_usage, "response": raw_text},
|
|
506
|
+
step_id=current_step_id,
|
|
507
|
+
status="success",
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def _resolve_tool_calls(self, response, raw_text: str) -> list:
|
|
511
|
+
"""Returns native tool calls, falling back to text parsing if absent."""
|
|
512
|
+
tool_calls = response.tool_calls or []
|
|
513
|
+
|
|
514
|
+
# Fallback text parsing if tool_calls not parsed natively
|
|
515
|
+
if not tool_calls and "```json" in raw_text:
|
|
516
|
+
parsed = parse_structured(raw_text)
|
|
517
|
+
if isinstance(parsed, ParseFailure):
|
|
518
|
+
logger.warning(
|
|
519
|
+
"FlashRunner response contained a JSON block that could"
|
|
520
|
+
f" not be parsed: {parsed.error}"
|
|
521
|
+
)
|
|
522
|
+
elif isinstance(parsed, dict) and "name" in parsed:
|
|
523
|
+
tool_calls = [
|
|
524
|
+
{
|
|
525
|
+
"name": parsed["name"],
|
|
526
|
+
"args": parsed.get("args", {}),
|
|
527
|
+
"id": str(uuid.uuid4()),
|
|
528
|
+
}
|
|
529
|
+
]
|
|
530
|
+
return tool_calls
|
|
531
|
+
|
|
532
|
+
# ------------------------------------------------------------------
|
|
533
|
+
# Per-tool-call helpers (act / record / report)
|
|
534
|
+
# ------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
async def _finalize_task_report(
|
|
537
|
+
self,
|
|
538
|
+
name: str,
|
|
539
|
+
args: dict,
|
|
540
|
+
tc_id: str,
|
|
541
|
+
raw_text: str,
|
|
542
|
+
step_token_usage: dict,
|
|
543
|
+
pre_screenshot_bytes,
|
|
544
|
+
xml_list,
|
|
545
|
+
messages: list[BaseMessage],
|
|
546
|
+
native_text: str | None = None,
|
|
547
|
+
) -> dict:
|
|
548
|
+
"""Records the final step, acknowledges the tool call, and flushes."""
|
|
549
|
+
final_report = args
|
|
550
|
+
if self.ctx.data_engine:
|
|
551
|
+
try:
|
|
552
|
+
if self.ctx.data_engine.current_step_id is None:
|
|
553
|
+
self.ctx.data_engine.allocate_step_id()
|
|
554
|
+
self.ctx.data_engine.record_step(
|
|
555
|
+
pre_screenshot_bytes=pre_screenshot_bytes,
|
|
556
|
+
ui_tree=xml_list,
|
|
557
|
+
action_taken={"action": "report_task_status", "args": args},
|
|
558
|
+
operator_raw_thinking=raw_text,
|
|
559
|
+
operator_native_thinking=native_text or None,
|
|
560
|
+
last_execution_result={"result": "Task completed with final report."},
|
|
561
|
+
extra_metadata={"token_usage": step_token_usage},
|
|
562
|
+
)
|
|
563
|
+
except Exception as step_err:
|
|
564
|
+
logger.warning(f"Error recording final step in FlashRunner: {step_err}")
|
|
565
|
+
messages.append(
|
|
566
|
+
ToolMessage(
|
|
567
|
+
tool_call_id=tc_id,
|
|
568
|
+
name=name,
|
|
569
|
+
content=json.dumps({"status": "acknowledged"}),
|
|
570
|
+
status="success",
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
if self.summarizer:
|
|
574
|
+
await self.summarizer.flush()
|
|
575
|
+
return final_report
|
|
576
|
+
|
|
577
|
+
async def _capture_post_screenshot(
|
|
578
|
+
self, exec_result, name: str, action_names, state: State | None = None
|
|
579
|
+
):
|
|
580
|
+
"""Returns the post-action screenshot, capturing a fallback if needed.
|
|
581
|
+
|
|
582
|
+
When a device action produced no observation (a failed action, an
|
|
583
|
+
unknown tool, ...) the screen is observed through the very same
|
|
584
|
+
pipeline the executor's success path uses (:func:`observe`), so the
|
|
585
|
+
next observation tail carries the indexed minimal UI list and
|
|
586
|
+
``state`` gets the same refreshed screenshot path and indexed
|
|
587
|
+
elements. Nothing is captured for non-action tools.
|
|
588
|
+
"""
|
|
589
|
+
post_img_bytes = exec_result.screenshot_bytes
|
|
590
|
+
if post_img_bytes or name not in action_names:
|
|
591
|
+
return post_img_bytes
|
|
592
|
+
try:
|
|
593
|
+
obs, screenshot_bytes = await observe(self.ctx, self.controller, settle_ms=0)
|
|
594
|
+
except Exception as shot_err:
|
|
595
|
+
logger.warning(f"Failed to capture fallback screenshot in FlashRunner: {shot_err}")
|
|
596
|
+
return post_img_bytes
|
|
597
|
+
if not obs.ok:
|
|
598
|
+
logger.warning(f"Failed to capture fallback screenshot in FlashRunner: {obs.message}")
|
|
599
|
+
return post_img_bytes
|
|
600
|
+
post_img_bytes = screenshot_bytes
|
|
601
|
+
if not exec_result.ui_elements_text:
|
|
602
|
+
exec_result.ui_elements_text = obs.elements_text
|
|
603
|
+
if not exec_result.screenshot_path and obs.screenshot_path:
|
|
604
|
+
exec_result.screenshot_path = obs.screenshot_path
|
|
605
|
+
if state is not None:
|
|
606
|
+
if obs.hierarchy_ok:
|
|
607
|
+
state.indexed_points = [el["center"] for el in obs.elements]
|
|
608
|
+
state.indexed_elements = obs.elements
|
|
609
|
+
if obs.screenshot_path:
|
|
610
|
+
state.latest_screenshot = obs.screenshot_path
|
|
611
|
+
return post_img_bytes
|
|
612
|
+
|
|
613
|
+
def _extract_normalized_coordinates(self, name: str, args: dict, resolved=None):
|
|
614
|
+
"""Extracts and enriches coordinate metadata for the recorded action.
|
|
615
|
+
|
|
616
|
+
``resolved`` is the normalized point the executor sent a point action
|
|
617
|
+
to; it is the coordinate of record when the model addressed an element
|
|
618
|
+
by index (the arguments then carry no point at all).
|
|
619
|
+
"""
|
|
620
|
+
norm_coords = None
|
|
621
|
+
norm_start = None
|
|
622
|
+
norm_end = None
|
|
623
|
+
if name in ("click", "tap", "long_press", "input_text") and (
|
|
624
|
+
isinstance(resolved, (list, tuple)) and len(resolved) == 2
|
|
625
|
+
):
|
|
626
|
+
return [int(resolved[0]), int(resolved[1])], None, None
|
|
627
|
+
if name == "swipe":
|
|
628
|
+
kind, target_val, _ = parse_swipe_parameters(args)
|
|
629
|
+
if kind == "coords" and isinstance(target_val, list) and len(target_val) == 4:
|
|
630
|
+
norm_coords = target_val
|
|
631
|
+
norm_start = target_val[:2]
|
|
632
|
+
norm_end = target_val[2:]
|
|
633
|
+
elif kind == "direction" and isinstance(target_val, str):
|
|
634
|
+
g_lower = target_val.lower()
|
|
635
|
+
if "up" in g_lower:
|
|
636
|
+
norm_coords = [600, 700, 600, 300]
|
|
637
|
+
elif "down" in g_lower:
|
|
638
|
+
norm_coords = [600, 300, 600, 700]
|
|
639
|
+
elif "left" in g_lower:
|
|
640
|
+
norm_coords = [750, 500, 250, 500]
|
|
641
|
+
elif "right" in g_lower:
|
|
642
|
+
norm_coords = [250, 500, 750, 500]
|
|
643
|
+
if norm_coords:
|
|
644
|
+
norm_start = norm_coords[:2]
|
|
645
|
+
norm_end = norm_coords[2:]
|
|
646
|
+
elif name in ("click", "tap", "long_press", "input_text"):
|
|
647
|
+
target = args.get("target") or args.get("coordinates")
|
|
648
|
+
if isinstance(target, (list, tuple)) and len(target) == 2:
|
|
649
|
+
norm_coords = list(target)
|
|
650
|
+
elif isinstance(target, str):
|
|
651
|
+
nums = re.findall(r"-?\d+(?:\.\d+)?", target)
|
|
652
|
+
if len(nums) == 2:
|
|
653
|
+
norm_coords = [int(float(nums[0])), int(float(nums[1]))]
|
|
654
|
+
return norm_coords, norm_start, norm_end
|
|
655
|
+
|
|
656
|
+
def _build_action_record(self, name: str, args: dict, exec_result) -> dict:
|
|
657
|
+
"""The action as the step record and the visual lens see it.
|
|
658
|
+
|
|
659
|
+
Flash records the model's own 0–1000 coordinates verbatim (an index
|
|
660
|
+
target records the point the executor resolved it to); the explicit
|
|
661
|
+
space marker keeps every later normalization pass (agent-friendly
|
|
662
|
+
steps, MCP inspector) a no-op on this record. The tool arguments ride
|
|
663
|
+
along under ``args``, and the target semantics the executor validated
|
|
664
|
+
(observed element fields for an index, the model's own
|
|
665
|
+
``target_description`` for a point) land top level in the shape Pro
|
|
666
|
+
records.
|
|
667
|
+
"""
|
|
668
|
+
metadata = getattr(exec_result, "metadata", None) or {}
|
|
669
|
+
resolved = metadata.get("target_coordinates")
|
|
670
|
+
norm_coords, norm_start, norm_end = self._extract_normalized_coordinates(
|
|
671
|
+
name, args, resolved=resolved
|
|
672
|
+
)
|
|
673
|
+
given = args.get("target") or args.get("coordinates") or args.get("sequence")
|
|
674
|
+
if isinstance(given, (int, float)) and not isinstance(given, bool):
|
|
675
|
+
given = None # An element index is not a coordinate.
|
|
676
|
+
action_dict = {
|
|
677
|
+
"action": name,
|
|
678
|
+
"coordinates": given or norm_coords,
|
|
679
|
+
COORDINATE_SPACE_KEY: COORDINATE_SPACE_NORMALIZED,
|
|
680
|
+
"args": args,
|
|
681
|
+
}
|
|
682
|
+
if norm_coords:
|
|
683
|
+
action_dict["normalized_coordinates"] = norm_coords
|
|
684
|
+
if norm_start and norm_end:
|
|
685
|
+
action_dict["normalized_start_coordinates"] = norm_start
|
|
686
|
+
action_dict["normalized_end_coordinates"] = norm_end
|
|
687
|
+
action_dict.update(metadata.get("target_semantics") or {})
|
|
688
|
+
return action_dict
|
|
689
|
+
|
|
690
|
+
def _record_action_step(
|
|
691
|
+
self,
|
|
692
|
+
name: str,
|
|
693
|
+
args: dict,
|
|
694
|
+
exec_result,
|
|
695
|
+
raw_text: str,
|
|
696
|
+
step_token_usage: dict,
|
|
697
|
+
pre_screenshot_bytes,
|
|
698
|
+
xml_list,
|
|
699
|
+
post_img_bytes,
|
|
700
|
+
injected: str | None = None,
|
|
701
|
+
native_text: str | None = None,
|
|
702
|
+
action_dict: dict | None = None,
|
|
703
|
+
):
|
|
704
|
+
"""Records telemetry / step in DataEngine; returns the step id or None."""
|
|
705
|
+
recorded_step_id = None
|
|
706
|
+
try:
|
|
707
|
+
if self.ctx.data_engine.current_step_id is None:
|
|
708
|
+
self.ctx.data_engine.allocate_step_id()
|
|
709
|
+
|
|
710
|
+
if action_dict is None:
|
|
711
|
+
action_dict = self._build_action_record(name, args, exec_result)
|
|
712
|
+
|
|
713
|
+
succeeded = exec_result.status == "success"
|
|
714
|
+
last_execution_result = {
|
|
715
|
+
"status": "dispatched" if succeeded else "failed",
|
|
716
|
+
"result": exec_result.text_summary,
|
|
717
|
+
}
|
|
718
|
+
if not succeeded:
|
|
719
|
+
last_execution_result["error"] = exec_result.text_summary
|
|
720
|
+
|
|
721
|
+
extra_metadata: dict = {"token_usage": step_token_usage}
|
|
722
|
+
if injected:
|
|
723
|
+
# Stamped verbatim on the step it reached: the chunk ledger
|
|
724
|
+
# keeps it as a never-evicted line at every compression level.
|
|
725
|
+
extra_metadata["injected_instruction"] = injected
|
|
726
|
+
|
|
727
|
+
recorded_step_id = self.ctx.data_engine.record_step(
|
|
728
|
+
pre_screenshot_bytes=pre_screenshot_bytes,
|
|
729
|
+
post_screenshot_bytes=post_img_bytes,
|
|
730
|
+
ui_tree=(exec_result.ui_elements_text or xml_list),
|
|
731
|
+
action_taken=action_dict,
|
|
732
|
+
operator_raw_thinking=raw_text,
|
|
733
|
+
operator_native_thinking=native_text or None,
|
|
734
|
+
last_execution_result=last_execution_result,
|
|
735
|
+
extra_metadata=extra_metadata,
|
|
736
|
+
)
|
|
737
|
+
self._notify_history_chunker(recorded_step_id)
|
|
738
|
+
except Exception as step_err:
|
|
739
|
+
logger.warning(f"Error recording step in FlashRunner: {step_err}")
|
|
740
|
+
return recorded_step_id
|
|
741
|
+
|
|
742
|
+
def _notify_history_chunker(self, step_id) -> None:
|
|
743
|
+
"""Stamps the recorded step for the chunk manager (single segment, no plan)."""
|
|
744
|
+
ledger = getattr(self.ctx, "transcript_ledger", None)
|
|
745
|
+
chunker = getattr(ledger, "chunker", None) if ledger is not None else None
|
|
746
|
+
if chunker is None or step_id is None:
|
|
747
|
+
return
|
|
748
|
+
try:
|
|
749
|
+
chunker.on_step_stamped(str(step_id), None)
|
|
750
|
+
except Exception as e:
|
|
751
|
+
logger.warning(f"History chunker step stamp failed: {e}")
|
|
752
|
+
|
|
753
|
+
async def _execute_and_record_action(
|
|
754
|
+
self,
|
|
755
|
+
name: str,
|
|
756
|
+
args: dict,
|
|
757
|
+
tc_id: str,
|
|
758
|
+
state: State,
|
|
759
|
+
messages: list[BaseMessage],
|
|
760
|
+
raw_text: str,
|
|
761
|
+
step_token_usage: dict,
|
|
762
|
+
pre_screenshot_bytes,
|
|
763
|
+
xml_list,
|
|
764
|
+
action_sequence: int,
|
|
765
|
+
turn: _TurnRecord,
|
|
766
|
+
injected: str | None = None,
|
|
767
|
+
native_text: str | None = None,
|
|
768
|
+
index_elements: list[dict] | None = None,
|
|
769
|
+
):
|
|
770
|
+
"""Executes one tool call, records it, and updates loop state.
|
|
771
|
+
|
|
772
|
+
The tool message carries the outcome text only; the post-action
|
|
773
|
+
screenshot and UI list become the next turn's observation tail.
|
|
774
|
+
``index_elements`` is the turn's element snapshot index targets
|
|
775
|
+
resolve against (see :meth:`_process_tool_calls`); ``None`` resolves
|
|
776
|
+
against the state's current list. Returns the (possibly updated)
|
|
777
|
+
pre_screenshot_bytes, xml_list, and action_sequence for the next
|
|
778
|
+
iteration.
|
|
779
|
+
"""
|
|
780
|
+
# Dynamic dispatch set: manifest device actions plus any backend
|
|
781
|
+
# extension tools, so extension steps are recorded like actions.
|
|
782
|
+
action_names = self.executor.action_tool_names
|
|
783
|
+
try:
|
|
784
|
+
exec_result = await self.executor.execute(
|
|
785
|
+
name, args, tc_id, state, index_elements=index_elements
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
post_img_bytes = await self._capture_post_screenshot(
|
|
789
|
+
exec_result, name, action_names, state
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
# One record shape for the DataEngine step and the visual lens.
|
|
793
|
+
action_dict = (
|
|
794
|
+
self._build_action_record(name, args, exec_result) if name in action_names else None
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
# Record telemetry / step in DataEngine
|
|
798
|
+
recorded_step_id = None
|
|
799
|
+
if self.ctx.data_engine and name in action_names:
|
|
800
|
+
recorded_step_id = self._record_action_step(
|
|
801
|
+
name,
|
|
802
|
+
args,
|
|
803
|
+
exec_result,
|
|
804
|
+
raw_text,
|
|
805
|
+
step_token_usage,
|
|
806
|
+
pre_screenshot_bytes,
|
|
807
|
+
xml_list,
|
|
808
|
+
post_img_bytes,
|
|
809
|
+
injected=injected,
|
|
810
|
+
native_text=native_text,
|
|
811
|
+
action_dict=action_dict,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if name in action_names:
|
|
815
|
+
turn.step_keys.append(str(recorded_step_id) if recorded_step_id else str(tc_id))
|
|
816
|
+
turn.actions.append((name, exec_result.status, exec_result.text_summary))
|
|
817
|
+
|
|
818
|
+
# ⚡ Non-blocking dispatch of objective visual transition summarizer
|
|
819
|
+
if self.summarizer and name in action_names:
|
|
820
|
+
action_sequence += 1
|
|
821
|
+
self.summarizer.dispatch(
|
|
822
|
+
step_number=action_sequence,
|
|
823
|
+
action_name=name,
|
|
824
|
+
# The recorded action minus its verb: the same shape the Pro
|
|
825
|
+
# summarizer hands the lens, so the phrase renders alike.
|
|
826
|
+
action_args={k: v for k, v in action_dict.items() if k != "action"},
|
|
827
|
+
pre_img_bytes=pre_screenshot_bytes,
|
|
828
|
+
post_img_bytes=post_img_bytes,
|
|
829
|
+
exec_outcome=exec_result.text_summary,
|
|
830
|
+
action_key=str(tc_id),
|
|
831
|
+
data_engine_step_id=recorded_step_id,
|
|
832
|
+
# The model's own reasoning for this turn steers the lens's
|
|
833
|
+
# attention; the target it named rides on ``args``.
|
|
834
|
+
focus=build_focus_context(
|
|
835
|
+
intent=raw_text,
|
|
836
|
+
goal=self.goal,
|
|
837
|
+
injected_instruction=injected,
|
|
838
|
+
),
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if post_img_bytes:
|
|
842
|
+
pre_screenshot_bytes = post_img_bytes
|
|
843
|
+
if exec_result.ui_elements_text:
|
|
844
|
+
xml_list = exec_result.ui_elements_text
|
|
845
|
+
|
|
846
|
+
# Helper tools may return multimodal blocks (a step screenshot);
|
|
847
|
+
# device actions always report text — their post screenshot is the
|
|
848
|
+
# next observation tail, never a tool-message image. The image
|
|
849
|
+
# carrier follows the model's provider (tool_result_messages).
|
|
850
|
+
raw_blocks = exec_result.raw_result if name not in action_names else None
|
|
851
|
+
content = (
|
|
852
|
+
raw_blocks
|
|
853
|
+
if isinstance(raw_blocks, list) and raw_blocks
|
|
854
|
+
else exec_result.text_summary or f"Action '{name}' dispatched."
|
|
855
|
+
)
|
|
856
|
+
messages.extend(
|
|
857
|
+
tool_result_messages(
|
|
858
|
+
tc_id, content, name=name, status=exec_result.status, llm=self._llm
|
|
859
|
+
)
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
except Exception as e:
|
|
863
|
+
logger.error(f"Error executing tool {name}: {e}")
|
|
864
|
+
if name in action_names:
|
|
865
|
+
turn.step_keys.append(str(tc_id))
|
|
866
|
+
turn.actions.append((name, "error", f"Error executing tool {name}: {e}"))
|
|
867
|
+
messages.append(
|
|
868
|
+
ToolMessage(
|
|
869
|
+
tool_call_id=tc_id,
|
|
870
|
+
name=name,
|
|
871
|
+
content=f"Error executing tool {name}: {e}",
|
|
872
|
+
status="error",
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
return pre_screenshot_bytes, xml_list, action_sequence
|
|
876
|
+
|
|
877
|
+
async def _invoke_model(self, llm, current_tools: list, messages: list[BaseMessage]):
|
|
878
|
+
"""Binds the active tools and invokes the model through the LLM gateway."""
|
|
879
|
+
# Bind active tools
|
|
880
|
+
bound_llm = llm.bind_tools(current_tools)
|
|
881
|
+
|
|
882
|
+
# Invoke Model. Streaming, live-token UI deltas, classified
|
|
883
|
+
# retries, and pause/resume are all owned by the LLM gateway
|
|
884
|
+
# (acomplete); a typed LLMCallError propagates if it gives up.
|
|
885
|
+
return await invoke_llm_with_timeout_message(
|
|
886
|
+
acomplete(bound_llm, messages), timeout_seconds=10, hard_timeout=180
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
async def _process_tool_calls(
|
|
890
|
+
self,
|
|
891
|
+
tool_calls: list,
|
|
892
|
+
state: State,
|
|
893
|
+
messages: list[BaseMessage],
|
|
894
|
+
raw_text: str,
|
|
895
|
+
step_token_usage: dict,
|
|
896
|
+
pre_screenshot_bytes,
|
|
897
|
+
xml_list,
|
|
898
|
+
action_sequence: int,
|
|
899
|
+
turn: _TurnRecord,
|
|
900
|
+
injected: str | None = None,
|
|
901
|
+
native_text: str | None = None,
|
|
902
|
+
):
|
|
903
|
+
"""Dispatches the turn's tool calls.
|
|
904
|
+
|
|
905
|
+
Every index target of the turn resolves against the element list the
|
|
906
|
+
model decided on: the list of the observation this turn was built
|
|
907
|
+
from, snapshotted here before the first action. Each action still
|
|
908
|
+
refreshes ``state.indexed_elements`` (that refreshed list is what the
|
|
909
|
+
next observation shows), so without the snapshot ``click(5)`` after
|
|
910
|
+
``click(3)`` would be resolved against the screen after the first
|
|
911
|
+
click.
|
|
912
|
+
|
|
913
|
+
The snapshot is the very list object the observation left on the
|
|
914
|
+
state (not a copy), and observations replace the state's list rather
|
|
915
|
+
than mutating it, so ``ask_explorer`` candidates registered before
|
|
916
|
+
any action land in the snapshot by construction. When a helper tool
|
|
917
|
+
appends candidates after an action has already replaced the state's
|
|
918
|
+
list, the explorer numbered them against that replaced list, so the
|
|
919
|
+
snapshot is re-taken from it: indices the explorer just handed out
|
|
920
|
+
resolve correctly, at the cost of the pre-action numbering (mixing
|
|
921
|
+
the two in one turn is the model's error, not a supported case).
|
|
922
|
+
|
|
923
|
+
Returns (final_report, pre_screenshot_bytes, xml_list, action_sequence);
|
|
924
|
+
final_report is non-None only when 'report_task_status' was called.
|
|
925
|
+
"""
|
|
926
|
+
action_names = self.executor.action_tool_names
|
|
927
|
+
index_elements = self._turn_index_snapshot(state)
|
|
928
|
+
for tc in tool_calls:
|
|
929
|
+
name = tc["name"].split(":")[-1] if ":" in tc["name"] else tc["name"]
|
|
930
|
+
args = tc.get("args") or {}
|
|
931
|
+
tc_id = tc.get("id") or str(uuid.uuid4())
|
|
932
|
+
logger.info(f"Executing Flash tool: {name}({args})")
|
|
933
|
+
|
|
934
|
+
if name == "report_task_status":
|
|
935
|
+
final_report = await self._finalize_task_report(
|
|
936
|
+
name,
|
|
937
|
+
args,
|
|
938
|
+
tc_id,
|
|
939
|
+
raw_text,
|
|
940
|
+
step_token_usage,
|
|
941
|
+
pre_screenshot_bytes,
|
|
942
|
+
xml_list,
|
|
943
|
+
messages,
|
|
944
|
+
native_text=native_text,
|
|
945
|
+
)
|
|
946
|
+
return final_report, pre_screenshot_bytes, xml_list, action_sequence
|
|
947
|
+
|
|
948
|
+
elements_before = getattr(state, "indexed_elements", None)
|
|
949
|
+
before_len = len(elements_before) if isinstance(elements_before, list) else 0
|
|
950
|
+
pre_screenshot_bytes, xml_list, action_sequence = await self._execute_and_record_action(
|
|
951
|
+
name,
|
|
952
|
+
args,
|
|
953
|
+
tc_id,
|
|
954
|
+
state,
|
|
955
|
+
messages,
|
|
956
|
+
raw_text,
|
|
957
|
+
step_token_usage,
|
|
958
|
+
pre_screenshot_bytes,
|
|
959
|
+
xml_list,
|
|
960
|
+
action_sequence,
|
|
961
|
+
turn,
|
|
962
|
+
injected=injected,
|
|
963
|
+
native_text=native_text,
|
|
964
|
+
index_elements=index_elements,
|
|
965
|
+
)
|
|
966
|
+
if name not in action_names:
|
|
967
|
+
index_elements = self._extend_index_snapshot(state, index_elements, before_len)
|
|
968
|
+
return None, pre_screenshot_bytes, xml_list, action_sequence
|
|
969
|
+
|
|
970
|
+
@staticmethod
|
|
971
|
+
def _turn_index_snapshot(state: State) -> list[dict]:
|
|
972
|
+
"""The element list this turn's index targets resolve against.
|
|
973
|
+
|
|
974
|
+
The state's list object itself, so in-place appends (explorer
|
|
975
|
+
candidates) before any action join the snapshot; an empty list is
|
|
976
|
+
installed on the state when there is none, for the same reason.
|
|
977
|
+
"""
|
|
978
|
+
elements = getattr(state, "indexed_elements", None)
|
|
979
|
+
if not isinstance(elements, list):
|
|
980
|
+
elements = []
|
|
981
|
+
try:
|
|
982
|
+
state.indexed_elements = elements
|
|
983
|
+
except (AttributeError, TypeError, ValueError):
|
|
984
|
+
pass
|
|
985
|
+
return elements
|
|
986
|
+
|
|
987
|
+
@staticmethod
|
|
988
|
+
def _extend_index_snapshot(state: State, snapshot: list[dict], before_len: int) -> list[dict]:
|
|
989
|
+
"""Re-takes the snapshot after a helper tool appended candidates to a
|
|
990
|
+
list that is no longer the snapshot (an action replaced it first).
|
|
991
|
+
|
|
992
|
+
Appends into the snapshot itself need nothing: the explorer numbered
|
|
993
|
+
them against the snapshot. Only growth of a different list object
|
|
994
|
+
moves the snapshot, since that is the list the new numbers refer to.
|
|
995
|
+
"""
|
|
996
|
+
current = getattr(state, "indexed_elements", None)
|
|
997
|
+
if not isinstance(current, list) or current is snapshot:
|
|
998
|
+
return snapshot
|
|
999
|
+
if len(current) > before_len:
|
|
1000
|
+
return current
|
|
1001
|
+
return snapshot
|
|
1002
|
+
|
|
1003
|
+
async def _prepare_conversation(self, state: State, tools_declaration: list):
|
|
1004
|
+
"""Installs the static prefix and captures the initial device state."""
|
|
1005
|
+
ledger = self._build_ledger()
|
|
1006
|
+
ledger.set_static_prefix(
|
|
1007
|
+
[SystemMessage(content=self._render_system_prompt(tools_declaration))]
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
# Capture Initial State (Screenshot + UI Tree)
|
|
1011
|
+
shot_path, img_bytes, xml_list = await capture_screenshot_and_parse_ui(
|
|
1012
|
+
self.ctx, state, self.controller, skip_settling=False
|
|
1013
|
+
)
|
|
1014
|
+
state.latest_screenshot = shot_path
|
|
1015
|
+
return ledger, img_bytes, xml_list
|
|
1016
|
+
|
|
1017
|
+
@staticmethod
|
|
1018
|
+
def _commit_turn(ledger: TranscriptLedger, turn: _TurnRecord | None) -> None:
|
|
1019
|
+
"""Commits the previous turn once its step ids and outcomes are known."""
|
|
1020
|
+
if turn is None:
|
|
1021
|
+
return
|
|
1022
|
+
ledger.commit_staged(
|
|
1023
|
+
step_key=turn.step_keys[0] if turn.step_keys else None,
|
|
1024
|
+
validator_result=turn.result(),
|
|
1025
|
+
extra_step_keys=turn.step_keys[1:],
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
@trace(type="agent", name="FlashRunner")
|
|
1029
|
+
async def run(self, state: State) -> dict:
|
|
1030
|
+
limit = self.turn_limit
|
|
1031
|
+
logger.info(
|
|
1032
|
+
f"Starting Artemis Flash reactive loop for goal: {self.goal}"
|
|
1033
|
+
f" (turn limit: {limit if limit else 'unlimited'})"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
# 1. Initialize Universal LLM via Service Layer
|
|
1037
|
+
llm = self._init_llm()
|
|
1038
|
+
self._llm = llm
|
|
1039
|
+
|
|
1040
|
+
tools_declaration = self._get_tools()
|
|
1041
|
+
report_only_tools = [t for t in tools_declaration if t.name == "report_task_status"]
|
|
1042
|
+
|
|
1043
|
+
ledger, img_bytes, xml_list = await self._prepare_conversation(state, tools_declaration)
|
|
1044
|
+
|
|
1045
|
+
turns = 0
|
|
1046
|
+
action_sequence = 0
|
|
1047
|
+
final_report = None
|
|
1048
|
+
current_pre_screenshot_bytes = img_bytes
|
|
1049
|
+
current_xml_list = xml_list
|
|
1050
|
+
previous_turn: _TurnRecord | None = None
|
|
1051
|
+
pending_notices: list[str] = []
|
|
1052
|
+
|
|
1053
|
+
while limit is None or turns < limit:
|
|
1054
|
+
turns += 1
|
|
1055
|
+
logger.info(f"--- Artemis Flash Turn {turns}{f'/{limit}' if limit else ''} ---")
|
|
1056
|
+
|
|
1057
|
+
if self.ctx.data_engine:
|
|
1058
|
+
self.ctx.data_engine.allocate_step_id()
|
|
1059
|
+
|
|
1060
|
+
# Commit the previous turn: its step ids and outcomes exist now.
|
|
1061
|
+
self._commit_turn(ledger, previous_turn)
|
|
1062
|
+
previous_turn = None
|
|
1063
|
+
|
|
1064
|
+
# Check for real-time injected instructions: the verbatim text
|
|
1065
|
+
# is stamped on the step, the guidance pair goes to the tail.
|
|
1066
|
+
injected, injected_guidance = await self._read_injected_instruction(
|
|
1067
|
+
ledger.elapsed_label()
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# Tool restriction on the final turn (bounded loops only)
|
|
1071
|
+
is_final = limit is not None and turns == limit
|
|
1072
|
+
tail = self._build_tail(
|
|
1073
|
+
ledger,
|
|
1074
|
+
turns,
|
|
1075
|
+
current_pre_screenshot_bytes,
|
|
1076
|
+
current_xml_list,
|
|
1077
|
+
injected=injected_guidance,
|
|
1078
|
+
notices=pending_notices,
|
|
1079
|
+
is_final=is_final,
|
|
1080
|
+
# The ledger judged the turn it just committed: a bare tool
|
|
1081
|
+
# call (or thinking only) earns a one-line reminder, no bounce.
|
|
1082
|
+
previous_turn_silent=bool(getattr(ledger, "last_turn_silent", False)),
|
|
1083
|
+
)
|
|
1084
|
+
pending_notices = []
|
|
1085
|
+
|
|
1086
|
+
# S + F + A + tail: chunk compression and the scrub edge advance here.
|
|
1087
|
+
messages = ledger.render([tail])
|
|
1088
|
+
turn_base = len(messages) - 1
|
|
1089
|
+
current_tools = report_only_tools if is_final else tools_declaration
|
|
1090
|
+
|
|
1091
|
+
response = await self._invoke_model(llm, current_tools, messages)
|
|
1092
|
+
|
|
1093
|
+
if response is None:
|
|
1094
|
+
break
|
|
1095
|
+
|
|
1096
|
+
messages.append(response)
|
|
1097
|
+
|
|
1098
|
+
# Extract thought and tool calls
|
|
1099
|
+
raw_text = self._extract_response_text(response)
|
|
1100
|
+
native_text = self._extract_response_thinking(response)
|
|
1101
|
+
|
|
1102
|
+
# Extract token usage metadata from response
|
|
1103
|
+
step_token_usage, measured = self._resolve_token_usage(response, messages, raw_text)
|
|
1104
|
+
# The ledger's compaction thresholds read the runner's own prompt
|
|
1105
|
+
# size, never the session-wide meter (which any agent overwrites).
|
|
1106
|
+
# The prompt as sent (``messages`` minus the just-appended reply)
|
|
1107
|
+
# rides along so the ledger can calibrate its chars-per-token
|
|
1108
|
+
# ratio — only from a provider measurement, never from the
|
|
1109
|
+
# estimate (same as the Pro operator).
|
|
1110
|
+
ledger.record_prompt_tokens(
|
|
1111
|
+
step_token_usage.get("prompt_tokens"),
|
|
1112
|
+
messages=messages[:-1] if measured else None,
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
self._record_llm_trace(step_token_usage, raw_text)
|
|
1116
|
+
|
|
1117
|
+
tool_calls = self._resolve_tool_calls(response, raw_text)
|
|
1118
|
+
|
|
1119
|
+
if not tool_calls:
|
|
1120
|
+
logger.info(
|
|
1121
|
+
f"FlashRunner received response without tool calls at turn {turns}:"
|
|
1122
|
+
f" {raw_text[:100]}..."
|
|
1123
|
+
)
|
|
1124
|
+
if is_final:
|
|
1125
|
+
final_report = {"status": "failed", "explanation": raw_text}
|
|
1126
|
+
break
|
|
1127
|
+
pending_notices.append(_NO_TOOL_CALL_NOTICE)
|
|
1128
|
+
ledger.stage_turn(messages[turn_base:])
|
|
1129
|
+
previous_turn = _TurnRecord()
|
|
1130
|
+
continue
|
|
1131
|
+
|
|
1132
|
+
# Process tool calls
|
|
1133
|
+
turn = _TurnRecord()
|
|
1134
|
+
(
|
|
1135
|
+
final_report_from_calls,
|
|
1136
|
+
current_pre_screenshot_bytes,
|
|
1137
|
+
current_xml_list,
|
|
1138
|
+
action_sequence,
|
|
1139
|
+
) = await self._process_tool_calls(
|
|
1140
|
+
tool_calls,
|
|
1141
|
+
state,
|
|
1142
|
+
messages,
|
|
1143
|
+
raw_text,
|
|
1144
|
+
step_token_usage,
|
|
1145
|
+
current_pre_screenshot_bytes,
|
|
1146
|
+
current_xml_list,
|
|
1147
|
+
action_sequence,
|
|
1148
|
+
turn,
|
|
1149
|
+
injected=injected,
|
|
1150
|
+
native_text=native_text,
|
|
1151
|
+
)
|
|
1152
|
+
ledger.stage_turn(messages[turn_base:])
|
|
1153
|
+
previous_turn = turn
|
|
1154
|
+
if final_report_from_calls is not None:
|
|
1155
|
+
return final_report_from_calls
|
|
1156
|
+
|
|
1157
|
+
if self.summarizer:
|
|
1158
|
+
await self.summarizer.flush()
|
|
1159
|
+
|
|
1160
|
+
if final_report is None:
|
|
1161
|
+
explanation = (
|
|
1162
|
+
"Max turns reached without final status report."
|
|
1163
|
+
if limit is not None and turns >= limit
|
|
1164
|
+
else "The model returned no response; the reactive loop stopped."
|
|
1165
|
+
)
|
|
1166
|
+
final_report = {"status": "failed", "explanation": explanation}
|
|
1167
|
+
return final_report
|