loopx 0.4.8__py3-none-any.whl
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.
- loopx/__init__.py +5 -0
- loopx/agent_onboarding.py +654 -0
- loopx/agent_registry.py +112 -0
- loopx/ark_managed_agent_host.py +59 -0
- loopx/authority.py +805 -0
- loopx/benchmark.py +2875 -0
- loopx/benchmark_adapters/__init__.py +1 -0
- loopx/benchmark_adapters/agentissue.py +2644 -0
- loopx/benchmark_adapters/agents_last_exam.py +3998 -0
- loopx/benchmark_adapters/edgebench.py +322 -0
- loopx/benchmark_adapters/skillsbench.py +5978 -0
- loopx/benchmark_adapters/skillsbench_acp_failure_policy.py +143 -0
- loopx/benchmark_adapters/skillsbench_acp_process.py +31 -0
- loopx/benchmark_adapters/skillsbench_acp_relay.py +4832 -0
- loopx/benchmark_adapters/skillsbench_batch.py +124 -0
- loopx/benchmark_adapters/skillsbench_bridge_guard.py +209 -0
- loopx/benchmark_adapters/skillsbench_bridge_summary.py +203 -0
- loopx/benchmark_adapters/skillsbench_codex_goal_recovery.py +271 -0
- loopx/benchmark_adapters/skillsbench_codex_goal_trace.py +81 -0
- loopx/benchmark_adapters/skillsbench_codex_runtime.py +339 -0
- loopx/benchmark_adapters/skillsbench_dockerfile_runtime.py +467 -0
- loopx/benchmark_adapters/skillsbench_failure_signals.py +652 -0
- loopx/benchmark_adapters/skillsbench_proxy_runtime.py +327 -0
- loopx/benchmark_adapters/skillsbench_remote_bridge.py +402 -0
- loopx/benchmark_adapters/skillsbench_result_discovery.py +143 -0
- loopx/benchmark_adapters/skillsbench_runner_profile.py +436 -0
- loopx/benchmark_adapters/skillsbench_runner_source.py +99 -0
- loopx/benchmark_adapters/skillsbench_setup_preflight.py +771 -0
- loopx/benchmark_adapters/skillsbench_signals.py +15 -0
- loopx/benchmark_adapters/skillsbench_task_source.py +141 -0
- loopx/benchmark_adapters/skillsbench_turn_route.py +723 -0
- loopx/benchmark_adapters/skillsbench_turn_runtime.py +1069 -0
- loopx/benchmark_adapters/skillsbench_typed_repair.py +689 -0
- loopx/benchmark_adapters/skillsbench_uv_cache.py +111 -0
- loopx/benchmark_adapters/skillsbench_verifier_bootstrap.py +227 -0
- loopx/benchmark_adapters/skillsbench_verifier_cache.py +138 -0
- loopx/benchmark_adapters/terminal_bench.py +10078 -0
- loopx/benchmark_case_analysis.py +1276 -0
- loopx/benchmark_case_state.py +1079 -0
- loopx/benchmark_core/__init__.py +239 -0
- loopx/benchmark_core/adapter.py +84 -0
- loopx/benchmark_core/artifacts.py +517 -0
- loopx/benchmark_core/attempts.py +199 -0
- loopx/benchmark_core/container_exec.py +216 -0
- loopx/benchmark_core/io.py +68 -0
- loopx/benchmark_core/lifecycle.py +211 -0
- loopx/benchmark_core/loop_protocol.py +689 -0
- loopx/benchmark_core/observable_handles.py +348 -0
- loopx/benchmark_core/parity.py +256 -0
- loopx/benchmark_core/remote_closeout.py +482 -0
- loopx/benchmark_core/rounds.py +215 -0
- loopx/benchmark_core/route_profile.py +509 -0
- loopx/benchmark_core/run_permissions.py +206 -0
- loopx/benchmark_core/split_control.py +925 -0
- loopx/benchmark_core/turn_fidelity.py +326 -0
- loopx/benchmark_ledger.py +3793 -0
- loopx/benchmark_ledger_countability.py +372 -0
- loopx/benchmark_ledger_current.py +724 -0
- loopx/benchmark_trajectory.py +405 -0
- loopx/benchmarks/__init__.py +1 -0
- loopx/benchmarks/qualification/__init__.py +1 -0
- loopx/benchmarks/qualification/release_outcome_baseline.py +360 -0
- loopx/benchmarks/read_models/__init__.py +1 -0
- loopx/benchmarks/read_models/benchmark_attempt_accounting.py +53 -0
- loopx/benchmarks/read_models/benchmark_comparison.py +414 -0
- loopx/benchmarks/read_models/benchmark_event_timeline.py +113 -0
- loopx/benchmarks/read_models/benchmark_experiment_report.py +475 -0
- loopx/benchmarks/read_models/benchmark_learning_ledger.py +137 -0
- loopx/benchmarks/read_models/benchmark_lifecycle_contracts.py +228 -0
- loopx/benchmarks/read_models/benchmark_projection.py +723 -0
- loopx/benchmarks/read_models/benchmark_result.py +146 -0
- loopx/benchmarks/read_models/benchmark_run_execution_contract.py +116 -0
- loopx/benchmarks/read_models/benchmark_run_failure.py +157 -0
- loopx/benchmarks/read_models/benchmark_run_metrics.py +213 -0
- loopx/benchmarks/read_models/benchmark_run_post_execution.py +635 -0
- loopx/benchmarks/read_models/benchmark_run_pre_execution.py +541 -0
- loopx/benchmarks/read_models/benchmark_status_compaction.py +1255 -0
- loopx/benchmarks/read_models/benchmark_status_runner.py +780 -0
- loopx/benchmarks/read_models/goal_start_control_score.py +857 -0
- loopx/benchmarks/read_models/skillsbench_post_run_debug.py +746 -0
- loopx/benchmarks/read_models/skillsbench_verifier_attribution.py +269 -0
- loopx/bootstrap.py +1116 -0
- loopx/bootstrap_command_pack.py +2167 -0
- loopx/boundary_authority.py +199 -0
- loopx/canary/__init__.py +1 -0
- loopx/canary/maintainability_ratchet.py +800 -0
- loopx/canary/planner.py +1984 -0
- loopx/canary/premerge.py +1130 -0
- loopx/canary/qualification_profiles.py +309 -0
- loopx/canary/quality_surface_catalog.py +838 -0
- loopx/canary/release_profiles.py +51 -0
- loopx/canary/runner.py +1107 -0
- loopx/canary/smoke_health.py +581 -0
- loopx/canary/smoke_profiles.py +212 -0
- loopx/capabilities/__init__.py +0 -0
- loopx/capabilities/agent_turn_recall/__init__.py +17 -0
- loopx/capabilities/agent_turn_recall/cli.py +369 -0
- loopx/capabilities/agent_turn_recall/core.py +296 -0
- loopx/capabilities/auto_research/__init__.py +16 -0
- loopx/capabilities/auto_research/bootstrap_contract.py +157 -0
- loopx/capabilities/auto_research/cli.py +1468 -0
- loopx/capabilities/auto_research/core.py +11 -0
- loopx/capabilities/auto_research/defaults.py +79 -0
- loopx/capabilities/auto_research/demo_e2e.py +1848 -0
- loopx/capabilities/auto_research/demo_supervisor.py +186 -0
- loopx/capabilities/auto_research/evidence_packet.py +767 -0
- loopx/capabilities/auto_research/human_view.py +794 -0
- loopx/capabilities/auto_research/kernel.py +191 -0
- loopx/capabilities/auto_research/knn_demo_workspace.py +322 -0
- loopx/capabilities/auto_research/live_evidence.py +248 -0
- loopx/capabilities/auto_research/preset.py +176 -0
- loopx/capabilities/auto_research/research_state.py +1085 -0
- loopx/capabilities/auto_research/role_profiles.py +394 -0
- loopx/capabilities/auto_research/rollout_append.py +97 -0
- loopx/capabilities/auto_research/terminal_result_contract.py +422 -0
- loopx/capabilities/auto_research/terminal_result_projection.py +171 -0
- loopx/capabilities/auto_research/terminal_result_query.py +233 -0
- loopx/capabilities/auto_research/terminal_results.py +349 -0
- loopx/capabilities/auto_research/user_contract.py +190 -0
- loopx/capabilities/auto_research/worker_loop.py +163 -0
- loopx/capabilities/auto_research/worker_runtime.py +777 -0
- loopx/capabilities/auto_research/worker_skill/SKILL.md +343 -0
- loopx/capabilities/benchmark_toolkit/__init__.py +19 -0
- loopx/capabilities/benchmark_toolkit/integrity.py +387 -0
- loopx/capabilities/catalog.py +1875 -0
- loopx/capabilities/change_quality/__init__.py +19 -0
- loopx/capabilities/change_quality/cli.py +171 -0
- loopx/capabilities/change_quality/context.py +156 -0
- loopx/capabilities/change_quality/oracles.py +269 -0
- loopx/capabilities/change_quality/policy.py +34 -0
- loopx/capabilities/change_quality/receipt.py +482 -0
- loopx/capabilities/change_quality/result.py +493 -0
- loopx/capabilities/change_quality/scope.py +171 -0
- loopx/capabilities/change_quality/shadow.py +680 -0
- loopx/capabilities/content_ops/__init__.py +0 -0
- loopx/capabilities/content_ops/cli.py +649 -0
- loopx/capabilities/content_ops/connector_packets.py +164 -0
- loopx/capabilities/content_ops/item_lifecycle.py +1000 -0
- loopx/capabilities/content_ops/layout.py +451 -0
- loopx/capabilities/content_ops/markdown.py +456 -0
- loopx/capabilities/content_ops/schemas.py +51 -0
- loopx/capabilities/content_ops/social_browser_x.py +107 -0
- loopx/capabilities/content_ops/surface.py +1956 -0
- loopx/capabilities/content_ops/templates/layout-catalog-v0.json +72 -0
- loopx/capabilities/context_providers/__init__.py +36 -0
- loopx/capabilities/context_providers/base.py +189 -0
- loopx/capabilities/context_providers/factory.py +32 -0
- loopx/capabilities/context_providers/openviking.py +702 -0
- loopx/capabilities/context_providers/service_ownership.py +185 -0
- loopx/capabilities/decision_context/__init__.py +129 -0
- loopx/capabilities/decision_context/architecture.py +83 -0
- loopx/capabilities/decision_context/assembler.py +849 -0
- loopx/capabilities/decision_context/catalog_entry.py +195 -0
- loopx/capabilities/decision_context/cli.py +310 -0
- loopx/capabilities/decision_context/cursor_commit.py +535 -0
- loopx/capabilities/decision_context/outcome_feedback.py +352 -0
- loopx/capabilities/decision_context/packets.py +654 -0
- loopx/capabilities/decision_context/private_state.py +189 -0
- loopx/capabilities/decision_context/profile.py +453 -0
- loopx/capabilities/decision_context/providers.py +228 -0
- loopx/capabilities/decision_context/review_settlement.py +136 -0
- loopx/capabilities/decision_context/runtime.py +273 -0
- loopx/capabilities/decision_context/sources.py +415 -0
- loopx/capabilities/explore/__init__.py +1 -0
- loopx/capabilities/explore/activation.py +198 -0
- loopx/capabilities/explore/adaptive_replay_planner.py +221 -0
- loopx/capabilities/explore/child_replay_runtime.py +463 -0
- loopx/capabilities/explore/composition_frontier.py +291 -0
- loopx/capabilities/explore/counterfactual_runtime.py +578 -0
- loopx/capabilities/explore/episode_runtime.py +647 -0
- loopx/capabilities/explore/harness_checkpoint.py +171 -0
- loopx/capabilities/explore/harness_gate.py +115 -0
- loopx/capabilities/explore/harness_runtime.py +1124 -0
- loopx/capabilities/explore/replay_metrics.py +206 -0
- loopx/capabilities/explore/replay_runtime.py +1271 -0
- loopx/capabilities/explore/resource_portfolio.py +173 -0
- loopx/capabilities/explore/result_log.py +974 -0
- loopx/capabilities/explore/router_state.py +432 -0
- loopx/capabilities/explore/source_history_reconcile.py +255 -0
- loopx/capabilities/explore/speculative_scheduler.py +498 -0
- loopx/capabilities/explore/todo_branch_plan.py +650 -0
- loopx/capabilities/explore/todo_evidence.py +141 -0
- loopx/capabilities/explore/trace_runtime.py +284 -0
- loopx/capabilities/explore/worker_branch_plan.py +1257 -0
- loopx/capabilities/integration_branch/__init__.py +13 -0
- loopx/capabilities/integration_branch/cli.py +148 -0
- loopx/capabilities/integration_branch/core.py +916 -0
- loopx/capabilities/issue_fix/__init__.py +19 -0
- loopx/capabilities/issue_fix/acceptance_loop.py +1050 -0
- loopx/capabilities/issue_fix/candidate_evidence.py +503 -0
- loopx/capabilities/issue_fix/candidate_preflight.py +676 -0
- loopx/capabilities/issue_fix/cli.py +1822 -0
- loopx/capabilities/issue_fix/cli_input.py +87 -0
- loopx/capabilities/issue_fix/content_ops_cli.py +148 -0
- loopx/capabilities/issue_fix/discovered_issue_promotion.py +947 -0
- loopx/capabilities/issue_fix/explore_projection.py +710 -0
- loopx/capabilities/issue_fix/feasibility.py +542 -0
- loopx/capabilities/issue_fix/github_public.py +661 -0
- loopx/capabilities/issue_fix/intake_surface.py +832 -0
- loopx/capabilities/issue_fix/metadata_preview.py +218 -0
- loopx/capabilities/issue_fix/metrics_projection.py +1340 -0
- loopx/capabilities/issue_fix/metrics_supplement.py +634 -0
- loopx/capabilities/issue_fix/metrics_supplement_cli.py +127 -0
- loopx/capabilities/issue_fix/outcome_projection.py +1235 -0
- loopx/capabilities/issue_fix/periodic_report.py +189 -0
- loopx/capabilities/issue_fix/pr_description.py +418 -0
- loopx/capabilities/issue_fix/pr_gate_reconcile.py +496 -0
- loopx/capabilities/issue_fix/pr_gate_reconcile_cli.py +464 -0
- loopx/capabilities/issue_fix/pr_lifecycle.py +1327 -0
- loopx/capabilities/issue_fix/pr_lifecycle_rollout.py +85 -0
- loopx/capabilities/issue_fix/pr_monitor_materialization.py +257 -0
- loopx/capabilities/issue_fix/pr_review_ack.py +439 -0
- loopx/capabilities/issue_fix/provider_hooks.py +24 -0
- loopx/capabilities/issue_fix/repository_commit_evidence.py +186 -0
- loopx/capabilities/issue_fix/repository_context.py +457 -0
- loopx/capabilities/issue_fix/repository_memory.py +459 -0
- loopx/capabilities/issue_fix/repository_memory_provider.py +1454 -0
- loopx/capabilities/issue_fix/repository_snapshot.py +454 -0
- loopx/capabilities/issue_fix/reviewer_cli.py +917 -0
- loopx/capabilities/issue_fix/reviewer_notification.py +882 -0
- loopx/capabilities/issue_fix/reviewer_notification_drain.py +942 -0
- loopx/capabilities/issue_fix/reviewer_recommendation.py +1057 -0
- loopx/capabilities/issue_fix/reviewer_request.py +1282 -0
- loopx/capabilities/issue_fix/reward_memory.py +879 -0
- loopx/capabilities/issue_fix/workflow_plan.py +1286 -0
- loopx/capabilities/material_lifecycle/__init__.py +161 -0
- loopx/capabilities/material_lifecycle/_validation.py +183 -0
- loopx/capabilities/material_lifecycle/apply.py +672 -0
- loopx/capabilities/material_lifecycle/architecture.py +122 -0
- loopx/capabilities/material_lifecycle/cli.py +161 -0
- loopx/capabilities/material_lifecycle/decision_planning.py +470 -0
- loopx/capabilities/material_lifecycle/explore_execution.py +306 -0
- loopx/capabilities/material_lifecycle/intake.py +869 -0
- loopx/capabilities/material_lifecycle/inventory.py +147 -0
- loopx/capabilities/material_lifecycle/lifecycle.py +98 -0
- loopx/capabilities/material_lifecycle/preparation.py +147 -0
- loopx/capabilities/material_lifecycle/project_skill.py +83 -0
- loopx/capabilities/material_lifecycle/ranking.py +267 -0
- loopx/capabilities/material_lifecycle/readable_projection.py +500 -0
- loopx/capabilities/material_lifecycle/rebuild.py +480 -0
- loopx/capabilities/material_lifecycle/settlement.py +238 -0
- loopx/capabilities/periodic_report/__init__.py +71 -0
- loopx/capabilities/periodic_report/adapters.py +939 -0
- loopx/capabilities/periodic_report/archive.py +422 -0
- loopx/capabilities/periodic_report/bindings.py +705 -0
- loopx/capabilities/periodic_report/cli.py +277 -0
- loopx/capabilities/periodic_report/core.py +691 -0
- loopx/capabilities/periodic_report/extension_envelope.py +66 -0
- loopx/capabilities/periodic_report/presets.py +103 -0
- loopx/capabilities/periodic_report/profile.py +235 -0
- loopx/capabilities/periodic_report/project_progress.py +179 -0
- loopx/capabilities/periodic_report/triggers.py +452 -0
- loopx/capabilities/pr_review_queue/__init__.py +17 -0
- loopx/capabilities/pr_review_queue/core.py +506 -0
- loopx/capabilities/pr_review_queue/review_contract.py +506 -0
- loopx/capabilities/registry.py +192 -0
- loopx/capabilities/reward_memory/__init__.py +75 -0
- loopx/capabilities/reward_memory/application.py +819 -0
- loopx/capabilities/reward_memory/architecture.py +572 -0
- loopx/capabilities/reward_memory/candidate_review.py +511 -0
- loopx/capabilities/reward_memory/cli.py +469 -0
- loopx/capabilities/reward_memory/dogfood.py +574 -0
- loopx/capabilities/reward_memory/evaluation.py +296 -0
- loopx/capabilities/reward_memory/evaluation_fixtures.py +362 -0
- loopx/capabilities/reward_memory/experiment.py +567 -0
- loopx/capabilities/reward_memory/health.py +222 -0
- loopx/capabilities/reward_memory/ingestion.py +519 -0
- loopx/capabilities/reward_memory/registry.py +600 -0
- loopx/capabilities/reward_memory/runtime_hooks.py +312 -0
- loopx/capabilities/reward_memory/scoped_feedback.py +173 -0
- loopx/capabilities/semantic_preference/__init__.py +12 -0
- loopx/capabilities/semantic_preference/cli.py +189 -0
- loopx/capabilities/semantic_preference/contract.py +592 -0
- loopx/capabilities/semantic_preference/reward_memory.py +62 -0
- loopx/capabilities/value_connectors/__init__.py +1 -0
- loopx/capabilities/value_connectors/cli.py +401 -0
- loopx/capabilities/value_connectors/finance_extension_migration.py +108 -0
- loopx/capabilities/value_connectors/install_check.py +147 -0
- loopx/capabilities/value_connectors/planner.py +733 -0
- loopx/capabilities/value_connectors/source_map.py +446 -0
- loopx/claude_goal_baseline.py +138 -0
- loopx/claude_goal_mode/__init__.py +23 -0
- loopx/claude_goal_mode/hooks/goal_policy.py +212 -0
- loopx/claude_goal_mode/hooks/goal_state.py +139 -0
- loopx/claude_goal_mode/mcp/loopx_mcp.py +167 -0
- loopx/claude_goal_mode/scripts/connect.py +103 -0
- loopx/claude_goal_mode/scripts/goalmode_cmd.py +241 -0
- loopx/claude_goal_mode/scripts/install.py +328 -0
- loopx/claude_goal_mode/statusline/goal_status.py +97 -0
- loopx/cli.py +836 -0
- loopx/cli_commands/__init__.py +334 -0
- loopx/cli_commands/_host_thread.py +13 -0
- loopx/cli_commands/agentissue_runner_flow.py +447 -0
- loopx/cli_commands/agents_last_exam.py +160 -0
- loopx/cli_commands/agents_last_exam_baked_input.py +302 -0
- loopx/cli_commands/agents_last_exam_host_codex.py +374 -0
- loopx/cli_commands/agents_last_exam_launch_dry_run.py +372 -0
- loopx/cli_commands/agents_last_exam_local_plan.py +322 -0
- loopx/cli_commands/agents_last_exam_runner_source.py +352 -0
- loopx/cli_commands/agents_last_exam_task_material.py +335 -0
- loopx/cli_commands/agents_last_exam_validation_gate.py +236 -0
- loopx/cli_commands/benchmark_boundary.py +499 -0
- loopx/cli_commands/benchmark_dispatch.py +161 -0
- loopx/cli_commands/benchmark_release_outcome.py +123 -0
- loopx/cli_commands/benchmark_review_lifecycle.py +1275 -0
- loopx/cli_commands/benchmark_run_ledger.py +763 -0
- loopx/cli_commands/benchmark_run_ledger_case_analysis.py +249 -0
- loopx/cli_commands/benchmark_run_ledger_classification.py +45 -0
- loopx/cli_commands/benchmark_run_ledger_maintenance.py +486 -0
- loopx/cli_commands/benchmark_run_ledger_maintenance_registration.py +342 -0
- loopx/cli_commands/benchmark_run_ledger_maintenance_rendering.py +233 -0
- loopx/cli_commands/benchmark_run_ledger_parity.py +92 -0
- loopx/cli_commands/bootstrap_connect.py +238 -0
- loopx/cli_commands/canary.py +707 -0
- loopx/cli_commands/canary_release_qualification.py +79 -0
- loopx/cli_commands/capability.py +96 -0
- loopx/cli_commands/doctor.py +43 -0
- loopx/cli_commands/dreaming.py +143 -0
- loopx/cli_commands/edgebench.py +205 -0
- loopx/cli_commands/evidence_log.py +275 -0
- loopx/cli_commands/explore.py +989 -0
- loopx/cli_commands/explore_planning_commands.py +157 -0
- loopx/cli_commands/extension.py +271 -0
- loopx/cli_commands/first_run_report.py +73 -0
- loopx/cli_commands/goal_channel.py +656 -0
- loopx/cli_commands/handoff_mode.py +158 -0
- loopx/cli_commands/history.py +622 -0
- loopx/cli_commands/host_mode_plan.py +113 -0
- loopx/cli_commands/lark_inbox.py +431 -0
- loopx/cli_commands/lark_kanban.py +629 -0
- loopx/cli_commands/ml_experiment.py +321 -0
- loopx/cli_commands/multi_agent.py +211 -0
- loopx/cli_commands/opencode2_goal_worker.py +217 -0
- loopx/cli_commands/pr_review.py +167 -0
- loopx/cli_commands/presentation.py +218 -0
- loopx/cli_commands/preset.py +96 -0
- loopx/cli_commands/project.py +150 -0
- loopx/cli_commands/project_lifecycle.py +915 -0
- loopx/cli_commands/quota.py +859 -0
- loopx/cli_commands/quota_registration.py +241 -0
- loopx/cli_commands/quota_request.py +113 -0
- loopx/cli_commands/ready_score.py +110 -0
- loopx/cli_commands/registry_admin.py +975 -0
- loopx/cli_commands/registry_admin_configure.py +344 -0
- loopx/cli_commands/registry_admin_peer.py +84 -0
- loopx/cli_commands/registry_authority.py +218 -0
- loopx/cli_commands/review_batch.py +146 -0
- loopx/cli_commands/slash_commands.py +145 -0
- loopx/cli_commands/start_goal.py +251 -0
- loopx/cli_commands/starter.py +175 -0
- loopx/cli_commands/starter_bootstrap.py +179 -0
- loopx/cli_commands/starter_bootstrap_registration.py +198 -0
- loopx/cli_commands/starter_runtime_idle.py +107 -0
- loopx/cli_commands/starter_scheduler.py +207 -0
- loopx/cli_commands/starter_session_runtime.py +152 -0
- loopx/cli_commands/starter_visible_common.py +54 -0
- loopx/cli_commands/starter_visible_driver.py +161 -0
- loopx/cli_commands/starter_visible_pilot.py +278 -0
- loopx/cli_commands/status.py +867 -0
- loopx/cli_commands/status_registration.py +239 -0
- loopx/cli_commands/summary_all.py +222 -0
- loopx/cli_commands/support_control.py +809 -0
- loopx/cli_commands/support_control_registry.py +68 -0
- loopx/cli_commands/support_control_supervisor.py +289 -0
- loopx/cli_commands/task_lease.py +306 -0
- loopx/cli_commands/terminal_bench_adapter.py +717 -0
- loopx/cli_commands/terminal_bench_environment_result.py +1246 -0
- loopx/cli_commands/todo.py +940 -0
- loopx/cli_commands/todo_argument_validation.py +572 -0
- loopx/cli_commands/todo_event.py +114 -0
- loopx/cli_commands/turn.py +804 -0
- loopx/cli_commands/version.py +46 -0
- loopx/cli_commands/worker_bridge.py +659 -0
- loopx/cli_rollout.py +314 -0
- loopx/codex_cli_goal_tui.py +672 -0
- loopx/codex_cli_probe.py +1530 -0
- loopx/codex_cli_probe_markdown.py +935 -0
- loopx/codex_cli_runtime_probe.py +733 -0
- loopx/codex_cli_scheduler.py +564 -0
- loopx/codex_goal_baseline.py +620 -0
- loopx/configuration_catalog.py +617 -0
- loopx/configure_goal.py +1375 -0
- loopx/contract.py +996 -0
- loopx/control_plane/__init__.py +71 -0
- loopx/control_plane/agents/__init__.py +1 -0
- loopx/control_plane/agents/agent_lane_recommendation.py +516 -0
- loopx/control_plane/agents/agent_scope.py +1578 -0
- loopx/control_plane/agents/agent_scope_frontier.py +60 -0
- loopx/control_plane/agents/capability_gate.py +531 -0
- loopx/control_plane/agents/identity.py +140 -0
- loopx/control_plane/agents/legacy_migration.py +169 -0
- loopx/control_plane/agents/management_projection.py +658 -0
- loopx/control_plane/agents/material_frontier.py +608 -0
- loopx/control_plane/agents/material_handoff.py +156 -0
- loopx/control_plane/agents/multi_agent/__init__.py +1 -0
- loopx/control_plane/agents/multi_agent/codex_executable.py +207 -0
- loopx/control_plane/agents/multi_agent/collective_round_ledger.py +387 -0
- loopx/control_plane/agents/multi_agent/contract.py +474 -0
- loopx/control_plane/agents/multi_agent/recipe.py +110 -0
- loopx/control_plane/agents/multi_agent/role_successor.py +297 -0
- loopx/control_plane/agents/multi_agent/runtime_scripts.py +426 -0
- loopx/control_plane/agents/multi_agent/visible_launch_policy.py +149 -0
- loopx/control_plane/agents/multi_agent/visible_wake_scheduler.py +392 -0
- loopx/control_plane/agents/profile.py +216 -0
- loopx/control_plane/agents/runtime_model.py +73 -0
- loopx/control_plane/agents/subagent_activity.py +164 -0
- loopx/control_plane/agents/supervisor.py +544 -0
- loopx/control_plane/agents/supervisor_events.py +462 -0
- loopx/control_plane/agents/supervisor_inject.py +204 -0
- loopx/control_plane/agents/work_mode.py +56 -0
- loopx/control_plane/agents/workspace_guard.py +364 -0
- loopx/control_plane/effect_program.py +644 -0
- loopx/control_plane/goals/__init__.py +1 -0
- loopx/control_plane/goals/active_state_event_projection.py +103 -0
- loopx/control_plane/goals/active_state_metadata.py +47 -0
- loopx/control_plane/goals/active_state_sections.py +58 -0
- loopx/control_plane/goals/configure_goal_service.py +354 -0
- loopx/control_plane/goals/contract_health.py +132 -0
- loopx/control_plane/goals/dreaming.py +152 -0
- loopx/control_plane/goals/global_registry_health.py +199 -0
- loopx/control_plane/goals/global_registry_shadow.py +33 -0
- loopx/control_plane/goals/goal_channel.py +34 -0
- loopx/control_plane/goals/goal_channel_projection.py +560 -0
- loopx/control_plane/goals/goal_frontier/__init__.py +1917 -0
- loopx/control_plane/goals/goal_frontier/ack_policy.py +149 -0
- loopx/control_plane/goals/goal_frontier/outcome_continuity.py +437 -0
- loopx/control_plane/goals/goal_frontier/replan_rules.py +210 -0
- loopx/control_plane/goals/goal_frontier/semantic_history.py +314 -0
- loopx/control_plane/goals/goal_frontier/terminal.py +180 -0
- loopx/control_plane/goals/goal_vision.py +443 -0
- loopx/control_plane/goals/goal_vision_policy.py +36 -0
- loopx/control_plane/goals/goal_vision_state.py +62 -0
- loopx/control_plane/goals/goal_vision_wait.py +290 -0
- loopx/control_plane/goals/path_resolution.py +20 -0
- loopx/control_plane/goals/start_contract.py +206 -0
- loopx/control_plane/goals/vision_checkpoint.py +92 -0
- loopx/control_plane/handoff/__init__.py +1 -0
- loopx/control_plane/handoff/cross_runtime_impl_review.py +311 -0
- loopx/control_plane/handoff/delivery_contract.py +161 -0
- loopx/control_plane/handoff/handoff_runs.py +71 -0
- loopx/control_plane/handoff/project_handoff.py +155 -0
- loopx/control_plane/handoff/review_batch.py +463 -0
- loopx/control_plane/handoff/review_packet_context.py +216 -0
- loopx/control_plane/heartbeat/agent.py +173 -0
- loopx/control_plane/heartbeat/budget.py +66 -0
- loopx/control_plane/heartbeat/builder.py +501 -0
- loopx/control_plane/heartbeat/host.py +64 -0
- loopx/control_plane/heartbeat/rules.py +68 -0
- loopx/control_plane/heartbeat/task_body.py +759 -0
- loopx/control_plane/heartbeat/visible_goal.py +86 -0
- loopx/control_plane/projects/__init__.py +1 -0
- loopx/control_plane/projects/contract.py +25 -0
- loopx/control_plane/projects/registry.py +663 -0
- loopx/control_plane/quota/__init__.py +1 -0
- loopx/control_plane/quota/cli_projection.py +704 -0
- loopx/control_plane/quota/decision_summary.py +431 -0
- loopx/control_plane/quota/effect_program.py +152 -0
- loopx/control_plane/quota/error_codes.py +19 -0
- loopx/control_plane/quota/goal_boundary.py +464 -0
- loopx/control_plane/quota/heartbeat_receipt.py +277 -0
- loopx/control_plane/quota/heartbeat_recommendation.py +718 -0
- loopx/control_plane/quota/host_poll_receipts.py +162 -0
- loopx/control_plane/quota/live_decision.py +142 -0
- loopx/control_plane/quota/monitor_poll.py +786 -0
- loopx/control_plane/quota/policy_constants.py +40 -0
- loopx/control_plane/quota/projection_repair.py +262 -0
- loopx/control_plane/quota/recent_runs.py +210 -0
- loopx/control_plane/quota/scheduler_ack.py +490 -0
- loopx/control_plane/quota/selected_todo_projection.py +139 -0
- loopx/control_plane/quota/settlement.py +437 -0
- loopx/control_plane/quota/settlement_cli.py +246 -0
- loopx/control_plane/quota/settlement_validation.py +64 -0
- loopx/control_plane/quota/settlement_workspace_causality.py +180 -0
- loopx/control_plane/quota/should_run.py +249 -0
- loopx/control_plane/quota/should_run_packet.py +1165 -0
- loopx/control_plane/quota/should_run_prepare.py +675 -0
- loopx/control_plane/quota/slot_accounting.py +1123 -0
- loopx/control_plane/quota/spend_sources.py +11 -0
- loopx/control_plane/quota/stall_repair.py +397 -0
- loopx/control_plane/quota/states.py +29 -0
- loopx/control_plane/quota/task_orchestration.py +448 -0
- loopx/control_plane/quota/task_orchestration_admission.py +497 -0
- loopx/control_plane/quota/turn_envelope.py +889 -0
- loopx/control_plane/quota/usage_summary.py +140 -0
- loopx/control_plane/reward_memory.py +43 -0
- loopx/control_plane/runtime/__init__.py +2 -0
- loopx/control_plane/runtime/active_user_assisted_pilot.py +275 -0
- loopx/control_plane/runtime/agent_scoped_evidence_log.py +435 -0
- loopx/control_plane/runtime/decision_freshness.py +203 -0
- loopx/control_plane/runtime/event_ledger.py +197 -0
- loopx/control_plane/runtime/event_store_migration_bridge.py +196 -0
- loopx/control_plane/runtime/goal_project_route.py +70 -0
- loopx/control_plane/runtime/local_state_write_correctness.py +242 -0
- loopx/control_plane/runtime/promotion_readiness.py +152 -0
- loopx/control_plane/runtime/public_safety.py +120 -0
- loopx/control_plane/runtime/run_artifacts.py +78 -0
- loopx/control_plane/runtime/run_compaction.py +397 -0
- loopx/control_plane/runtime/run_context_retention.py +241 -0
- loopx/control_plane/runtime/run_history.py +132 -0
- loopx/control_plane/runtime/run_index_duplicates.py +205 -0
- loopx/control_plane/runtime/run_index_rebuild.py +263 -0
- loopx/control_plane/runtime/run_ingest_health.py +336 -0
- loopx/control_plane/runtime/runtime_projection_route.py +624 -0
- loopx/control_plane/runtime/runtime_projection_writer.py +98 -0
- loopx/control_plane/runtime/session_runtime.py +339 -0
- loopx/control_plane/runtime/shared_runtime_material_projection.py +332 -0
- loopx/control_plane/runtime/shared_runtime_refresh_projection.py +183 -0
- loopx/control_plane/runtime/stale_latest_run.py +90 -0
- loopx/control_plane/runtime/status_classifications.py +49 -0
- loopx/control_plane/runtime/status_projection_cache.py +235 -0
- loopx/control_plane/runtime/stride_observation.py +144 -0
- loopx/control_plane/runtime/time.py +39 -0
- loopx/control_plane/runtime/trajectory_hygiene.py +149 -0
- loopx/control_plane/runtime/validation_command.py +69 -0
- loopx/control_plane/scheduler/__init__.py +1 -0
- loopx/control_plane/scheduler/ack.py +329 -0
- loopx/control_plane/scheduler/arbitration.py +188 -0
- loopx/control_plane/scheduler/automation_liveness.py +183 -0
- loopx/control_plane/scheduler/execution_context.py +555 -0
- loopx/control_plane/scheduler/external_evidence_observation.py +428 -0
- loopx/control_plane/scheduler/monitor_display.py +143 -0
- loopx/control_plane/scheduler/monitor_poll_policy.py +161 -0
- loopx/control_plane/scheduler/monitor_poll_writeback.py +351 -0
- loopx/control_plane/scheduler/monitor_target.py +64 -0
- loopx/control_plane/scheduler/monitor_todo.py +146 -0
- loopx/control_plane/scheduler/monitor_wait.py +237 -0
- loopx/control_plane/scheduler/scheduler_hint.py +1284 -0
- loopx/control_plane/scheduler/state.py +354 -0
- loopx/control_plane/scheduler/state_transition_rules.py +179 -0
- loopx/control_plane/scheduler/time.py +10 -0
- loopx/control_plane/settlement_driver.py +293 -0
- loopx/control_plane/status/__init__.py +6 -0
- loopx/control_plane/status/active_state_projection.py +105 -0
- loopx/control_plane/status/agent_lane_projection.py +375 -0
- loopx/control_plane/status/attention_projection.py +74 -0
- loopx/control_plane/status/autonomous_replan_projection.py +103 -0
- loopx/control_plane/status/collection.py +140 -0
- loopx/control_plane/status/contract_projection.py +31 -0
- loopx/control_plane/status/dreaming_projection.py +52 -0
- loopx/control_plane/status/goal_attention_projection.py +157 -0
- loopx/control_plane/status/lifecycle_projection.py +110 -0
- loopx/control_plane/status/monitor_display_projection.py +69 -0
- loopx/control_plane/status/registry_health_projection.py +75 -0
- loopx/control_plane/status/run_projection.py +70 -0
- loopx/control_plane/status/runtime_summaries.py +161 -0
- loopx/control_plane/testing/__init__.py +1 -0
- loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +1371 -0
- loopx/control_plane/testing/canary_harness.py +182 -0
- loopx/control_plane/testing/capability_monitor_repair_tool_behavior.py +674 -0
- loopx/control_plane/testing/cli_output_budget.py +807 -0
- loopx/control_plane/testing/cli_output_differential.py +250 -0
- loopx/control_plane/testing/cli_output_semantics.py +87 -0
- loopx/control_plane/testing/control_plane_composition_scenarios.py +225 -0
- loopx/control_plane/testing/decision_replay.py +268 -0
- loopx/control_plane/testing/doubao_model_behavior_actor.py +559 -0
- loopx/control_plane/testing/model_behavior_corpus.py +344 -0
- loopx/control_plane/testing/model_behavior_qualification.py +769 -0
- loopx/control_plane/testing/model_behavior_retained_cases.py +235 -0
- loopx/control_plane/testing/model_tool_behavior.py +536 -0
- loopx/control_plane/testing/onboarding_model_behavior_qualification.py +642 -0
- loopx/control_plane/testing/quota_fixtures.py +208 -0
- loopx/control_plane/testing/quota_should_run_parity.py +57 -0
- loopx/control_plane/testing/release_commit_qualification.py +671 -0
- loopx/control_plane/testing/replan_semantic_action_behavior.py +1302 -0
- loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +527 -0
- loopx/control_plane/testing/selected_todo_tool_behavior.py +1002 -0
- loopx/control_plane/testing/terminal_settlement_tool_behavior.py +656 -0
- loopx/control_plane/todos/__init__.py +1 -0
- loopx/control_plane/todos/active_state_editing.py +296 -0
- loopx/control_plane/todos/active_state_todo_parser.py +138 -0
- loopx/control_plane/todos/active_state_todos.py +175 -0
- loopx/control_plane/todos/addition.py +103 -0
- loopx/control_plane/todos/claim_visibility.py +253 -0
- loopx/control_plane/todos/completed_archive.py +139 -0
- loopx/control_plane/todos/completion_fence.py +49 -0
- loopx/control_plane/todos/completion_policy.py +153 -0
- loopx/control_plane/todos/completion_validation.py +248 -0
- loopx/control_plane/todos/completion_validation_accountability.py +27 -0
- loopx/control_plane/todos/completion_validation_projection.py +57 -0
- loopx/control_plane/todos/contract.py +1476 -0
- loopx/control_plane/todos/decision_scope.py +554 -0
- loopx/control_plane/todos/deferred_resume.py +546 -0
- loopx/control_plane/todos/durable_completion.py +201 -0
- loopx/control_plane/todos/event_writeback.py +484 -0
- loopx/control_plane/todos/frontier_deadline.py +132 -0
- loopx/control_plane/todos/handoff_gate.py +283 -0
- loopx/control_plane/todos/handoff_mode.py +444 -0
- loopx/control_plane/todos/handoff_note.py +202 -0
- loopx/control_plane/todos/line_update.py +361 -0
- loopx/control_plane/todos/list_projection.py +205 -0
- loopx/control_plane/todos/markdown.py +199 -0
- loopx/control_plane/todos/monitor_metadata.py +88 -0
- loopx/control_plane/todos/mutation_authority.py +299 -0
- loopx/control_plane/todos/projection.py +655 -0
- loopx/control_plane/todos/quota_summary.py +1138 -0
- loopx/control_plane/todos/route_continuation.py +267 -0
- loopx/control_plane/todos/succession_warning.py +174 -0
- loopx/control_plane/todos/summary_item.py +223 -0
- loopx/control_plane/todos/text.py +30 -0
- loopx/control_plane/todos/todo_index.py +226 -0
- loopx/control_plane/todos/todo_summary.py +1458 -0
- loopx/control_plane/todos/unblock_resume.py +326 -0
- loopx/control_plane/todos/user_gate.py +263 -0
- loopx/control_plane/todos/write_hint.py +63 -0
- loopx/control_plane/todos/write_policy.py +135 -0
- loopx/control_plane/turn_driver/__init__.py +85 -0
- loopx/control_plane/turn_driver/codex_cli.py +502 -0
- loopx/control_plane/turn_driver/driver.py +355 -0
- loopx/control_plane/turn_driver/executor.py +1468 -0
- loopx/control_plane/turn_driver/loop_controller.py +669 -0
- loopx/control_plane/turn_driver/settlement.py +318 -0
- loopx/control_plane/turn_driver/transaction.py +375 -0
- loopx/control_plane/work_items/__init__.py +1 -0
- loopx/control_plane/work_items/attention_fields.py +56 -0
- loopx/control_plane/work_items/attention_item.py +77 -0
- loopx/control_plane/work_items/attention_queue.py +322 -0
- loopx/control_plane/work_items/attention_routing.py +213 -0
- loopx/control_plane/work_items/autonomous_candidates.py +135 -0
- loopx/control_plane/work_items/autonomous_replan_ack.py +276 -0
- loopx/control_plane/work_items/autonomous_replan_obligation.py +786 -0
- loopx/control_plane/work_items/backlog_hygiene.py +59 -0
- loopx/control_plane/work_items/capability_monitor_fallback.py +221 -0
- loopx/control_plane/work_items/delivery_batch_scale.py +66 -0
- loopx/control_plane/work_items/delivery_outcome.py +152 -0
- loopx/control_plane/work_items/delivery_signals.py +113 -0
- loopx/control_plane/work_items/execution_obligation.py +235 -0
- loopx/control_plane/work_items/goal_route_hint.py +320 -0
- loopx/control_plane/work_items/interaction_contract.py +1540 -0
- loopx/control_plane/work_items/issue_meta_surface.py +159 -0
- loopx/control_plane/work_items/lifecycle.py +139 -0
- loopx/control_plane/work_items/operator_inbox.py +266 -0
- loopx/control_plane/work_items/outcome_followthrough.py +69 -0
- loopx/control_plane/work_items/primary_action.py +326 -0
- loopx/control_plane/work_items/progress_observation.py +630 -0
- loopx/control_plane/work_items/project_asset.py +675 -0
- loopx/control_plane/work_items/repair_delta.py +693 -0
- loopx/control_plane/work_items/runtime_capability_reentry.py +168 -0
- loopx/control_plane/work_items/semantic_replan_writeback.py +177 -0
- loopx/control_plane/work_items/status_contract.py +49 -0
- loopx/control_plane/work_items/task_graph.py +1046 -0
- loopx/control_plane/work_items/task_lease.py +1254 -0
- loopx/control_plane/work_items/task_lease_settlement.py +422 -0
- loopx/control_plane/work_items/work_lane.py +510 -0
- loopx/control_plane/work_items/work_lane_context.py +161 -0
- loopx/demo.py +247 -0
- loopx/diagnose.py +633 -0
- loopx/doctor.py +1251 -0
- loopx/domain_packs/__init__.py +1 -0
- loopx/domain_packs/issue_fix.py +571 -0
- loopx/domain_packs/ml_experiment.py +854 -0
- loopx/domain_state.py +137 -0
- loopx/dreaming.py +706 -0
- loopx/entrypoint.py +16 -0
- loopx/event_sourced_state.py +981 -0
- loopx/execution_profile.py +286 -0
- loopx/experiments/__init__.py +1 -0
- loopx/experiments/planner_worker/__init__.py +1 -0
- loopx/experiments/planner_worker/contract.py +523 -0
- loopx/experiments/planner_worker/runtime.py +391 -0
- loopx/experiments/planner_worker/traex.py +461 -0
- loopx/explore_graph.py +11 -0
- loopx/extensions/__init__.py +1 -0
- loopx/extensions/bundled.py +28 -0
- loopx/extensions/execution_envelope.py +126 -0
- loopx/extensions/lark/__init__.py +11 -0
- loopx/extensions/lark/event_collector.py +478 -0
- loopx/extensions/lark/event_collector_runtime.py +506 -0
- loopx/extensions/lark/event_inbox.py +454 -0
- loopx/extensions/lark/extension.toml +88 -0
- loopx/extensions/lark/goal_channel.py +44 -0
- loopx/extensions/lark/goal_channel_contracts.py +388 -0
- loopx/extensions/lark/goal_channel_lifecycle.py +218 -0
- loopx/extensions/lark/goal_channel_runtime.py +792 -0
- loopx/extensions/lark/goal_channel_setup.py +805 -0
- loopx/extensions/lark/goal_channel_targets.py +215 -0
- loopx/extensions/lark/goal_channel_transport.py +281 -0
- loopx/extensions/lark/inbox_reactions.py +650 -0
- loopx/extensions/lark/inbox_reply.py +430 -0
- loopx/extensions/lark/presentation/__init__.py +11 -0
- loopx/extensions/lark/presentation/explore_results.py +2276 -0
- loopx/extensions/lark/presentation/explore_singleflight.py +127 -0
- loopx/extensions/lark/presentation/explore_source_guard.py +121 -0
- loopx/extensions/lark/presentation/explore_stage_document.py +703 -0
- loopx/extensions/lark/presentation/explore_visual_integrity.py +122 -0
- loopx/extensions/lark/presentation/explore_visual_readback.py +452 -0
- loopx/extensions/lark/presentation/explore_visual_styles.py +156 -0
- loopx/extensions/lark/presentation/issue_fix_surface.py +612 -0
- loopx/extensions/lark/presentation/kanban.py +2791 -0
- loopx/extensions/lark/presentation/message_card.py +112 -0
- loopx/extensions/lark/presentation/periodic_report.py +261 -0
- loopx/extensions/lark/presentation/projection_rows.py +600 -0
- loopx/extensions/lark/presentation/record_io.py +95 -0
- loopx/extensions/lark/presentation/sync_receipt.py +145 -0
- loopx/extensions/lark/private_json.py +40 -0
- loopx/extensions/lark/provider.py +86 -0
- loopx/extensions/lark/reviewer_notification.py +604 -0
- loopx/extensions/manifest.py +385 -0
- loopx/extensions/openviking_periodic_report/__init__.py +17 -0
- loopx/extensions/openviking_periodic_report/activation.py +173 -0
- loopx/extensions/openviking_periodic_report/extension.toml +17 -0
- loopx/extensions/openviking_periodic_report/provider.py +355 -0
- loopx/extensions/openviking_periodic_report/sink.py +117 -0
- loopx/extensions/openviking_semantic_preference/__init__.py +5 -0
- loopx/extensions/openviking_semantic_preference/extension.toml +16 -0
- loopx/extensions/openviking_semantic_preference/history_export.py +484 -0
- loopx/extensions/openviking_semantic_preference/project_peer.py +68 -0
- loopx/extensions/openviking_semantic_preference/provider.py +312 -0
- loopx/extensions/presentation.py +979 -0
- loopx/extensions/process_runtime.py +204 -0
- loopx/extensions/readiness.py +168 -0
- loopx/extensions/runtime.py +931 -0
- loopx/extensions/scaffold.py +335 -0
- loopx/feedback.py +581 -0
- loopx/file_lock.py +382 -0
- loopx/global_registry.py +842 -0
- loopx/global_risks.py +970 -0
- loopx/global_todos.py +568 -0
- loopx/handoff_budget.py +28 -0
- loopx/heartbeat_prequota.py +80 -0
- loopx/heartbeat_prompt.py +159 -0
- loopx/help_surface.py +516 -0
- loopx/history.py +1507 -0
- loopx/host_loop_activation.py +1311 -0
- loopx/host_mode_planner.py +991 -0
- loopx/install_contract.py +1 -0
- loopx/interface_budget.py +196 -0
- loopx/long_task_cadence.py +208 -0
- loopx/materials.py +185 -0
- loopx/ml_experiment.py +3 -0
- loopx/onboarding.py +214 -0
- loopx/opencode2_goal_mode/README.md +81 -0
- loopx/opencode2_goal_mode/__init__.py +9 -0
- loopx/opencode2_goal_mode/opencode2-goal-worker.mjs +1018 -0
- loopx/opencode_goal_mode/README.md +99 -0
- loopx/opencode_goal_mode/__init__.py +13 -0
- loopx/opencode_goal_mode/goal-bridge-runtime.mjs +858 -0
- loopx/opencode_goal_mode/loopx-goal.js +8 -0
- loopx/operator_gate.py +420 -0
- loopx/orchestration.py +127 -0
- loopx/paths.py +59 -0
- loopx/pi_goal_mode/README.md +67 -0
- loopx/pi_goal_mode/__init__.py +13 -0
- loopx/pi_goal_mode/loopx-goal.ts +254 -0
- loopx/pi_goal_mode/pi-goal-loop-runtime.mjs +574 -0
- loopx/pr_review.py +1206 -0
- loopx/presentation/__init__.py +1 -0
- loopx/presentation/explore_views.py +1334 -0
- loopx/presentation/markdown.py +61 -0
- loopx/presentation/projection_source_reconcile.py +140 -0
- loopx/presentation/public_safety.py +42 -0
- loopx/presentation/renderers/__init__.py +17 -0
- loopx/presentation/renderers/goal_channel_html.py +269 -0
- loopx/presentation/renderers/periodic_report_html.py +786 -0
- loopx/presentation/renderers/periodic_report_markdown.py +184 -0
- loopx/presentation/renderers/quota_event_markdown.py +116 -0
- loopx/presentation/renderers/quota_markdown.py +1112 -0
- loopx/presentation/renderers/status_markdown.py +1570 -0
- loopx/presentation/renderers/trajectory_hygiene_markdown.py +39 -0
- loopx/presentation/renderers/turn_envelope_markdown.py +33 -0
- loopx/presentation/sinks/__init__.py +5 -0
- loopx/presentation/sinks/openviking_periodic_report.py +7 -0
- loopx/presentation/static_site.py +691 -0
- loopx/presets.py +369 -0
- loopx/project_alias.py +217 -0
- loopx/project_map.py +589 -0
- loopx/project_prompt.py +1153 -0
- loopx/project_skill_cli.py +125 -0
- loopx/project_skill_delivery.py +470 -0
- loopx/project_uninstall.py +462 -0
- loopx/promotion_gate.py +197 -0
- loopx/quota.py +1197 -0
- loopx/ready_score.py +413 -0
- loopx/registry.py +621 -0
- loopx/registry_writability.py +64 -0
- loopx/release_candidate.py +148 -0
- loopx/release_manifest.py +316 -0
- loopx/repository_identity.py +100 -0
- loopx/review_packet.py +1024 -0
- loopx/rollout_event_log.py +505 -0
- loopx/runtime.py +112 -0
- loopx/self_update.py +750 -0
- loopx/session_runtime.py +418 -0
- loopx/skill_install_readback.py +500 -0
- loopx/slash_command_install.py +1393 -0
- loopx/slash_commands.py +264 -0
- loopx/state_backup.py +573 -0
- loopx/state_migration.py +350 -0
- loopx/state_projection.py +809 -0
- loopx/state_refresh.py +1416 -0
- loopx/status.py +1383 -0
- loopx/status_server.py +935 -0
- loopx/summary_all.py +725 -0
- loopx/terminal_bench_agent.py +2056 -0
- loopx/thread_agent_binding.py +408 -0
- loopx/todo_followups.py +168 -0
- loopx/todo_suggestion_prompt.py +204 -0
- loopx/todos.py +2229 -0
- loopx/turn_identity.py +17 -0
- loopx/upgrade.py +1083 -0
- loopx/visible_governance.py +667 -0
- loopx/visible_multi_agent_launcher.py +1253 -0
- loopx/visible_multi_agent_tmux.py +429 -0
- loopx/worker_bridge.py +1574 -0
- loopx-0.4.8.dist-info/METADATA +708 -0
- loopx-0.4.8.dist-info/RECORD +811 -0
- loopx-0.4.8.dist-info/WHEEL +5 -0
- loopx-0.4.8.dist-info/entry_points.txt +5 -0
- loopx-0.4.8.dist-info/licenses/LICENSE +202 -0
- loopx-0.4.8.dist-info/licenses/LICENSE-MIT +21 -0
- loopx-0.4.8.dist-info/licenses/NOTICE +6 -0
- loopx-0.4.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
"""One bounded LoopX Turn host execution with resumable local receipts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ...authority import validate_public_safe_text
|
|
15
|
+
from ...file_lock import LockAcquireTimeoutError, exclusive_file_lock
|
|
16
|
+
from ...runtime import validate_goal_id_path_segment
|
|
17
|
+
from ..effect_program import (
|
|
18
|
+
SettlementResult,
|
|
19
|
+
SettlementStepKind,
|
|
20
|
+
interpret_turn_journal,
|
|
21
|
+
interpret_turn_result_packet,
|
|
22
|
+
settlement_result_payload,
|
|
23
|
+
)
|
|
24
|
+
from ..goals.goal_vision import normalize_goal_vision_packet
|
|
25
|
+
from ..work_items.delivery_batch_scale import require_delivery_batch_scale
|
|
26
|
+
from ..work_items.delivery_outcome import require_delivery_outcome
|
|
27
|
+
from .driver import selected_turn_todo
|
|
28
|
+
from .settlement import (
|
|
29
|
+
completion_writeback_outcome,
|
|
30
|
+
execute_turn_driver_settlement,
|
|
31
|
+
execute_verified_turn_terminal_closeout,
|
|
32
|
+
terminal_closeout_requirement,
|
|
33
|
+
)
|
|
34
|
+
from .transaction import (
|
|
35
|
+
LOOPX_TURN_EXECUTION_SCHEMA_VERSION,
|
|
36
|
+
LOOPX_TURN_RESULT_SCHEMA_VERSION,
|
|
37
|
+
TRANSACTION_PHASES,
|
|
38
|
+
LoopXTurnResultKind,
|
|
39
|
+
build_loopx_turn_transaction_plan,
|
|
40
|
+
validate_loopx_turn_receipt,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION = "loopx_turn_host_request_v0"
|
|
44
|
+
LOOPX_TURN_JOURNAL_INSPECTION_SCHEMA_VERSION = "loopx_turn_journal_inspection_v0"
|
|
45
|
+
LOOPX_TURN_JOURNAL_SCHEMA_VERSION = "loopx_turn_journal_v0"
|
|
46
|
+
LOOPX_TURN_TASK_VALIDATION_SCHEMA_VERSION = "loopx_turn_task_validation_v0"
|
|
47
|
+
HOST_RESULT_MAX_BYTES = 12_000
|
|
48
|
+
HOST_ARG_MAX_COUNT = 32
|
|
49
|
+
HOST_ARG_MAX_CHARS = 1_024
|
|
50
|
+
HOST_AGENT_VISION_JSON_MAX_CHARS = 3_200
|
|
51
|
+
HOST_PATH_DELTA_MODES = {"", "unchanged", "material_replan"}
|
|
52
|
+
HOST_RESULT_TEXT_LIMITS = (
|
|
53
|
+
("classification", 120),
|
|
54
|
+
("recommended_action", 1_200),
|
|
55
|
+
("next_action", 1_200),
|
|
56
|
+
("vision_unchanged_reason", 240),
|
|
57
|
+
("summary", 400),
|
|
58
|
+
)
|
|
59
|
+
TURN_KEY_RE = re.compile(r"^sha256:(?P<digest>[0-9a-f]{64})$")
|
|
60
|
+
|
|
61
|
+
MATERIAL_HOST_RESULT_KINDS = {
|
|
62
|
+
LoopXTurnResultKind.VALIDATED_PROGRESS,
|
|
63
|
+
LoopXTurnResultKind.VALIDATED_COMPLETION,
|
|
64
|
+
LoopXTurnResultKind.REPAIR_REQUIRED,
|
|
65
|
+
LoopXTurnResultKind.REPLAN_REQUIRED,
|
|
66
|
+
}
|
|
67
|
+
STOP_HOST_RESULT_KINDS = {
|
|
68
|
+
LoopXTurnResultKind.USER_ACTION_REQUIRED,
|
|
69
|
+
LoopXTurnResultKind.WAIT,
|
|
70
|
+
}
|
|
71
|
+
HOST_RESULT_FIELDS = {
|
|
72
|
+
"schema_version",
|
|
73
|
+
"turn_key",
|
|
74
|
+
"result_kind",
|
|
75
|
+
"completed_phases",
|
|
76
|
+
"classification",
|
|
77
|
+
"recommended_action",
|
|
78
|
+
"next_action",
|
|
79
|
+
"delivery_batch_scale",
|
|
80
|
+
"delivery_outcome",
|
|
81
|
+
"vision_unchanged_reason",
|
|
82
|
+
"path_delta_mode",
|
|
83
|
+
"agent_vision_json",
|
|
84
|
+
"summary",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Writeback = Callable[[dict[str, Any]], dict[str, Any]]
|
|
88
|
+
CompletionWriteback = Callable[[dict[str, Any]], dict[str, Any]]
|
|
89
|
+
CompletionIntent = Callable[[dict[str, Any]], dict[str, Any]]
|
|
90
|
+
TerminalCloseout = Callable[[dict[str, Any]], dict[str, Any]]
|
|
91
|
+
Spend = Callable[[], dict[str, Any]]
|
|
92
|
+
Scheduler = Callable[[dict[str, Any]], dict[str, Any]]
|
|
93
|
+
HostRunner = Callable[[Mapping[str, Any]], dict[str, Any]]
|
|
94
|
+
TaskValidator = Callable[
|
|
95
|
+
[Mapping[str, Any], Mapping[str, Any]],
|
|
96
|
+
Mapping[str, Any],
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class BuiltInHostError(RuntimeError):
|
|
101
|
+
"""A public-safe built-in host failure classification."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, reason: str) -> None:
|
|
104
|
+
super().__init__(reason)
|
|
105
|
+
self.reason = reason
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _normalize_argv(value: Sequence[str], *, label: str) -> list[str]:
|
|
109
|
+
argv = [str(item) for item in value]
|
|
110
|
+
if not argv:
|
|
111
|
+
raise ValueError(f"{label} command must contain at least one argv item")
|
|
112
|
+
if len(argv) > HOST_ARG_MAX_COUNT:
|
|
113
|
+
raise ValueError(f"{label} command exceeds {HOST_ARG_MAX_COUNT} argv items")
|
|
114
|
+
for item in argv:
|
|
115
|
+
if not item or "\x00" in item or len(item) > HOST_ARG_MAX_CHARS:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"{label} command contains an empty, NUL, or oversized argv item"
|
|
118
|
+
)
|
|
119
|
+
return argv
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normalize_host_argv(value: Sequence[str]) -> list[str]:
|
|
123
|
+
return _normalize_argv(value, label="host")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_task_validator_argv(value: Sequence[str]) -> list[str]:
|
|
127
|
+
return _normalize_argv(value, label="task validator")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def build_loopx_turn_host_request(plan: Mapping[str, Any]) -> dict[str, Any]:
|
|
131
|
+
transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
|
|
132
|
+
turn_key = str(transaction.get("turn_key") or "")
|
|
133
|
+
if not TURN_KEY_RE.fullmatch(turn_key):
|
|
134
|
+
raise ValueError("LoopX Turn plan has no valid transaction turn_key")
|
|
135
|
+
route = plan.get("route") if isinstance(plan.get("route"), dict) else {}
|
|
136
|
+
if route.get("would_invoke_host") is not True:
|
|
137
|
+
raise ValueError("LoopX Turn route is not host executable")
|
|
138
|
+
request = {
|
|
139
|
+
"schema_version": LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION,
|
|
140
|
+
"turn_key": turn_key,
|
|
141
|
+
"route": route.get("kind"),
|
|
142
|
+
"session": plan.get("session"),
|
|
143
|
+
"turn_envelope": plan.get("turn_envelope"),
|
|
144
|
+
"result_contract": {
|
|
145
|
+
"schema_version": LOOPX_TURN_RESULT_SCHEMA_VERSION,
|
|
146
|
+
"completed_phases": list(TRANSACTION_PHASES[:2]),
|
|
147
|
+
"stdout": "one public-safe JSON object",
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
child_operations = plan.get("child_operations")
|
|
151
|
+
if isinstance(child_operations, list) and child_operations:
|
|
152
|
+
request["child_operations"] = child_operations
|
|
153
|
+
return request
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _bounded_public_text(
|
|
157
|
+
result: Mapping[str, Any],
|
|
158
|
+
field: str,
|
|
159
|
+
*,
|
|
160
|
+
limit: int,
|
|
161
|
+
required: bool,
|
|
162
|
+
errors: list[str],
|
|
163
|
+
) -> str | None:
|
|
164
|
+
text = str(result.get(field) or "").strip()
|
|
165
|
+
if required and not text:
|
|
166
|
+
errors.append(f"{field} is required")
|
|
167
|
+
return None
|
|
168
|
+
if not text:
|
|
169
|
+
return None
|
|
170
|
+
if len(text) > limit:
|
|
171
|
+
errors.append(f"{field} exceeds {limit} characters")
|
|
172
|
+
return None
|
|
173
|
+
try:
|
|
174
|
+
validate_public_safe_text(f"host_result.{field}", text)
|
|
175
|
+
except ValueError as exc:
|
|
176
|
+
errors.append(str(exc))
|
|
177
|
+
return None
|
|
178
|
+
return text
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _normalize_host_path_delta(
|
|
182
|
+
plan: Mapping[str, Any],
|
|
183
|
+
result: Mapping[str, Any],
|
|
184
|
+
*,
|
|
185
|
+
unchanged_reason: str,
|
|
186
|
+
errors: list[str],
|
|
187
|
+
) -> tuple[str, dict[str, Any] | None]:
|
|
188
|
+
path_delta_mode = str(result.get("path_delta_mode") or "").strip()
|
|
189
|
+
raw_agent_vision = result.get("agent_vision_json")
|
|
190
|
+
if raw_agent_vision is None:
|
|
191
|
+
agent_vision_json = ""
|
|
192
|
+
elif isinstance(raw_agent_vision, str):
|
|
193
|
+
agent_vision_json = raw_agent_vision.strip()
|
|
194
|
+
else:
|
|
195
|
+
agent_vision_json = ""
|
|
196
|
+
errors.append("agent_vision_json must be a JSON string")
|
|
197
|
+
|
|
198
|
+
# Older generic hosts only supplied an unchanged reason. Keep that
|
|
199
|
+
# contract valid while making new hosts classify material path changes.
|
|
200
|
+
if not path_delta_mode:
|
|
201
|
+
path_delta_mode = "material_replan" if agent_vision_json else "unchanged"
|
|
202
|
+
if path_delta_mode not in HOST_PATH_DELTA_MODES - {""}:
|
|
203
|
+
errors.append("path_delta_mode must be unchanged or material_replan")
|
|
204
|
+
|
|
205
|
+
agent_vision: dict[str, Any] | None = None
|
|
206
|
+
if agent_vision_json:
|
|
207
|
+
if len(agent_vision_json) > HOST_AGENT_VISION_JSON_MAX_CHARS:
|
|
208
|
+
errors.append(
|
|
209
|
+
"agent_vision_json exceeds "
|
|
210
|
+
f"{HOST_AGENT_VISION_JSON_MAX_CHARS} characters"
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
try:
|
|
214
|
+
packet = json.loads(agent_vision_json)
|
|
215
|
+
if not isinstance(packet, dict):
|
|
216
|
+
raise TypeError("agent_vision_json must decode to a JSON object")
|
|
217
|
+
envelope = (
|
|
218
|
+
plan.get("turn_envelope")
|
|
219
|
+
if isinstance(plan.get("turn_envelope"), dict)
|
|
220
|
+
else {}
|
|
221
|
+
)
|
|
222
|
+
agent_vision = normalize_goal_vision_packet(
|
|
223
|
+
packet,
|
|
224
|
+
goal_id=str(envelope.get("goal_id") or ""),
|
|
225
|
+
agent_id=str(envelope.get("agent_id") or "") or None,
|
|
226
|
+
)
|
|
227
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
228
|
+
errors.append(f"invalid agent_vision_json: {exc}")
|
|
229
|
+
|
|
230
|
+
if path_delta_mode == "material_replan":
|
|
231
|
+
if agent_vision is None:
|
|
232
|
+
errors.append(
|
|
233
|
+
"material_replan requires agent_vision_json with goal_path_delta_v0"
|
|
234
|
+
)
|
|
235
|
+
elif not isinstance(agent_vision.get("path_delta"), dict):
|
|
236
|
+
errors.append(
|
|
237
|
+
"material_replan agent_vision_json requires goal_path_delta_v0"
|
|
238
|
+
)
|
|
239
|
+
elif agent_vision["path_delta"].get("outcome") != "replan":
|
|
240
|
+
errors.append(
|
|
241
|
+
"material_replan goal_path_delta_v0 outcome must be replan"
|
|
242
|
+
)
|
|
243
|
+
if unchanged_reason:
|
|
244
|
+
errors.append(
|
|
245
|
+
"material_replan cannot also declare vision_unchanged_reason"
|
|
246
|
+
)
|
|
247
|
+
elif path_delta_mode == "unchanged":
|
|
248
|
+
if agent_vision is not None:
|
|
249
|
+
errors.append("unchanged path_delta_mode cannot include agent_vision_json")
|
|
250
|
+
if not unchanged_reason:
|
|
251
|
+
errors.append("unchanged path_delta_mode requires vision_unchanged_reason")
|
|
252
|
+
|
|
253
|
+
return path_delta_mode, agent_vision
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def validate_loopx_turn_host_result(
|
|
257
|
+
plan: Mapping[str, Any],
|
|
258
|
+
value: Mapping[str, Any],
|
|
259
|
+
*,
|
|
260
|
+
completion_lifecycle_configured: bool = False,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
result = dict(value)
|
|
263
|
+
errors: list[str] = []
|
|
264
|
+
unknown = sorted(set(result) - HOST_RESULT_FIELDS)
|
|
265
|
+
if unknown:
|
|
266
|
+
errors.append("unsupported host result fields: " + ", ".join(unknown))
|
|
267
|
+
if result.get("schema_version") != LOOPX_TURN_RESULT_SCHEMA_VERSION:
|
|
268
|
+
errors.append("unsupported host result schema_version")
|
|
269
|
+
|
|
270
|
+
transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
|
|
271
|
+
turn_key = str(transaction.get("turn_key") or "")
|
|
272
|
+
if not turn_key or str(result.get("turn_key") or "") != turn_key:
|
|
273
|
+
errors.append("host result turn_key does not match the transaction plan")
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
kind = LoopXTurnResultKind(str(result.get("result_kind") or ""))
|
|
277
|
+
except ValueError:
|
|
278
|
+
kind = None
|
|
279
|
+
errors.append("unsupported host result kind")
|
|
280
|
+
if (
|
|
281
|
+
kind is LoopXTurnResultKind.VALIDATED_COMPLETION
|
|
282
|
+
and not completion_lifecycle_configured
|
|
283
|
+
):
|
|
284
|
+
errors.append("validated_completion requires a todo lifecycle adapter")
|
|
285
|
+
if (
|
|
286
|
+
kind not in MATERIAL_HOST_RESULT_KINDS | STOP_HOST_RESULT_KINDS
|
|
287
|
+
and kind is not LoopXTurnResultKind.VALIDATED_COMPLETION
|
|
288
|
+
):
|
|
289
|
+
errors.append("host result kind is not accepted by run-once")
|
|
290
|
+
|
|
291
|
+
phases = result.get("completed_phases")
|
|
292
|
+
if phases != list(TRANSACTION_PHASES[:2]):
|
|
293
|
+
errors.append("host result completed_phases must be host_execute, typed_result")
|
|
294
|
+
|
|
295
|
+
material = kind in MATERIAL_HOST_RESULT_KINDS
|
|
296
|
+
normalized = {
|
|
297
|
+
"schema_version": LOOPX_TURN_RESULT_SCHEMA_VERSION,
|
|
298
|
+
"turn_key": turn_key,
|
|
299
|
+
"result_kind": kind.value if kind else None,
|
|
300
|
+
"completed_phases": list(TRANSACTION_PHASES[:2]),
|
|
301
|
+
}
|
|
302
|
+
for field, limit in HOST_RESULT_TEXT_LIMITS:
|
|
303
|
+
text = _bounded_public_text(
|
|
304
|
+
result,
|
|
305
|
+
field,
|
|
306
|
+
limit=limit,
|
|
307
|
+
required=material and field not in {"summary", "vision_unchanged_reason"},
|
|
308
|
+
errors=errors,
|
|
309
|
+
)
|
|
310
|
+
if text:
|
|
311
|
+
normalized[field] = text
|
|
312
|
+
if material:
|
|
313
|
+
try:
|
|
314
|
+
normalized["delivery_batch_scale"] = require_delivery_batch_scale(
|
|
315
|
+
result.get("delivery_batch_scale")
|
|
316
|
+
).value
|
|
317
|
+
except ValueError as exc:
|
|
318
|
+
errors.append(str(exc))
|
|
319
|
+
try:
|
|
320
|
+
normalized["delivery_outcome"] = require_delivery_outcome(
|
|
321
|
+
result.get("delivery_outcome")
|
|
322
|
+
).value
|
|
323
|
+
except ValueError as exc:
|
|
324
|
+
errors.append(str(exc))
|
|
325
|
+
|
|
326
|
+
unchanged_reason = str(normalized.get("vision_unchanged_reason") or "")
|
|
327
|
+
path_delta_mode, agent_vision = _normalize_host_path_delta(
|
|
328
|
+
plan,
|
|
329
|
+
result,
|
|
330
|
+
unchanged_reason=unchanged_reason,
|
|
331
|
+
errors=errors,
|
|
332
|
+
)
|
|
333
|
+
if (
|
|
334
|
+
path_delta_mode == "material_replan"
|
|
335
|
+
and kind is not LoopXTurnResultKind.REPLAN_REQUIRED
|
|
336
|
+
):
|
|
337
|
+
errors.append(
|
|
338
|
+
"material_replan path_delta_mode requires result_kind replan_required"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
normalized["path_delta_mode"] = path_delta_mode
|
|
342
|
+
if agent_vision is not None:
|
|
343
|
+
normalized["agent_vision"] = agent_vision
|
|
344
|
+
elif str(result.get("path_delta_mode") or "").strip() or str(
|
|
345
|
+
result.get("agent_vision_json") or ""
|
|
346
|
+
).strip():
|
|
347
|
+
errors.append(
|
|
348
|
+
"wait and user_action_required results cannot declare a path delta"
|
|
349
|
+
)
|
|
350
|
+
return {
|
|
351
|
+
"ok": not errors,
|
|
352
|
+
"result": normalized,
|
|
353
|
+
"errors": errors,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _task_validation_receipt(
|
|
358
|
+
*,
|
|
359
|
+
status: str,
|
|
360
|
+
validator_kind: str,
|
|
361
|
+
summary: str,
|
|
362
|
+
recovery_kind: str | None = None,
|
|
363
|
+
exit_code: int | None = None,
|
|
364
|
+
) -> dict[str, Any]:
|
|
365
|
+
errors: list[str] = []
|
|
366
|
+
if status not in {
|
|
367
|
+
"passed",
|
|
368
|
+
"progress",
|
|
369
|
+
"failed",
|
|
370
|
+
"inconclusive",
|
|
371
|
+
"unavailable",
|
|
372
|
+
"not_required",
|
|
373
|
+
}:
|
|
374
|
+
errors.append("unsupported task validation status")
|
|
375
|
+
if not validator_kind or len(validator_kind) > 80:
|
|
376
|
+
errors.append("task validator kind must contain at most 80 characters")
|
|
377
|
+
if not summary or len(summary) > 240:
|
|
378
|
+
errors.append("task validation summary must contain at most 240 characters")
|
|
379
|
+
for field, text in (("validator_kind", validator_kind), ("summary", summary)):
|
|
380
|
+
if text:
|
|
381
|
+
try:
|
|
382
|
+
validate_public_safe_text(f"task_validation.{field}", text)
|
|
383
|
+
except ValueError as exc:
|
|
384
|
+
errors.append(str(exc))
|
|
385
|
+
if recovery_kind is not None and recovery_kind not in {
|
|
386
|
+
LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
387
|
+
LoopXTurnResultKind.REPLAN_REQUIRED.value,
|
|
388
|
+
}:
|
|
389
|
+
errors.append("task validation recovery_kind must be repair_required or replan_required")
|
|
390
|
+
if status in {"failed", "inconclusive", "unavailable"} and recovery_kind is None:
|
|
391
|
+
errors.append("failed task validation requires a typed recovery_kind")
|
|
392
|
+
if status in {"passed", "progress", "not_required"} and recovery_kind is not None:
|
|
393
|
+
errors.append("successful task validation cannot declare recovery_kind")
|
|
394
|
+
if exit_code is not None and (not isinstance(exit_code, int) or exit_code < 0):
|
|
395
|
+
errors.append("task validation exit_code must be a non-negative integer")
|
|
396
|
+
if status == "passed" and exit_code not in {None, 0}:
|
|
397
|
+
errors.append("passed task validation cannot declare a non-zero exit_code")
|
|
398
|
+
if status == "progress" and exit_code in {None, 0}:
|
|
399
|
+
errors.append("progress task validation requires a non-zero exit_code")
|
|
400
|
+
effective_status = status if not errors else "inconclusive"
|
|
401
|
+
effective_recovery_kind = recovery_kind
|
|
402
|
+
if errors and effective_recovery_kind is None:
|
|
403
|
+
effective_recovery_kind = LoopXTurnResultKind.REPAIR_REQUIRED.value
|
|
404
|
+
return {
|
|
405
|
+
"ok": not errors and status in {"passed", "progress", "not_required"},
|
|
406
|
+
"schema_version": LOOPX_TURN_TASK_VALIDATION_SCHEMA_VERSION,
|
|
407
|
+
"status": effective_status,
|
|
408
|
+
"validator_kind": validator_kind or "invalid",
|
|
409
|
+
"summary": summary or "task validation receipt is invalid",
|
|
410
|
+
"recovery_kind": effective_recovery_kind,
|
|
411
|
+
"exit_code": exit_code,
|
|
412
|
+
"errors": errors,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _run_task_validator(
|
|
417
|
+
plan: Mapping[str, Any],
|
|
418
|
+
result: Mapping[str, Any],
|
|
419
|
+
*,
|
|
420
|
+
validator: TaskValidator | None,
|
|
421
|
+
) -> dict[str, Any]:
|
|
422
|
+
if validator is None:
|
|
423
|
+
return _task_validation_receipt(
|
|
424
|
+
status="unavailable",
|
|
425
|
+
validator_kind="none",
|
|
426
|
+
summary="independent task validator is required for material host results",
|
|
427
|
+
recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
428
|
+
)
|
|
429
|
+
try:
|
|
430
|
+
value = validator(plan, result)
|
|
431
|
+
except Exception: # noqa: BLE001 - validator plugins fail closed at this boundary
|
|
432
|
+
return _task_validation_receipt(
|
|
433
|
+
status="inconclusive",
|
|
434
|
+
validator_kind="callback",
|
|
435
|
+
summary="independent task validator did not produce a receipt",
|
|
436
|
+
recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
437
|
+
)
|
|
438
|
+
if not isinstance(value, Mapping):
|
|
439
|
+
return _task_validation_receipt(
|
|
440
|
+
status="inconclusive",
|
|
441
|
+
validator_kind="callback",
|
|
442
|
+
summary="independent task validator returned an invalid receipt",
|
|
443
|
+
recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
444
|
+
)
|
|
445
|
+
unknown = sorted(
|
|
446
|
+
set(value)
|
|
447
|
+
- {
|
|
448
|
+
"status",
|
|
449
|
+
"validator_kind",
|
|
450
|
+
"summary",
|
|
451
|
+
"recovery_kind",
|
|
452
|
+
"exit_code",
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
if unknown:
|
|
456
|
+
return _task_validation_receipt(
|
|
457
|
+
status="inconclusive",
|
|
458
|
+
validator_kind="callback",
|
|
459
|
+
summary="independent task validator returned unsupported receipt fields",
|
|
460
|
+
recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
461
|
+
)
|
|
462
|
+
status = str(value.get("status") or "")
|
|
463
|
+
if status == "not_required":
|
|
464
|
+
return _task_validation_receipt(
|
|
465
|
+
status="inconclusive",
|
|
466
|
+
validator_kind="callback",
|
|
467
|
+
summary="material host results cannot skip independent task validation",
|
|
468
|
+
recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
469
|
+
)
|
|
470
|
+
exit_code_value = value.get("exit_code")
|
|
471
|
+
if exit_code_value is not None and (
|
|
472
|
+
not isinstance(exit_code_value, int) or isinstance(exit_code_value, bool)
|
|
473
|
+
):
|
|
474
|
+
return _task_validation_receipt(
|
|
475
|
+
status="inconclusive",
|
|
476
|
+
validator_kind="callback",
|
|
477
|
+
summary="independent task validator returned an invalid exit code",
|
|
478
|
+
recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
479
|
+
)
|
|
480
|
+
return _task_validation_receipt(
|
|
481
|
+
status=status,
|
|
482
|
+
validator_kind=str(value.get("validator_kind") or ""),
|
|
483
|
+
summary=str(value.get("summary") or ""),
|
|
484
|
+
recovery_kind=(
|
|
485
|
+
str(value["recovery_kind"])
|
|
486
|
+
if value.get("recovery_kind") is not None
|
|
487
|
+
else None
|
|
488
|
+
),
|
|
489
|
+
exit_code=exit_code_value,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def build_loopx_turn_command_validator(
|
|
494
|
+
argv: Sequence[str],
|
|
495
|
+
*,
|
|
496
|
+
project: Path,
|
|
497
|
+
timeout_seconds: float,
|
|
498
|
+
failure_recovery_kind: str = LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
499
|
+
) -> TaskValidator:
|
|
500
|
+
"""Build a trusted argv-only postcondition validator for one Turn host workspace."""
|
|
501
|
+
|
|
502
|
+
normalized = _normalize_task_validator_argv(argv)
|
|
503
|
+
if failure_recovery_kind not in {
|
|
504
|
+
LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
505
|
+
LoopXTurnResultKind.REPLAN_REQUIRED.value,
|
|
506
|
+
}:
|
|
507
|
+
raise ValueError(
|
|
508
|
+
"task validator failure recovery must be repair_required or replan_required"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def validate(
|
|
512
|
+
_plan: Mapping[str, Any],
|
|
513
|
+
result: Mapping[str, Any],
|
|
514
|
+
) -> Mapping[str, Any]:
|
|
515
|
+
try:
|
|
516
|
+
completed = subprocess.run(
|
|
517
|
+
normalized,
|
|
518
|
+
cwd=project,
|
|
519
|
+
input=json.dumps(result, ensure_ascii=False, separators=(",", ":")),
|
|
520
|
+
text=True,
|
|
521
|
+
stdout=subprocess.DEVNULL,
|
|
522
|
+
stderr=subprocess.DEVNULL,
|
|
523
|
+
timeout=max(1.0, timeout_seconds),
|
|
524
|
+
check=False,
|
|
525
|
+
)
|
|
526
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
527
|
+
return {
|
|
528
|
+
"status": "inconclusive",
|
|
529
|
+
"validator_kind": "command",
|
|
530
|
+
"summary": "independent task validation command could not complete",
|
|
531
|
+
"recovery_kind": LoopXTurnResultKind.REPAIR_REQUIRED.value,
|
|
532
|
+
}
|
|
533
|
+
if completed.returncode != 0:
|
|
534
|
+
return {
|
|
535
|
+
"status": "failed",
|
|
536
|
+
"validator_kind": "command",
|
|
537
|
+
"summary": "independent task validation command returned non-zero",
|
|
538
|
+
"recovery_kind": failure_recovery_kind,
|
|
539
|
+
"exit_code": completed.returncode,
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
"status": "passed",
|
|
543
|
+
"validator_kind": "command",
|
|
544
|
+
"summary": "independent task validation command passed",
|
|
545
|
+
"exit_code": 0,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return validate
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def turn_journal_path(runtime_root: Path, *, goal_id: str, turn_key: str) -> Path:
|
|
552
|
+
match = TURN_KEY_RE.fullmatch(turn_key)
|
|
553
|
+
if not match:
|
|
554
|
+
raise ValueError("turn_key must be a sha256 digest")
|
|
555
|
+
return runtime_root / "goals" / goal_id / "turns" / f"{match.group('digest')}.json"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _write_journal(path: Path, journal: Mapping[str, Any]) -> None:
|
|
559
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
560
|
+
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
561
|
+
temporary = Path(temporary_name)
|
|
562
|
+
try:
|
|
563
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
564
|
+
json.dump(journal, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
|
565
|
+
handle.write("\n")
|
|
566
|
+
os.replace(temporary, path)
|
|
567
|
+
finally:
|
|
568
|
+
temporary.unlink(missing_ok=True)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _load_journal(path: Path) -> dict[str, Any] | None:
|
|
572
|
+
if not path.exists():
|
|
573
|
+
return None
|
|
574
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
575
|
+
if not isinstance(value, dict) or value.get("schema_version") != LOOPX_TURN_JOURNAL_SCHEMA_VERSION:
|
|
576
|
+
raise ValueError("LoopX Turn journal has an unsupported schema")
|
|
577
|
+
return value
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def inspect_loopx_turn_journal(
|
|
581
|
+
runtime_root: Path,
|
|
582
|
+
*,
|
|
583
|
+
goal_id: str,
|
|
584
|
+
agent_id: str,
|
|
585
|
+
turn_key: str,
|
|
586
|
+
) -> dict[str, object]:
|
|
587
|
+
"""Inspect one canonical Turn journal without granting execution authority."""
|
|
588
|
+
|
|
589
|
+
safe_goal_id = validate_goal_id_path_segment(goal_id)
|
|
590
|
+
if not agent_id or agent_id != agent_id.strip():
|
|
591
|
+
raise ValueError("agent_id must be a non-empty exact identity")
|
|
592
|
+
path = turn_journal_path(
|
|
593
|
+
runtime_root,
|
|
594
|
+
goal_id=safe_goal_id,
|
|
595
|
+
turn_key=turn_key,
|
|
596
|
+
)
|
|
597
|
+
try:
|
|
598
|
+
with exclusive_file_lock(path):
|
|
599
|
+
journal = _load_journal(path)
|
|
600
|
+
except json.JSONDecodeError:
|
|
601
|
+
raise ValueError("LoopX Turn journal contains malformed JSON") from None
|
|
602
|
+
except LockAcquireTimeoutError:
|
|
603
|
+
raise
|
|
604
|
+
except OSError:
|
|
605
|
+
raise ValueError("LoopX Turn journal could not be read") from None
|
|
606
|
+
if journal is None:
|
|
607
|
+
raise ValueError("LoopX Turn journal does not exist")
|
|
608
|
+
|
|
609
|
+
turn = interpret_turn_journal(
|
|
610
|
+
journal,
|
|
611
|
+
goal_id=safe_goal_id,
|
|
612
|
+
agent_id=agent_id,
|
|
613
|
+
turn_key=turn_key,
|
|
614
|
+
)
|
|
615
|
+
context = turn.request.context
|
|
616
|
+
return {
|
|
617
|
+
"ok": True,
|
|
618
|
+
"schema_version": LOOPX_TURN_JOURNAL_INSPECTION_SCHEMA_VERSION,
|
|
619
|
+
"decision": turn.observation.decision,
|
|
620
|
+
"journal_status": context["journal_status"],
|
|
621
|
+
"replay_legal": context["replay_legal"],
|
|
622
|
+
"goal_matches": context["goal_matches"],
|
|
623
|
+
"owner_matches": context["owner_matches"],
|
|
624
|
+
"turn_key_matches": context["turn_key_matches"],
|
|
625
|
+
"phases_form_ordered_prefix": context["phases_form_ordered_prefix"],
|
|
626
|
+
"completed_phases": list(context["completed_phases"]),
|
|
627
|
+
"tombstone_retained": context["tombstone_retained"],
|
|
628
|
+
"violations": list(context["violations"]),
|
|
629
|
+
"effects": [],
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _journal_committed_effect_id(journal: Mapping[str, Any]) -> str | None:
|
|
634
|
+
"""Return the settlement effect id under which the journal was written.
|
|
635
|
+
|
|
636
|
+
Legacy journals created before the typed settlement binding have no
|
|
637
|
+
``settlement_plan``; callers then cannot prove provenance and the
|
|
638
|
+
identity cross-check is skipped, preserving legacy resume behavior.
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
stored_plan = journal.get("plan")
|
|
642
|
+
if not isinstance(stored_plan, Mapping):
|
|
643
|
+
return None
|
|
644
|
+
transaction = stored_plan.get("transaction")
|
|
645
|
+
if not isinstance(transaction, Mapping):
|
|
646
|
+
return None
|
|
647
|
+
settlement_plan = transaction.get("settlement_plan")
|
|
648
|
+
if not isinstance(settlement_plan, Mapping):
|
|
649
|
+
return None
|
|
650
|
+
identity = settlement_plan.get("identity")
|
|
651
|
+
if not isinstance(identity, Mapping):
|
|
652
|
+
return None
|
|
653
|
+
effect_id = str(identity.get("effect_id") or "").strip()
|
|
654
|
+
return effect_id or None
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def load_loopx_turn_plan_from_journal(
|
|
658
|
+
runtime_root: Path,
|
|
659
|
+
*,
|
|
660
|
+
goal_id: str,
|
|
661
|
+
turn_key: str,
|
|
662
|
+
) -> dict[str, Any]:
|
|
663
|
+
path = turn_journal_path(runtime_root, goal_id=goal_id, turn_key=turn_key)
|
|
664
|
+
with exclusive_file_lock(path):
|
|
665
|
+
journal = _load_journal(path)
|
|
666
|
+
if journal is None:
|
|
667
|
+
raise ValueError("LoopX Turn resume journal does not exist")
|
|
668
|
+
plan = journal.get("plan")
|
|
669
|
+
if not isinstance(plan, dict):
|
|
670
|
+
raise TypeError("LoopX Turn resume journal does not contain a plan")
|
|
671
|
+
transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
|
|
672
|
+
if transaction.get("turn_key") != turn_key or journal.get("turn_key") != turn_key:
|
|
673
|
+
raise ValueError("LoopX Turn resume journal has mismatched turn lineage")
|
|
674
|
+
envelope = plan.get("turn_envelope") if isinstance(plan.get("turn_envelope"), dict) else {}
|
|
675
|
+
if envelope.get("goal_id") != goal_id or journal.get("goal_id") != goal_id:
|
|
676
|
+
raise ValueError("LoopX Turn resume journal belongs to another goal")
|
|
677
|
+
return dict(plan)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _receipt(
|
|
681
|
+
plan: Mapping[str, Any],
|
|
682
|
+
result: Mapping[str, Any],
|
|
683
|
+
*,
|
|
684
|
+
completed_phases: Sequence[str],
|
|
685
|
+
failure_kind: LoopXTurnResultKind | None = None,
|
|
686
|
+
failed_phase: str | None = None,
|
|
687
|
+
) -> dict[str, Any]:
|
|
688
|
+
payload = {
|
|
689
|
+
"schema_version": LOOPX_TURN_RESULT_SCHEMA_VERSION,
|
|
690
|
+
"turn_key": result.get("turn_key"),
|
|
691
|
+
"result_kind": (
|
|
692
|
+
failure_kind.value if failure_kind else result.get("result_kind")
|
|
693
|
+
),
|
|
694
|
+
"completed_phases": list(completed_phases),
|
|
695
|
+
"failed_phase": failed_phase,
|
|
696
|
+
}
|
|
697
|
+
return validate_loopx_turn_receipt(
|
|
698
|
+
plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {},
|
|
699
|
+
payload,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _host_failure(
|
|
704
|
+
plan: Mapping[str, Any],
|
|
705
|
+
*,
|
|
706
|
+
kind: LoopXTurnResultKind,
|
|
707
|
+
completed_phases: Sequence[str],
|
|
708
|
+
failed_phase: str,
|
|
709
|
+
reason: str,
|
|
710
|
+
) -> dict[str, Any]:
|
|
711
|
+
transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
|
|
712
|
+
result = {"turn_key": transaction.get("turn_key"), "result_kind": kind.value}
|
|
713
|
+
return {
|
|
714
|
+
"ok": False,
|
|
715
|
+
"reason": reason,
|
|
716
|
+
"receipt": _receipt(
|
|
717
|
+
plan,
|
|
718
|
+
result,
|
|
719
|
+
completed_phases=completed_phases,
|
|
720
|
+
failure_kind=kind,
|
|
721
|
+
failed_phase=failed_phase,
|
|
722
|
+
),
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _run_host(
|
|
727
|
+
request: Mapping[str, Any],
|
|
728
|
+
*,
|
|
729
|
+
argv: Sequence[str],
|
|
730
|
+
project: Path,
|
|
731
|
+
timeout_seconds: float,
|
|
732
|
+
) -> dict[str, Any]:
|
|
733
|
+
try:
|
|
734
|
+
completed = subprocess.run(
|
|
735
|
+
list(argv),
|
|
736
|
+
cwd=project,
|
|
737
|
+
input=json.dumps(request, ensure_ascii=False, separators=(",", ":")),
|
|
738
|
+
text=True,
|
|
739
|
+
capture_output=True,
|
|
740
|
+
timeout=max(1.0, timeout_seconds),
|
|
741
|
+
check=False,
|
|
742
|
+
)
|
|
743
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
744
|
+
return {"ok": False, "reason": type(exc).__name__, "returncode": None}
|
|
745
|
+
if completed.returncode != 0:
|
|
746
|
+
return {
|
|
747
|
+
"ok": False,
|
|
748
|
+
"reason": "host command returned non-zero",
|
|
749
|
+
"returncode": completed.returncode,
|
|
750
|
+
"stderr_chars": len(completed.stderr),
|
|
751
|
+
}
|
|
752
|
+
encoded = completed.stdout.encode("utf-8")
|
|
753
|
+
if len(encoded) > HOST_RESULT_MAX_BYTES:
|
|
754
|
+
return {"ok": False, "reason": "host stdout exceeded the result budget", "returncode": 0}
|
|
755
|
+
try:
|
|
756
|
+
value = json.loads(completed.stdout)
|
|
757
|
+
except json.JSONDecodeError:
|
|
758
|
+
return {"ok": False, "reason": "host stdout is not one JSON value", "returncode": 0}
|
|
759
|
+
if not isinstance(value, dict):
|
|
760
|
+
return {"ok": False, "reason": "host stdout must be one JSON object", "returncode": 0}
|
|
761
|
+
return {"ok": True, "value": value, "returncode": 0}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _run_host_runner(
|
|
765
|
+
request: Mapping[str, Any],
|
|
766
|
+
*,
|
|
767
|
+
runner: HostRunner,
|
|
768
|
+
) -> dict[str, Any]:
|
|
769
|
+
try:
|
|
770
|
+
value = runner(request)
|
|
771
|
+
except BuiltInHostError as exc:
|
|
772
|
+
return {"ok": False, "reason": exc.reason, "returncode": None}
|
|
773
|
+
except Exception as exc: # noqa: BLE001 - host adapters fail closed at boundary
|
|
774
|
+
return {"ok": False, "reason": type(exc).__name__, "returncode": None}
|
|
775
|
+
if not isinstance(value, dict):
|
|
776
|
+
return {"ok": False, "reason": "built-in host result must be one JSON object"}
|
|
777
|
+
encoded = json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
778
|
+
if len(encoded) > HOST_RESULT_MAX_BYTES:
|
|
779
|
+
return {"ok": False, "reason": "built-in host result exceeded the result budget"}
|
|
780
|
+
return {"ok": True, "value": value, "returncode": 0}
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _compact_callback(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
784
|
+
return {
|
|
785
|
+
key: payload.get(key)
|
|
786
|
+
for key in ("ok", "appended", "classification", "generated_at", "slots", "reason")
|
|
787
|
+
if key in payload
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _mapping(value: Any) -> dict[str, Any]:
|
|
792
|
+
return dict(value) if isinstance(value, Mapping) else {}
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _execution_payload(
|
|
796
|
+
plan: Mapping[str, Any],
|
|
797
|
+
journal: Mapping[str, Any],
|
|
798
|
+
*,
|
|
799
|
+
execute: bool,
|
|
800
|
+
replayed: bool,
|
|
801
|
+
effects: Mapping[str, bool],
|
|
802
|
+
) -> dict[str, Any]:
|
|
803
|
+
transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
|
|
804
|
+
turn_key = str(transaction.get("turn_key") or "")
|
|
805
|
+
planned_host = plan.get("host") if isinstance(plan.get("host"), dict) else {}
|
|
806
|
+
writeback = _mapping(journal.get("writeback"))
|
|
807
|
+
todo_completion = _mapping(writeback.get("completion"))
|
|
808
|
+
quota_spent = effects.get("quota_spent") is True or "quota_spend" in list(
|
|
809
|
+
journal.get("completed_phases") or []
|
|
810
|
+
)
|
|
811
|
+
return {
|
|
812
|
+
"ok": journal.get("status") in {
|
|
813
|
+
"preview",
|
|
814
|
+
"committed",
|
|
815
|
+
"stopped",
|
|
816
|
+
"scheduler_action_required",
|
|
817
|
+
},
|
|
818
|
+
"schema_version": LOOPX_TURN_EXECUTION_SCHEMA_VERSION,
|
|
819
|
+
"mode": "run_once",
|
|
820
|
+
"dry_run": not execute,
|
|
821
|
+
"replayed": replayed,
|
|
822
|
+
"resume_turn_key": turn_key,
|
|
823
|
+
"journal_ref": f"turn:{turn_key.removeprefix('sha256:')[:16]}",
|
|
824
|
+
"status": journal.get("status"),
|
|
825
|
+
"execution_mode": planned_host.get("execution_mode"),
|
|
826
|
+
"host": journal.get("host"),
|
|
827
|
+
"result_kind": journal.get("result_kind"),
|
|
828
|
+
"validation": journal.get("task_validation"),
|
|
829
|
+
"receipt": journal.get("receipt"),
|
|
830
|
+
"scheduler": journal.get("scheduler"),
|
|
831
|
+
"effects": dict(effects),
|
|
832
|
+
"quota_slot_spend_count": 1 if quota_spent else 0,
|
|
833
|
+
**(
|
|
834
|
+
{"settlement_result": journal["settlement_result"]}
|
|
835
|
+
if isinstance(journal.get("settlement_result"), Mapping)
|
|
836
|
+
else {}
|
|
837
|
+
),
|
|
838
|
+
**({"todo_completion": todo_completion} if todo_completion else {}),
|
|
839
|
+
**({"reason": journal.get("reason")} if journal.get("reason") else {}),
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _host_result_stage(
|
|
844
|
+
plan: Mapping[str, Any],
|
|
845
|
+
request: Mapping[str, Any],
|
|
846
|
+
*,
|
|
847
|
+
host_runner: HostRunner | None,
|
|
848
|
+
argv: Sequence[str] | None,
|
|
849
|
+
completion_lifecycle_configured: bool,
|
|
850
|
+
project: Path,
|
|
851
|
+
timeout_seconds: float,
|
|
852
|
+
journal: dict[str, Any],
|
|
853
|
+
journal_path: Path,
|
|
854
|
+
effects: dict[str, bool],
|
|
855
|
+
) -> tuple[dict[str, Any] | None, list[str], dict[str, Any] | None]:
|
|
856
|
+
completed_phases = list(journal.get("completed_phases") or [])
|
|
857
|
+
result = (
|
|
858
|
+
journal.get("host_result")
|
|
859
|
+
if isinstance(journal.get("host_result"), dict)
|
|
860
|
+
else None
|
|
861
|
+
)
|
|
862
|
+
if "typed_result" not in completed_phases:
|
|
863
|
+
host_observation = (
|
|
864
|
+
_run_host_runner(request, runner=host_runner)
|
|
865
|
+
if host_runner is not None
|
|
866
|
+
else _run_host(
|
|
867
|
+
request,
|
|
868
|
+
argv=argv or [],
|
|
869
|
+
project=project,
|
|
870
|
+
timeout_seconds=timeout_seconds,
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
effects["host_invoked"] = True
|
|
874
|
+
if not host_observation.get("ok"):
|
|
875
|
+
failure = _host_failure(
|
|
876
|
+
plan,
|
|
877
|
+
kind=LoopXTurnResultKind.HOST_FAILURE,
|
|
878
|
+
completed_phases=[],
|
|
879
|
+
failed_phase="host_execute",
|
|
880
|
+
reason=str(host_observation.get("reason") or "host execution failed"),
|
|
881
|
+
)
|
|
882
|
+
journal.update(
|
|
883
|
+
status="failed",
|
|
884
|
+
reason=failure["reason"],
|
|
885
|
+
receipt=failure["receipt"],
|
|
886
|
+
completed_phases=[],
|
|
887
|
+
result_kind=LoopXTurnResultKind.HOST_FAILURE.value,
|
|
888
|
+
)
|
|
889
|
+
_write_journal(journal_path, journal)
|
|
890
|
+
return (
|
|
891
|
+
None,
|
|
892
|
+
[],
|
|
893
|
+
_execution_payload(
|
|
894
|
+
plan,
|
|
895
|
+
journal,
|
|
896
|
+
execute=True,
|
|
897
|
+
replayed=False,
|
|
898
|
+
effects=effects,
|
|
899
|
+
),
|
|
900
|
+
)
|
|
901
|
+
result = dict(host_observation["value"])
|
|
902
|
+
completed_phases = list(TRANSACTION_PHASES[:2])
|
|
903
|
+
|
|
904
|
+
validation = validate_loopx_turn_host_result(
|
|
905
|
+
plan,
|
|
906
|
+
result or {},
|
|
907
|
+
completion_lifecycle_configured=completion_lifecycle_configured,
|
|
908
|
+
)
|
|
909
|
+
if not validation.get("ok"):
|
|
910
|
+
failure = _host_failure(
|
|
911
|
+
plan,
|
|
912
|
+
kind=LoopXTurnResultKind.VALIDATION_FAILED,
|
|
913
|
+
completed_phases=list(TRANSACTION_PHASES[:2]),
|
|
914
|
+
failed_phase="validation",
|
|
915
|
+
reason="; ".join(
|
|
916
|
+
validation.get("errors") or ["host result validation failed"]
|
|
917
|
+
),
|
|
918
|
+
)
|
|
919
|
+
journal.update(
|
|
920
|
+
status="failed",
|
|
921
|
+
reason=failure["reason"],
|
|
922
|
+
receipt=failure["receipt"],
|
|
923
|
+
completed_phases=list(TRANSACTION_PHASES[:2]),
|
|
924
|
+
result_kind=LoopXTurnResultKind.VALIDATION_FAILED.value,
|
|
925
|
+
validation_stage="host_result_contract",
|
|
926
|
+
)
|
|
927
|
+
_write_journal(journal_path, journal)
|
|
928
|
+
return (
|
|
929
|
+
None,
|
|
930
|
+
list(TRANSACTION_PHASES[:2]),
|
|
931
|
+
_execution_payload(
|
|
932
|
+
plan,
|
|
933
|
+
journal,
|
|
934
|
+
execute=True,
|
|
935
|
+
replayed=False,
|
|
936
|
+
effects=effects,
|
|
937
|
+
),
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
normalized = dict(validation["result"])
|
|
941
|
+
if len(completed_phases) < 2:
|
|
942
|
+
completed_phases = list(TRANSACTION_PHASES[:2])
|
|
943
|
+
journal.update(
|
|
944
|
+
host_result=normalized,
|
|
945
|
+
result_kind=normalized.get("result_kind"),
|
|
946
|
+
completed_phases=completed_phases,
|
|
947
|
+
)
|
|
948
|
+
_write_journal(journal_path, journal)
|
|
949
|
+
return normalized, completed_phases, None
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _task_validation_stage(
|
|
953
|
+
plan: Mapping[str, Any],
|
|
954
|
+
result: Mapping[str, Any],
|
|
955
|
+
*,
|
|
956
|
+
task_validator: TaskValidator | None,
|
|
957
|
+
completed_phases: list[str],
|
|
958
|
+
journal: dict[str, Any],
|
|
959
|
+
journal_path: Path,
|
|
960
|
+
effects: dict[str, bool],
|
|
961
|
+
) -> tuple[list[str], dict[str, Any] | None]:
|
|
962
|
+
turn = interpret_turn_result_packet(result)
|
|
963
|
+
kind = LoopXTurnResultKind(turn.observation.decision)
|
|
964
|
+
if kind in STOP_HOST_RESULT_KINDS:
|
|
965
|
+
completed_phases = list(TRANSACTION_PHASES[:3])
|
|
966
|
+
journal.update(
|
|
967
|
+
status="stopped",
|
|
968
|
+
completed_phases=completed_phases,
|
|
969
|
+
task_validation=_task_validation_receipt(
|
|
970
|
+
status="not_required",
|
|
971
|
+
validator_kind="stop_result",
|
|
972
|
+
summary="task validation is not required for a typed stop result",
|
|
973
|
+
),
|
|
974
|
+
receipt=_receipt(plan, result, completed_phases=completed_phases),
|
|
975
|
+
scheduler={"disposition": "not_applicable"},
|
|
976
|
+
)
|
|
977
|
+
_write_journal(journal_path, journal)
|
|
978
|
+
return completed_phases, _execution_payload(
|
|
979
|
+
plan,
|
|
980
|
+
journal,
|
|
981
|
+
execute=True,
|
|
982
|
+
replayed=False,
|
|
983
|
+
effects=effects,
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
stored_task_validation = (
|
|
987
|
+
journal.get("task_validation")
|
|
988
|
+
if isinstance(journal.get("task_validation"), dict)
|
|
989
|
+
else None
|
|
990
|
+
)
|
|
991
|
+
task_validation = (
|
|
992
|
+
stored_task_validation
|
|
993
|
+
if "validation" in completed_phases
|
|
994
|
+
and stored_task_validation is not None
|
|
995
|
+
and stored_task_validation.get("ok") is True
|
|
996
|
+
else _run_task_validator(
|
|
997
|
+
plan,
|
|
998
|
+
result,
|
|
999
|
+
validator=task_validator,
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
journal["task_validation"] = task_validation
|
|
1003
|
+
if not task_validation.get("ok"):
|
|
1004
|
+
reason = str(
|
|
1005
|
+
task_validation.get("summary") or "independent task validation failed"
|
|
1006
|
+
)
|
|
1007
|
+
failure = _host_failure(
|
|
1008
|
+
plan,
|
|
1009
|
+
kind=LoopXTurnResultKind.VALIDATION_FAILED,
|
|
1010
|
+
completed_phases=list(TRANSACTION_PHASES[:2]),
|
|
1011
|
+
failed_phase="validation",
|
|
1012
|
+
reason=reason,
|
|
1013
|
+
)
|
|
1014
|
+
journal.update(
|
|
1015
|
+
status="failed",
|
|
1016
|
+
reason=reason,
|
|
1017
|
+
receipt=failure["receipt"],
|
|
1018
|
+
completed_phases=list(TRANSACTION_PHASES[:2]),
|
|
1019
|
+
result_kind=LoopXTurnResultKind.VALIDATION_FAILED.value,
|
|
1020
|
+
validation_stage="task_postcondition",
|
|
1021
|
+
)
|
|
1022
|
+
_write_journal(journal_path, journal)
|
|
1023
|
+
return list(TRANSACTION_PHASES[:2]), _execution_payload(
|
|
1024
|
+
plan,
|
|
1025
|
+
journal,
|
|
1026
|
+
execute=True,
|
|
1027
|
+
replayed=False,
|
|
1028
|
+
effects=effects,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
if "validation" not in completed_phases:
|
|
1032
|
+
completed_phases = list(TRANSACTION_PHASES[:3])
|
|
1033
|
+
journal.update(
|
|
1034
|
+
result_kind=result.get("result_kind"),
|
|
1035
|
+
completed_phases=completed_phases,
|
|
1036
|
+
validation_stage="task_postcondition",
|
|
1037
|
+
)
|
|
1038
|
+
_write_journal(journal_path, journal)
|
|
1039
|
+
return completed_phases, None
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _ensure_turn_settlement_plan(
|
|
1043
|
+
plan: Mapping[str, Any],
|
|
1044
|
+
transaction_plan: dict[str, Any],
|
|
1045
|
+
) -> None:
|
|
1046
|
+
"""Upgrade a legacy transaction plan with a typed settlement plan.
|
|
1047
|
+
|
|
1048
|
+
Turn plans produced by older LoopX releases (for example plans read from a
|
|
1049
|
+
scored-workspace image) do not carry ``settlement_plan``. Rebuild it from
|
|
1050
|
+
the plan's own ``turn_envelope`` lineage so the typed closeout can bind
|
|
1051
|
+
validation -> writeback -> spend instead of failing every legacy run.
|
|
1052
|
+
"""
|
|
1053
|
+
|
|
1054
|
+
if isinstance(transaction_plan.get("settlement_plan"), Mapping):
|
|
1055
|
+
return
|
|
1056
|
+
envelope = plan.get("turn_envelope")
|
|
1057
|
+
if not isinstance(envelope, Mapping):
|
|
1058
|
+
return
|
|
1059
|
+
selected_todo = selected_turn_todo(envelope)
|
|
1060
|
+
lineage = {
|
|
1061
|
+
"goal_id": str(envelope.get("goal_id") or ""),
|
|
1062
|
+
"agent_id": str(envelope.get("agent_id") or ""),
|
|
1063
|
+
"todo_id": str(selected_todo.get("todo_id") or ""),
|
|
1064
|
+
}
|
|
1065
|
+
if not all(lineage.values()):
|
|
1066
|
+
return
|
|
1067
|
+
host = plan.get("host")
|
|
1068
|
+
host_fields = host if isinstance(host, Mapping) else {}
|
|
1069
|
+
built = build_loopx_turn_transaction_plan(
|
|
1070
|
+
planned=True,
|
|
1071
|
+
lineage=lineage,
|
|
1072
|
+
host=str(host_fields.get("kind") or "generic-cli"),
|
|
1073
|
+
execution_mode=str(
|
|
1074
|
+
host_fields.get("execution_mode") or "isolated-headless"
|
|
1075
|
+
),
|
|
1076
|
+
session_action=str(host_fields.get("session_action") or "resume"),
|
|
1077
|
+
turn_instance_id=transaction_plan.get("turn_instance_id"),
|
|
1078
|
+
)
|
|
1079
|
+
settlement_plan = built.get("settlement_plan")
|
|
1080
|
+
if isinstance(settlement_plan, Mapping):
|
|
1081
|
+
transaction_plan["settlement_plan"] = settlement_plan
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _typed_settlement_stage(
|
|
1085
|
+
plan: Mapping[str, Any],
|
|
1086
|
+
result: dict[str, Any],
|
|
1087
|
+
*,
|
|
1088
|
+
completed_phases: list[str],
|
|
1089
|
+
journal: dict[str, Any],
|
|
1090
|
+
journal_path: Path,
|
|
1091
|
+
effects: dict[str, bool],
|
|
1092
|
+
writeback: Writeback,
|
|
1093
|
+
completion_writeback: CompletionWriteback | None,
|
|
1094
|
+
completion_intent: CompletionIntent | None,
|
|
1095
|
+
terminal_closeout: TerminalCloseout | None,
|
|
1096
|
+
spend: Spend,
|
|
1097
|
+
scheduler: Scheduler,
|
|
1098
|
+
) -> dict[str, Any]:
|
|
1099
|
+
transaction_plan = (
|
|
1100
|
+
plan.get("transaction")
|
|
1101
|
+
if isinstance(plan.get("transaction"), Mapping)
|
|
1102
|
+
else {}
|
|
1103
|
+
)
|
|
1104
|
+
_ensure_turn_settlement_plan(plan, transaction_plan)
|
|
1105
|
+
|
|
1106
|
+
terminal_closeout_required = False
|
|
1107
|
+
completion_intent_error: str | None = None
|
|
1108
|
+
if result.get("result_kind") == LoopXTurnResultKind.VALIDATED_COMPLETION.value:
|
|
1109
|
+
if (
|
|
1110
|
+
completion_writeback is None
|
|
1111
|
+
or completion_intent is None
|
|
1112
|
+
or terminal_closeout is None
|
|
1113
|
+
):
|
|
1114
|
+
raise ValueError(
|
|
1115
|
+
"validated_completion requires intent, lifecycle writeback, "
|
|
1116
|
+
"and terminal closeout adapters"
|
|
1117
|
+
)
|
|
1118
|
+
terminal_closeout_required, completion_intent_error = (
|
|
1119
|
+
terminal_closeout_requirement(
|
|
1120
|
+
plan=plan,
|
|
1121
|
+
result=result,
|
|
1122
|
+
journal=journal,
|
|
1123
|
+
completion_intent=completion_intent,
|
|
1124
|
+
)
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
def writeback_effect() -> Mapping[str, Any]:
|
|
1128
|
+
if completion_intent_error:
|
|
1129
|
+
return {
|
|
1130
|
+
"ok": False,
|
|
1131
|
+
"appended": False,
|
|
1132
|
+
"reason": completion_intent_error,
|
|
1133
|
+
}
|
|
1134
|
+
if (
|
|
1135
|
+
result.get("result_kind")
|
|
1136
|
+
== LoopXTurnResultKind.VALIDATED_COMPLETION.value
|
|
1137
|
+
and not terminal_closeout_required
|
|
1138
|
+
):
|
|
1139
|
+
if completion_writeback is None:
|
|
1140
|
+
raise ValueError("validated_completion requires a todo lifecycle adapter")
|
|
1141
|
+
callback_payload = completion_writeback(result)
|
|
1142
|
+
completion_outcome = completion_writeback_outcome(
|
|
1143
|
+
callback_payload,
|
|
1144
|
+
plan=plan,
|
|
1145
|
+
)
|
|
1146
|
+
if completion_outcome is None:
|
|
1147
|
+
return {
|
|
1148
|
+
"ok": False,
|
|
1149
|
+
"appended": False,
|
|
1150
|
+
"reason": str(
|
|
1151
|
+
callback_payload.get("reason")
|
|
1152
|
+
or callback_payload.get("error")
|
|
1153
|
+
or (
|
|
1154
|
+
"todo lifecycle adapter returned an invalid "
|
|
1155
|
+
"completion outcome"
|
|
1156
|
+
)
|
|
1157
|
+
),
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
**callback_payload,
|
|
1161
|
+
"completion": completion_outcome,
|
|
1162
|
+
}
|
|
1163
|
+
return writeback(result)
|
|
1164
|
+
|
|
1165
|
+
def checkpoint(
|
|
1166
|
+
step_kind: SettlementStepKind,
|
|
1167
|
+
payload: Mapping[str, Any],
|
|
1168
|
+
phases: tuple[str, ...],
|
|
1169
|
+
) -> None:
|
|
1170
|
+
if step_kind is SettlementStepKind.DURABLE_WRITEBACK:
|
|
1171
|
+
effects["state_written"] = True
|
|
1172
|
+
journal["writeback"] = {
|
|
1173
|
+
**_compact_callback(payload),
|
|
1174
|
+
**(
|
|
1175
|
+
{"completion": payload["completion"]}
|
|
1176
|
+
if isinstance(payload.get("completion"), dict)
|
|
1177
|
+
else {}
|
|
1178
|
+
),
|
|
1179
|
+
}
|
|
1180
|
+
elif step_kind is SettlementStepKind.QUOTA_SPEND:
|
|
1181
|
+
effects["quota_spent"] = True
|
|
1182
|
+
journal["quota_spend"] = _compact_callback(payload)
|
|
1183
|
+
journal["completed_phases"] = list(phases)
|
|
1184
|
+
_write_journal(journal_path, journal)
|
|
1185
|
+
|
|
1186
|
+
settlement_result = execute_turn_driver_settlement(
|
|
1187
|
+
transaction_plan,
|
|
1188
|
+
transaction_phases=TRANSACTION_PHASES,
|
|
1189
|
+
completed_phases=completed_phases,
|
|
1190
|
+
writeback_payload=(
|
|
1191
|
+
journal.get("writeback")
|
|
1192
|
+
if isinstance(journal.get("writeback"), Mapping)
|
|
1193
|
+
else None
|
|
1194
|
+
),
|
|
1195
|
+
quota_spend_payload=(
|
|
1196
|
+
journal.get("quota_spend")
|
|
1197
|
+
if isinstance(journal.get("quota_spend"), Mapping)
|
|
1198
|
+
else None
|
|
1199
|
+
),
|
|
1200
|
+
writeback=writeback_effect,
|
|
1201
|
+
spend=spend,
|
|
1202
|
+
checkpoint=checkpoint,
|
|
1203
|
+
committed_effect_id=_journal_committed_effect_id(journal),
|
|
1204
|
+
)
|
|
1205
|
+
settlement_state = settlement_result.value
|
|
1206
|
+
if settlement_result.failure is None and terminal_closeout_required:
|
|
1207
|
+
assert terminal_closeout is not None
|
|
1208
|
+
|
|
1209
|
+
def terminal_checkpoint(payload: Mapping[str, Any]) -> None:
|
|
1210
|
+
compact = {
|
|
1211
|
+
**_compact_callback(payload),
|
|
1212
|
+
**(
|
|
1213
|
+
{"completion": payload["completion"]}
|
|
1214
|
+
if isinstance(payload.get("completion"), Mapping)
|
|
1215
|
+
else {}
|
|
1216
|
+
),
|
|
1217
|
+
}
|
|
1218
|
+
journal["terminal_closeout"] = compact
|
|
1219
|
+
journal["writeback"] = {
|
|
1220
|
+
**_mapping(journal.get("writeback")),
|
|
1221
|
+
"completion": compact.get("completion"),
|
|
1222
|
+
}
|
|
1223
|
+
_write_journal(journal_path, journal)
|
|
1224
|
+
|
|
1225
|
+
terminal_result = execute_verified_turn_terminal_closeout(
|
|
1226
|
+
transaction_plan,
|
|
1227
|
+
plan=plan,
|
|
1228
|
+
committed_payload=(
|
|
1229
|
+
journal.get("terminal_closeout")
|
|
1230
|
+
if isinstance(journal.get("terminal_closeout"), Mapping)
|
|
1231
|
+
else None
|
|
1232
|
+
),
|
|
1233
|
+
closeout=lambda: terminal_closeout(result),
|
|
1234
|
+
checkpoint=terminal_checkpoint,
|
|
1235
|
+
committed_effect_id=_journal_committed_effect_id(journal),
|
|
1236
|
+
)
|
|
1237
|
+
settlement_result = SettlementResult(
|
|
1238
|
+
value=settlement_state if terminal_result.failure is None else None,
|
|
1239
|
+
receipts=(*settlement_result.receipts, *terminal_result.receipts),
|
|
1240
|
+
failure=terminal_result.failure,
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
journal["settlement_result"] = settlement_result_payload(settlement_result)
|
|
1244
|
+
if settlement_result.failure is not None:
|
|
1245
|
+
failure_step = settlement_result.failure.step_kind
|
|
1246
|
+
result_kind = (
|
|
1247
|
+
LoopXTurnResultKind.VALIDATION_FAILED
|
|
1248
|
+
if failure_step is SettlementStepKind.VALIDATION
|
|
1249
|
+
else LoopXTurnResultKind.WRITEBACK_FAILED
|
|
1250
|
+
if failure_step is SettlementStepKind.DURABLE_WRITEBACK
|
|
1251
|
+
else LoopXTurnResultKind.QUOTA_SPEND_FAILED
|
|
1252
|
+
if failure_step is SettlementStepKind.QUOTA_SPEND
|
|
1253
|
+
else LoopXTurnResultKind.TERMINAL_CLOSEOUT_FAILED
|
|
1254
|
+
)
|
|
1255
|
+
completed_phases = list(journal.get("completed_phases") or completed_phases)
|
|
1256
|
+
failure = _host_failure(
|
|
1257
|
+
plan,
|
|
1258
|
+
kind=result_kind,
|
|
1259
|
+
completed_phases=completed_phases,
|
|
1260
|
+
failed_phase=failure_step.value,
|
|
1261
|
+
reason=settlement_result.failure.reason,
|
|
1262
|
+
)
|
|
1263
|
+
journal.update(
|
|
1264
|
+
status="failed",
|
|
1265
|
+
result_kind=result_kind.value,
|
|
1266
|
+
reason=failure["reason"],
|
|
1267
|
+
receipt=failure["receipt"],
|
|
1268
|
+
)
|
|
1269
|
+
_write_journal(journal_path, journal)
|
|
1270
|
+
return _execution_payload(
|
|
1271
|
+
plan,
|
|
1272
|
+
journal,
|
|
1273
|
+
execute=True,
|
|
1274
|
+
replayed=False,
|
|
1275
|
+
effects=effects,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
settlement_state = settlement_result.value
|
|
1279
|
+
if settlement_state is None or settlement_state.quota_spend is None:
|
|
1280
|
+
raise ValueError("typed Turn settlement completed without a quota spend receipt")
|
|
1281
|
+
completed_phases = list(settlement_state.completed_phases)
|
|
1282
|
+
spend_payload = dict(settlement_state.quota_spend)
|
|
1283
|
+
_write_journal(journal_path, journal)
|
|
1284
|
+
|
|
1285
|
+
scheduler_payload = scheduler(spend_payload)
|
|
1286
|
+
journal["scheduler"] = scheduler_payload
|
|
1287
|
+
if scheduler_payload.get("completed") is not True:
|
|
1288
|
+
journal.update(
|
|
1289
|
+
status="scheduler_action_required",
|
|
1290
|
+
receipt=_receipt(plan, result, completed_phases=completed_phases),
|
|
1291
|
+
)
|
|
1292
|
+
_write_journal(journal_path, journal)
|
|
1293
|
+
return _execution_payload(
|
|
1294
|
+
plan,
|
|
1295
|
+
journal,
|
|
1296
|
+
execute=True,
|
|
1297
|
+
replayed=False,
|
|
1298
|
+
effects=effects,
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
completed_phases = list(TRANSACTION_PHASES)
|
|
1302
|
+
effects["scheduler_acknowledged"] = bool(scheduler_payload.get("acknowledged"))
|
|
1303
|
+
journal.update(
|
|
1304
|
+
status="committed",
|
|
1305
|
+
completed_phases=completed_phases,
|
|
1306
|
+
receipt=_receipt(plan, result, completed_phases=completed_phases),
|
|
1307
|
+
)
|
|
1308
|
+
_write_journal(journal_path, journal)
|
|
1309
|
+
return _execution_payload(
|
|
1310
|
+
plan,
|
|
1311
|
+
journal,
|
|
1312
|
+
execute=True,
|
|
1313
|
+
replayed=False,
|
|
1314
|
+
effects=effects,
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def run_loopx_turn_once(
|
|
1319
|
+
plan: Mapping[str, Any],
|
|
1320
|
+
*,
|
|
1321
|
+
host_argv: Sequence[str] | None = None,
|
|
1322
|
+
host_runner: HostRunner | None = None,
|
|
1323
|
+
project: Path,
|
|
1324
|
+
runtime_root: Path,
|
|
1325
|
+
goal_id: str,
|
|
1326
|
+
timeout_seconds: float,
|
|
1327
|
+
execute: bool,
|
|
1328
|
+
retry_failed: bool = False,
|
|
1329
|
+
task_validator: TaskValidator | None = None,
|
|
1330
|
+
writeback: Writeback | None = None,
|
|
1331
|
+
completion_writeback: CompletionWriteback | None = None,
|
|
1332
|
+
completion_intent: CompletionIntent | None = None,
|
|
1333
|
+
terminal_closeout: TerminalCloseout | None = None,
|
|
1334
|
+
spend: Spend | None = None,
|
|
1335
|
+
scheduler: Scheduler | None = None,
|
|
1336
|
+
) -> dict[str, Any]:
|
|
1337
|
+
if host_runner is not None and host_argv is not None:
|
|
1338
|
+
raise ValueError("run-once accepts either host_argv or host_runner, not both")
|
|
1339
|
+
if host_runner is None:
|
|
1340
|
+
argv = normalize_host_argv(host_argv or [])
|
|
1341
|
+
host_projection = {"executable": Path(argv[0]).name, "argv_count": len(argv)}
|
|
1342
|
+
else:
|
|
1343
|
+
argv = None
|
|
1344
|
+
planned_host = plan.get("host") if isinstance(plan.get("host"), dict) else {}
|
|
1345
|
+
host_projection = {
|
|
1346
|
+
"executable": "built-in",
|
|
1347
|
+
"kind": str(planned_host.get("kind") or "codex-cli"),
|
|
1348
|
+
}
|
|
1349
|
+
request = build_loopx_turn_host_request(plan)
|
|
1350
|
+
empty_effects = {
|
|
1351
|
+
"host_invoked": False,
|
|
1352
|
+
"state_written": False,
|
|
1353
|
+
"quota_spent": False,
|
|
1354
|
+
"scheduler_acknowledged": False,
|
|
1355
|
+
}
|
|
1356
|
+
if not execute:
|
|
1357
|
+
preview = {
|
|
1358
|
+
"schema_version": LOOPX_TURN_JOURNAL_SCHEMA_VERSION,
|
|
1359
|
+
"status": "preview",
|
|
1360
|
+
"host": host_projection,
|
|
1361
|
+
"result_kind": None,
|
|
1362
|
+
"receipt": None,
|
|
1363
|
+
"scheduler": {"disposition": "not_evaluated"},
|
|
1364
|
+
}
|
|
1365
|
+
return _execution_payload(
|
|
1366
|
+
plan,
|
|
1367
|
+
preview,
|
|
1368
|
+
execute=False,
|
|
1369
|
+
replayed=False,
|
|
1370
|
+
effects=empty_effects,
|
|
1371
|
+
)
|
|
1372
|
+
if writeback is None or spend is None or scheduler is None:
|
|
1373
|
+
raise ValueError("executing run-once requires writeback, spend, and scheduler callbacks")
|
|
1374
|
+
|
|
1375
|
+
turn_key = str(request["turn_key"])
|
|
1376
|
+
journal_path = turn_journal_path(runtime_root, goal_id=goal_id, turn_key=turn_key)
|
|
1377
|
+
with exclusive_file_lock(journal_path):
|
|
1378
|
+
journal = _load_journal(journal_path)
|
|
1379
|
+
if journal and (
|
|
1380
|
+
journal.get("status") in {"committed", "stopped"}
|
|
1381
|
+
or journal.get("status") == "failed" and not retry_failed
|
|
1382
|
+
):
|
|
1383
|
+
return _execution_payload(
|
|
1384
|
+
plan,
|
|
1385
|
+
journal,
|
|
1386
|
+
execute=True,
|
|
1387
|
+
replayed=True,
|
|
1388
|
+
effects=empty_effects,
|
|
1389
|
+
)
|
|
1390
|
+
if journal and journal.get("status") == "failed":
|
|
1391
|
+
receipt = journal.get("receipt") if isinstance(journal.get("receipt"), dict) else {}
|
|
1392
|
+
if receipt.get("failed_phase") == "validation":
|
|
1393
|
+
if journal.get("validation_stage") != "task_postcondition":
|
|
1394
|
+
journal.pop("host_result", None)
|
|
1395
|
+
journal.pop("result_kind", None)
|
|
1396
|
+
journal["completed_phases"] = (
|
|
1397
|
+
list(TRANSACTION_PHASES[:2])
|
|
1398
|
+
if isinstance(journal.get("host_result"), dict)
|
|
1399
|
+
else []
|
|
1400
|
+
)
|
|
1401
|
+
journal.pop("task_validation", None)
|
|
1402
|
+
journal.pop("validation_stage", None)
|
|
1403
|
+
journal.pop("reason", None)
|
|
1404
|
+
journal.pop("receipt", None)
|
|
1405
|
+
journal["status"] = "in_progress"
|
|
1406
|
+
_write_journal(journal_path, journal)
|
|
1407
|
+
if journal is None:
|
|
1408
|
+
journal = {
|
|
1409
|
+
"schema_version": LOOPX_TURN_JOURNAL_SCHEMA_VERSION,
|
|
1410
|
+
"turn_key": turn_key,
|
|
1411
|
+
"goal_id": goal_id,
|
|
1412
|
+
"status": "in_progress",
|
|
1413
|
+
"host": host_projection,
|
|
1414
|
+
"completed_phases": [],
|
|
1415
|
+
"plan": dict(plan),
|
|
1416
|
+
}
|
|
1417
|
+
_write_journal(journal_path, journal)
|
|
1418
|
+
|
|
1419
|
+
effects = dict(empty_effects)
|
|
1420
|
+
result, completed_phases, terminal = _host_result_stage(
|
|
1421
|
+
plan,
|
|
1422
|
+
request,
|
|
1423
|
+
host_runner=host_runner,
|
|
1424
|
+
argv=argv,
|
|
1425
|
+
completion_lifecycle_configured=all(
|
|
1426
|
+
callback is not None
|
|
1427
|
+
for callback in (
|
|
1428
|
+
completion_writeback,
|
|
1429
|
+
completion_intent,
|
|
1430
|
+
terminal_closeout,
|
|
1431
|
+
)
|
|
1432
|
+
),
|
|
1433
|
+
project=project,
|
|
1434
|
+
timeout_seconds=timeout_seconds,
|
|
1435
|
+
journal=journal,
|
|
1436
|
+
journal_path=journal_path,
|
|
1437
|
+
effects=effects,
|
|
1438
|
+
)
|
|
1439
|
+
if terminal is not None:
|
|
1440
|
+
return terminal
|
|
1441
|
+
assert result is not None
|
|
1442
|
+
|
|
1443
|
+
completed_phases, terminal = _task_validation_stage(
|
|
1444
|
+
plan,
|
|
1445
|
+
result,
|
|
1446
|
+
task_validator=task_validator,
|
|
1447
|
+
completed_phases=completed_phases,
|
|
1448
|
+
journal=journal,
|
|
1449
|
+
journal_path=journal_path,
|
|
1450
|
+
effects=effects,
|
|
1451
|
+
)
|
|
1452
|
+
if terminal is not None:
|
|
1453
|
+
return terminal
|
|
1454
|
+
|
|
1455
|
+
return _typed_settlement_stage(
|
|
1456
|
+
plan,
|
|
1457
|
+
result,
|
|
1458
|
+
completed_phases=completed_phases,
|
|
1459
|
+
journal=journal,
|
|
1460
|
+
journal_path=journal_path,
|
|
1461
|
+
effects=effects,
|
|
1462
|
+
writeback=writeback,
|
|
1463
|
+
completion_writeback=completion_writeback,
|
|
1464
|
+
completion_intent=completion_intent,
|
|
1465
|
+
terminal_closeout=terminal_closeout,
|
|
1466
|
+
spend=spend,
|
|
1467
|
+
scheduler=scheduler,
|
|
1468
|
+
)
|