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,1055 @@
|
|
|
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
|
+
import asyncio
|
|
16
|
+
import base64
|
|
17
|
+
import functools
|
|
18
|
+
import json
|
|
19
|
+
from collections import Counter
|
|
20
|
+
from typing import Annotated, Literal
|
|
21
|
+
|
|
22
|
+
from langchain_core.messages import ToolMessage
|
|
23
|
+
from langchain_core.tools import StructuredTool
|
|
24
|
+
from langchain_core.tools.base import InjectedToolCallId
|
|
25
|
+
from langgraph.constants import END, START
|
|
26
|
+
from langgraph.graph import StateGraph
|
|
27
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
28
|
+
from langgraph.prebuilt import InjectedState
|
|
29
|
+
from pydantic import BaseModel, Field
|
|
30
|
+
|
|
31
|
+
from artemis.agents.checker.checker import run_final_check, verdicts_allow_release
|
|
32
|
+
from artemis.agents.operator.operator import OperatorNode
|
|
33
|
+
from artemis.agents.planner.planner import (
|
|
34
|
+
PlannerNode,
|
|
35
|
+
run_async_planner_validation,
|
|
36
|
+
)
|
|
37
|
+
from artemis.agents.summarizer.summarizer import SummarizerNode
|
|
38
|
+
from artemis.agents.validator.validator import ValidatorNode
|
|
39
|
+
from artemis.context import ArtemisContext
|
|
40
|
+
from artemis.graph.checkpoints import (
|
|
41
|
+
append_ledger_record,
|
|
42
|
+
attempt_trace_id,
|
|
43
|
+
compute_run_outcome,
|
|
44
|
+
final_check_enabled,
|
|
45
|
+
FINAL_CHECKPOINT_ID,
|
|
46
|
+
FINAL_SUBGOAL_TEXT,
|
|
47
|
+
harvest_finished_checkpoints,
|
|
48
|
+
has_ledger_records,
|
|
49
|
+
is_checker_note_key,
|
|
50
|
+
publish_checker_event,
|
|
51
|
+
queue_checkpoints,
|
|
52
|
+
read_ledger,
|
|
53
|
+
reinstate_check_items,
|
|
54
|
+
retire_check_items,
|
|
55
|
+
revert_subgoal_status,
|
|
56
|
+
settle_all_checkpoints,
|
|
57
|
+
spawn_pending_checkpoints,
|
|
58
|
+
sync_finding_lines,
|
|
59
|
+
verdict_payloads,
|
|
60
|
+
write_run_outcome,
|
|
61
|
+
)
|
|
62
|
+
from artemis.graph.perception import perception_node
|
|
63
|
+
from artemis.graph.state import State
|
|
64
|
+
from artemis.graph.visibility import strict_state
|
|
65
|
+
from artemis.tools.command_tool import (
|
|
66
|
+
HANG_GUIDANCE,
|
|
67
|
+
manage_task_wrapper,
|
|
68
|
+
run_adb_command_wrapper,
|
|
69
|
+
)
|
|
70
|
+
from artemis.tools.diagnostic_tool import ask_diagnoser_wrapper
|
|
71
|
+
from artemis.tools.explorer_tool import ask_explorer_wrapper
|
|
72
|
+
from artemis.tools.index import get_tools_from_wrappers
|
|
73
|
+
from artemis.tools.scratchpad import (
|
|
74
|
+
append_note_wrapper,
|
|
75
|
+
list_notes_wrapper,
|
|
76
|
+
read_note_wrapper,
|
|
77
|
+
save_note_wrapper,
|
|
78
|
+
update_note_wrapper,
|
|
79
|
+
)
|
|
80
|
+
from artemis.tools.tool_wrapper import invoke_tool_with_injection
|
|
81
|
+
from artemis.tools.video_tool import get_video_analyzer_tool
|
|
82
|
+
from artemis.utils.notes import (
|
|
83
|
+
SAVE_NOTE_ARG_CONTENT_DESC,
|
|
84
|
+
SAVE_NOTE_ARG_KEY_DESC,
|
|
85
|
+
UPDATE_NOTE_ARG_KEY_DESC,
|
|
86
|
+
UPDATE_NOTE_ARG_REPLACEMENT_DESC,
|
|
87
|
+
UPDATE_NOTE_ARG_TARGET_DESC,
|
|
88
|
+
get_note_file_path,
|
|
89
|
+
)
|
|
90
|
+
from artemis.utils.plan_grammar import (
|
|
91
|
+
milestones_changed,
|
|
92
|
+
missing_check_items,
|
|
93
|
+
new_top_level_completions,
|
|
94
|
+
parse_plan,
|
|
95
|
+
render_check_line,
|
|
96
|
+
restore_missing_check_items,
|
|
97
|
+
subgoal_hash,
|
|
98
|
+
unintended_milestone_edits,
|
|
99
|
+
)
|
|
100
|
+
from artemis.utils.task_tree import get_active_subgoal_hashes
|
|
101
|
+
from artemis.utils.cython_compat import CyFunctionDetector
|
|
102
|
+
from third_party.mobile_use.graph import (
|
|
103
|
+
CONVERGENCE_NODE,
|
|
104
|
+
add_convergence_edges,
|
|
105
|
+
add_convergence_node,
|
|
106
|
+
)
|
|
107
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
108
|
+
|
|
109
|
+
logger = get_logger(__name__)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _notify_history_chunker(ctx: ArtemisContext, method: str, *args) -> None:
|
|
113
|
+
"""Best-effort event feed to the transcript's L2/L3 chunk manager (M3).
|
|
114
|
+
|
|
115
|
+
The chunker only exists when the transcript flag is on; every failure is
|
|
116
|
+
swallowed — history compression must never gate the execution path.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
chunker = getattr(getattr(ctx, "transcript_ledger", None), "chunker", None)
|
|
120
|
+
if chunker is not None:
|
|
121
|
+
getattr(chunker, method)(*args)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.debug(f"History chunker notification '{method}' skipped: {e}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def execution_check_node(state: State, ctx: ArtemisContext):
|
|
127
|
+
state = strict_state(state, "execution_check")
|
|
128
|
+
logger.info("Starting execution_check_node")
|
|
129
|
+
|
|
130
|
+
if not ctx.data_engine:
|
|
131
|
+
return {"checker_success": True}
|
|
132
|
+
|
|
133
|
+
checker_success = True
|
|
134
|
+
|
|
135
|
+
def _record_turn(extra_metadata: dict) -> str | None:
|
|
136
|
+
"""Records this Operator turn in the DataEngine — unconditionally: every
|
|
137
|
+
turn (including planner-rejected ones) leaves a step record."""
|
|
138
|
+
raw_data = state.operator_raw_data
|
|
139
|
+
if not raw_data:
|
|
140
|
+
return None
|
|
141
|
+
try:
|
|
142
|
+
screenshot_b64 = raw_data.get("screenshot_b64")
|
|
143
|
+
xml_hierarchy = raw_data.get("xml_hierarchy")
|
|
144
|
+
ocr_results = raw_data.get("ocr_results")
|
|
145
|
+
|
|
146
|
+
screenshot_bytes = base64.b64decode(screenshot_b64)
|
|
147
|
+
|
|
148
|
+
action_taken = None
|
|
149
|
+
if state.structured_decisions:
|
|
150
|
+
try:
|
|
151
|
+
action_taken = json.loads(state.structured_decisions)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f"Failed to parse structured decisions: {e}")
|
|
154
|
+
|
|
155
|
+
subgoal_hash_val, sub_subgoal_hash = _get_active_subgoal_hashes(ctx)
|
|
156
|
+
# The user's injected instruction is stamped verbatim on the step
|
|
157
|
+
# it reached: the chunk ledger (band ③) preserves it as a
|
|
158
|
+
# never-evicted line at every compression level (§3.3).
|
|
159
|
+
injected = getattr(state, "injected_instruction", None)
|
|
160
|
+
step_id = ctx.data_engine.record_step(
|
|
161
|
+
pre_screenshot_bytes=screenshot_bytes,
|
|
162
|
+
ui_tree=xml_hierarchy,
|
|
163
|
+
ocr_result=ocr_results,
|
|
164
|
+
action_taken=action_taken,
|
|
165
|
+
operator_raw_thinking=getattr(state, "operator_raw_thinking", None),
|
|
166
|
+
operator_native_thinking=getattr(state, "operator_native_thinking", None),
|
|
167
|
+
extra_metadata={
|
|
168
|
+
"subgoal_hash": subgoal_hash_val,
|
|
169
|
+
"sub_subgoal_hash": sub_subgoal_hash,
|
|
170
|
+
"width": raw_data.get("width"),
|
|
171
|
+
"height": raw_data.get("height"),
|
|
172
|
+
**({"injected_instruction": injected} if injected else {}),
|
|
173
|
+
**extra_metadata,
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
logger.info(f"Recorded step in DataEngine: {step_id}")
|
|
177
|
+
# Milestone-switch fact source: the stamped subgoal hash of every
|
|
178
|
+
# executed step, compared consecutively inside the chunk manager.
|
|
179
|
+
_notify_history_chunker(ctx, "on_step_stamped", str(step_id), subgoal_hash_val)
|
|
180
|
+
return str(step_id)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"Failed to record step in DataEngine: {e}")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
# Harvest finished checkpoint attempts (non-blocking: done() tasks only).
|
|
186
|
+
check_findings = harvest_finished_checkpoints(ctx, state)
|
|
187
|
+
turn_metadata: dict = {}
|
|
188
|
+
|
|
189
|
+
if hasattr(ctx, "planner_task") and ctx.planner_task:
|
|
190
|
+
logger.info("Waiting for planner validation task to complete...")
|
|
191
|
+
awaited_planner_task = ctx.planner_task
|
|
192
|
+
try:
|
|
193
|
+
planner_result = await awaited_planner_task
|
|
194
|
+
ctx.planner_task = None
|
|
195
|
+
|
|
196
|
+
flagged = bool(planner_result and planner_result.get("status") == "failed")
|
|
197
|
+
if flagged:
|
|
198
|
+
# Advisory outcome: the plan change stays applied and the
|
|
199
|
+
# turn's actions run. The Operator only receives the concern
|
|
200
|
+
# and its reason through operator_feedback — the one feedback
|
|
201
|
+
# channel its next prompt renders.
|
|
202
|
+
logger.warning("Planner validation flagged the task plan changes.")
|
|
203
|
+
feedback = planner_result.get(
|
|
204
|
+
"feedback", "The reviewer had a concern about this plan change."
|
|
205
|
+
)
|
|
206
|
+
check_findings.append(
|
|
207
|
+
"[planner] Advisory review of your recent top-level plan"
|
|
208
|
+
f" change: {feedback} The change was NOT rolled back —"
|
|
209
|
+
" weigh this concern against your own observations and"
|
|
210
|
+
" correct the plan only if you agree."
|
|
211
|
+
)
|
|
212
|
+
turn_metadata["planner_flagged"] = True
|
|
213
|
+
else:
|
|
214
|
+
logger.info("Planner validation approved the task plan changes.")
|
|
215
|
+
|
|
216
|
+
# Advance the ratchet baseline either way: the reviewed content is
|
|
217
|
+
# the new reference, so an already-flagged change is not re-flagged
|
|
218
|
+
# on every subsequent write.
|
|
219
|
+
if ctx.pending_validated_plan is not None:
|
|
220
|
+
ctx.last_validated_plan = ctx.pending_validated_plan
|
|
221
|
+
ctx.pending_validated_plan = None
|
|
222
|
+
except asyncio.CancelledError:
|
|
223
|
+
if not awaited_planner_task.cancelled():
|
|
224
|
+
raise # this node itself is being cancelled, not the awaited task
|
|
225
|
+
logger.info("Planner validation task was superseded by a newer plan write.")
|
|
226
|
+
ctx.planner_task = None
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"Planner validation task failed: {e}")
|
|
229
|
+
|
|
230
|
+
# Every Operator turn is recorded — verification never gates history.
|
|
231
|
+
current_step_id = _record_turn(turn_metadata)
|
|
232
|
+
|
|
233
|
+
# Spawn queued checkpoints only now: the just-recorded step is the evidence
|
|
234
|
+
# anchor (its pre screenshot plus the previous turn's post state capture
|
|
235
|
+
# the completion moment). Superseded-but-finished attempts are booked here.
|
|
236
|
+
check_findings.extend(await spawn_pending_checkpoints(ctx, state, current_step_id))
|
|
237
|
+
|
|
238
|
+
update = {
|
|
239
|
+
"checker_success": checker_success,
|
|
240
|
+
"current_step_id": current_step_id,
|
|
241
|
+
"operator_feedback": check_findings or None,
|
|
242
|
+
}
|
|
243
|
+
if checker_success:
|
|
244
|
+
update["subagent_calls"] = []
|
|
245
|
+
return update
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def exit_settlement_node(state: State, ctx: ArtemisContext):
|
|
249
|
+
"""Two-phase exit: settlement (unconditional) then final review (gated).
|
|
250
|
+
|
|
251
|
+
Phase 1 — settlement barrier: the only intentional blocking wait in the
|
|
252
|
+
whole flow. Every started check attempt gets booked into the ledger before
|
|
253
|
+
exit, no matter which switches are on.
|
|
254
|
+
|
|
255
|
+
Phase 2 — final review (``disable_final_check`` off): audits the USER'S
|
|
256
|
+
ORIGINAL GOAL plus all declared check items. Unmet verify criteria route
|
|
257
|
+
back into the loop (bounded by ``final_check_max_attempts``); assert
|
|
258
|
+
failures never do.
|
|
259
|
+
"""
|
|
260
|
+
state = strict_state(state, "exit_settlement")
|
|
261
|
+
logger.info("Starting exit_settlement_node")
|
|
262
|
+
update: dict = {"exit_settlement_route": "end"}
|
|
263
|
+
if not ctx.data_engine:
|
|
264
|
+
return update
|
|
265
|
+
|
|
266
|
+
# Phase 1: settle every outstanding attempt (bounded by settlement_timeout).
|
|
267
|
+
await settle_all_checkpoints(ctx, state)
|
|
268
|
+
# Queued-but-never-spawned checkpoints are dropped here: their items simply
|
|
269
|
+
# remain unchecked and are reported as such.
|
|
270
|
+
ctx.pending_checkpoints.clear()
|
|
271
|
+
|
|
272
|
+
base_dir = ctx.data_engine.base_dir
|
|
273
|
+
plan_text = ""
|
|
274
|
+
plan_path = get_note_file_path(base_dir, "task_plan")
|
|
275
|
+
if plan_path.exists():
|
|
276
|
+
try:
|
|
277
|
+
plan_text = plan_path.read_text(encoding="utf-8")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Failed to read task plan at settlement: {e}")
|
|
280
|
+
snapshot = parse_plan(plan_text)
|
|
281
|
+
|
|
282
|
+
setup = ctx.execution_setup
|
|
283
|
+
user_stopped = bool(getattr(state, "user_stop_requested", False))
|
|
284
|
+
halted = bool(getattr(ctx, "assert_halt", False))
|
|
285
|
+
max_attempts = int(getattr(setup, "final_check_max_attempts", 3) or 3) if setup else 3
|
|
286
|
+
|
|
287
|
+
verify_blocked = False
|
|
288
|
+
blocked_findings: list[str] = []
|
|
289
|
+
if final_check_enabled(ctx):
|
|
290
|
+
attempt_no = ctx.final_check_attempts + 1
|
|
291
|
+
ctx.final_check_attempts = attempt_no
|
|
292
|
+
final_attempt_id = f"final#{attempt_no}"
|
|
293
|
+
checkpoint_timeout = float(getattr(setup, "checkpoint_timeout", 180.0) or 180.0)
|
|
294
|
+
ledger = read_ledger(base_dir)
|
|
295
|
+
|
|
296
|
+
def _final_event(status: str, verdicts: list[dict], **extra) -> None:
|
|
297
|
+
publish_checker_event(
|
|
298
|
+
ctx,
|
|
299
|
+
{
|
|
300
|
+
"event": "attempt_finished",
|
|
301
|
+
"phase": "final",
|
|
302
|
+
"attempt_id": final_attempt_id,
|
|
303
|
+
"checkpoint_id": FINAL_CHECKPOINT_ID,
|
|
304
|
+
"subgoal_text": FINAL_SUBGOAL_TEXT,
|
|
305
|
+
"trace_id": attempt_trace_id(ctx, final_attempt_id),
|
|
306
|
+
"status": status,
|
|
307
|
+
"verdicts": verdicts,
|
|
308
|
+
**extra,
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
report = await asyncio.wait_for(
|
|
314
|
+
run_final_check(
|
|
315
|
+
ctx,
|
|
316
|
+
goal=getattr(state, "initial_goal", ""),
|
|
317
|
+
plan_text=plan_text,
|
|
318
|
+
ledger=ledger,
|
|
319
|
+
check_items=list(snapshot.all_check_items),
|
|
320
|
+
attempt_id=final_attempt_id,
|
|
321
|
+
),
|
|
322
|
+
timeout=checkpoint_timeout,
|
|
323
|
+
)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
# Fail-open: release, but the verdict value stays inconclusive.
|
|
326
|
+
logger.warning(f"Final check errored ({e}); releasing fail-open.")
|
|
327
|
+
error_verdicts: list[dict] = []
|
|
328
|
+
for ci in snapshot.all_check_items:
|
|
329
|
+
append_ledger_record(
|
|
330
|
+
base_dir,
|
|
331
|
+
{
|
|
332
|
+
"attempt_id": final_attempt_id,
|
|
333
|
+
"checkpoint_id": FINAL_CHECKPOINT_ID,
|
|
334
|
+
"subgoal_text": FINAL_SUBGOAL_TEXT,
|
|
335
|
+
"trace_id": attempt_trace_id(ctx, final_attempt_id),
|
|
336
|
+
"item_text": ci.text,
|
|
337
|
+
"kind": ci.kind,
|
|
338
|
+
"when": ci.when,
|
|
339
|
+
"status": "inconclusive",
|
|
340
|
+
"evidence": f"final check error: {e}",
|
|
341
|
+
"anchor_step_id": None,
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
error_verdicts.append(
|
|
345
|
+
{
|
|
346
|
+
"item_text": ci.text,
|
|
347
|
+
"kind": ci.kind,
|
|
348
|
+
"status": "inconclusive",
|
|
349
|
+
"evidence": f"final check error: {e}",
|
|
350
|
+
"suggestion": "",
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
_final_event("error", error_verdicts, error=str(e), route="end")
|
|
354
|
+
report = None
|
|
355
|
+
|
|
356
|
+
if report is not None:
|
|
357
|
+
for v in report.verdicts:
|
|
358
|
+
when = next(
|
|
359
|
+
(
|
|
360
|
+
ci.when
|
|
361
|
+
for ci in snapshot.all_check_items
|
|
362
|
+
if ci.kind == v.kind and ci.text == v.item_text
|
|
363
|
+
),
|
|
364
|
+
"at_end",
|
|
365
|
+
)
|
|
366
|
+
append_ledger_record(
|
|
367
|
+
base_dir,
|
|
368
|
+
{
|
|
369
|
+
"attempt_id": final_attempt_id,
|
|
370
|
+
"checkpoint_id": FINAL_CHECKPOINT_ID,
|
|
371
|
+
"subgoal_text": FINAL_SUBGOAL_TEXT,
|
|
372
|
+
"trace_id": attempt_trace_id(ctx, final_attempt_id),
|
|
373
|
+
"item_text": v.item_text,
|
|
374
|
+
"kind": v.kind,
|
|
375
|
+
"when": when,
|
|
376
|
+
"status": v.status,
|
|
377
|
+
"evidence": v.evidence,
|
|
378
|
+
"suggestion": v.suggestion,
|
|
379
|
+
"anchor_step_id": None,
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
passed = verdicts_allow_release(report) and not report.unmet_subgoals
|
|
384
|
+
if not passed:
|
|
385
|
+
if user_stopped or halted or attempt_no >= max_attempts:
|
|
386
|
+
verify_blocked = True
|
|
387
|
+
for v in report.verdicts:
|
|
388
|
+
if v.kind == "verify" and v.status == "failed":
|
|
389
|
+
blocked_findings.append(
|
|
390
|
+
f"[verify failed] '{v.item_text}': {v.evidence}"
|
|
391
|
+
)
|
|
392
|
+
for text in report.unmet_subgoals:
|
|
393
|
+
blocked_findings.append(f"[unmet subgoal] '{text}'")
|
|
394
|
+
logger.warning(
|
|
395
|
+
"Final check found unmet verify criteria but the retry"
|
|
396
|
+
" budget is exhausted (or a stop/halt is latched);"
|
|
397
|
+
" ending with a blocked outcome."
|
|
398
|
+
)
|
|
399
|
+
_final_event(
|
|
400
|
+
"done",
|
|
401
|
+
verdict_payloads(report.verdicts),
|
|
402
|
+
unmet_subgoals=list(report.unmet_subgoals),
|
|
403
|
+
findings=list(blocked_findings),
|
|
404
|
+
route="blocked",
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
findings: list[str] = []
|
|
408
|
+
for text in report.unmet_subgoals:
|
|
409
|
+
if revert_subgoal_status(ctx, subgoal_hash(text)):
|
|
410
|
+
findings.append(
|
|
411
|
+
f"[final check] Subgoal '{text}' was set back to"
|
|
412
|
+
" in-progress: its acceptance criteria are not met."
|
|
413
|
+
)
|
|
414
|
+
for v in report.verdicts:
|
|
415
|
+
if v.kind == "verify" and v.status == "failed":
|
|
416
|
+
suggestion = f" Suggestion: {v.suggestion}" if v.suggestion else ""
|
|
417
|
+
findings.append(
|
|
418
|
+
f"[verify failed] '{v.item_text}': {v.evidence}{suggestion}"
|
|
419
|
+
)
|
|
420
|
+
update["operator_feedback"] = findings or None
|
|
421
|
+
update["exit_settlement_route"] = "continue"
|
|
422
|
+
_final_event(
|
|
423
|
+
"done",
|
|
424
|
+
verdict_payloads(report.verdicts),
|
|
425
|
+
unmet_subgoals=list(report.unmet_subgoals),
|
|
426
|
+
findings=list(findings),
|
|
427
|
+
route="continue",
|
|
428
|
+
)
|
|
429
|
+
return update
|
|
430
|
+
else:
|
|
431
|
+
_final_event(
|
|
432
|
+
"done",
|
|
433
|
+
verdict_payloads(report.verdicts),
|
|
434
|
+
unmet_subgoals=[],
|
|
435
|
+
findings=[],
|
|
436
|
+
route="end",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# END path: assemble the machine-readable run outcome.
|
|
440
|
+
records = read_ledger(base_dir)
|
|
441
|
+
retired = getattr(ctx, "guidance_retired_checks", None)
|
|
442
|
+
outcome = compute_run_outcome(
|
|
443
|
+
snapshot,
|
|
444
|
+
records,
|
|
445
|
+
verify_blocked=verify_blocked,
|
|
446
|
+
retired=retired if isinstance(retired, set) else None,
|
|
447
|
+
)
|
|
448
|
+
if snapshot.all_check_items or records or verify_blocked:
|
|
449
|
+
update["run_outcome"] = outcome.model_dump()
|
|
450
|
+
# BLOCKED/partial wrap-ups carry the last findings in the metadata file.
|
|
451
|
+
extra: dict = {"last_findings": blocked_findings} if blocked_findings else {}
|
|
452
|
+
# Which UI-hierarchy source served the run, so a report reader can tell a
|
|
453
|
+
# helper run from a UIAutomator2 fallback without reading logs.
|
|
454
|
+
from artemis.clients.screen_client_factory import hierarchy_backend_summary
|
|
455
|
+
|
|
456
|
+
environment = hierarchy_backend_summary(getattr(ctx, "ui_adb_client", None))
|
|
457
|
+
if environment:
|
|
458
|
+
extra["environment"] = environment
|
|
459
|
+
write_run_outcome(base_dir, outcome, extra or None)
|
|
460
|
+
publish_checker_event(
|
|
461
|
+
ctx,
|
|
462
|
+
{
|
|
463
|
+
"event": "run_outcome",
|
|
464
|
+
"phase": "outcome",
|
|
465
|
+
**outcome.model_dump(),
|
|
466
|
+
"last_findings": list(blocked_findings),
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
logger.info(
|
|
470
|
+
f"Run outcome: task_status={outcome.task_status},"
|
|
471
|
+
f" tests(passed={outcome.tests.passed}, failed={outcome.tests.failed},"
|
|
472
|
+
f" inconclusive={outcome.tests.inconclusive},"
|
|
473
|
+
f" unchecked={outcome.tests.unchecked},"
|
|
474
|
+
f" retired={outcome.tests.retired})"
|
|
475
|
+
)
|
|
476
|
+
return update
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def exit_settlement_gate(state: State) -> Literal["continue", "end"]:
|
|
480
|
+
route = getattr(state, "exit_settlement_route", None)
|
|
481
|
+
return "continue" if route == "continue" else "end"
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def execution_check_edge(
|
|
485
|
+
state: State,
|
|
486
|
+
) -> Literal["execute_decisions", "review_subgoals"]:
|
|
487
|
+
if state.checker_success:
|
|
488
|
+
if state.structured_decisions:
|
|
489
|
+
return "execute_decisions"
|
|
490
|
+
return "review_subgoals"
|
|
491
|
+
return "review_subgoals"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _get_active_subgoal_hashes(ctx: ArtemisContext) -> tuple[str, str | None]:
|
|
495
|
+
"""Parses task_plan.md to find the active subgoal and sub-subgoal hashes."""
|
|
496
|
+
|
|
497
|
+
if not ctx.data_engine:
|
|
498
|
+
return "default", None
|
|
499
|
+
|
|
500
|
+
task_plan_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
|
|
501
|
+
|
|
502
|
+
if not task_plan_path.exists():
|
|
503
|
+
return "default", None
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
content = task_plan_path.read_text(encoding="utf-8")
|
|
507
|
+
|
|
508
|
+
return get_active_subgoal_hashes(content)
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logger.error(f"Failed to parse active subgoal: {e}")
|
|
511
|
+
|
|
512
|
+
return "default", None
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def check_plan_mutation_rejections(
|
|
516
|
+
content_before: str, content_after: str, state: State | None = None
|
|
517
|
+
) -> str | None:
|
|
518
|
+
"""Checks if a task plan modification violates the machine-channel safety rules."""
|
|
519
|
+
before = parse_plan(content_before)
|
|
520
|
+
after = parse_plan(content_after)
|
|
521
|
+
|
|
522
|
+
# 1. Incomplete nested subgoals check
|
|
523
|
+
if after.all_top_level_done and any(
|
|
524
|
+
(item.is_pending or item.is_active) and not item.is_top_level for item in after.items
|
|
525
|
+
):
|
|
526
|
+
return (
|
|
527
|
+
"Your changes to the task plan were not applied. Your changes would cause the"
|
|
528
|
+
" task to end completely, but there seem to be goals in the task plan"
|
|
529
|
+
" that have not been marked as completed. If you believe these goals"
|
|
530
|
+
" are already completed, please update their statuses."
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# 2. Continuous monitoring protection: a [Loop:continuous] milestone can only
|
|
534
|
+
# transition to [x] (or disappear) after the user injects an explicit stop
|
|
535
|
+
# signal (user_stop_requested); intent is never inferred from wording.
|
|
536
|
+
user_stopped = bool(state is not None and getattr(state, "user_stop_requested", False))
|
|
537
|
+
|
|
538
|
+
if before.continuous_top_level and not user_stopped:
|
|
539
|
+
if not after.continuous_top_level:
|
|
540
|
+
return (
|
|
541
|
+
"Your changes to the task plan were rejected. You cannot delete an active"
|
|
542
|
+
" [Loop:continuous] continuous monitoring milestone or remove its tag."
|
|
543
|
+
" Please keep the milestone active (in progress [/]) and record each"
|
|
544
|
+
" check cycle as an indented subtask."
|
|
545
|
+
)
|
|
546
|
+
if any(item.is_done for item in after.continuous_top_level):
|
|
547
|
+
return (
|
|
548
|
+
"Your changes to the task plan were rejected. [Loop:continuous] continuous"
|
|
549
|
+
" monitoring milestones must remain in progress ([/]) and cannot be"
|
|
550
|
+
" unilaterally marked as completed [x] until the user injects an explicit"
|
|
551
|
+
" stop signal. Please keep it active ([/]) and record each polling check"
|
|
552
|
+
" as an indented subtask."
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def check_unintended_rewrite(content_before: str, content_after: str) -> str | None:
|
|
559
|
+
"""Detects the hand-slip signature of a full-file rewrite: top-level milestones
|
|
560
|
+
whose status did not change but whose text drifted. Declared edits via
|
|
561
|
+
update_note (explicit target/replacement) are exempt.
|
|
562
|
+
"""
|
|
563
|
+
edits = unintended_milestone_edits(parse_plan(content_before), parse_plan(content_after))
|
|
564
|
+
if not edits:
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
listing = "\n".join(f'- "{b}"\n -> "{a}"' for b, a in edits)
|
|
568
|
+
return (
|
|
569
|
+
"Your changes to the task plan were not applied. Your rewrite reworded"
|
|
570
|
+
" top-level milestones whose status you did not change — this usually means"
|
|
571
|
+
" the plan was regenerated from memory and historical milestones drifted"
|
|
572
|
+
f" unintentionally:\n{listing}\nIf a rewording was intentional, apply it"
|
|
573
|
+
" explicitly with the update_note tool (target/replacement). Otherwise,"
|
|
574
|
+
" re-apply only the changes you actually intended."
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _reject_checker_note_write(key: str, tool_call_id: str | None) -> ToolMessage | None:
|
|
579
|
+
"""Guard for the system-authored checker note channel.
|
|
580
|
+
|
|
581
|
+
Note keys with the reserved ``checker-`` / ``checker:`` prefix are written
|
|
582
|
+
exclusively by the checkpoint verifier (repair logs); model-side writes are
|
|
583
|
+
rejected with guidance instead of silently colliding with the log.
|
|
584
|
+
"""
|
|
585
|
+
if not is_checker_note_key(key):
|
|
586
|
+
return None
|
|
587
|
+
return ToolMessage(
|
|
588
|
+
content=(
|
|
589
|
+
f"The note key '{key}' was not written: keys with the 'checker-'"
|
|
590
|
+
" prefix are reserved for system-authored verification repair"
|
|
591
|
+
" logs. Those notes are read-only for you — read them with"
|
|
592
|
+
" read_note. Record your own observations under a different key."
|
|
593
|
+
),
|
|
594
|
+
tool_call_id=tool_call_id or "",
|
|
595
|
+
status="success",
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class NoteArgs(BaseModel):
|
|
600
|
+
model_config = {"ignored_types": (CyFunctionDetector,)}
|
|
601
|
+
key: str = Field(..., description=SAVE_NOTE_ARG_KEY_DESC)
|
|
602
|
+
content: str = Field(..., description=SAVE_NOTE_ARG_CONTENT_DESC)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
async def _process_plan_write(
|
|
606
|
+
ctx: ArtemisContext,
|
|
607
|
+
state: State | None,
|
|
608
|
+
task_plan_path,
|
|
609
|
+
content_before: str,
|
|
610
|
+
content_after: str,
|
|
611
|
+
result,
|
|
612
|
+
declared_intent: bool,
|
|
613
|
+
):
|
|
614
|
+
"""Shared post-write pipeline for every task_plan mutation.
|
|
615
|
+
|
|
616
|
+
Constitution order: hard rejections with rollback (machine-channel rules
|
|
617
|
+
only), then ratchet-baseline planner validation, then checker trigger on
|
|
618
|
+
new top-level completions. ``declared_intent`` marks surgical update_note
|
|
619
|
+
edits, which are exempt from the full-rewrite hand-slip check.
|
|
620
|
+
"""
|
|
621
|
+
rejection_message = check_plan_mutation_rejections(content_before, content_after, state)
|
|
622
|
+
if rejection_message is None and not declared_intent:
|
|
623
|
+
rejection_message = check_unintended_rewrite(content_before, content_after)
|
|
624
|
+
if rejection_message:
|
|
625
|
+
logger.info(f"Rejected task plan update: {rejection_message}")
|
|
626
|
+
if task_plan_path.exists():
|
|
627
|
+
task_plan_path.write_text(content_before, encoding="utf-8")
|
|
628
|
+
|
|
629
|
+
# Guidance, not a tool failure: status stays "success" so the Operator
|
|
630
|
+
# loop treats it as feedback content (matching the legacy envelope
|
|
631
|
+
# behavior where the SystemMessage carried no error status).
|
|
632
|
+
return ToolMessage(content=rejection_message, tool_call_id="", status="success")
|
|
633
|
+
|
|
634
|
+
# Deterministic check-line guard (Operator-source writes only; the Planner
|
|
635
|
+
# node writes through unwrapped tools and retains revision authority):
|
|
636
|
+
# deleted/rewritten check lines are merged back in a pure text operation.
|
|
637
|
+
# This is content-driven and independent of any switch — a resumed plan
|
|
638
|
+
# with check lines stays protected even when checking is disabled.
|
|
639
|
+
# The one exception is user guidance: it outranks the declared standards,
|
|
640
|
+
# so every check line that existed when an instruction arrived (recorded
|
|
641
|
+
# by perception in ``ctx.guidance_unprotected_checks``) may be dropped or
|
|
642
|
+
# reworded at any later write — the Operator usually re-plans over several
|
|
643
|
+
# surgical edits, turns after the guidance landed. Lines added after the
|
|
644
|
+
# instruction are protected again until the next one.
|
|
645
|
+
#
|
|
646
|
+
# ``missing_check_items`` diffs on the full signature (kind, when, text,
|
|
647
|
+
# parent hash), so rewording a milestone re-signs every check line under
|
|
648
|
+
# it. Such a line has merely moved: it is still declared, so it is neither
|
|
649
|
+
# merged back (that would duplicate it as a task-level ``@end`` orphan)
|
|
650
|
+
# nor retired (the ledger, the harvest gate and the run outcome all key a
|
|
651
|
+
# check item by ``(kind, text)``). Only a line whose ``(kind, text)`` is
|
|
652
|
+
# gone from the new plan counts as dropped.
|
|
653
|
+
before_snapshot = parse_plan(content_before)
|
|
654
|
+
after_snapshot = parse_plan(content_after)
|
|
655
|
+
missing = missing_check_items(before_snapshot, after_snapshot)
|
|
656
|
+
unprotected = getattr(ctx, "guidance_unprotected_checks", None)
|
|
657
|
+
if not isinstance(unprotected, set):
|
|
658
|
+
unprotected = set()
|
|
659
|
+
still_declared = Counter((ci.kind, ci.text) for ci in after_snapshot.check_items)
|
|
660
|
+
dropped: list = []
|
|
661
|
+
for ci in missing:
|
|
662
|
+
if still_declared.get((ci.kind, ci.text), 0) > 0:
|
|
663
|
+
still_declared[(ci.kind, ci.text)] -= 1
|
|
664
|
+
continue # moved under a reworded milestone / re-timed: still declared
|
|
665
|
+
dropped.append(ci)
|
|
666
|
+
waived = [ci for ci in dropped if (ci.kind, ci.text) in unprotected]
|
|
667
|
+
protected = [ci for ci in dropped if (ci.kind, ci.text) not in unprotected]
|
|
668
|
+
if waived:
|
|
669
|
+
logger.info(
|
|
670
|
+
"Plan write dropped declared check lines because of user guidance"
|
|
671
|
+
f" (the change stands): {[render_check_line(ci).strip() for ci in waived]}"
|
|
672
|
+
)
|
|
673
|
+
# Records the retirement (harvest gate + run outcome) and withdraws any
|
|
674
|
+
# standing finding headline those lines had already produced.
|
|
675
|
+
retire_check_items(ctx, [(ci.kind, ci.text) for ci in waived])
|
|
676
|
+
# A retired line the Operator declares again is live again: the harvest
|
|
677
|
+
# gate and the run outcome must agree with the plan on disk.
|
|
678
|
+
reinstate_check_items(ctx, [(ci.kind, ci.text) for ci in after_snapshot.check_items])
|
|
679
|
+
if protected:
|
|
680
|
+
merged = restore_missing_check_items(content_before, content_after, items=protected)
|
|
681
|
+
if merged is not None:
|
|
682
|
+
logger.info(
|
|
683
|
+
"Plan write removed or rewrote declared check lines; merging them"
|
|
684
|
+
" back (check standards are protected deterministically):"
|
|
685
|
+
f" {[render_check_line(ci).strip() for ci in protected]}"
|
|
686
|
+
)
|
|
687
|
+
content_after = merged
|
|
688
|
+
try:
|
|
689
|
+
task_plan_path.write_text(content_after, encoding="utf-8")
|
|
690
|
+
except Exception as e:
|
|
691
|
+
logger.error(f"Failed to write merged check lines: {e}")
|
|
692
|
+
|
|
693
|
+
# Deterministic finding-line projection: unresolved verify findings are
|
|
694
|
+
# re-rendered from the checkpoint repair state on every plan write, so a
|
|
695
|
+
# model deletion grows back and a resolved finding is retired. Finding
|
|
696
|
+
# lines live outside the checkbox/check machine channel, so this rewrite
|
|
697
|
+
# never influences the parses below.
|
|
698
|
+
sync_finding_lines(ctx)
|
|
699
|
+
|
|
700
|
+
before = before_snapshot
|
|
701
|
+
after = parse_plan(content_after)
|
|
702
|
+
|
|
703
|
+
# Ratchet baseline: judge the current plan against the last *validated*
|
|
704
|
+
# plan rather than the previous write, so many small edits cannot drift
|
|
705
|
+
# the milestones below any per-edit trigger.
|
|
706
|
+
if ctx.last_validated_plan is None:
|
|
707
|
+
ctx.last_validated_plan = content_before
|
|
708
|
+
|
|
709
|
+
disable_planner_validation = (
|
|
710
|
+
ctx.execution_setup and ctx.execution_setup.disable_planner_validation
|
|
711
|
+
)
|
|
712
|
+
if not disable_planner_validation:
|
|
713
|
+
baseline = parse_plan(ctx.last_validated_plan)
|
|
714
|
+
if milestones_changed(baseline, after):
|
|
715
|
+
logger.info(
|
|
716
|
+
"Top-level task plan diverged from the validated baseline."
|
|
717
|
+
" Triggering async Planner Validation."
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
if ctx.planner_task and not ctx.planner_task.done():
|
|
721
|
+
# A newer write supersedes the in-flight validation.
|
|
722
|
+
ctx.planner_task.cancel()
|
|
723
|
+
|
|
724
|
+
ctx.pending_validated_plan = content_after
|
|
725
|
+
initial_goal = (
|
|
726
|
+
state.initial_goal if state and hasattr(state, "initial_goal") else "Unknown Goal"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
ctx.planner_task = asyncio.create_task(
|
|
730
|
+
run_async_planner_validation(
|
|
731
|
+
ctx,
|
|
732
|
+
initial_goal,
|
|
733
|
+
ctx.last_validated_plan,
|
|
734
|
+
content_after,
|
|
735
|
+
getattr(state, "operator_raw_thinking", None),
|
|
736
|
+
getattr(state, "operator_native_thinking", None),
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if isinstance(result, ToolMessage) and isinstance(result.content, str):
|
|
741
|
+
result.content = (
|
|
742
|
+
f"{result.content}\n\nWe have applied your changes"
|
|
743
|
+
" to the task plan. A lightweight background review"
|
|
744
|
+
" is checking the milestone change against the"
|
|
745
|
+
" original goal; if it has a concern you will get an"
|
|
746
|
+
" advisory hint with the reason (the change stays"
|
|
747
|
+
" applied either way)."
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Queue (never spawn) a checkpoint for EVERY newly completed top-level
|
|
751
|
+
# subgoal carrying on_complete check items. Spawning happens in
|
|
752
|
+
# execution_check_node after this turn's step is recorded, so the evidence
|
|
753
|
+
# anchor points at the correct step.
|
|
754
|
+
new_completions = new_top_level_completions(before, after)
|
|
755
|
+
if new_completions:
|
|
756
|
+
queue_checkpoints(ctx, state, after, new_completions, content_after)
|
|
757
|
+
# History chunking (M3): a top-level completion only queues an
|
|
758
|
+
# UNCONFIRMED boundary — the next stamped step confirms it via an
|
|
759
|
+
# actual subgoal-hash change (pseudo-switch protection, §3.3).
|
|
760
|
+
_notify_history_chunker(ctx, "queue_boundary_hint")
|
|
761
|
+
|
|
762
|
+
return result
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
async def _intercept_plan_write(
|
|
766
|
+
ctx: ArtemisContext,
|
|
767
|
+
original_tool,
|
|
768
|
+
args: dict,
|
|
769
|
+
tool_call_id,
|
|
770
|
+
state,
|
|
771
|
+
*,
|
|
772
|
+
declared_intent: bool,
|
|
773
|
+
content_after_fallback: str = "",
|
|
774
|
+
):
|
|
775
|
+
"""Run a note write and process any resulting task-plan changes."""
|
|
776
|
+
rejection = _reject_checker_note_write(args["key"], tool_call_id)
|
|
777
|
+
if rejection is not None:
|
|
778
|
+
return rejection
|
|
779
|
+
if args["key"] != "task_plan" or not ctx.data_engine:
|
|
780
|
+
return await invoke_tool_with_injection(
|
|
781
|
+
original_tool, args, tool_call_id, state, record_trace=False
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
task_plan_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
|
|
785
|
+
|
|
786
|
+
content_before = ""
|
|
787
|
+
if task_plan_path.exists():
|
|
788
|
+
content_before = task_plan_path.read_text(encoding="utf-8")
|
|
789
|
+
|
|
790
|
+
result = await invoke_tool_with_injection(
|
|
791
|
+
original_tool, args, tool_call_id, state, record_trace=False
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
content_after = content_after_fallback
|
|
795
|
+
if task_plan_path.exists():
|
|
796
|
+
content_after = task_plan_path.read_text(encoding="utf-8")
|
|
797
|
+
|
|
798
|
+
return await _process_plan_write(
|
|
799
|
+
ctx,
|
|
800
|
+
state,
|
|
801
|
+
task_plan_path,
|
|
802
|
+
content_before,
|
|
803
|
+
content_after,
|
|
804
|
+
result,
|
|
805
|
+
declared_intent=declared_intent,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def wrap_note_tool(ctx: ArtemisContext, original_tool):
|
|
810
|
+
|
|
811
|
+
async def wrapped_note_tool(
|
|
812
|
+
key: str,
|
|
813
|
+
content: str,
|
|
814
|
+
tool_call_id: Annotated[str, InjectedToolCallId] = None,
|
|
815
|
+
state: Annotated[State, InjectedState] = None,
|
|
816
|
+
):
|
|
817
|
+
"""Wrapped tool to intercept task plan updates."""
|
|
818
|
+
return await _intercept_plan_write(
|
|
819
|
+
ctx,
|
|
820
|
+
original_tool,
|
|
821
|
+
{"key": key, "content": content},
|
|
822
|
+
tool_call_id,
|
|
823
|
+
state,
|
|
824
|
+
declared_intent=False,
|
|
825
|
+
content_after_fallback=content,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
return StructuredTool.from_function(
|
|
829
|
+
func=lambda *a, **kw: None,
|
|
830
|
+
coroutine=wrapped_note_tool,
|
|
831
|
+
name=original_tool.name,
|
|
832
|
+
description=original_tool.description,
|
|
833
|
+
args_schema=NoteArgs,
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class UpdateNoteArgs(BaseModel):
|
|
838
|
+
model_config = {"ignored_types": (CyFunctionDetector,)}
|
|
839
|
+
key: str = Field(..., description=UPDATE_NOTE_ARG_KEY_DESC)
|
|
840
|
+
target: str = Field(..., description=UPDATE_NOTE_ARG_TARGET_DESC)
|
|
841
|
+
replacement: str = Field(..., description=UPDATE_NOTE_ARG_REPLACEMENT_DESC)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def wrap_update_note_tool(ctx: ArtemisContext, original_tool):
|
|
845
|
+
|
|
846
|
+
async def wrapped_update_note(
|
|
847
|
+
key: str,
|
|
848
|
+
target: str,
|
|
849
|
+
replacement: str,
|
|
850
|
+
tool_call_id: Annotated[str, InjectedToolCallId] = None,
|
|
851
|
+
state: Annotated[State, InjectedState] = None,
|
|
852
|
+
):
|
|
853
|
+
"""Wrapped tool to intercept task plan updates via update_note."""
|
|
854
|
+
return await _intercept_plan_write(
|
|
855
|
+
ctx,
|
|
856
|
+
original_tool,
|
|
857
|
+
{"key": key, "target": target, "replacement": replacement},
|
|
858
|
+
tool_call_id,
|
|
859
|
+
state,
|
|
860
|
+
declared_intent=True,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
return StructuredTool.from_function(
|
|
864
|
+
func=lambda *a, **kw: None,
|
|
865
|
+
coroutine=wrapped_update_note,
|
|
866
|
+
name=original_tool.name,
|
|
867
|
+
description=original_tool.description,
|
|
868
|
+
args_schema=UpdateNoteArgs,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def convergence_gate(
|
|
873
|
+
state: State, ctx: ArtemisContext
|
|
874
|
+
) -> Literal["continue", "exit_settlement", "end"]:
|
|
875
|
+
logger.info("Starting convergence_gate")
|
|
876
|
+
|
|
877
|
+
if not ctx.data_engine:
|
|
878
|
+
return "end"
|
|
879
|
+
|
|
880
|
+
def _read_plan() -> str:
|
|
881
|
+
file_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
|
|
882
|
+
if not file_path.exists():
|
|
883
|
+
return ""
|
|
884
|
+
try:
|
|
885
|
+
return file_path.read_text(encoding="utf-8")
|
|
886
|
+
except Exception:
|
|
887
|
+
return ""
|
|
888
|
+
|
|
889
|
+
def _terminal_route() -> Literal["exit_settlement", "end"]:
|
|
890
|
+
"""Settlement and final review are decoupled from each other but share
|
|
891
|
+
the entry: any check item, in-flight/queued attempt, ledger record, or
|
|
892
|
+
an enabled final review forces the settlement node."""
|
|
893
|
+
plan_snapshot = parse_plan(_read_plan())
|
|
894
|
+
needs_settlement = (
|
|
895
|
+
bool(plan_snapshot.all_check_items)
|
|
896
|
+
or bool(getattr(ctx, "checkpoint_tasks", None))
|
|
897
|
+
or bool(getattr(ctx, "pending_checkpoints", None))
|
|
898
|
+
or has_ledger_records(ctx.data_engine.base_dir)
|
|
899
|
+
or final_check_enabled(ctx)
|
|
900
|
+
)
|
|
901
|
+
return "exit_settlement" if needs_settlement else "end"
|
|
902
|
+
|
|
903
|
+
# A halt latched by an assert failure (policy 'halt') terminates the run
|
|
904
|
+
# regardless of plan progress.
|
|
905
|
+
if getattr(ctx, "assert_halt", False):
|
|
906
|
+
logger.warning("Assert-halt latched; routing to exit settlement.")
|
|
907
|
+
return _terminal_route()
|
|
908
|
+
|
|
909
|
+
# Check planner-validation result. ``None`` means "not yet evaluated"
|
|
910
|
+
# (the first pass after the Planner node) — only an explicit False is a
|
|
911
|
+
# failure; otherwise the log would misreport the initial plan as rejected.
|
|
912
|
+
if getattr(state, "checker_success", None) is False:
|
|
913
|
+
logger.info("Plan validation failed, returning to operator for retry.")
|
|
914
|
+
return "continue"
|
|
915
|
+
|
|
916
|
+
file_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
|
|
917
|
+
if not file_path.exists():
|
|
918
|
+
logger.warning(f"Task plan file not found at {file_path}")
|
|
919
|
+
return "continue"
|
|
920
|
+
try:
|
|
921
|
+
task_plan_content = file_path.read_text(encoding="utf-8")
|
|
922
|
+
except Exception as e:
|
|
923
|
+
logger.error(f"Failed to read task plan: {e}")
|
|
924
|
+
return "continue"
|
|
925
|
+
|
|
926
|
+
snapshot = parse_plan(task_plan_content)
|
|
927
|
+
|
|
928
|
+
if not snapshot.has_top_level:
|
|
929
|
+
# An empty plan does not bypass an enabled final review: the review
|
|
930
|
+
# object is the user's original goal, not the plan.
|
|
931
|
+
logger.info("No subgoals found in file, ending")
|
|
932
|
+
return _terminal_route()
|
|
933
|
+
|
|
934
|
+
if snapshot.all_top_level_done:
|
|
935
|
+
user_stopped = bool(getattr(state, "user_stop_requested", False)) if state else False
|
|
936
|
+
if snapshot.continuous_top_level and not user_stopped:
|
|
937
|
+
logger.warning(
|
|
938
|
+
"All subgoals marked [x] but task plan has a [Loop:continuous]"
|
|
939
|
+
" milestone without a user stop signal. Continuing."
|
|
940
|
+
)
|
|
941
|
+
return "continue"
|
|
942
|
+
logger.info("All subgoals are completed, ending the goal")
|
|
943
|
+
return _terminal_route()
|
|
944
|
+
|
|
945
|
+
return "continue"
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
async def get_graph(ctx: ArtemisContext) -> CompiledStateGraph:
|
|
949
|
+
graph_builder = StateGraph(State)
|
|
950
|
+
|
|
951
|
+
# Get native tools
|
|
952
|
+
native_wrappers = [
|
|
953
|
+
save_note_wrapper,
|
|
954
|
+
read_note_wrapper,
|
|
955
|
+
list_notes_wrapper,
|
|
956
|
+
update_note_wrapper,
|
|
957
|
+
append_note_wrapper,
|
|
958
|
+
]
|
|
959
|
+
|
|
960
|
+
native_tools = get_tools_from_wrappers(ctx, native_wrappers)
|
|
961
|
+
|
|
962
|
+
## Define nodes
|
|
963
|
+
graph_builder.add_node("planner", PlannerNode(ctx, tools=native_tools))
|
|
964
|
+
|
|
965
|
+
# Wrap native tools for Operator
|
|
966
|
+
operator_native_tools = []
|
|
967
|
+
for t in native_tools:
|
|
968
|
+
if t.name == "save_note":
|
|
969
|
+
operator_native_tools.append(wrap_note_tool(ctx, t))
|
|
970
|
+
elif t.name == "update_note":
|
|
971
|
+
operator_native_tools.append(wrap_update_note_tool(ctx, t))
|
|
972
|
+
elif t.name == "append_note":
|
|
973
|
+
operator_native_tools.append(wrap_note_tool(ctx, t))
|
|
974
|
+
else:
|
|
975
|
+
operator_native_tools.append(t)
|
|
976
|
+
|
|
977
|
+
# Get specialized tools for Operator (history tools: shared definitions)
|
|
978
|
+
from artemis.tools.history import HISTORY_TOOL_WRAPPERS
|
|
979
|
+
|
|
980
|
+
operator_specialized_wrappers = [
|
|
981
|
+
ask_diagnoser_wrapper,
|
|
982
|
+
run_adb_command_wrapper,
|
|
983
|
+
manage_task_wrapper,
|
|
984
|
+
ask_explorer_wrapper,
|
|
985
|
+
*HISTORY_TOOL_WRAPPERS,
|
|
986
|
+
]
|
|
987
|
+
# ask_committee does not follow the Operator's pre-decision / turn-ending contract.
|
|
988
|
+
|
|
989
|
+
operator_specialized_tools = get_tools_from_wrappers(ctx, operator_specialized_wrappers)
|
|
990
|
+
|
|
991
|
+
# Customize descriptions for run_adb_command and manage_task to match exploratory role
|
|
992
|
+
for t in operator_specialized_tools:
|
|
993
|
+
if t.name == "run_adb_command":
|
|
994
|
+
t.description = (
|
|
995
|
+
"[SHELL] Executes a shell command directly on the Android"
|
|
996
|
+
" mobile device via ADB shell. Use it when the device action"
|
|
997
|
+
" tools cannot achieve the goal, or to start background"
|
|
998
|
+
" instrumentation (performance, power, traces); never to bypass"
|
|
999
|
+
" goals that the action tools can achieve.\nCan run synchronously"
|
|
1000
|
+
" or transition to a background task if execution takes longer"
|
|
1001
|
+
" than WaitMsBeforeAsync.\nSupports persistent environments"
|
|
1002
|
+
" (environment variables) on the phone across invocations.\n" + HANG_GUIDANCE
|
|
1003
|
+
)
|
|
1004
|
+
elif t.name == "manage_task":
|
|
1005
|
+
t.description = (
|
|
1006
|
+
"[SHELL] Manage background ADB shell tasks launched via"
|
|
1007
|
+
" run_adb_command to monitor or terminate ongoing background"
|
|
1008
|
+
" commands."
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
operator_tools = operator_native_tools + operator_specialized_tools
|
|
1012
|
+
|
|
1013
|
+
if ctx.execution_setup and ctx.execution_setup.video_recording_tools_enabled:
|
|
1014
|
+
operator_tools.append(get_video_analyzer_tool(ctx, role="operator"))
|
|
1015
|
+
|
|
1016
|
+
graph_builder.add_node("operator", OperatorNode(ctx, tools=operator_tools))
|
|
1017
|
+
graph_builder.add_node("validator", ValidatorNode(ctx))
|
|
1018
|
+
|
|
1019
|
+
graph_builder.add_node("summarizer", SummarizerNode(ctx))
|
|
1020
|
+
graph_builder.add_node("execution_check", functools.partial(execution_check_node, ctx=ctx))
|
|
1021
|
+
graph_builder.add_node("perception", functools.partial(perception_node, ctx=ctx))
|
|
1022
|
+
add_convergence_node(graph_builder)
|
|
1023
|
+
graph_builder.add_node("exit_settlement", functools.partial(exit_settlement_node, ctx=ctx))
|
|
1024
|
+
|
|
1025
|
+
## Linking nodes
|
|
1026
|
+
graph_builder.add_edge(START, "planner")
|
|
1027
|
+
graph_builder.add_edge("planner", CONVERGENCE_NODE)
|
|
1028
|
+
graph_builder.add_edge("operator", "execution_check")
|
|
1029
|
+
graph_builder.add_conditional_edges(
|
|
1030
|
+
"execution_check",
|
|
1031
|
+
execution_check_edge,
|
|
1032
|
+
{
|
|
1033
|
+
"review_subgoals": CONVERGENCE_NODE,
|
|
1034
|
+
"execute_decisions": "validator",
|
|
1035
|
+
},
|
|
1036
|
+
)
|
|
1037
|
+
graph_builder.add_edge("validator", "summarizer")
|
|
1038
|
+
graph_builder.add_edge("summarizer", CONVERGENCE_NODE)
|
|
1039
|
+
|
|
1040
|
+
add_convergence_edges(
|
|
1041
|
+
graph_builder,
|
|
1042
|
+
functools.partial(convergence_gate, ctx=ctx),
|
|
1043
|
+
{"continue": "perception", "exit_settlement": "exit_settlement", "end": END},
|
|
1044
|
+
)
|
|
1045
|
+
graph_builder.add_conditional_edges(
|
|
1046
|
+
source="exit_settlement",
|
|
1047
|
+
path=exit_settlement_gate,
|
|
1048
|
+
path_map={
|
|
1049
|
+
"continue": "perception",
|
|
1050
|
+
"end": END,
|
|
1051
|
+
},
|
|
1052
|
+
)
|
|
1053
|
+
graph_builder.add_edge("perception", "operator")
|
|
1054
|
+
|
|
1055
|
+
return graph_builder.compile()
|