agentevolve-optimizer 0.5.0__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.
- agent_evolve/__init__.py +722 -0
- agent_evolve/agentic.py +2800 -0
- agent_evolve/api.py +767 -0
- agent_evolve/application/__init__.py +1580 -0
- agent_evolve/application/action_allocation.py +744 -0
- agent_evolve/application/action_allocation_frame.py +347 -0
- agent_evolve/application/action_allocation_frame_commit.py +185 -0
- agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
- agent_evolve/application/action_allocation_frame_v3.py +338 -0
- agent_evolve/application/action_archive_value.py +497 -0
- agent_evolve/application/action_evidence_consistency.py +455 -0
- agent_evolve/application/action_forecast_partitioning.py +1471 -0
- agent_evolve/application/action_metric_projection.py +211 -0
- agent_evolve/application/action_role_value.py +680 -0
- agent_evolve/application/action_score_authorities.py +363 -0
- agent_evolve/application/action_structural_signature.py +116 -0
- agent_evolve/application/action_target_realization.py +402 -0
- agent_evolve/application/agentic_evolution.py +7734 -0
- agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
- agent_evolve/application/anchor_residual_identification.py +463 -0
- agent_evolve/application/archive_conditioned_action_target.py +208 -0
- agent_evolve/application/artifact_journal.py +246 -0
- agent_evolve/application/artifact_replay.py +347 -0
- agent_evolve/application/budgeted_optimizer.py +1828 -0
- agent_evolve/application/calibrated_campaign.py +485 -0
- agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
- agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
- agent_evolve/application/campaign_capacity_recourse.py +254 -0
- agent_evolve/application/campaign_contextual_outcomes.py +119 -0
- agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
- agent_evolve/application/campaign_evidence_registry.py +262 -0
- agent_evolve/application/campaign_execution.py +2537 -0
- agent_evolve/application/campaign_generation_audit.py +942 -0
- agent_evolve/application/campaign_learning.py +1812 -0
- agent_evolve/application/campaign_learning_runtime.py +1977 -0
- agent_evolve/application/campaign_search_phase.py +227 -0
- agent_evolve/application/campaign_selector_context_extension.py +220 -0
- agent_evolve/application/campaign_variation_envelope.py +649 -0
- agent_evolve/application/campaign_variation_trace.py +451 -0
- agent_evolve/application/candidate_archive_consequence.py +128 -0
- agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
- agent_evolve/application/composite_outcome_updater.py +145 -0
- agent_evolve/application/composition_portfolio_selection.py +363 -0
- agent_evolve/application/concurrent_stage.py +144 -0
- agent_evolve/application/contextual_action_allocation.py +181 -0
- agent_evolve/application/contextual_campaign_outcomes.py +267 -0
- agent_evolve/application/contextual_campaign_planning.py +1366 -0
- agent_evolve/application/contextual_delayed_credit.py +651 -0
- agent_evolve/application/contextual_search_controller.py +2374 -0
- agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
- agent_evolve/application/decision_metric_projection.py +112 -0
- agent_evolve/application/derived_action_semantics.py +129 -0
- agent_evolve/application/detailed_evaluation.py +449 -0
- agent_evolve/application/earned_lineage.py +1011 -0
- agent_evolve/application/effective_choice_audit.py +484 -0
- agent_evolve/application/empirical_consequence_calibration.py +908 -0
- agent_evolve/application/evaluation_accounting.py +325 -0
- agent_evolve/application/evaluation_cache.py +199 -0
- agent_evolve/application/evaluation_escrow.py +547 -0
- agent_evolve/application/evaluation_recourse.py +253 -0
- agent_evolve/application/event_recorder.py +151 -0
- agent_evolve/application/evolution_campaign.py +1840 -0
- agent_evolve/application/executable_hypothesis.py +323 -0
- agent_evolve/application/factorial_branch_pilot.py +772 -0
- agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
- agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
- agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
- agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
- agent_evolve/application/finite_action_selection.py +188 -0
- agent_evolve/application/finite_action_set.py +306 -0
- agent_evolve/application/finite_action_transition.py +537 -0
- agent_evolve/application/finite_variation_eligibility.py +296 -0
- agent_evolve/application/forecast_geometry_portfolio.py +799 -0
- agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
- agent_evolve/application/front_proximity_admission.py +311 -0
- agent_evolve/application/front_proximity_parent_basis.py +458 -0
- agent_evolve/application/frozen_hurdle_score.py +659 -0
- agent_evolve/application/g3_causal_screen.py +2257 -0
- agent_evolve/application/g3_causal_validation.py +1046 -0
- agent_evolve/application/g3_postseal_curation.py +818 -0
- agent_evolve/application/gated_agentic_generator.py +205 -0
- agent_evolve/application/generation_feedback.py +293 -0
- agent_evolve/application/generative_proposal_journal.py +185 -0
- agent_evolve/application/geometry_conditional_elasticity.py +453 -0
- agent_evolve/application/global_wave_action_allocation.py +1151 -0
- agent_evolve/application/head_mass_conditional_seat.py +268 -0
- agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
- agent_evolve/application/identifiable_reflection_learning.py +395 -0
- agent_evolve/application/identifiable_reflection_request.py +364 -0
- agent_evolve/application/in_memory_residual_archive.py +341 -0
- agent_evolve/application/insight_memory.py +1804 -0
- agent_evolve/application/live_runtime_manifest.py +758 -0
- agent_evolve/application/llm_task_queue.py +769 -0
- agent_evolve/application/matched_finite_action_block.py +409 -0
- agent_evolve/application/materialized_action_broker.py +2328 -0
- agent_evolve/application/materialized_action_constraints.py +83 -0
- agent_evolve/application/materialized_variation.py +211 -0
- agent_evolve/application/multi_option_evolution.py +1536 -0
- agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
- agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
- agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
- agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
- agent_evolve/application/outcome_relation.py +193 -0
- agent_evolve/application/paired_allocation_comparison.py +241 -0
- agent_evolve/application/paired_block_schedule.py +127 -0
- agent_evolve/application/parent_measurement.py +226 -0
- agent_evolve/application/pareto_archive.py +811 -0
- agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
- agent_evolve/application/portfolio_evolution.py +2950 -0
- agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
- agent_evolve/application/portfolio_memory_attribution.py +581 -0
- agent_evolve/application/portfolio_memory_dose.py +788 -0
- agent_evolve/application/portfolio_memory_matched_control.py +938 -0
- agent_evolve/application/portfolio_memory_transfer.py +297 -0
- agent_evolve/application/portfolio_optimization_memory.py +363 -0
- agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
- agent_evolve/application/portfolio_projection.py +335 -0
- agent_evolve/application/portfolio_recombination.py +2032 -0
- agent_evolve/application/post_evolution_reflection.py +834 -0
- agent_evolve/application/postcommit_rank_authority.py +245 -0
- agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
- agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
- agent_evolve/application/prequential_residual_exploration.py +343 -0
- agent_evolve/application/prequential_score_portfolio.py +954 -0
- agent_evolve/application/projections.py +292 -0
- agent_evolve/application/protected_action_committee.py +1027 -0
- agent_evolve/application/protected_branch_pilot.py +376 -0
- agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
- agent_evolve/application/provider_replay.py +910 -0
- agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
- agent_evolve/application/recombination_residual_expert.py +403 -0
- agent_evolve/application/reflection_workflow.py +571 -0
- agent_evolve/application/region_conditional_credit.py +911 -0
- agent_evolve/application/residual_campaign_runtime.py +531 -0
- agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
- agent_evolve/application/residual_headroom_ledger.py +1544 -0
- agent_evolve/application/residual_learning_transaction.py +396 -0
- agent_evolve/application/residual_portfolio_evolution.py +1228 -0
- agent_evolve/application/residual_reachability.py +749 -0
- agent_evolve/application/residual_stage_credit.py +499 -0
- agent_evolve/application/same_prefix_paired_audit.py +1580 -0
- agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
- agent_evolve/application/sequential_lineage_allocation.py +1017 -0
- agent_evolve/application/sequential_market_replay.py +1395 -0
- agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
- agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
- agent_evolve/application/single_score_action_allocation.py +299 -0
- agent_evolve/application/source_exposure_allocation.py +906 -0
- agent_evolve/application/staged_memory.py +210 -0
- agent_evolve/application/stratified_cold_start_allocation.py +732 -0
- agent_evolve/application/support_guarded_hurdle_score.py +549 -0
- agent_evolve/application/target_conditioned_action_forecast.py +595 -0
- agent_evolve/application/target_conditioned_campaign.py +566 -0
- agent_evolve/application/treatment_assignment.py +201 -0
- agent_evolve/application/trusted_objective_evidence.py +217 -0
- agent_evolve/application/two_stage_action_evolution.py +1131 -0
- agent_evolve/application/v8lite_allocation_policy.py +1083 -0
- agent_evolve/application/v9_candidate_policy.py +1303 -0
- agent_evolve/bootstrap.py +108 -0
- agent_evolve/campaign_presets.py +517 -0
- agent_evolve/campaign_profiles.py +452 -0
- agent_evolve/campaign_variation_topology.py +288 -0
- agent_evolve/campaign_workload.py +950 -0
- agent_evolve/cli.py +797 -0
- agent_evolve/contract.py +241 -0
- agent_evolve/core/__init__.py +91 -0
- agent_evolve/core/action_semantics.py +411 -0
- agent_evolve/core/authored.py +105 -0
- agent_evolve/core/formatting.py +286 -0
- agent_evolve/core/optimization_semantics.py +324 -0
- agent_evolve/core/problem.py +167 -0
- agent_evolve/core/results.py +323 -0
- agent_evolve/core/stats.py +70 -0
- agent_evolve/core/telemetry.py +100 -0
- agent_evolve/domain/__init__.py +89 -0
- agent_evolve/domain/artifact.py +162 -0
- agent_evolve/domain/durable_text.py +68 -0
- agent_evolve/domain/event.py +1454 -0
- agent_evolve/domain/finite_action_set.py +426 -0
- agent_evolve/domain/finite_variation.py +526 -0
- agent_evolve/domain/generative_emission.py +559 -0
- agent_evolve/domain/ids.py +163 -0
- agent_evolve/domain/inline_text.py +106 -0
- agent_evolve/domain/insight.py +27 -0
- agent_evolve/domain/lineage.py +737 -0
- agent_evolve/domain/llm_task_queue.py +960 -0
- agent_evolve/domain/outcome.py +96 -0
- agent_evolve/domain/patch.py +854 -0
- agent_evolve/domain/typed_json.py +542 -0
- agent_evolve/domain/variation_space.py +158 -0
- agent_evolve/driver.py +1014 -0
- agent_evolve/harness/__init__.py +29 -0
- agent_evolve/harness/base.py +242 -0
- agent_evolve/harness/directives.py +163 -0
- agent_evolve/harness/generative_seal.py +479 -0
- agent_evolve/harness/registry.py +41 -0
- agent_evolve/infrastructure/__init__.py +39 -0
- agent_evolve/infrastructure/artifacts/__init__.py +6 -0
- agent_evolve/infrastructure/artifacts/_verification.py +67 -0
- agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
- agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
- agent_evolve/infrastructure/asyncio_runtime.py +109 -0
- agent_evolve/infrastructure/authored_runtime.py +188 -0
- agent_evolve/infrastructure/authored_worker.py +171 -0
- agent_evolve/infrastructure/clock.py +53 -0
- agent_evolve/infrastructure/events/__init__.py +6 -0
- agent_evolve/infrastructure/events/_validation.py +89 -0
- agent_evolve/infrastructure/events/in_memory.py +56 -0
- agent_evolve/infrastructure/events/jsonl.py +193 -0
- agent_evolve/infrastructure/exception_provenance.py +215 -0
- agent_evolve/infrastructure/ids.py +118 -0
- agent_evolve/infrastructure/lineage_codec.py +1836 -0
- agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
- agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
- agent_evolve/infrastructure/resource_lease.py +370 -0
- agent_evolve/infrastructure/sanitization/__init__.py +8 -0
- agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
- agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
- agent_evolve/infrastructure/stream_liveness.py +383 -0
- agent_evolve/infrastructure/subprocess_boundary.py +136 -0
- agent_evolve/integrations/__init__.py +1 -0
- agent_evolve/integrations/botorch/__init__.py +28 -0
- agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
- agent_evolve/integrations/completion.py +242 -0
- agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
- agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
- agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
- agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
- agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
- agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
- agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
- agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
- agent_evolve/integrations/pydantic_ai/harness.py +159 -0
- agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
- agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
- agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
- agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
- agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
- agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
- agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
- agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
- agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
- agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
- agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
- agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
- agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
- agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
- agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
- agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
- agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
- agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
- agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
- agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
- agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
- agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
- agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
- agent_evolve/integrations/pymoo_adapter.py +242 -0
- agent_evolve/policies/__init__.py +17 -0
- agent_evolve/policies/check.py +469 -0
- agent_evolve/policies/emit_scaffold.py +451 -0
- agent_evolve/policies/feedback/__init__.py +37 -0
- agent_evolve/policies/feedback/held_out_asn.py +1325 -0
- agent_evolve/policies/genetic.py +607 -0
- agent_evolve/policies/llm_backoff.py +183 -0
- agent_evolve/policies/llm_chooser.py +226 -0
- agent_evolve/policies/llm_generator.py +1760 -0
- agent_evolve/policies/llm_init.py +267 -0
- agent_evolve/policies/llm_operator.py +109 -0
- agent_evolve/policies/llm_prior.py +194 -0
- agent_evolve/policies/llm_surrogate.py +334 -0
- agent_evolve/policies/measurement_evidence.py +704 -0
- agent_evolve/policies/memory/__init__.py +223 -0
- agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
- agent_evolve/policies/memory/compatibility_matching.py +593 -0
- agent_evolve/policies/memory/global_falsification.py +1841 -0
- agent_evolve/policies/memory/prompt_shape.py +503 -0
- agent_evolve/policies/memory/randomized_subset.py +714 -0
- agent_evolve/policies/memory/staged_causal.py +1270 -0
- agent_evolve/policies/memory/treatment_compliance.py +759 -0
- agent_evolve/policies/objective_resolution/__init__.py +17 -0
- agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
- agent_evolve/policies/operator_portfolio.py +407 -0
- agent_evolve/policies/reguidance.py +1133 -0
- agent_evolve/policies/reward/__init__.py +83 -0
- agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
- agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
- agent_evolve/policies/reward/affine_hypervolume.py +490 -0
- agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
- agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
- agent_evolve/policies/reward/frozen_archive.py +360 -0
- agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
- agent_evolve/policies/search_state.py +208 -0
- agent_evolve/policies/selection/__init__.py +345 -0
- agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
- agent_evolve/policies/selection/affine_frontier_context.py +330 -0
- agent_evolve/policies/selection/affine_frontier_target.py +473 -0
- agent_evolve/policies/selection/archive_elite.py +1346 -0
- agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
- agent_evolve/policies/selection/calibrated_slate.py +1394 -0
- agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
- agent_evolve/policies/selection/common_candidate_pool.py +685 -0
- agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
- agent_evolve/policies/selection/disjoint_pairs.py +479 -0
- agent_evolve/policies/selection/elite_explorer.py +719 -0
- agent_evolve/policies/selection/finite_action.py +187 -0
- agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
- agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
- agent_evolve/policies/selection/forecast_calibration.py +922 -0
- agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
- agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
- agent_evolve/policies/selection/full_support_slate.py +91 -0
- agent_evolve/policies/selection/meaningful_direction.py +240 -0
- agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
- agent_evolve/policies/selection/model_anchored_slate.py +826 -0
- agent_evolve/policies/selection/phenotype_recourse.py +979 -0
- agent_evolve/policies/selection/proposal_support.py +368 -0
- agent_evolve/policies/selection/random_portfolio.py +254 -0
- agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
- agent_evolve/policies/selection/residual_frontier.py +463 -0
- agent_evolve/policies/selection/residual_frontier_target.py +605 -0
- agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
- agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
- agent_evolve/policies/selection/target_conditioned_features.py +812 -0
- agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
- agent_evolve/policies/selection/task_keyed_palette.py +906 -0
- agent_evolve/policies/semantics.py +147 -0
- agent_evolve/policies/structure.py +362 -0
- agent_evolve/policies/structured_output_budget.py +62 -0
- agent_evolve/policies/surrogate.py +696 -0
- agent_evolve/policies/variation/__init__.py +1 -0
- agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
- agent_evolve/policies/variation/crossover_inheritance.py +575 -0
- agent_evolve/policies/variation/disjoint_recombination.py +611 -0
- agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
- agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
- agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
- agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
- agent_evolve/policies/variation/typed_patch.py +1981 -0
- agent_evolve/policies/weighted_prior.py +394 -0
- agent_evolve/ports/__init__.py +383 -0
- agent_evolve/ports/action_allocation.py +733 -0
- agent_evolve/ports/action_allocation_frame.py +1153 -0
- agent_evolve/ports/action_allocation_frame_commit.py +294 -0
- agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
- agent_evolve/ports/action_allocation_frame_v3.py +995 -0
- agent_evolve/ports/action_forecast.py +1568 -0
- agent_evolve/ports/action_metric_projection.py +165 -0
- agent_evolve/ports/agentic_generator.py +1561 -0
- agent_evolve/ports/archive_context.py +136 -0
- agent_evolve/ports/artifact_sanitizer.py +44 -0
- agent_evolve/ports/artifact_store.py +225 -0
- agent_evolve/ports/clock.py +13 -0
- agent_evolve/ports/contextual_search_allocation.py +827 -0
- agent_evolve/ports/decision_metric_projection.py +258 -0
- agent_evolve/ports/event_store.py +55 -0
- agent_evolve/ports/executable_hypothesis.py +557 -0
- agent_evolve/ports/finite_acquisition.py +377 -0
- agent_evolve/ports/finite_acquisition_batch.py +296 -0
- agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
- agent_evolve/ports/finite_acquisition_json.py +247 -0
- agent_evolve/ports/finite_acquisition_space.py +168 -0
- agent_evolve/ports/finite_action_selection.py +348 -0
- agent_evolve/ports/finite_action_set.py +256 -0
- agent_evolve/ports/frontier_target.py +396 -0
- agent_evolve/ports/generation_failure.py +43 -0
- agent_evolve/ports/hard_feasibility.py +233 -0
- agent_evolve/ports/id_factory.py +34 -0
- agent_evolve/ports/llm_task_queue.py +93 -0
- agent_evolve/ports/objective_resolution.py +419 -0
- agent_evolve/ports/paired_allocation_comparison.py +401 -0
- agent_evolve/ports/paired_block_schedule.py +475 -0
- agent_evolve/ports/parent_measurement.py +336 -0
- agent_evolve/ports/portfolio_memory_dose.py +643 -0
- agent_evolve/ports/portfolio_selection.py +3169 -0
- agent_evolve/ports/postcommit_rank_authority.py +467 -0
- agent_evolve/ports/presented_action_evidence.py +794 -0
- agent_evolve/ports/resource_lease.py +162 -0
- agent_evolve/ports/structured_generator.py +734 -0
- agent_evolve/ports/structured_output_budget.py +120 -0
- agent_evolve/ports/subprocess_boundary.py +138 -0
- agent_evolve/ports/treatment_assignment.py +466 -0
- agent_evolve/ports/variation_catalog.py +76 -0
- agent_evolve/ports/variation_source.py +226 -0
- agent_evolve/proposal_mode.py +157 -0
- agent_evolve/proposers/__init__.py +10 -0
- agent_evolve/proposers/random_proposer.py +188 -0
- agent_evolve/provider_accounting.py +163 -0
- agent_evolve/py.typed +0 -0
- agent_evolve/reference_method.py +1570 -0
- agent_evolve/session/__init__.py +11 -0
- agent_evolve/session/authorship.py +864 -0
- agent_evolve/session/evaluate.py +236 -0
- agent_evolve/session/fidelity.py +237 -0
- agent_evolve/session/genetic_loop.py +742 -0
- agent_evolve/session/loop.py +803 -0
- agent_evolve/session/screening.py +671 -0
- agent_evolve/settings.py +376 -0
- agent_evolve/workload_kit.py +368 -0
- agent_evolve/workload_prompt.py +398 -0
- agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
- agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
- agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
- agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
- agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
- agentevolve_optimizer-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1804 @@
|
|
|
1
|
+
"""Lifecycle-gated insight memory built around randomized subset credit.
|
|
2
|
+
|
|
3
|
+
The bank stores immutable versions, logs one assignment per operator invocation,
|
|
4
|
+
and updates contextual retrieval scores only from identified selected-vs-unselected
|
|
5
|
+
contrasts. Unselected insights are never mechanically penalized, and untested
|
|
6
|
+
reflection output cannot enter retrieval without a recorded promotion.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field, replace
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from fractions import Fraction
|
|
17
|
+
from typing import Mapping, Sequence
|
|
18
|
+
|
|
19
|
+
from agent_evolve.domain.ids import (
|
|
20
|
+
CandidateId,
|
|
21
|
+
LLMCallId,
|
|
22
|
+
OperatorInvocationId,
|
|
23
|
+
)
|
|
24
|
+
from agent_evolve.domain.finite_variation import FiniteActionEvidenceBinding
|
|
25
|
+
from agent_evolve.domain.insight import InsightRef
|
|
26
|
+
from agent_evolve.domain.patch import require_sha256
|
|
27
|
+
from agent_evolve.domain.typed_json import (
|
|
28
|
+
FrozenJsonObject,
|
|
29
|
+
canonical_typed_json_bytes,
|
|
30
|
+
freeze_json,
|
|
31
|
+
thaw_json,
|
|
32
|
+
)
|
|
33
|
+
from agent_evolve.policies.memory.randomized_subset import (
|
|
34
|
+
EpsilonGreedySubsetSelector,
|
|
35
|
+
InsightSelectionDecision,
|
|
36
|
+
InsightTrial,
|
|
37
|
+
estimate_marginal_effect,
|
|
38
|
+
)
|
|
39
|
+
from agent_evolve.ports.agentic_generator import (
|
|
40
|
+
InsightDraft,
|
|
41
|
+
MAX_REFLECTION_SEMANTIC_VOCABULARY_ENTRIES,
|
|
42
|
+
ReflectionConsumerScope,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_SPACE = re.compile(r"\s+")
|
|
47
|
+
_OPERATOR_KIND_TOKEN = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
48
|
+
_CANONICAL_JSON_PATH = re.compile(
|
|
49
|
+
r"^\$\.[^.\[\]\s]+(?:\.[^.\[\]\s]+|\[(?:0|[1-9][0-9]*)\])*$"
|
|
50
|
+
)
|
|
51
|
+
_LOWER_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
52
|
+
_EVIDENCE_REFERENCE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$")
|
|
53
|
+
_FACT_SCHEMA_ID = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
|
|
54
|
+
_FACTOR_CAPABILITY_TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,191}$")
|
|
55
|
+
_INSIGHT_EVIDENCE_LINEAGE_DOMAIN = b"agent-evolve:insight-evidence-lineage:v1\x00"
|
|
56
|
+
_INSIGHT_EVIDENCE_LINEAGE_V2_DOMAIN = (
|
|
57
|
+
b"agent-evolve:insight-evidence-lineage:v2-empirical-snapshots\x00"
|
|
58
|
+
)
|
|
59
|
+
_EMPIRICAL_EVIDENCE_SNAPSHOT_DOMAIN = b"agent-evolve:empirical-evidence-snapshot:v1\x00"
|
|
60
|
+
_QUARANTINE_TEST_ADMISSION_DOMAIN = b"agent-evolve:quarantine-test-admission:v1\x00"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class QuarantineAssignmentStructuralError(ValueError):
|
|
64
|
+
"""A valid owned quarantine card cannot act within the requested scope."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InsightLifecycleState(str, Enum):
|
|
68
|
+
"""Explicit retrieval lifecycle for one exact insight version."""
|
|
69
|
+
|
|
70
|
+
SEED = "seed"
|
|
71
|
+
QUARANTINED = "quarantined"
|
|
72
|
+
PROMOTED = "promoted"
|
|
73
|
+
DEPRECATED = "deprecated"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def retrievable(self) -> bool:
|
|
77
|
+
return self in {type(self).SEED, type(self).PROMOTED}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class InsightOrigin(str, Enum):
|
|
81
|
+
"""How an insight entered the bank; lifecycle transitions do not alter it."""
|
|
82
|
+
|
|
83
|
+
SEED = "seed"
|
|
84
|
+
REFLECTION = "reflection"
|
|
85
|
+
MANUAL = "manual"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class InsightRelationKind(str, Enum):
|
|
89
|
+
"""Declared semantic relationships; the bank never infers these."""
|
|
90
|
+
|
|
91
|
+
REVISES = "revises"
|
|
92
|
+
DUPLICATES = "duplicates"
|
|
93
|
+
CONTRADICTS = "contradicts"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class EmpiricalEvidenceSnapshot:
|
|
98
|
+
"""Engine-issued facts for one exact contrast, never model-authored prose."""
|
|
99
|
+
|
|
100
|
+
contrast_id: str
|
|
101
|
+
fact_schema_id: str
|
|
102
|
+
fact_schema_version: int
|
|
103
|
+
fact_schema_definition_sha256: str
|
|
104
|
+
facts: FrozenJsonObject
|
|
105
|
+
optimization_semantics_definition_sha256: str | None = None
|
|
106
|
+
action_semantics_definition_sha256: str | None = None
|
|
107
|
+
|
|
108
|
+
def __post_init__(self) -> None:
|
|
109
|
+
if (
|
|
110
|
+
type(self.contrast_id) is not str
|
|
111
|
+
or _LOWER_SHA256.fullmatch(self.contrast_id) is None
|
|
112
|
+
):
|
|
113
|
+
raise ValueError("contrast_id must be a lowercase SHA-256 ID")
|
|
114
|
+
if (
|
|
115
|
+
type(self.fact_schema_id) is not str
|
|
116
|
+
or _FACT_SCHEMA_ID.fullmatch(self.fact_schema_id) is None
|
|
117
|
+
):
|
|
118
|
+
raise ValueError("fact_schema_id must use the closed lowercase grammar")
|
|
119
|
+
if type(self.fact_schema_version) is not int or self.fact_schema_version <= 0:
|
|
120
|
+
raise ValueError("fact_schema_version must be a positive exact integer")
|
|
121
|
+
if (
|
|
122
|
+
type(self.fact_schema_definition_sha256) is not str
|
|
123
|
+
or _LOWER_SHA256.fullmatch(self.fact_schema_definition_sha256) is None
|
|
124
|
+
):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"fact_schema_definition_sha256 must be a lowercase SHA-256 ID"
|
|
127
|
+
)
|
|
128
|
+
if type(self.facts) is not FrozenJsonObject:
|
|
129
|
+
raise TypeError("facts must be an exact FrozenJsonObject")
|
|
130
|
+
if freeze_json(self.facts) is not self.facts:
|
|
131
|
+
raise TypeError("facts must already be frozen typed JSON")
|
|
132
|
+
if not self.facts.items:
|
|
133
|
+
raise ValueError("facts must be non-empty")
|
|
134
|
+
for name in (
|
|
135
|
+
"optimization_semantics_definition_sha256",
|
|
136
|
+
"action_semantics_definition_sha256",
|
|
137
|
+
):
|
|
138
|
+
value = getattr(self, name)
|
|
139
|
+
if value is not None and (
|
|
140
|
+
type(value) is not str or _LOWER_SHA256.fullmatch(value) is None
|
|
141
|
+
):
|
|
142
|
+
raise ValueError(f"{name} must be a lowercase SHA-256 ID or None")
|
|
143
|
+
|
|
144
|
+
def _identity_record(self) -> dict[str, object]:
|
|
145
|
+
self.__post_init__()
|
|
146
|
+
return {
|
|
147
|
+
"schema_version": 1,
|
|
148
|
+
"contrast_id": self.contrast_id,
|
|
149
|
+
"fact_schema_id": self.fact_schema_id,
|
|
150
|
+
"fact_schema_version": self.fact_schema_version,
|
|
151
|
+
"fact_schema_definition_sha256": (self.fact_schema_definition_sha256),
|
|
152
|
+
"facts": thaw_json(self.facts),
|
|
153
|
+
"optimization_semantics_definition_sha256": (
|
|
154
|
+
self.optimization_semantics_definition_sha256
|
|
155
|
+
),
|
|
156
|
+
"action_semantics_definition_sha256": (
|
|
157
|
+
self.action_semantics_definition_sha256
|
|
158
|
+
),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def snapshot_sha256(self) -> str:
|
|
163
|
+
return hashlib.sha256(
|
|
164
|
+
_EMPIRICAL_EVIDENCE_SNAPSHOT_DOMAIN
|
|
165
|
+
+ canonical_typed_json_bytes(freeze_json(self._identity_record()))
|
|
166
|
+
).hexdigest()
|
|
167
|
+
|
|
168
|
+
def to_record(self) -> dict[str, object]:
|
|
169
|
+
return {**self._identity_record(), "snapshot_sha256": self.snapshot_sha256}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True, slots=True)
|
|
173
|
+
class InsightRelation:
|
|
174
|
+
kind: InsightRelationKind
|
|
175
|
+
target: InsightRef
|
|
176
|
+
note: str | None = None
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
if not isinstance(self.kind, InsightRelationKind):
|
|
180
|
+
raise TypeError("relation kind must be an InsightRelationKind")
|
|
181
|
+
if not isinstance(self.target, InsightRef):
|
|
182
|
+
raise TypeError("relation target must be an InsightRef")
|
|
183
|
+
if self.note is not None and (
|
|
184
|
+
type(self.note) is not str
|
|
185
|
+
or not self.note.strip()
|
|
186
|
+
or self.note != self.note.strip()
|
|
187
|
+
):
|
|
188
|
+
raise ValueError("relation note must be non-empty canonical text")
|
|
189
|
+
|
|
190
|
+
def to_record(self) -> dict[str, object]:
|
|
191
|
+
"""Return the complete deterministic durable relation projection."""
|
|
192
|
+
|
|
193
|
+
self.__post_init__()
|
|
194
|
+
InsightRef.__post_init__(self.target)
|
|
195
|
+
return {
|
|
196
|
+
"kind": self.kind.value,
|
|
197
|
+
"target": {
|
|
198
|
+
"insight_id": self.target.insight_id.value,
|
|
199
|
+
"version": self.target.version,
|
|
200
|
+
},
|
|
201
|
+
"note": self.note,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True, slots=True)
|
|
206
|
+
class InsightEvidenceLineage:
|
|
207
|
+
"""Machine-verifiable evidence boundary supplied to one reflection call.
|
|
208
|
+
|
|
209
|
+
``available_contrast_ids`` records what the reflector was allowed to use;
|
|
210
|
+
``cited_contrast_ids`` records only exact full IDs supplied through that
|
|
211
|
+
draft's structured citation field. The distinction avoids treating prompt
|
|
212
|
+
availability or incidental prose substrings as an evidence citation.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
reflection_call_id: LLMCallId
|
|
216
|
+
source_operator_invocation_ids: tuple[OperatorInvocationId, ...]
|
|
217
|
+
source_candidate_ids: tuple[CandidateId, ...]
|
|
218
|
+
available_contrast_ids: tuple[str, ...]
|
|
219
|
+
cited_contrast_ids: tuple[str, ...] = ()
|
|
220
|
+
finite_action_bindings: tuple[FiniteActionEvidenceBinding, ...] = ()
|
|
221
|
+
empirical_evidence: tuple[EmpiricalEvidenceSnapshot, ...] = ()
|
|
222
|
+
|
|
223
|
+
def __post_init__(self) -> None:
|
|
224
|
+
if type(self.reflection_call_id) is not LLMCallId:
|
|
225
|
+
raise TypeError("reflection_call_id must be an exact LLMCallId")
|
|
226
|
+
LLMCallId.__post_init__(self.reflection_call_id)
|
|
227
|
+
_validate_exact_sorted_ids(
|
|
228
|
+
self.source_operator_invocation_ids,
|
|
229
|
+
OperatorInvocationId,
|
|
230
|
+
name="source_operator_invocation_ids",
|
|
231
|
+
)
|
|
232
|
+
_validate_exact_sorted_ids(
|
|
233
|
+
self.source_candidate_ids,
|
|
234
|
+
CandidateId,
|
|
235
|
+
name="source_candidate_ids",
|
|
236
|
+
)
|
|
237
|
+
for name in ("available_contrast_ids", "cited_contrast_ids"):
|
|
238
|
+
values = getattr(self, name)
|
|
239
|
+
if type(values) is not tuple or any(
|
|
240
|
+
type(value) is not str or _LOWER_SHA256.fullmatch(value) is None
|
|
241
|
+
for value in values
|
|
242
|
+
):
|
|
243
|
+
raise TypeError(f"{name} must be a tuple of lowercase SHA-256 IDs")
|
|
244
|
+
if values != tuple(sorted(set(values))):
|
|
245
|
+
raise ValueError(f"{name} must be unique and canonically sorted")
|
|
246
|
+
if not set(self.cited_contrast_ids).issubset(self.available_contrast_ids):
|
|
247
|
+
raise ValueError("cited_contrast_ids must be available to the reflection")
|
|
248
|
+
if type(self.finite_action_bindings) is not tuple or any(
|
|
249
|
+
type(binding) is not FiniteActionEvidenceBinding
|
|
250
|
+
for binding in self.finite_action_bindings
|
|
251
|
+
):
|
|
252
|
+
raise TypeError(
|
|
253
|
+
"finite_action_bindings must be an exact tuple of "
|
|
254
|
+
"FiniteActionEvidenceBinding values"
|
|
255
|
+
)
|
|
256
|
+
for binding in self.finite_action_bindings:
|
|
257
|
+
FiniteActionEvidenceBinding.__post_init__(binding)
|
|
258
|
+
binding_contrast_ids = tuple(
|
|
259
|
+
binding.contrast_id for binding in self.finite_action_bindings
|
|
260
|
+
)
|
|
261
|
+
if binding_contrast_ids != tuple(sorted(set(binding_contrast_ids))):
|
|
262
|
+
raise ValueError(
|
|
263
|
+
"finite_action_bindings must have unique canonical contrast order"
|
|
264
|
+
)
|
|
265
|
+
if not set(binding_contrast_ids).issubset(self.cited_contrast_ids):
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"finite action evidence must bind a cited reflection contrast"
|
|
268
|
+
)
|
|
269
|
+
if type(self.empirical_evidence) is not tuple or any(
|
|
270
|
+
type(value) is not EmpiricalEvidenceSnapshot
|
|
271
|
+
for value in self.empirical_evidence
|
|
272
|
+
):
|
|
273
|
+
raise TypeError(
|
|
274
|
+
"empirical_evidence must be an exact tuple of "
|
|
275
|
+
"EmpiricalEvidenceSnapshot values"
|
|
276
|
+
)
|
|
277
|
+
for snapshot in self.empirical_evidence:
|
|
278
|
+
EmpiricalEvidenceSnapshot.__post_init__(snapshot)
|
|
279
|
+
empirical_contrast_ids = tuple(
|
|
280
|
+
value.contrast_id for value in self.empirical_evidence
|
|
281
|
+
)
|
|
282
|
+
if empirical_contrast_ids != tuple(sorted(set(empirical_contrast_ids))):
|
|
283
|
+
raise ValueError(
|
|
284
|
+
"empirical_evidence must have unique canonical contrast order"
|
|
285
|
+
)
|
|
286
|
+
if self.empirical_evidence and empirical_contrast_ids != (
|
|
287
|
+
self.cited_contrast_ids
|
|
288
|
+
):
|
|
289
|
+
raise ValueError(
|
|
290
|
+
"empirical evidence must exactly cover every cited contrast"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _identity_record(self) -> dict[str, object]:
|
|
294
|
+
self.__post_init__()
|
|
295
|
+
record: dict[str, object] = {
|
|
296
|
+
"schema_version": 2 if self.empirical_evidence else 1,
|
|
297
|
+
"reflection_call_id": self.reflection_call_id.value,
|
|
298
|
+
"source_operator_invocation_ids": [
|
|
299
|
+
value.value for value in self.source_operator_invocation_ids
|
|
300
|
+
],
|
|
301
|
+
"source_candidate_ids": [
|
|
302
|
+
value.value for value in self.source_candidate_ids
|
|
303
|
+
],
|
|
304
|
+
"available_contrast_ids": list(self.available_contrast_ids),
|
|
305
|
+
"cited_contrast_ids": list(self.cited_contrast_ids),
|
|
306
|
+
"finite_action_bindings": [
|
|
307
|
+
binding.to_record() for binding in self.finite_action_bindings
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
if self.empirical_evidence:
|
|
311
|
+
record["empirical_evidence"] = [
|
|
312
|
+
snapshot.to_record() for snapshot in self.empirical_evidence
|
|
313
|
+
]
|
|
314
|
+
return record
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def identity_sha256(self) -> str:
|
|
318
|
+
"""Bind citations, source IDs, and exact action-conditioned evidence."""
|
|
319
|
+
|
|
320
|
+
domain = (
|
|
321
|
+
_INSIGHT_EVIDENCE_LINEAGE_V2_DOMAIN
|
|
322
|
+
if self.empirical_evidence
|
|
323
|
+
else _INSIGHT_EVIDENCE_LINEAGE_DOMAIN
|
|
324
|
+
)
|
|
325
|
+
return hashlib.sha256(
|
|
326
|
+
domain + canonical_typed_json_bytes(freeze_json(self._identity_record()))
|
|
327
|
+
).hexdigest()
|
|
328
|
+
|
|
329
|
+
def to_record(self) -> dict[str, object]:
|
|
330
|
+
"""Return deterministic durable provenance without benchmark objects."""
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
**self._identity_record(),
|
|
334
|
+
"lineage_identity_sha256": self.identity_sha256,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def portfolio_action_evidence(
|
|
339
|
+
self,
|
|
340
|
+
) -> tuple[FiniteActionEvidenceBinding, ...]:
|
|
341
|
+
"""Immutable framework-neutral projection for portfolio card adapters."""
|
|
342
|
+
|
|
343
|
+
self.__post_init__()
|
|
344
|
+
return self.finite_action_bindings
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def compose_epistemic_prompt_payload(
|
|
348
|
+
*,
|
|
349
|
+
empirical_evidence: tuple[EmpiricalEvidenceSnapshot, ...],
|
|
350
|
+
hypothesis: FrozenJsonObject,
|
|
351
|
+
) -> FrozenJsonObject:
|
|
352
|
+
"""Keep trusted observations structurally separate from model hypotheses.
|
|
353
|
+
|
|
354
|
+
Benchmark adapters may scrub or otherwise project a hypothesis before this
|
|
355
|
+
call, but they cannot author empirical snapshots. The returned typed JSON
|
|
356
|
+
is suitable for a card prompt payload and makes the epistemic boundary
|
|
357
|
+
machine-inspectable rather than relying on cautionary prose.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
if type(empirical_evidence) is not tuple or any(
|
|
361
|
+
type(value) is not EmpiricalEvidenceSnapshot for value in empirical_evidence
|
|
362
|
+
):
|
|
363
|
+
raise TypeError("empirical_evidence must be an exact tuple of snapshots")
|
|
364
|
+
for snapshot in empirical_evidence:
|
|
365
|
+
EmpiricalEvidenceSnapshot.__post_init__(snapshot)
|
|
366
|
+
contrast_ids = tuple(value.contrast_id for value in empirical_evidence)
|
|
367
|
+
if contrast_ids != tuple(sorted(set(contrast_ids))):
|
|
368
|
+
raise ValueError("empirical_evidence must have unique canonical contrast order")
|
|
369
|
+
if type(hypothesis) is not FrozenJsonObject:
|
|
370
|
+
raise TypeError("hypothesis must be an exact FrozenJsonObject")
|
|
371
|
+
if freeze_json(hypothesis) is not hypothesis:
|
|
372
|
+
raise TypeError("hypothesis must already be frozen typed JSON")
|
|
373
|
+
hypothesis_record = thaw_json(hypothesis)
|
|
374
|
+
if hypothesis_record.get("epistemic_status") != "unverified_hypothesis":
|
|
375
|
+
raise ValueError(
|
|
376
|
+
"hypothesis must declare epistemic_status=unverified_hypothesis"
|
|
377
|
+
)
|
|
378
|
+
payload = freeze_json(
|
|
379
|
+
{
|
|
380
|
+
"schema_version": 1,
|
|
381
|
+
"empirical_facts": [
|
|
382
|
+
snapshot.to_record() for snapshot in empirical_evidence
|
|
383
|
+
],
|
|
384
|
+
"hypothesis": hypothesis_record,
|
|
385
|
+
"interpretation_policy": {
|
|
386
|
+
"empirical_facts_are_observations": True,
|
|
387
|
+
"hypothesis_is_observation": False,
|
|
388
|
+
"mechanism_requires_independent_validation": True,
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
if type(payload) is not FrozenJsonObject:
|
|
393
|
+
raise AssertionError("epistemic prompt payload must freeze to an object")
|
|
394
|
+
return payload
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _validate_reflection_action_lineage(
|
|
398
|
+
draft: InsightDraft,
|
|
399
|
+
lineage: InsightEvidenceLineage,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Reject action recommendations detached from their cited finite evidence."""
|
|
402
|
+
|
|
403
|
+
if type(draft) is not InsightDraft:
|
|
404
|
+
raise TypeError("draft must be an exact InsightDraft")
|
|
405
|
+
InsightDraft.__post_init__(draft)
|
|
406
|
+
if type(lineage) is not InsightEvidenceLineage:
|
|
407
|
+
raise TypeError("lineage must be an exact InsightEvidenceLineage")
|
|
408
|
+
InsightEvidenceLineage.__post_init__(lineage)
|
|
409
|
+
binding_contrast_ids = tuple(
|
|
410
|
+
binding.contrast_id for binding in lineage.finite_action_bindings
|
|
411
|
+
)
|
|
412
|
+
if not set(binding_contrast_ids).issubset(draft.evidence_contrast_ids):
|
|
413
|
+
raise ValueError(
|
|
414
|
+
"finite action evidence must bind a contrast cited by the draft"
|
|
415
|
+
)
|
|
416
|
+
if not draft.recommended_option_ids:
|
|
417
|
+
return
|
|
418
|
+
if draft.evidence_contrast_ids != lineage.cited_contrast_ids:
|
|
419
|
+
raise ValueError(
|
|
420
|
+
"exact-action draft citations differ from their evidence lineage"
|
|
421
|
+
)
|
|
422
|
+
if binding_contrast_ids != lineage.cited_contrast_ids:
|
|
423
|
+
raise ValueError(
|
|
424
|
+
"exact-action reflection requires one finite action binding per citation"
|
|
425
|
+
)
|
|
426
|
+
bound_option_ids = tuple(
|
|
427
|
+
sorted({binding.option_id for binding in lineage.finite_action_bindings})
|
|
428
|
+
)
|
|
429
|
+
if bound_option_ids != draft.recommended_option_ids:
|
|
430
|
+
raise ValueError(
|
|
431
|
+
"exact-action recommendation differs from its evidence action bindings"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass(frozen=True, slots=True)
|
|
436
|
+
class ReflectedInsightBatchItem:
|
|
437
|
+
"""One staged reflection card and its independently verified lineage."""
|
|
438
|
+
|
|
439
|
+
draft: InsightDraft
|
|
440
|
+
evidence_lineage: InsightEvidenceLineage
|
|
441
|
+
|
|
442
|
+
def __post_init__(self) -> None:
|
|
443
|
+
if type(self.draft) is not InsightDraft:
|
|
444
|
+
raise TypeError("draft must be an exact InsightDraft")
|
|
445
|
+
InsightDraft.__post_init__(self.draft)
|
|
446
|
+
if type(self.evidence_lineage) is not InsightEvidenceLineage:
|
|
447
|
+
raise TypeError("evidence_lineage must be an exact InsightEvidenceLineage")
|
|
448
|
+
InsightEvidenceLineage.__post_init__(self.evidence_lineage)
|
|
449
|
+
_validate_reflection_action_lineage(self.draft, self.evidence_lineage)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@dataclass(frozen=True, slots=True)
|
|
453
|
+
class InsightLifecycleTransition:
|
|
454
|
+
"""Append-only audit record for an explicit lifecycle change."""
|
|
455
|
+
|
|
456
|
+
sequence: int
|
|
457
|
+
reference: InsightRef
|
|
458
|
+
prior_state: InsightLifecycleState
|
|
459
|
+
new_state: InsightLifecycleState
|
|
460
|
+
reason: str
|
|
461
|
+
supporting_evidence: tuple[str, ...] = ()
|
|
462
|
+
|
|
463
|
+
def __post_init__(self) -> None:
|
|
464
|
+
if type(self.sequence) is not int or self.sequence <= 0:
|
|
465
|
+
raise ValueError("transition sequence must be positive")
|
|
466
|
+
if not isinstance(self.reference, InsightRef):
|
|
467
|
+
raise TypeError("transition reference must be an InsightRef")
|
|
468
|
+
if not isinstance(self.prior_state, InsightLifecycleState) or not isinstance(
|
|
469
|
+
self.new_state, InsightLifecycleState
|
|
470
|
+
):
|
|
471
|
+
raise TypeError("transition states must be InsightLifecycleState values")
|
|
472
|
+
if (
|
|
473
|
+
type(self.reason) is not str
|
|
474
|
+
or not self.reason.strip()
|
|
475
|
+
or self.reason != self.reason.strip()
|
|
476
|
+
or len(self.reason) > 1_024
|
|
477
|
+
):
|
|
478
|
+
raise ValueError("transition reason must be canonical non-empty text")
|
|
479
|
+
if type(self.supporting_evidence) is not tuple:
|
|
480
|
+
raise TypeError("supporting_evidence must be an exact tuple")
|
|
481
|
+
if _supporting_evidence(self.supporting_evidence) != self.supporting_evidence:
|
|
482
|
+
raise ValueError("supporting_evidence must use canonical sorted order")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@dataclass(frozen=True, slots=True)
|
|
486
|
+
class InsightLifecycleChangeRequest:
|
|
487
|
+
"""One proposed lifecycle change for atomic batch publication."""
|
|
488
|
+
|
|
489
|
+
reference: InsightRef
|
|
490
|
+
new_state: InsightLifecycleState
|
|
491
|
+
reason: str
|
|
492
|
+
supporting_evidence: tuple[str, ...] = ()
|
|
493
|
+
|
|
494
|
+
def __post_init__(self) -> None:
|
|
495
|
+
if type(self.reference) is not InsightRef:
|
|
496
|
+
raise TypeError("reference must be an exact InsightRef")
|
|
497
|
+
InsightRef.__post_init__(self.reference)
|
|
498
|
+
if self.new_state not in {
|
|
499
|
+
InsightLifecycleState.PROMOTED,
|
|
500
|
+
InsightLifecycleState.DEPRECATED,
|
|
501
|
+
}:
|
|
502
|
+
raise ValueError("new_state must be promoted or deprecated")
|
|
503
|
+
if (
|
|
504
|
+
type(self.reason) is not str
|
|
505
|
+
or not self.reason.strip()
|
|
506
|
+
or self.reason != self.reason.strip()
|
|
507
|
+
or len(self.reason) > 1_024
|
|
508
|
+
):
|
|
509
|
+
raise ValueError("reason must be canonical non-empty text")
|
|
510
|
+
if type(self.supporting_evidence) is not tuple:
|
|
511
|
+
raise TypeError("supporting_evidence must be an exact tuple")
|
|
512
|
+
if _supporting_evidence(self.supporting_evidence) != self.supporting_evidence:
|
|
513
|
+
raise ValueError("supporting_evidence must use canonical sorted order")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _validate_exact_sorted_ids(
|
|
517
|
+
values: tuple, expected_type: type, *, name: str
|
|
518
|
+
) -> None:
|
|
519
|
+
if type(values) is not tuple or any(
|
|
520
|
+
type(value) is not expected_type for value in values
|
|
521
|
+
):
|
|
522
|
+
raise TypeError(
|
|
523
|
+
f"{name} must be an exact tuple of {expected_type.__name__} values"
|
|
524
|
+
)
|
|
525
|
+
for value in values:
|
|
526
|
+
expected_type.__post_init__(value)
|
|
527
|
+
if values != tuple(sorted(set(values))):
|
|
528
|
+
raise ValueError(f"{name} must be unique and canonically sorted")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _supporting_evidence(values: Sequence[str]) -> tuple[str, ...]:
|
|
532
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
|
|
533
|
+
raise TypeError("supporting_evidence must be a sequence of evidence IDs")
|
|
534
|
+
canonical = tuple(values)
|
|
535
|
+
if any(
|
|
536
|
+
type(value) is not str or _EVIDENCE_REFERENCE.fullmatch(value) is None
|
|
537
|
+
for value in canonical
|
|
538
|
+
):
|
|
539
|
+
raise ValueError("supporting_evidence must contain bounded evidence-ID tokens")
|
|
540
|
+
if len(set(canonical)) != len(canonical):
|
|
541
|
+
raise ValueError("supporting_evidence cannot contain duplicates")
|
|
542
|
+
return tuple(sorted(canonical))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _claim_key(value: str) -> str:
|
|
546
|
+
return _SPACE.sub(" ", value.strip().casefold())
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _operator_kind_token(value: str) -> str:
|
|
550
|
+
if type(value) is not str or _OPERATOR_KIND_TOKEN.fullmatch(value) is None:
|
|
551
|
+
raise ValueError(
|
|
552
|
+
"operator kinds must be lowercase tokens containing only letters, "
|
|
553
|
+
"digits, and underscores"
|
|
554
|
+
)
|
|
555
|
+
return value
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _applicable_operator_kinds(values: Sequence[str]) -> tuple[str, ...]:
|
|
559
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
|
|
560
|
+
raise TypeError("applicable_operator_kinds must be a sequence of strings")
|
|
561
|
+
canonical = tuple(sorted(_operator_kind_token(value) for value in values))
|
|
562
|
+
if len(set(canonical)) != len(canonical):
|
|
563
|
+
raise ValueError("applicable_operator_kinds cannot contain duplicates")
|
|
564
|
+
return canonical
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _canonical_paths(values: Sequence[str], *, name: str) -> tuple[str, ...]:
|
|
568
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
|
|
569
|
+
raise TypeError(f"{name} must be a sequence of canonical JSON paths")
|
|
570
|
+
paths = tuple(values)
|
|
571
|
+
if any(
|
|
572
|
+
type(path) is not str or _CANONICAL_JSON_PATH.fullmatch(path) is None
|
|
573
|
+
for path in paths
|
|
574
|
+
):
|
|
575
|
+
raise ValueError(
|
|
576
|
+
f"{name} must contain exact canonical JSON paths beginning with $."
|
|
577
|
+
)
|
|
578
|
+
if len(set(paths)) != len(paths):
|
|
579
|
+
raise ValueError(f"{name} cannot contain duplicates")
|
|
580
|
+
return tuple(sorted(paths))
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _canonical_factor_capabilities(
|
|
584
|
+
values: Sequence[str] | None,
|
|
585
|
+
) -> tuple[str, ...] | None:
|
|
586
|
+
if values is None:
|
|
587
|
+
return None
|
|
588
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
|
|
589
|
+
raise TypeError("factor_capabilities must be a sequence of strings or None")
|
|
590
|
+
capabilities = tuple(values)
|
|
591
|
+
if any(
|
|
592
|
+
type(value) is not str or _FACTOR_CAPABILITY_TOKEN.fullmatch(value) is None
|
|
593
|
+
for value in capabilities
|
|
594
|
+
):
|
|
595
|
+
raise ValueError("factor_capabilities must contain canonical capability tokens")
|
|
596
|
+
if len(set(capabilities)) != len(capabilities):
|
|
597
|
+
raise ValueError("factor_capabilities cannot contain duplicates")
|
|
598
|
+
if len(capabilities) > MAX_REFLECTION_SEMANTIC_VOCABULARY_ENTRIES:
|
|
599
|
+
raise ValueError("factor_capabilities exceeds the semantic vocabulary cap")
|
|
600
|
+
return tuple(sorted(capabilities))
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _paths_overlap(first: str, second: str) -> bool:
|
|
604
|
+
"""Whether either canonical JSON path is an ancestor of the other."""
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
first == second
|
|
608
|
+
or first.startswith(second + ".")
|
|
609
|
+
or first.startswith(second + "[")
|
|
610
|
+
or second.startswith(first + ".")
|
|
611
|
+
or second.startswith(first + "[")
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@dataclass(frozen=True, slots=True)
|
|
616
|
+
class QuarantineTestAdmissionReceipt:
|
|
617
|
+
"""Bank-issued authority for randomized use of exact quarantine versions."""
|
|
618
|
+
|
|
619
|
+
references: tuple[InsightRef, ...]
|
|
620
|
+
operator_kind: str
|
|
621
|
+
editable_paths: tuple[str, ...]
|
|
622
|
+
source_admission_request_sha256: str
|
|
623
|
+
memory_trial_count_cutoff: int
|
|
624
|
+
receipt_sha256: str = field(init=False)
|
|
625
|
+
|
|
626
|
+
def __post_init__(self) -> None:
|
|
627
|
+
if (
|
|
628
|
+
type(self.references) is not tuple
|
|
629
|
+
or not self.references
|
|
630
|
+
or any(type(value) is not InsightRef for value in self.references)
|
|
631
|
+
):
|
|
632
|
+
raise ValueError("references must be a non-empty exact tuple")
|
|
633
|
+
if self.references != tuple(sorted(set(self.references))):
|
|
634
|
+
raise ValueError("references must be unique and canonical")
|
|
635
|
+
operator = _operator_kind_token(self.operator_kind)
|
|
636
|
+
if operator != self.operator_kind:
|
|
637
|
+
raise ValueError("operator_kind must be canonical")
|
|
638
|
+
paths = _canonical_paths(self.editable_paths, name="editable_paths")
|
|
639
|
+
if paths != self.editable_paths:
|
|
640
|
+
raise ValueError("editable_paths must be canonical")
|
|
641
|
+
if (
|
|
642
|
+
type(self.source_admission_request_sha256) is not str
|
|
643
|
+
or _LOWER_SHA256.fullmatch(self.source_admission_request_sha256) is None
|
|
644
|
+
):
|
|
645
|
+
raise ValueError(
|
|
646
|
+
"source_admission_request_sha256 must be a lowercase SHA-256 ID"
|
|
647
|
+
)
|
|
648
|
+
if (
|
|
649
|
+
type(self.memory_trial_count_cutoff) is not int
|
|
650
|
+
or self.memory_trial_count_cutoff < 0
|
|
651
|
+
):
|
|
652
|
+
raise ValueError("memory_trial_count_cutoff must be non-negative")
|
|
653
|
+
object.__setattr__(
|
|
654
|
+
self,
|
|
655
|
+
"receipt_sha256",
|
|
656
|
+
hashlib.sha256(
|
|
657
|
+
_QUARANTINE_TEST_ADMISSION_DOMAIN
|
|
658
|
+
+ canonical_typed_json_bytes(freeze_json(self._unsigned_record()))
|
|
659
|
+
).hexdigest(),
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
663
|
+
return {
|
|
664
|
+
"schema_version": 1,
|
|
665
|
+
"references": [
|
|
666
|
+
{
|
|
667
|
+
"insight_id": value.insight_id.value,
|
|
668
|
+
"version": value.version,
|
|
669
|
+
}
|
|
670
|
+
for value in self.references
|
|
671
|
+
],
|
|
672
|
+
"operator_kind": self.operator_kind,
|
|
673
|
+
"editable_paths": list(self.editable_paths),
|
|
674
|
+
"source_admission_request_sha256": (self.source_admission_request_sha256),
|
|
675
|
+
"memory_trial_count_cutoff": self.memory_trial_count_cutoff,
|
|
676
|
+
"scope": "quarantine_diagnostic_only",
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
def to_record(self) -> dict[str, object]:
|
|
680
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
@dataclass(frozen=True, slots=True)
|
|
684
|
+
class InsightMemoryEntry:
|
|
685
|
+
reference: InsightRef
|
|
686
|
+
draft: InsightDraft
|
|
687
|
+
initial_score: float
|
|
688
|
+
applicable_operator_kinds: tuple[str, ...] = ()
|
|
689
|
+
lifecycle_state: InsightLifecycleState = InsightLifecycleState.SEED
|
|
690
|
+
origin: InsightOrigin = InsightOrigin.SEED
|
|
691
|
+
evidence_lineage: InsightEvidenceLineage | None = None
|
|
692
|
+
relations: tuple[InsightRelation, ...] = ()
|
|
693
|
+
|
|
694
|
+
def __post_init__(self) -> None:
|
|
695
|
+
if type(self.reference) is not InsightRef:
|
|
696
|
+
raise TypeError("reference must be an exact InsightRef")
|
|
697
|
+
InsightRef.__post_init__(self.reference)
|
|
698
|
+
if type(self.draft) is not InsightDraft:
|
|
699
|
+
raise TypeError("draft must be an exact InsightDraft")
|
|
700
|
+
InsightDraft.__post_init__(self.draft)
|
|
701
|
+
if type(self.initial_score) is not float or not math.isfinite(
|
|
702
|
+
self.initial_score
|
|
703
|
+
):
|
|
704
|
+
raise TypeError("initial_score must be a finite canonical float")
|
|
705
|
+
canonical = _applicable_operator_kinds(self.applicable_operator_kinds)
|
|
706
|
+
if canonical != self.applicable_operator_kinds:
|
|
707
|
+
raise ValueError(
|
|
708
|
+
"applicable_operator_kinds must use canonical sorted order"
|
|
709
|
+
)
|
|
710
|
+
if not isinstance(self.lifecycle_state, InsightLifecycleState):
|
|
711
|
+
raise TypeError("lifecycle_state must be an InsightLifecycleState")
|
|
712
|
+
if not isinstance(self.origin, InsightOrigin):
|
|
713
|
+
raise TypeError("origin must be an InsightOrigin")
|
|
714
|
+
if self.lifecycle_state is InsightLifecycleState.SEED:
|
|
715
|
+
if self.origin is not InsightOrigin.SEED:
|
|
716
|
+
raise ValueError(
|
|
717
|
+
"only seed-origin insights may have seed lifecycle state"
|
|
718
|
+
)
|
|
719
|
+
elif self.origin is InsightOrigin.SEED and self.lifecycle_state not in {
|
|
720
|
+
InsightLifecycleState.SEED,
|
|
721
|
+
InsightLifecycleState.DEPRECATED,
|
|
722
|
+
}:
|
|
723
|
+
raise ValueError(
|
|
724
|
+
"seed-origin insights can only remain seed or be deprecated"
|
|
725
|
+
)
|
|
726
|
+
if self.origin is InsightOrigin.REFLECTION:
|
|
727
|
+
if self.evidence_lineage is None:
|
|
728
|
+
raise ValueError("reflection-origin insights require evidence lineage")
|
|
729
|
+
elif self.evidence_lineage is not None:
|
|
730
|
+
raise ValueError("reflection evidence lineage requires reflection origin")
|
|
731
|
+
if self.evidence_lineage is not None:
|
|
732
|
+
if type(self.evidence_lineage) is not InsightEvidenceLineage:
|
|
733
|
+
raise TypeError(
|
|
734
|
+
"evidence_lineage must be an exact InsightEvidenceLineage"
|
|
735
|
+
)
|
|
736
|
+
InsightEvidenceLineage.__post_init__(self.evidence_lineage)
|
|
737
|
+
_validate_reflection_action_lineage(
|
|
738
|
+
self.draft,
|
|
739
|
+
self.evidence_lineage,
|
|
740
|
+
)
|
|
741
|
+
if type(self.relations) is not tuple or any(
|
|
742
|
+
not isinstance(relation, InsightRelation) for relation in self.relations
|
|
743
|
+
):
|
|
744
|
+
raise TypeError("relations must be a tuple of InsightRelation values")
|
|
745
|
+
relation_keys = tuple(
|
|
746
|
+
(relation.target, relation.kind.value) for relation in self.relations
|
|
747
|
+
)
|
|
748
|
+
if relation_keys != tuple(sorted(set(relation_keys))):
|
|
749
|
+
raise ValueError("relations must be unique and canonically sorted")
|
|
750
|
+
|
|
751
|
+
@property
|
|
752
|
+
def retrievable(self) -> bool:
|
|
753
|
+
return self.lifecycle_state.retrievable
|
|
754
|
+
|
|
755
|
+
def to_record(self) -> dict[str, object]:
|
|
756
|
+
"""Return a complete, lossless, JSON-ready durable projection.
|
|
757
|
+
|
|
758
|
+
The draft record carries every immutable model-authored field while
|
|
759
|
+
its hashes make later content drift directly detectable. Scores use
|
|
760
|
+
hexadecimal float text so a write/read cycle cannot change the exact
|
|
761
|
+
binary value. Lifecycle eligibility is emitted explicitly because it
|
|
762
|
+
is operational state, even though it is derivable from the lifecycle
|
|
763
|
+
enum.
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
self.__post_init__()
|
|
767
|
+
return {
|
|
768
|
+
"schema_version": 1,
|
|
769
|
+
"reference": {
|
|
770
|
+
"insight_id": self.reference.insight_id.value,
|
|
771
|
+
"version": self.reference.version,
|
|
772
|
+
},
|
|
773
|
+
"draft": self.draft.content_record(),
|
|
774
|
+
"draft_content_sha256": self.draft.content_sha256,
|
|
775
|
+
"draft_hypothesis_sha256": self.draft.hypothesis_sha256,
|
|
776
|
+
"initial_score_hex": self.initial_score.hex(),
|
|
777
|
+
"applicable_operator_kinds": list(self.applicable_operator_kinds),
|
|
778
|
+
"lifecycle_state": self.lifecycle_state.value,
|
|
779
|
+
"retrievable": self.retrievable,
|
|
780
|
+
"origin": self.origin.value,
|
|
781
|
+
"evidence_lineage": (
|
|
782
|
+
None
|
|
783
|
+
if self.evidence_lineage is None
|
|
784
|
+
else self.evidence_lineage.to_record()
|
|
785
|
+
),
|
|
786
|
+
"relations": [relation.to_record() for relation in self.relations],
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class InsightMemoryBank:
|
|
791
|
+
"""Small replaceable policy object for online development experiments."""
|
|
792
|
+
|
|
793
|
+
def __init__(
|
|
794
|
+
self,
|
|
795
|
+
*,
|
|
796
|
+
id_factory,
|
|
797
|
+
exploration_probability: Fraction = Fraction(1, 2),
|
|
798
|
+
shrinkage_effective_sample_size: float = 4.0,
|
|
799
|
+
) -> None:
|
|
800
|
+
if shrinkage_effective_sample_size <= 0:
|
|
801
|
+
raise ValueError("shrinkage_effective_sample_size must be positive")
|
|
802
|
+
self._ids = id_factory
|
|
803
|
+
self._selector = EpsilonGreedySubsetSelector(exploration_probability)
|
|
804
|
+
self._shrinkage_ess = float(shrinkage_effective_sample_size)
|
|
805
|
+
self._entries: dict[InsightRef, InsightMemoryEntry] = {}
|
|
806
|
+
self._claim_index: dict[str, InsightRef] = {}
|
|
807
|
+
self._scores: dict[tuple[str, InsightRef], float] = {}
|
|
808
|
+
self._trials: list[InsightTrial] = []
|
|
809
|
+
self._transitions: list[InsightLifecycleTransition] = []
|
|
810
|
+
self._quarantine_admissions: dict[
|
|
811
|
+
str,
|
|
812
|
+
QuarantineTestAdmissionReceipt,
|
|
813
|
+
] = {}
|
|
814
|
+
self._quarantine_admission_by_reference: dict[InsightRef, str] = {}
|
|
815
|
+
|
|
816
|
+
@property
|
|
817
|
+
def entries(self) -> tuple[InsightMemoryEntry, ...]:
|
|
818
|
+
return tuple(self._entries[key] for key in sorted(self._entries))
|
|
819
|
+
|
|
820
|
+
@property
|
|
821
|
+
def trials(self) -> tuple[InsightTrial, ...]:
|
|
822
|
+
return tuple(self._trials)
|
|
823
|
+
|
|
824
|
+
@property
|
|
825
|
+
def transitions(self) -> tuple[InsightLifecycleTransition, ...]:
|
|
826
|
+
return tuple(self._transitions)
|
|
827
|
+
|
|
828
|
+
def entries_for(
|
|
829
|
+
self, references: Sequence[InsightRef]
|
|
830
|
+
) -> tuple[InsightMemoryEntry, ...]:
|
|
831
|
+
"""Return immutable exact-version entries in caller-supplied order."""
|
|
832
|
+
|
|
833
|
+
owned = self._owned_reference_sequence(references, name="references")
|
|
834
|
+
return tuple(self._entries[reference] for reference in owned)
|
|
835
|
+
|
|
836
|
+
def add(
|
|
837
|
+
self,
|
|
838
|
+
draft: InsightDraft,
|
|
839
|
+
*,
|
|
840
|
+
initial_score: float | None = None,
|
|
841
|
+
applicable_operator_kinds: Sequence[str] = (),
|
|
842
|
+
origin: InsightOrigin = InsightOrigin.SEED,
|
|
843
|
+
lifecycle_state: InsightLifecycleState | None = None,
|
|
844
|
+
evidence_lineage: InsightEvidenceLineage | None = None,
|
|
845
|
+
relations: Sequence[InsightRelation] = (),
|
|
846
|
+
) -> tuple[InsightMemoryEntry, bool]:
|
|
847
|
+
"""Add one immutable v1 insight, or return an exact-claim match.
|
|
848
|
+
|
|
849
|
+
Existing callers create eligible seed priors by default. Reflection and
|
|
850
|
+
manual origins are forced to start quarantined; promotion is a separate,
|
|
851
|
+
recorded operation.
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
if type(draft) is not InsightDraft:
|
|
855
|
+
raise TypeError("draft must be an exact InsightDraft")
|
|
856
|
+
InsightDraft.__post_init__(draft)
|
|
857
|
+
_canonical_paths(draft.affected_paths, name="draft.affected_paths")
|
|
858
|
+
operator_kinds = _applicable_operator_kinds(applicable_operator_kinds)
|
|
859
|
+
state = self._initial_state(origin, lifecycle_state, evidence_lineage)
|
|
860
|
+
if origin is InsightOrigin.REFLECTION:
|
|
861
|
+
assert evidence_lineage is not None
|
|
862
|
+
_validate_reflection_action_lineage(draft, evidence_lineage)
|
|
863
|
+
canonical_relations = self._canonical_relations(relations)
|
|
864
|
+
if any(
|
|
865
|
+
relation.kind is InsightRelationKind.REVISES
|
|
866
|
+
for relation in canonical_relations
|
|
867
|
+
):
|
|
868
|
+
raise ValueError("use add_revision to create a revises relationship")
|
|
869
|
+
key = _claim_key(draft.claim)
|
|
870
|
+
existing = self._claim_index.get(key)
|
|
871
|
+
if existing is not None:
|
|
872
|
+
return self._entries[existing], False
|
|
873
|
+
score = 0.0 if initial_score is None else float(initial_score)
|
|
874
|
+
if not math.isfinite(score):
|
|
875
|
+
raise ValueError("initial_score must be finite")
|
|
876
|
+
reference = InsightRef(self._ids.new_insight_id(), 1)
|
|
877
|
+
entry = InsightMemoryEntry(
|
|
878
|
+
reference=reference,
|
|
879
|
+
draft=draft,
|
|
880
|
+
initial_score=score,
|
|
881
|
+
applicable_operator_kinds=operator_kinds,
|
|
882
|
+
lifecycle_state=state,
|
|
883
|
+
origin=origin,
|
|
884
|
+
evidence_lineage=evidence_lineage,
|
|
885
|
+
relations=canonical_relations,
|
|
886
|
+
)
|
|
887
|
+
self._insert(entry)
|
|
888
|
+
return entry, True
|
|
889
|
+
|
|
890
|
+
def add_reflection_batch(
|
|
891
|
+
self,
|
|
892
|
+
items: tuple[ReflectedInsightBatchItem, ...],
|
|
893
|
+
*,
|
|
894
|
+
initial_score: float = 0.0,
|
|
895
|
+
applicable_operator_kinds: Sequence[str] = (),
|
|
896
|
+
) -> tuple[InsightMemoryEntry, ...]:
|
|
897
|
+
"""Publish a complete reflection batch atomically or publish nothing.
|
|
898
|
+
|
|
899
|
+
Reflection shards are independently generated, but a scientific batch
|
|
900
|
+
is useful only when every expected origin yielded a new quarantine
|
|
901
|
+
card. All validation and duplicate checks therefore precede entry
|
|
902
|
+
construction and insertion.
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
if type(items) is not tuple or not items:
|
|
906
|
+
raise ValueError("a reflection batch must be a non-empty exact tuple")
|
|
907
|
+
if any(type(item) is not ReflectedInsightBatchItem for item in items):
|
|
908
|
+
raise TypeError(
|
|
909
|
+
"reflection batch must contain exact ReflectedInsightBatchItem values"
|
|
910
|
+
)
|
|
911
|
+
score = float(initial_score)
|
|
912
|
+
if not math.isfinite(score):
|
|
913
|
+
raise ValueError("initial_score must be finite")
|
|
914
|
+
operator_kinds = _applicable_operator_kinds(applicable_operator_kinds)
|
|
915
|
+
|
|
916
|
+
claim_keys: list[str] = []
|
|
917
|
+
for item in items:
|
|
918
|
+
ReflectedInsightBatchItem.__post_init__(item)
|
|
919
|
+
_canonical_paths(item.draft.affected_paths, name="draft.affected_paths")
|
|
920
|
+
self._initial_state(
|
|
921
|
+
InsightOrigin.REFLECTION,
|
|
922
|
+
InsightLifecycleState.QUARANTINED,
|
|
923
|
+
item.evidence_lineage,
|
|
924
|
+
)
|
|
925
|
+
claim_keys.append(_claim_key(item.draft.claim))
|
|
926
|
+
if len(set(claim_keys)) != len(claim_keys):
|
|
927
|
+
raise ValueError("reflection batch contains duplicate normalized claims")
|
|
928
|
+
if any(key in self._claim_index for key in claim_keys):
|
|
929
|
+
raise ValueError("reflection batch collides with an existing claim")
|
|
930
|
+
|
|
931
|
+
staged = tuple(
|
|
932
|
+
InsightMemoryEntry(
|
|
933
|
+
reference=InsightRef(self._ids.new_insight_id(), 1),
|
|
934
|
+
draft=item.draft,
|
|
935
|
+
initial_score=score,
|
|
936
|
+
applicable_operator_kinds=operator_kinds,
|
|
937
|
+
lifecycle_state=InsightLifecycleState.QUARANTINED,
|
|
938
|
+
origin=InsightOrigin.REFLECTION,
|
|
939
|
+
evidence_lineage=item.evidence_lineage,
|
|
940
|
+
)
|
|
941
|
+
for item in items
|
|
942
|
+
)
|
|
943
|
+
staged_references = tuple(entry.reference for entry in staged)
|
|
944
|
+
if len(set(staged_references)) != len(staged_references) or any(
|
|
945
|
+
reference in self._entries for reference in staged_references
|
|
946
|
+
):
|
|
947
|
+
raise ValueError(
|
|
948
|
+
"reflection batch ID allocation collided before publication"
|
|
949
|
+
)
|
|
950
|
+
for entry in staged:
|
|
951
|
+
self._insert(entry)
|
|
952
|
+
return staged
|
|
953
|
+
|
|
954
|
+
def extend(
|
|
955
|
+
self,
|
|
956
|
+
drafts: Sequence[InsightDraft],
|
|
957
|
+
*,
|
|
958
|
+
initial_score: float | None = None,
|
|
959
|
+
applicable_operator_kinds: Sequence[str] = (),
|
|
960
|
+
origin: InsightOrigin = InsightOrigin.SEED,
|
|
961
|
+
lifecycle_state: InsightLifecycleState | None = None,
|
|
962
|
+
evidence_lineage: InsightEvidenceLineage | None = None,
|
|
963
|
+
relations: Sequence[InsightRelation] = (),
|
|
964
|
+
) -> tuple[InsightMemoryEntry, ...]:
|
|
965
|
+
added: list[InsightMemoryEntry] = []
|
|
966
|
+
for draft in drafts:
|
|
967
|
+
entry, is_new = self.add(
|
|
968
|
+
draft,
|
|
969
|
+
initial_score=initial_score,
|
|
970
|
+
applicable_operator_kinds=applicable_operator_kinds,
|
|
971
|
+
origin=origin,
|
|
972
|
+
lifecycle_state=lifecycle_state,
|
|
973
|
+
evidence_lineage=evidence_lineage,
|
|
974
|
+
relations=relations,
|
|
975
|
+
)
|
|
976
|
+
if is_new:
|
|
977
|
+
added.append(entry)
|
|
978
|
+
return tuple(added)
|
|
979
|
+
|
|
980
|
+
def add_revision(
|
|
981
|
+
self,
|
|
982
|
+
predecessor: InsightRef,
|
|
983
|
+
draft: InsightDraft,
|
|
984
|
+
*,
|
|
985
|
+
initial_score: float | None = None,
|
|
986
|
+
applicable_operator_kinds: Sequence[str] = (),
|
|
987
|
+
origin: InsightOrigin = InsightOrigin.MANUAL,
|
|
988
|
+
evidence_lineage: InsightEvidenceLineage | None = None,
|
|
989
|
+
relations: Sequence[InsightRelation] = (),
|
|
990
|
+
revision_note: str = "explicit semantic revision",
|
|
991
|
+
) -> InsightMemoryEntry:
|
|
992
|
+
"""Create a quarantined next version without superseding its predecessor.
|
|
993
|
+
|
|
994
|
+
The caller must separately promote this version and deprecate the old
|
|
995
|
+
version after validation. This keeps semantic edits from silently
|
|
996
|
+
inheriting the predecessor's retrieval eligibility or credit.
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
prior = self._owned_entry(predecessor)
|
|
1000
|
+
versions = tuple(
|
|
1001
|
+
reference.version
|
|
1002
|
+
for reference in self._entries
|
|
1003
|
+
if reference.insight_id == predecessor.insight_id
|
|
1004
|
+
)
|
|
1005
|
+
if predecessor.version != max(versions):
|
|
1006
|
+
raise ValueError("predecessor must be the latest version of its insight")
|
|
1007
|
+
if origin is InsightOrigin.SEED:
|
|
1008
|
+
raise ValueError("revisions must use manual or reflection origin")
|
|
1009
|
+
if type(draft) is not InsightDraft:
|
|
1010
|
+
raise TypeError("draft must be an exact InsightDraft")
|
|
1011
|
+
InsightDraft.__post_init__(draft)
|
|
1012
|
+
_canonical_paths(draft.affected_paths, name="draft.affected_paths")
|
|
1013
|
+
operator_kinds = _applicable_operator_kinds(applicable_operator_kinds)
|
|
1014
|
+
state = self._initial_state(origin, None, evidence_lineage)
|
|
1015
|
+
if origin is InsightOrigin.REFLECTION:
|
|
1016
|
+
assert evidence_lineage is not None
|
|
1017
|
+
_validate_reflection_action_lineage(draft, evidence_lineage)
|
|
1018
|
+
user_relations = self._canonical_relations(relations)
|
|
1019
|
+
if any(
|
|
1020
|
+
relation.kind is InsightRelationKind.REVISES for relation in user_relations
|
|
1021
|
+
):
|
|
1022
|
+
raise ValueError("add_revision declares its predecessor automatically")
|
|
1023
|
+
declared = (
|
|
1024
|
+
*user_relations,
|
|
1025
|
+
InsightRelation(
|
|
1026
|
+
InsightRelationKind.REVISES,
|
|
1027
|
+
predecessor,
|
|
1028
|
+
revision_note,
|
|
1029
|
+
),
|
|
1030
|
+
)
|
|
1031
|
+
canonical_relations = self._canonical_relations(declared)
|
|
1032
|
+
score = 0.0 if initial_score is None else float(initial_score)
|
|
1033
|
+
if not math.isfinite(score):
|
|
1034
|
+
raise ValueError("initial_score must be finite")
|
|
1035
|
+
reference = InsightRef(predecessor.insight_id, predecessor.version + 1)
|
|
1036
|
+
claim_match = self._claim_index.get(_claim_key(draft.claim))
|
|
1037
|
+
if claim_match is not None and claim_match != predecessor:
|
|
1038
|
+
raise ValueError(
|
|
1039
|
+
"revision claim exactly matches a different insight version"
|
|
1040
|
+
)
|
|
1041
|
+
entry = InsightMemoryEntry(
|
|
1042
|
+
reference=reference,
|
|
1043
|
+
draft=draft,
|
|
1044
|
+
initial_score=score,
|
|
1045
|
+
applicable_operator_kinds=operator_kinds,
|
|
1046
|
+
lifecycle_state=state,
|
|
1047
|
+
origin=origin,
|
|
1048
|
+
evidence_lineage=evidence_lineage,
|
|
1049
|
+
relations=canonical_relations,
|
|
1050
|
+
)
|
|
1051
|
+
self._insert(entry)
|
|
1052
|
+
# Retain the predecessor's exact-claim lookup if the prose changed; for
|
|
1053
|
+
# an unchanged claim, future exact matches resolve to the latest version.
|
|
1054
|
+
if _claim_key(prior.draft.claim) == _claim_key(draft.claim):
|
|
1055
|
+
self._claim_index[_claim_key(draft.claim)] = reference
|
|
1056
|
+
return entry
|
|
1057
|
+
|
|
1058
|
+
def apply_lifecycle_batch(
|
|
1059
|
+
self,
|
|
1060
|
+
requests: tuple[InsightLifecycleChangeRequest, ...],
|
|
1061
|
+
) -> tuple[InsightMemoryEntry, ...]:
|
|
1062
|
+
"""Validate and publish independent lifecycle changes atomically.
|
|
1063
|
+
|
|
1064
|
+
Reflection blocks and generation barriers can resolve several cards at
|
|
1065
|
+
once. Publishing one transition at a time would leave a partially
|
|
1066
|
+
mutated bank if a later card were foreign or in the wrong state. This
|
|
1067
|
+
method stages every updated entry and transition first, then commits in
|
|
1068
|
+
canonical reference order so gather completion order has no meaning.
|
|
1069
|
+
"""
|
|
1070
|
+
|
|
1071
|
+
if type(requests) is not tuple or not requests:
|
|
1072
|
+
raise ValueError("lifecycle batch must be a non-empty exact tuple")
|
|
1073
|
+
if any(type(value) is not InsightLifecycleChangeRequest for value in requests):
|
|
1074
|
+
raise TypeError(
|
|
1075
|
+
"lifecycle batch must contain exact InsightLifecycleChangeRequest values"
|
|
1076
|
+
)
|
|
1077
|
+
for value in requests:
|
|
1078
|
+
InsightLifecycleChangeRequest.__post_init__(value)
|
|
1079
|
+
canonical = tuple(sorted(requests, key=lambda value: value.reference))
|
|
1080
|
+
references = tuple(value.reference for value in canonical)
|
|
1081
|
+
if len(set(references)) != len(references):
|
|
1082
|
+
raise ValueError("lifecycle batch cannot repeat an insight reference")
|
|
1083
|
+
|
|
1084
|
+
updated_entries: list[InsightMemoryEntry] = []
|
|
1085
|
+
transitions: list[InsightLifecycleTransition] = []
|
|
1086
|
+
for offset, request in enumerate(canonical, start=1):
|
|
1087
|
+
entry = self._owned_entry(request.reference)
|
|
1088
|
+
if request.new_state is InsightLifecycleState.PROMOTED:
|
|
1089
|
+
if entry.lifecycle_state is not InsightLifecycleState.QUARANTINED:
|
|
1090
|
+
raise ValueError("only a quarantined insight can be promoted")
|
|
1091
|
+
if not request.supporting_evidence:
|
|
1092
|
+
raise ValueError(
|
|
1093
|
+
"promotion requires at least one supporting evidence ID"
|
|
1094
|
+
)
|
|
1095
|
+
elif entry.lifecycle_state is InsightLifecycleState.DEPRECATED:
|
|
1096
|
+
raise ValueError("a deprecated insight cannot transition again")
|
|
1097
|
+
transition = InsightLifecycleTransition(
|
|
1098
|
+
sequence=len(self._transitions) + offset,
|
|
1099
|
+
reference=request.reference,
|
|
1100
|
+
prior_state=entry.lifecycle_state,
|
|
1101
|
+
new_state=request.new_state,
|
|
1102
|
+
reason=request.reason,
|
|
1103
|
+
supporting_evidence=request.supporting_evidence,
|
|
1104
|
+
)
|
|
1105
|
+
transitions.append(transition)
|
|
1106
|
+
updated_entries.append(replace(entry, lifecycle_state=request.new_state))
|
|
1107
|
+
|
|
1108
|
+
for entry in updated_entries:
|
|
1109
|
+
self._entries[entry.reference] = entry
|
|
1110
|
+
self._transitions.extend(transitions)
|
|
1111
|
+
return tuple(updated_entries)
|
|
1112
|
+
|
|
1113
|
+
def promote(
|
|
1114
|
+
self,
|
|
1115
|
+
reference: InsightRef,
|
|
1116
|
+
*,
|
|
1117
|
+
reason: str,
|
|
1118
|
+
supporting_evidence: Sequence[str],
|
|
1119
|
+
) -> InsightMemoryEntry:
|
|
1120
|
+
"""Promote one tested quarantine entry into retrieval eligibility."""
|
|
1121
|
+
|
|
1122
|
+
evidence = _supporting_evidence(supporting_evidence)
|
|
1123
|
+
return self.apply_lifecycle_batch(
|
|
1124
|
+
(
|
|
1125
|
+
InsightLifecycleChangeRequest(
|
|
1126
|
+
reference=reference,
|
|
1127
|
+
new_state=InsightLifecycleState.PROMOTED,
|
|
1128
|
+
reason=reason,
|
|
1129
|
+
supporting_evidence=evidence,
|
|
1130
|
+
),
|
|
1131
|
+
)
|
|
1132
|
+
)[0]
|
|
1133
|
+
|
|
1134
|
+
def deprecate(
|
|
1135
|
+
self,
|
|
1136
|
+
reference: InsightRef,
|
|
1137
|
+
*,
|
|
1138
|
+
reason: str,
|
|
1139
|
+
supporting_evidence: Sequence[str] = (),
|
|
1140
|
+
) -> InsightMemoryEntry:
|
|
1141
|
+
"""Make a seed, quarantine, or promoted entry permanently ineligible."""
|
|
1142
|
+
|
|
1143
|
+
return self.apply_lifecycle_batch(
|
|
1144
|
+
(
|
|
1145
|
+
InsightLifecycleChangeRequest(
|
|
1146
|
+
reference=reference,
|
|
1147
|
+
new_state=InsightLifecycleState.DEPRECATED,
|
|
1148
|
+
reason=reason,
|
|
1149
|
+
supporting_evidence=_supporting_evidence(supporting_evidence),
|
|
1150
|
+
),
|
|
1151
|
+
)
|
|
1152
|
+
)[0]
|
|
1153
|
+
|
|
1154
|
+
def _initial_state(
|
|
1155
|
+
self,
|
|
1156
|
+
origin: InsightOrigin,
|
|
1157
|
+
lifecycle_state: InsightLifecycleState | None,
|
|
1158
|
+
evidence_lineage: InsightEvidenceLineage | None,
|
|
1159
|
+
) -> InsightLifecycleState:
|
|
1160
|
+
if not isinstance(origin, InsightOrigin):
|
|
1161
|
+
raise TypeError("origin must be an InsightOrigin")
|
|
1162
|
+
expected = (
|
|
1163
|
+
InsightLifecycleState.SEED
|
|
1164
|
+
if origin is InsightOrigin.SEED
|
|
1165
|
+
else InsightLifecycleState.QUARANTINED
|
|
1166
|
+
)
|
|
1167
|
+
state = expected if lifecycle_state is None else lifecycle_state
|
|
1168
|
+
if not isinstance(state, InsightLifecycleState):
|
|
1169
|
+
raise TypeError("lifecycle_state must be an InsightLifecycleState")
|
|
1170
|
+
if state is not expected:
|
|
1171
|
+
raise ValueError(
|
|
1172
|
+
f"{origin.value}-origin insights must start in {expected.value} state"
|
|
1173
|
+
)
|
|
1174
|
+
if origin is InsightOrigin.REFLECTION:
|
|
1175
|
+
if evidence_lineage is None:
|
|
1176
|
+
raise ValueError("reflection-origin insights require evidence lineage")
|
|
1177
|
+
if type(evidence_lineage) is not InsightEvidenceLineage:
|
|
1178
|
+
raise TypeError(
|
|
1179
|
+
"evidence_lineage must be an exact InsightEvidenceLineage"
|
|
1180
|
+
)
|
|
1181
|
+
InsightEvidenceLineage.__post_init__(evidence_lineage)
|
|
1182
|
+
elif evidence_lineage is not None:
|
|
1183
|
+
raise ValueError("evidence_lineage is only valid for reflection origin")
|
|
1184
|
+
return state
|
|
1185
|
+
|
|
1186
|
+
def _canonical_relations(
|
|
1187
|
+
self, relations: Sequence[InsightRelation]
|
|
1188
|
+
) -> tuple[InsightRelation, ...]:
|
|
1189
|
+
if isinstance(relations, (str, bytes)) or not isinstance(relations, Sequence):
|
|
1190
|
+
raise TypeError("relations must be a sequence of InsightRelation values")
|
|
1191
|
+
values = tuple(relations)
|
|
1192
|
+
if any(not isinstance(relation, InsightRelation) for relation in values):
|
|
1193
|
+
raise TypeError("relations must contain InsightRelation values")
|
|
1194
|
+
keys = tuple((relation.target, relation.kind.value) for relation in values)
|
|
1195
|
+
if len(set(keys)) != len(keys):
|
|
1196
|
+
raise ValueError("relations cannot contain duplicate target-kind pairs")
|
|
1197
|
+
foreign = tuple(
|
|
1198
|
+
relation.target
|
|
1199
|
+
for relation in values
|
|
1200
|
+
if relation.target not in self._entries
|
|
1201
|
+
)
|
|
1202
|
+
if foreign:
|
|
1203
|
+
raise ValueError("relations cannot target a foreign insight reference")
|
|
1204
|
+
return tuple(sorted(values, key=lambda item: (item.target, item.kind.value)))
|
|
1205
|
+
|
|
1206
|
+
def _insert(self, entry: InsightMemoryEntry) -> None:
|
|
1207
|
+
if entry.reference in self._entries:
|
|
1208
|
+
raise ValueError("insight reference already exists")
|
|
1209
|
+
self._entries[entry.reference] = entry
|
|
1210
|
+
self._claim_index[_claim_key(entry.draft.claim)] = entry.reference
|
|
1211
|
+
|
|
1212
|
+
def _owned_entry(self, reference: InsightRef) -> InsightMemoryEntry:
|
|
1213
|
+
if not isinstance(reference, InsightRef):
|
|
1214
|
+
raise TypeError("reference must be an InsightRef")
|
|
1215
|
+
try:
|
|
1216
|
+
return self._entries[reference]
|
|
1217
|
+
except KeyError as exc:
|
|
1218
|
+
raise ValueError("reference is foreign to this insight bank") from exc
|
|
1219
|
+
|
|
1220
|
+
def _transition(
|
|
1221
|
+
self,
|
|
1222
|
+
reference: InsightRef,
|
|
1223
|
+
new_state: InsightLifecycleState,
|
|
1224
|
+
*,
|
|
1225
|
+
reason: str,
|
|
1226
|
+
supporting_evidence: tuple[str, ...],
|
|
1227
|
+
) -> InsightMemoryEntry:
|
|
1228
|
+
entry = self._owned_entry(reference)
|
|
1229
|
+
if new_state is InsightLifecycleState.PROMOTED:
|
|
1230
|
+
if entry.lifecycle_state is not InsightLifecycleState.QUARANTINED:
|
|
1231
|
+
raise ValueError("only a quarantined insight can be promoted")
|
|
1232
|
+
elif new_state is InsightLifecycleState.DEPRECATED:
|
|
1233
|
+
if entry.lifecycle_state is InsightLifecycleState.DEPRECATED:
|
|
1234
|
+
raise ValueError("a deprecated insight cannot transition again")
|
|
1235
|
+
else: # pragma: no cover - private method has two public call sites.
|
|
1236
|
+
raise ValueError("unsupported insight lifecycle transition")
|
|
1237
|
+
transition = InsightLifecycleTransition(
|
|
1238
|
+
sequence=len(self._transitions) + 1,
|
|
1239
|
+
reference=reference,
|
|
1240
|
+
prior_state=entry.lifecycle_state,
|
|
1241
|
+
new_state=new_state,
|
|
1242
|
+
reason=reason,
|
|
1243
|
+
supporting_evidence=supporting_evidence,
|
|
1244
|
+
)
|
|
1245
|
+
updated = replace(entry, lifecycle_state=new_state)
|
|
1246
|
+
self._entries[reference] = updated
|
|
1247
|
+
self._transitions.append(transition)
|
|
1248
|
+
return updated
|
|
1249
|
+
|
|
1250
|
+
def _eligible_reference_subset(
|
|
1251
|
+
self,
|
|
1252
|
+
eligible_references: Sequence[InsightRef] | None,
|
|
1253
|
+
) -> tuple[InsightRef, ...]:
|
|
1254
|
+
if eligible_references is None:
|
|
1255
|
+
return tuple(
|
|
1256
|
+
reference
|
|
1257
|
+
for reference, entry in sorted(self._entries.items())
|
|
1258
|
+
if entry.retrievable
|
|
1259
|
+
)
|
|
1260
|
+
if isinstance(eligible_references, (str, bytes)) or not isinstance(
|
|
1261
|
+
eligible_references, Sequence
|
|
1262
|
+
):
|
|
1263
|
+
raise TypeError(
|
|
1264
|
+
"eligible_references must be a sequence of InsightRef values"
|
|
1265
|
+
)
|
|
1266
|
+
references = tuple(eligible_references)
|
|
1267
|
+
if any(not isinstance(reference, InsightRef) for reference in references):
|
|
1268
|
+
raise TypeError("eligible_references must contain InsightRef values")
|
|
1269
|
+
if len(set(references)) != len(references):
|
|
1270
|
+
raise ValueError("eligible_references cannot contain duplicates")
|
|
1271
|
+
foreign = tuple(
|
|
1272
|
+
reference for reference in references if reference not in self._entries
|
|
1273
|
+
)
|
|
1274
|
+
if foreign:
|
|
1275
|
+
raise ValueError("eligible_references contains a foreign insight reference")
|
|
1276
|
+
ineligible = tuple(
|
|
1277
|
+
reference
|
|
1278
|
+
for reference in references
|
|
1279
|
+
if not self._entries[reference].retrievable
|
|
1280
|
+
)
|
|
1281
|
+
if ineligible:
|
|
1282
|
+
raise ValueError(
|
|
1283
|
+
"eligible_references contains a lifecycle-ineligible insight reference"
|
|
1284
|
+
)
|
|
1285
|
+
return tuple(sorted(references))
|
|
1286
|
+
|
|
1287
|
+
def eligible_references(
|
|
1288
|
+
self,
|
|
1289
|
+
*,
|
|
1290
|
+
operator_kind: str,
|
|
1291
|
+
editable_paths: Sequence[str] | None = None,
|
|
1292
|
+
consumer_scope: ReflectionConsumerScope | None = None,
|
|
1293
|
+
factor_capabilities: Sequence[str] | None = None,
|
|
1294
|
+
) -> tuple[InsightRef, ...]:
|
|
1295
|
+
"""Return deterministic structurally and semantically compatible cards.
|
|
1296
|
+
|
|
1297
|
+
Applicability is deliberately structural: natural-language triggers are
|
|
1298
|
+
not interpreted. An empty operator-kind tuple on an entry means that the
|
|
1299
|
+
insight applies to every valid operator token. The v3 semantic filters
|
|
1300
|
+
are opt-in so historical callers retain their exact behavior. When a
|
|
1301
|
+
filter is supplied, semantic-v3 cards fail closed against their declared
|
|
1302
|
+
consumer scopes and required factor capabilities; legacy cards remain
|
|
1303
|
+
governed by their historical operator/path contract.
|
|
1304
|
+
"""
|
|
1305
|
+
|
|
1306
|
+
operator = _operator_kind_token(operator_kind)
|
|
1307
|
+
paths = (
|
|
1308
|
+
None
|
|
1309
|
+
if editable_paths is None
|
|
1310
|
+
else _canonical_paths(editable_paths, name="editable_paths")
|
|
1311
|
+
)
|
|
1312
|
+
if consumer_scope is not None and type(consumer_scope) is not (
|
|
1313
|
+
ReflectionConsumerScope
|
|
1314
|
+
):
|
|
1315
|
+
raise TypeError(
|
|
1316
|
+
"consumer_scope must be an exact ReflectionConsumerScope or None"
|
|
1317
|
+
)
|
|
1318
|
+
capabilities = _canonical_factor_capabilities(factor_capabilities)
|
|
1319
|
+
return tuple(
|
|
1320
|
+
reference
|
|
1321
|
+
for reference, entry in sorted(self._entries.items())
|
|
1322
|
+
if entry.retrievable
|
|
1323
|
+
and self._structurally_applicable(entry, operator, paths)
|
|
1324
|
+
and self._semantically_applicable(
|
|
1325
|
+
entry,
|
|
1326
|
+
consumer_scope=consumer_scope,
|
|
1327
|
+
factor_capabilities=capabilities,
|
|
1328
|
+
)
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
@staticmethod
|
|
1332
|
+
def _semantically_applicable(
|
|
1333
|
+
entry: InsightMemoryEntry,
|
|
1334
|
+
*,
|
|
1335
|
+
consumer_scope: ReflectionConsumerScope | None,
|
|
1336
|
+
factor_capabilities: tuple[str, ...] | None,
|
|
1337
|
+
) -> bool:
|
|
1338
|
+
draft = entry.draft
|
|
1339
|
+
if not draft.has_semantic_contract:
|
|
1340
|
+
return True
|
|
1341
|
+
return (consumer_scope is None or consumer_scope in draft.consumer_scopes) and (
|
|
1342
|
+
factor_capabilities is None
|
|
1343
|
+
or set(draft.factor_capabilities).issubset(factor_capabilities)
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
@staticmethod
|
|
1347
|
+
def _structurally_applicable(
|
|
1348
|
+
entry: InsightMemoryEntry,
|
|
1349
|
+
operator_kind: str,
|
|
1350
|
+
editable_paths: tuple[str, ...] | None,
|
|
1351
|
+
) -> bool:
|
|
1352
|
+
return (
|
|
1353
|
+
not entry.applicable_operator_kinds
|
|
1354
|
+
or operator_kind in entry.applicable_operator_kinds
|
|
1355
|
+
) and (
|
|
1356
|
+
editable_paths is None
|
|
1357
|
+
or any(
|
|
1358
|
+
_paths_overlap(editable, affected)
|
|
1359
|
+
for editable in editable_paths
|
|
1360
|
+
for affected in entry.draft.affected_paths
|
|
1361
|
+
)
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1364
|
+
def validate_quarantine_test_assignment(
|
|
1365
|
+
self,
|
|
1366
|
+
references: Sequence[InsightRef],
|
|
1367
|
+
*,
|
|
1368
|
+
operator_kind: str,
|
|
1369
|
+
editable_paths: Sequence[str] | None = None,
|
|
1370
|
+
) -> tuple[InsightRef, ...]:
|
|
1371
|
+
"""Validate exact quarantined versions for one isolated test invocation.
|
|
1372
|
+
|
|
1373
|
+
This is deliberately separate from :meth:`eligible_references`: a
|
|
1374
|
+
quarantined hypothesis may be named for a controlled test, but remains
|
|
1375
|
+
unavailable to normal retrieval. The returned tuple is an immutable
|
|
1376
|
+
value snapshot in caller-supplied order; no entry or mutable bank state
|
|
1377
|
+
is exposed.
|
|
1378
|
+
"""
|
|
1379
|
+
|
|
1380
|
+
assigned = self._owned_reference_sequence(
|
|
1381
|
+
references,
|
|
1382
|
+
name="quarantine_test_insights",
|
|
1383
|
+
)
|
|
1384
|
+
operator = _operator_kind_token(operator_kind)
|
|
1385
|
+
paths = (
|
|
1386
|
+
None
|
|
1387
|
+
if editable_paths is None
|
|
1388
|
+
else _canonical_paths(editable_paths, name="editable_paths")
|
|
1389
|
+
)
|
|
1390
|
+
wrong_lifecycle = tuple(
|
|
1391
|
+
reference
|
|
1392
|
+
for reference in assigned
|
|
1393
|
+
if self._entries[reference].lifecycle_state
|
|
1394
|
+
is not InsightLifecycleState.QUARANTINED
|
|
1395
|
+
)
|
|
1396
|
+
if wrong_lifecycle:
|
|
1397
|
+
raise ValueError(
|
|
1398
|
+
"quarantine_test_insights must contain only quarantined insight "
|
|
1399
|
+
"references"
|
|
1400
|
+
)
|
|
1401
|
+
inapplicable = tuple(
|
|
1402
|
+
reference
|
|
1403
|
+
for reference in assigned
|
|
1404
|
+
if not self._structurally_applicable(
|
|
1405
|
+
self._entries[reference], operator, paths
|
|
1406
|
+
)
|
|
1407
|
+
)
|
|
1408
|
+
if inapplicable:
|
|
1409
|
+
raise QuarantineAssignmentStructuralError(
|
|
1410
|
+
"quarantine_test_insights contains a structurally inapplicable "
|
|
1411
|
+
"insight reference"
|
|
1412
|
+
)
|
|
1413
|
+
return assigned
|
|
1414
|
+
|
|
1415
|
+
def admit_quarantine_test_assignment(
|
|
1416
|
+
self,
|
|
1417
|
+
references: Sequence[InsightRef],
|
|
1418
|
+
*,
|
|
1419
|
+
operator_kind: str,
|
|
1420
|
+
source_admission_request_sha256: str,
|
|
1421
|
+
editable_paths: Sequence[str] | None = None,
|
|
1422
|
+
) -> QuarantineTestAdmissionReceipt:
|
|
1423
|
+
"""Issue one exact diagnostic authority and retain it for later joins."""
|
|
1424
|
+
|
|
1425
|
+
assigned = self.validate_quarantine_test_assignment(
|
|
1426
|
+
references,
|
|
1427
|
+
operator_kind=operator_kind,
|
|
1428
|
+
editable_paths=editable_paths,
|
|
1429
|
+
)
|
|
1430
|
+
if any(
|
|
1431
|
+
reference in self._quarantine_admission_by_reference
|
|
1432
|
+
for reference in assigned
|
|
1433
|
+
):
|
|
1434
|
+
raise ValueError("a quarantine insight was already admitted for testing")
|
|
1435
|
+
receipt = QuarantineTestAdmissionReceipt(
|
|
1436
|
+
references=tuple(sorted(assigned)),
|
|
1437
|
+
operator_kind=_operator_kind_token(operator_kind),
|
|
1438
|
+
editable_paths=(
|
|
1439
|
+
()
|
|
1440
|
+
if editable_paths is None
|
|
1441
|
+
else _canonical_paths(editable_paths, name="editable_paths")
|
|
1442
|
+
),
|
|
1443
|
+
source_admission_request_sha256=source_admission_request_sha256,
|
|
1444
|
+
memory_trial_count_cutoff=len(self._trials),
|
|
1445
|
+
)
|
|
1446
|
+
if receipt.receipt_sha256 in self._quarantine_admissions:
|
|
1447
|
+
raise ValueError("quarantine admission receipt identity collided")
|
|
1448
|
+
self._quarantine_admissions[receipt.receipt_sha256] = receipt
|
|
1449
|
+
for reference in receipt.references:
|
|
1450
|
+
self._quarantine_admission_by_reference[reference] = receipt.receipt_sha256
|
|
1451
|
+
return receipt
|
|
1452
|
+
|
|
1453
|
+
def validate_quarantine_test_admission(
|
|
1454
|
+
self,
|
|
1455
|
+
receipt: QuarantineTestAdmissionReceipt,
|
|
1456
|
+
*,
|
|
1457
|
+
eligible_references: Sequence[InsightRef],
|
|
1458
|
+
subset_authorization_sha256: str | None = None,
|
|
1459
|
+
) -> tuple[InsightRef, ...]:
|
|
1460
|
+
"""Revalidate bank issuance, lifecycle, and the randomized catalog.
|
|
1461
|
+
|
|
1462
|
+
By default the still-active admitted cohort must equal the assignment
|
|
1463
|
+
cohort exactly. A prospectively selected subset is permitted only
|
|
1464
|
+
when its external policy receipt is explicitly bound into memory
|
|
1465
|
+
credit. This preserves the original anti-cherry-picking default while
|
|
1466
|
+
allowing parent-local positivity filtering to happen before dispatch.
|
|
1467
|
+
"""
|
|
1468
|
+
|
|
1469
|
+
if type(receipt) is not QuarantineTestAdmissionReceipt:
|
|
1470
|
+
raise TypeError("receipt must be an exact QuarantineTestAdmissionReceipt")
|
|
1471
|
+
QuarantineTestAdmissionReceipt.__post_init__(receipt)
|
|
1472
|
+
issued = self._quarantine_admissions.get(receipt.receipt_sha256)
|
|
1473
|
+
if issued != receipt:
|
|
1474
|
+
raise ValueError("quarantine admission was not issued by this memory bank")
|
|
1475
|
+
if subset_authorization_sha256 is not None:
|
|
1476
|
+
require_sha256(
|
|
1477
|
+
subset_authorization_sha256,
|
|
1478
|
+
"subset_authorization_sha256",
|
|
1479
|
+
)
|
|
1480
|
+
eligible = self._owned_reference_sequence(
|
|
1481
|
+
eligible_references,
|
|
1482
|
+
name="eligible_references",
|
|
1483
|
+
)
|
|
1484
|
+
quarantined = tuple(
|
|
1485
|
+
sorted(
|
|
1486
|
+
reference
|
|
1487
|
+
for reference in eligible
|
|
1488
|
+
if self._entries[reference].lifecycle_state
|
|
1489
|
+
is InsightLifecycleState.QUARANTINED
|
|
1490
|
+
)
|
|
1491
|
+
)
|
|
1492
|
+
currently_quarantined_admitted = tuple(
|
|
1493
|
+
reference
|
|
1494
|
+
for reference in receipt.references
|
|
1495
|
+
if self._entries[reference].lifecycle_state
|
|
1496
|
+
is InsightLifecycleState.QUARANTINED
|
|
1497
|
+
)
|
|
1498
|
+
if subset_authorization_sha256 is None:
|
|
1499
|
+
if quarantined != currently_quarantined_admitted:
|
|
1500
|
+
raise ValueError(
|
|
1501
|
+
"eligible quarantine cards differ from the still-active references "
|
|
1502
|
+
"of the issued admission"
|
|
1503
|
+
)
|
|
1504
|
+
elif not quarantined or not set(quarantined).issubset(
|
|
1505
|
+
currently_quarantined_admitted
|
|
1506
|
+
):
|
|
1507
|
+
raise ValueError(
|
|
1508
|
+
"authorized quarantine subset must be a non-empty subset of the "
|
|
1509
|
+
"still-active issued admission"
|
|
1510
|
+
)
|
|
1511
|
+
if any(
|
|
1512
|
+
self._quarantine_admission_by_reference.get(reference)
|
|
1513
|
+
!= receipt.receipt_sha256
|
|
1514
|
+
for reference in quarantined
|
|
1515
|
+
):
|
|
1516
|
+
raise ValueError("quarantine admission reference index changed")
|
|
1517
|
+
return quarantined
|
|
1518
|
+
|
|
1519
|
+
def quarantine_test_admission_receipt(
|
|
1520
|
+
self,
|
|
1521
|
+
receipt_sha256: str,
|
|
1522
|
+
) -> QuarantineTestAdmissionReceipt:
|
|
1523
|
+
"""Return one immutable bank-issued diagnostic authority by exact hash."""
|
|
1524
|
+
|
|
1525
|
+
if (
|
|
1526
|
+
type(receipt_sha256) is not str
|
|
1527
|
+
or _LOWER_SHA256.fullmatch(receipt_sha256) is None
|
|
1528
|
+
):
|
|
1529
|
+
raise ValueError("receipt_sha256 must be a lowercase SHA-256 ID")
|
|
1530
|
+
try:
|
|
1531
|
+
return self._quarantine_admissions[receipt_sha256]
|
|
1532
|
+
except KeyError as exc:
|
|
1533
|
+
raise ValueError(
|
|
1534
|
+
"quarantine admission was not issued by this memory bank"
|
|
1535
|
+
) from exc
|
|
1536
|
+
|
|
1537
|
+
def _owned_reference_sequence(
|
|
1538
|
+
self,
|
|
1539
|
+
references: Sequence[InsightRef],
|
|
1540
|
+
*,
|
|
1541
|
+
name: str,
|
|
1542
|
+
) -> tuple[InsightRef, ...]:
|
|
1543
|
+
if isinstance(references, (str, bytes)) or not isinstance(references, Sequence):
|
|
1544
|
+
raise TypeError(f"{name} must be a sequence of InsightRef values")
|
|
1545
|
+
values = tuple(references)
|
|
1546
|
+
if any(not isinstance(reference, InsightRef) for reference in values):
|
|
1547
|
+
raise TypeError(f"{name} must contain InsightRef values")
|
|
1548
|
+
if len(set(values)) != len(values):
|
|
1549
|
+
raise ValueError(f"{name} cannot contain duplicates")
|
|
1550
|
+
if any(reference not in self._entries for reference in values):
|
|
1551
|
+
raise ValueError(f"{name} contains a foreign insight reference")
|
|
1552
|
+
return values
|
|
1553
|
+
|
|
1554
|
+
def score_snapshot(
|
|
1555
|
+
self,
|
|
1556
|
+
context_hash: str,
|
|
1557
|
+
*,
|
|
1558
|
+
eligible_references: Sequence[InsightRef] | None = None,
|
|
1559
|
+
) -> Mapping[InsightRef, float]:
|
|
1560
|
+
eligible = self._eligible_reference_subset(eligible_references)
|
|
1561
|
+
return {
|
|
1562
|
+
reference: self._scores.get(
|
|
1563
|
+
(context_hash, reference), self._entries[reference].initial_score
|
|
1564
|
+
)
|
|
1565
|
+
for reference in eligible
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
def select(
|
|
1569
|
+
self,
|
|
1570
|
+
*,
|
|
1571
|
+
context_hash: str,
|
|
1572
|
+
subset_size: int,
|
|
1573
|
+
rng,
|
|
1574
|
+
exploration_probability: Fraction | None = None,
|
|
1575
|
+
score_context_hash: str | None = None,
|
|
1576
|
+
eligible_references: Sequence[InsightRef] | None = None,
|
|
1577
|
+
) -> InsightSelectionDecision:
|
|
1578
|
+
eligible = self._eligible_reference_subset(eligible_references)
|
|
1579
|
+
selector = (
|
|
1580
|
+
self._selector
|
|
1581
|
+
if exploration_probability is None
|
|
1582
|
+
else EpsilonGreedySubsetSelector(exploration_probability)
|
|
1583
|
+
)
|
|
1584
|
+
return selector.select(
|
|
1585
|
+
context_hash=context_hash,
|
|
1586
|
+
eligible=eligible,
|
|
1587
|
+
scores=self.score_snapshot(
|
|
1588
|
+
score_context_hash or context_hash,
|
|
1589
|
+
eligible_references=eligible,
|
|
1590
|
+
),
|
|
1591
|
+
subset_size=min(subset_size, len(eligible)),
|
|
1592
|
+
rng=rng,
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
def prompt_records(
|
|
1596
|
+
self, references: Sequence[InsightRef]
|
|
1597
|
+
) -> tuple[dict[str, object], ...]:
|
|
1598
|
+
"""Return detached, JSON-ready records for exact owned references.
|
|
1599
|
+
|
|
1600
|
+
Every mapping and nested container is freshly allocated. Mutating a
|
|
1601
|
+
returned prompt record therefore cannot mutate lifecycle, provenance,
|
|
1602
|
+
scores, or any other state held by this bank.
|
|
1603
|
+
"""
|
|
1604
|
+
|
|
1605
|
+
owned = self._owned_reference_sequence(references, name="references")
|
|
1606
|
+
records = []
|
|
1607
|
+
for reference in owned:
|
|
1608
|
+
entry = self._entries[reference]
|
|
1609
|
+
record: dict[str, object] = {
|
|
1610
|
+
"insight_id": reference.insight_id.value,
|
|
1611
|
+
"version": reference.version,
|
|
1612
|
+
"claim": entry.draft.claim,
|
|
1613
|
+
"trigger": entry.draft.trigger,
|
|
1614
|
+
"mechanism": entry.draft.mechanism,
|
|
1615
|
+
"evidence_summary": entry.draft.evidence_summary,
|
|
1616
|
+
"affected_paths": list(entry.draft.affected_paths),
|
|
1617
|
+
"confidence_at_creation": entry.draft.confidence,
|
|
1618
|
+
"lifecycle_state": entry.lifecycle_state.value,
|
|
1619
|
+
"origin": entry.origin.value,
|
|
1620
|
+
"semantic_relations": [
|
|
1621
|
+
{
|
|
1622
|
+
"kind": relation.kind.value,
|
|
1623
|
+
"target_insight_id": relation.target.insight_id.value,
|
|
1624
|
+
"target_version": relation.target.version,
|
|
1625
|
+
"note": relation.note,
|
|
1626
|
+
}
|
|
1627
|
+
for relation in entry.relations
|
|
1628
|
+
],
|
|
1629
|
+
}
|
|
1630
|
+
intervention = entry.draft.intervention_record()
|
|
1631
|
+
if intervention is not None:
|
|
1632
|
+
record.update(intervention)
|
|
1633
|
+
semantic = entry.draft.semantic_record()
|
|
1634
|
+
if semantic is not None:
|
|
1635
|
+
record.update(semantic)
|
|
1636
|
+
records.append(record)
|
|
1637
|
+
return tuple(records)
|
|
1638
|
+
|
|
1639
|
+
def selected_prompt_records(
|
|
1640
|
+
self, decision: InsightSelectionDecision
|
|
1641
|
+
) -> tuple[dict[str, object], ...]:
|
|
1642
|
+
"""Backward-compatible prompt projection for retrieval decisions."""
|
|
1643
|
+
|
|
1644
|
+
return self.prompt_records(decision.selected)
|
|
1645
|
+
|
|
1646
|
+
def record_trial(
|
|
1647
|
+
self,
|
|
1648
|
+
*,
|
|
1649
|
+
credit_unit_id: OperatorInvocationId,
|
|
1650
|
+
candidate_ids: tuple[CandidateId, ...],
|
|
1651
|
+
reward_definition_hash: str,
|
|
1652
|
+
decision: InsightSelectionDecision,
|
|
1653
|
+
reward: float,
|
|
1654
|
+
) -> InsightTrial:
|
|
1655
|
+
trial = InsightTrial(
|
|
1656
|
+
credit_unit_id=credit_unit_id,
|
|
1657
|
+
candidate_ids=candidate_ids,
|
|
1658
|
+
reward_definition_hash=reward_definition_hash,
|
|
1659
|
+
decision=decision,
|
|
1660
|
+
reward=float(reward),
|
|
1661
|
+
)
|
|
1662
|
+
return self.record_trials_batch((trial,))[0]
|
|
1663
|
+
|
|
1664
|
+
def _prepare_trials_batch(
|
|
1665
|
+
self,
|
|
1666
|
+
trials: tuple[InsightTrial, ...],
|
|
1667
|
+
) -> tuple[tuple[InsightTrial, ...], dict[tuple[str, InsightRef], float]]:
|
|
1668
|
+
if type(trials) is not tuple or not trials:
|
|
1669
|
+
raise ValueError("trials must be a non-empty exact tuple")
|
|
1670
|
+
if any(type(trial) is not InsightTrial for trial in trials):
|
|
1671
|
+
raise TypeError("trials must contain exact InsightTrial values")
|
|
1672
|
+
for trial in trials:
|
|
1673
|
+
InsightTrial.__post_init__(trial)
|
|
1674
|
+
self.entries_for(trial.decision.eligible)
|
|
1675
|
+
|
|
1676
|
+
canonical = tuple(sorted(trials, key=lambda trial: trial.credit_unit_id.value))
|
|
1677
|
+
proposed = (*self._trials, *canonical)
|
|
1678
|
+
contexts = tuple(sorted({trial.decision.context_hash for trial in canonical}))
|
|
1679
|
+
|
|
1680
|
+
# Estimator validation also rejects duplicate credit/candidate units
|
|
1681
|
+
# across the existing bank and the complete incoming batch. Compute
|
|
1682
|
+
# all score mutations before touching either mutable collection.
|
|
1683
|
+
for trial in canonical:
|
|
1684
|
+
for reference in trial.decision.eligible:
|
|
1685
|
+
estimate_marginal_effect(
|
|
1686
|
+
proposed,
|
|
1687
|
+
reference,
|
|
1688
|
+
context_hash=trial.decision.context_hash,
|
|
1689
|
+
)
|
|
1690
|
+
score_updates: dict[tuple[str, InsightRef], float] = {}
|
|
1691
|
+
for context_hash in contexts:
|
|
1692
|
+
score_updates.update(self._context_score_updates(proposed, context_hash))
|
|
1693
|
+
|
|
1694
|
+
return canonical, score_updates
|
|
1695
|
+
|
|
1696
|
+
def preview_trials_batch(
|
|
1697
|
+
self,
|
|
1698
|
+
trials: tuple[InsightTrial, ...],
|
|
1699
|
+
) -> tuple[InsightTrial, ...]:
|
|
1700
|
+
"""Validate and canonicalize a prospective batch without mutation."""
|
|
1701
|
+
|
|
1702
|
+
canonical, _ = self._prepare_trials_batch(trials)
|
|
1703
|
+
return canonical
|
|
1704
|
+
|
|
1705
|
+
def record_trials_batch(
|
|
1706
|
+
self,
|
|
1707
|
+
trials: tuple[InsightTrial, ...],
|
|
1708
|
+
) -> tuple[InsightTrial, ...]:
|
|
1709
|
+
"""Atomically publish a canonical batch of completed credit units.
|
|
1710
|
+
|
|
1711
|
+
Concurrent campaign arms must not update retrieval scores as each arm
|
|
1712
|
+
happens to finish. This boundary validates the complete proposed
|
|
1713
|
+
state, computes every affected score against that same state, and only
|
|
1714
|
+
then publishes trials in stable credit-unit order. The returned tuple
|
|
1715
|
+
is therefore independent of caller/gather completion order.
|
|
1716
|
+
"""
|
|
1717
|
+
|
|
1718
|
+
canonical, score_updates = self._prepare_trials_batch(trials)
|
|
1719
|
+
|
|
1720
|
+
self._trials.extend(canonical)
|
|
1721
|
+
self._scores.update(score_updates)
|
|
1722
|
+
return canonical
|
|
1723
|
+
|
|
1724
|
+
def _refresh_context_scores(self, context_hash: str) -> None:
|
|
1725
|
+
self._scores.update(
|
|
1726
|
+
self._context_score_updates(tuple(self._trials), context_hash)
|
|
1727
|
+
)
|
|
1728
|
+
|
|
1729
|
+
def _context_score_updates(
|
|
1730
|
+
self,
|
|
1731
|
+
trials: Sequence[InsightTrial],
|
|
1732
|
+
context_hash: str,
|
|
1733
|
+
) -> dict[tuple[str, InsightRef], float]:
|
|
1734
|
+
updates: dict[tuple[str, InsightRef], float] = {}
|
|
1735
|
+
for reference, entry in self._entries.items():
|
|
1736
|
+
estimate = estimate_marginal_effect(
|
|
1737
|
+
trials,
|
|
1738
|
+
reference,
|
|
1739
|
+
context_hash=context_hash,
|
|
1740
|
+
)
|
|
1741
|
+
if not estimate.identified:
|
|
1742
|
+
continue
|
|
1743
|
+
support = min(
|
|
1744
|
+
estimate.treated_effective_sample_size,
|
|
1745
|
+
estimate.control_effective_sample_size,
|
|
1746
|
+
)
|
|
1747
|
+
shrinkage = support / (support + self._shrinkage_ess)
|
|
1748
|
+
updates[(context_hash, reference)] = (
|
|
1749
|
+
entry.initial_score + shrinkage * float(estimate.effect)
|
|
1750
|
+
)
|
|
1751
|
+
return updates
|
|
1752
|
+
|
|
1753
|
+
def score_evidence(self, context_hash: str) -> tuple[dict[str, object], ...]:
|
|
1754
|
+
evidence = []
|
|
1755
|
+
for reference, entry in sorted(self._entries.items()):
|
|
1756
|
+
estimate = estimate_marginal_effect(
|
|
1757
|
+
self._trials,
|
|
1758
|
+
reference,
|
|
1759
|
+
context_hash=context_hash,
|
|
1760
|
+
)
|
|
1761
|
+
evidence.append(
|
|
1762
|
+
{
|
|
1763
|
+
"insight_id": reference.insight_id.value,
|
|
1764
|
+
"version": reference.version,
|
|
1765
|
+
"lifecycle_state": entry.lifecycle_state.value,
|
|
1766
|
+
"retrievable": entry.retrievable,
|
|
1767
|
+
"retrieval_score": self._scores.get(
|
|
1768
|
+
(context_hash, reference), entry.initial_score
|
|
1769
|
+
),
|
|
1770
|
+
"effect": estimate.effect,
|
|
1771
|
+
"treated_trials": estimate.treated_trials,
|
|
1772
|
+
"control_trials": estimate.control_trials,
|
|
1773
|
+
"treated_ess": estimate.treated_effective_sample_size,
|
|
1774
|
+
"control_ess": estimate.control_effective_sample_size,
|
|
1775
|
+
"identified": estimate.identified,
|
|
1776
|
+
}
|
|
1777
|
+
)
|
|
1778
|
+
return tuple(evidence)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def context_stratum_hash(*, problem_id: str, operator_kind: str, phase: str) -> str:
|
|
1782
|
+
payload = f"{problem_id}\x00{operator_kind}\x00{phase}".encode(
|
|
1783
|
+
"utf-8", errors="strict"
|
|
1784
|
+
)
|
|
1785
|
+
return hashlib.sha256(b"agent-evolve:insight-context:v1\x00" + payload).hexdigest()
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
__all__ = [
|
|
1789
|
+
"EmpiricalEvidenceSnapshot",
|
|
1790
|
+
"InsightEvidenceLineage",
|
|
1791
|
+
"InsightLifecycleChangeRequest",
|
|
1792
|
+
"InsightLifecycleState",
|
|
1793
|
+
"InsightLifecycleTransition",
|
|
1794
|
+
"InsightMemoryBank",
|
|
1795
|
+
"InsightMemoryEntry",
|
|
1796
|
+
"InsightOrigin",
|
|
1797
|
+
"InsightRelation",
|
|
1798
|
+
"InsightRelationKind",
|
|
1799
|
+
"QuarantineAssignmentStructuralError",
|
|
1800
|
+
"QuarantineTestAdmissionReceipt",
|
|
1801
|
+
"ReflectedInsightBatchItem",
|
|
1802
|
+
"compose_epistemic_prompt_payload",
|
|
1803
|
+
"context_stratum_hash",
|
|
1804
|
+
]
|