phronomy 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.mutant.yml +9 -9
- data/CHANGELOG.md +52 -0
- data/CONTRIBUTING.md +8 -8
- data/README.md +9 -3
- data/benchmark/bench_agent_invoke.rb +25 -50
- data/benchmark/bench_context_assembler.rb +14 -11
- data/benchmark/bench_regression.rb +1 -1
- data/benchmark/bench_tool_schema.rb +1 -1
- data/docs/architecture/agent-chat-and-state-ownership.md +7 -1
- data/docs/architecture/agent-transition-ownership.md +13 -89
- data/docs/architecture/backend-groups.md +152 -0
- data/docs/architecture/before-llm-input.md +3 -3
- data/docs/architecture/context-management.md +1 -1
- data/docs/architecture/execution-abstraction-review.md +88 -0
- data/docs/architecture/execution-boundaries.md +28 -0
- data/docs/architecture/execution-metadata-and-values.md +6 -0
- data/docs/architecture/multi-agent-handoff.md +6 -5
- data/docs/architecture/persistence.md +27 -9
- data/docs/architecture/r8-unit1.md +104 -0
- data/docs/architecture/r8-unit10.md +72 -0
- data/docs/architecture/r8-unit11.md +71 -0
- data/docs/architecture/r8-unit12.md +67 -0
- data/docs/architecture/r8-unit13.md +61 -0
- data/docs/architecture/r8-unit14.md +72 -0
- data/docs/architecture/r8-unit15.md +65 -0
- data/docs/architecture/r8-unit16.md +54 -0
- data/docs/architecture/r8-unit17.md +73 -0
- data/docs/architecture/r8-unit18.md +92 -0
- data/docs/architecture/r8-unit19.md +61 -0
- data/docs/architecture/r8-unit2.md +55 -0
- data/docs/architecture/r8-unit20.md +51 -0
- data/docs/architecture/r8-unit21.md +79 -0
- data/docs/architecture/r8-unit22.md +74 -0
- data/docs/architecture/r8-unit23.md +81 -0
- data/docs/architecture/r8-unit3.md +130 -0
- data/docs/architecture/r8-unit4.md +79 -0
- data/docs/architecture/r8-unit5.md +115 -0
- data/docs/architecture/r8-unit6.md +71 -0
- data/docs/architecture/r8-unit7.md +70 -0
- data/docs/architecture/r8-unit8.md +78 -0
- data/docs/architecture/r8-unit9.md +73 -0
- data/docs/architecture/refactoring-closure.md +10 -1
- data/docs/architecture/remaining-refactoring-plan.md +16 -0
- data/docs/architecture/storage-execution-boundaries.md +54 -0
- data/docs/architecture/tool-schema-recording-gap.md +6 -0
- data/docs/architecture/tracing.md +1 -1
- data/docs/architecture/workflow-terminal-ownership-design.md +2 -2
- data/docs/architecture.md +24 -11
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
- data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +2 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +2 -0
- data/docs/decisions/032-storage-backend-composition.md +5 -0
- data/docs/decisions/034-handoff-runner-coordination-ownership.md +2 -0
- data/docs/decisions/035-tool-executor-capability-ownership.md +1 -1
- data/docs/decisions/038-responsibility-based-source-layout.md +14 -0
- data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +8 -0
- data/docs/decisions/040-configuration-default-composition.md +12 -1
- data/docs/decisions/045-worker-input-restriction-ownership.md +4 -0
- data/docs/decisions/051-execution-outcome-worker-ownership.md +2 -0
- data/docs/decisions/056-workflow-terminal-policy-ownership.md +1 -1
- data/docs/decisions/058-neutral-storage-primitives.md +5 -0
- data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
- data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
- data/docs/decisions/061-persistence-failure-contracts.md +89 -0
- data/docs/decisions/062-content-store-service.md +52 -0
- data/docs/decisions/063-execution-contracts-and-services.md +88 -0
- data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
- data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
- data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
- data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
- data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
- data/docs/decisions/README.md +18 -1
- data/docs/execution-boundaries-preview.md +45 -0
- data/docs/features.md +8 -3
- data/docs/getting-started.md +9 -6
- data/docs/migrations/0.22.md +2 -2
- data/docs/migrations/durable-semantic-coordination-v2.md +2 -0
- data/docs/migrations/handoff-runner-multi-agent.md +2 -0
- data/docs/migrations/persistence-failure-contracts.md +67 -0
- data/docs/migrations/r8-unit10.md +36 -0
- data/docs/migrations/r8-unit11.md +33 -0
- data/docs/migrations/r8-unit12.md +31 -0
- data/docs/migrations/r8-unit13.md +36 -0
- data/docs/migrations/r8-unit14.md +31 -0
- data/docs/migrations/r8-unit15.md +31 -0
- data/docs/migrations/r8-unit16.md +26 -0
- data/docs/migrations/r8-unit17.md +28 -0
- data/docs/migrations/r8-unit18.md +29 -0
- data/docs/migrations/r8-unit19.md +28 -0
- data/docs/migrations/r8-unit20.md +24 -0
- data/docs/migrations/r8-unit21.md +28 -0
- data/docs/migrations/r8-unit22.md +29 -0
- data/docs/migrations/r8-unit23.md +20 -0
- data/docs/migrations/r8-unit4.md +67 -0
- data/docs/migrations/r8-unit5.md +75 -0
- data/docs/migrations/r8-unit6.md +109 -0
- data/docs/migrations/r8-unit7.md +31 -0
- data/docs/migrations/r8-unit8.md +33 -0
- data/docs/migrations/r8-unit9.md +28 -0
- data/docs/migrations/storage-async-client.md +103 -0
- data/docs/migrations/vector-async-clients.md +110 -0
- data/docs/persistence-backends.md +38 -21
- data/docs/runtime-and-concurrency.md +26 -1
- data/lib/phronomy/agent/admission.rb +78 -0
- data/lib/phronomy/agent/{lifecycle_contract/agent_already_exists_error.rb → agent_already_exists_error.rb} +1 -1
- data/lib/phronomy/agent/{lifecycle_contract/agent_busy_error.rb → agent_busy_error.rb} +1 -1
- data/lib/phronomy/agent/{lifecycle_contract/agent_purged_error.rb → agent_purged_error.rb} +1 -1
- data/lib/phronomy/{recovery → agent/api}/execution_rehydration_required_error.rb +1 -1
- data/lib/phronomy/agent/async_event_api.rb +37 -32
- data/lib/phronomy/agent/base.rb +74 -72
- data/lib/phronomy/agent/before_llm_input.rb +64 -0
- data/lib/phronomy/agent/composition/run_once.rb +1 -1
- data/lib/phronomy/agent/{context_assembly/context_candidate_resolver.rb → context_candidate_resolver.rb} +2 -2
- data/lib/phronomy/agent/{context_assembly/context_assembler.rb → context_preparation.rb} +35 -221
- data/lib/phronomy/agent/control_request.rb +21 -0
- data/lib/phronomy/agent/execution/agent_execution.rb +1 -1
- data/lib/phronomy/agent/execution/agent_invocation.rb +27 -53
- data/lib/phronomy/agent/execution/approval_resume_commit.rb +1 -1
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +17 -127
- data/lib/phronomy/agent/execution/exact_execution.rb +44 -23
- data/lib/phronomy/agent/execution/execution_cancellation.rb +8 -4
- data/lib/phronomy/agent/execution/execution_coordinator.rb +214 -395
- data/lib/phronomy/agent/execution/execution_metadata.rb +1 -0
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +70 -126
- data/lib/phronomy/agent/execution/execution_registry.rb +6 -2
- data/lib/phronomy/agent/execution/execution_session_runner.rb +22 -27
- data/lib/phronomy/agent/execution/initial_preparation.rb +18 -116
- data/lib/phronomy/agent/execution/{agent_invocation_session_builder.rb → invocation_actions.rb} +54 -194
- data/lib/phronomy/agent/execution/invocation_transitions.rb +39 -1
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +7 -7
- data/lib/phronomy/agent/execution_change.rb +161 -0
- data/lib/phronomy/agent/execution_environment.rb +36 -0
- data/lib/phronomy/agent/execution_extension_state.rb +21 -0
- data/lib/phronomy/agent/execution_observation.rb +35 -0
- data/lib/phronomy/agent/filterable.rb +182 -0
- data/lib/phronomy/agent/handoff/{handoff_context.rb → transfer_context.rb} +4 -4
- data/lib/phronomy/agent/handoff/{handoff_projection.rb → transfer_projection.rb} +14 -33
- data/lib/phronomy/{llm_contract/transport_error.rb → agent/handoff_error.rb} +1 -1
- data/lib/phronomy/agent/journal/journal_projection.rb +1 -1
- data/lib/phronomy/agent/knowledge_item.rb +13 -0
- data/lib/phronomy/agent/lifecycle/agent_root.rb +1 -1
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +18 -23
- data/lib/phronomy/agent/persistence/agent_repository.rb +29 -21
- data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
- data/lib/phronomy/agent/persistence/codec.rb +1 -21
- data/lib/phronomy/agent/persistence/execution_repository.rb +57 -41
- data/lib/phronomy/agent/persistence/journal_repository.rb +27 -18
- data/lib/phronomy/agent/persistence/records.rb +33 -0
- data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
- data/lib/phronomy/agent/persistence/storage_schema.rb +7 -2
- data/lib/phronomy/agent/persistence/watermark.rb +13 -11
- data/lib/phronomy/agent/recovery/invocation_restorer.rb +13 -21
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +10 -9
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +27 -27
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +19 -21
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +23 -16
- data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
- data/lib/phronomy/agent/recovery/recovery_support.rb +8 -11
- data/lib/phronomy/agent/reserved_execution.rb +16 -0
- data/lib/phronomy/agent/retention.rb +17 -0
- data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
- data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
- data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
- data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
- data/lib/phronomy/agent/{context_assembly/ruby_llm_materializer.rb → runtime_input.rb} +18 -35
- data/lib/phronomy/agent/{context_assembly/saved_context_reader.rb → saved_context_reader.rb} +6 -4
- data/lib/phronomy/agent/settings.rb +47 -0
- data/lib/phronomy/agent/{context_assembly/state_writer.rb → state_writer.rb} +4 -3
- data/lib/phronomy/agent/store.rb +253 -0
- data/lib/phronomy/agent/{lifecycle_contract/stream_callback_error.rb → stream_callback_error.rb} +1 -1
- data/lib/phronomy/agent/tool_execution/tool_binding.rb +4 -64
- data/lib/phronomy/agent/tool_execution/tool_invocation.rb +50 -219
- data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
- data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
- data/lib/phronomy/configuration/runtime_settings.rb +49 -0
- data/lib/phronomy/content_store/storage_schema.rb +1 -0
- data/lib/phronomy/content_store/stored_contents.rb +2 -1
- data/lib/phronomy/context/assembly.rb +202 -0
- data/lib/phronomy/{llm_contract/context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +3 -1
- data/lib/phronomy/context/candidate.rb +51 -0
- data/lib/phronomy/context/constraint.rb +47 -0
- data/lib/phronomy/{agent/context_contract → context}/context_plan.rb +1 -1
- data/lib/phronomy/{agent/context_contract → context}/context_plan_validator.rb +3 -3
- data/lib/phronomy/{agent/context_contract → context}/context_policy.rb +1 -1
- data/lib/phronomy/{agent/context_contract → context}/context_policy_input.rb +1 -1
- data/lib/phronomy/{agent/context_assembly → context}/context_policy_input_builder.rb +1 -1
- data/lib/phronomy/context/default_policy.rb +140 -0
- data/lib/phronomy/context/final_budget_validator.rb +33 -0
- data/lib/phronomy/{agent/context_contract → context}/llm_input_build_context.rb +1 -1
- data/lib/phronomy/{agent/context_contract → context}/llm_input_manifest.rb +19 -19
- data/lib/phronomy/{agent/context_contract → context}/llm_input_patch.rb +1 -1
- data/lib/phronomy/context/manifest_error.rb +10 -0
- data/lib/phronomy/context/prompt_template.rb +98 -0
- data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
- data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
- data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
- data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
- data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
- data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
- data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
- data/lib/phronomy/embeddings/async/async_client.rb +44 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
- data/lib/phronomy/embeddings/base.rb +43 -0
- data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
- data/lib/phronomy/embeddings/transport_error.rb +11 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +36 -21
- data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
- data/lib/phronomy/engine/diagnostics.rb +1 -1
- data/lib/phronomy/engine/event_loop.rb +7 -7
- data/lib/phronomy/engine/execution_binding.rb +23 -0
- data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
- data/lib/phronomy/engine/fsm_protocol.rb +1 -6
- data/lib/phronomy/engine/fsm_session.rb +3 -3
- data/lib/phronomy/engine/metrics.rb +16 -14
- data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
- data/lib/phronomy/engine/runtime.rb +25 -12
- data/lib/phronomy/engine/terminal_decision.rb +7 -0
- data/lib/phronomy/execution/async_operation.rb +41 -0
- data/lib/phronomy/{engine → execution}/blocking.rb +2 -2
- data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +1 -1
- data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
- data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
- data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
- data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
- data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
- data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
- data/lib/phronomy/{engine → execution}/execution.rb +61 -27
- data/lib/phronomy/execution/execution_receiver.rb +78 -0
- data/lib/phronomy/execution/execution_scope_state.rb +35 -0
- data/lib/phronomy/{engine → execution}/invocation_context.rb +0 -15
- data/lib/phronomy/execution/invocation_controls.rb +73 -0
- data/lib/phronomy/execution/runnable.rb +24 -0
- data/lib/phronomy/{engine → execution}/task_result.rb +3 -3
- data/lib/phronomy/execution/wait_policy.rb +24 -0
- data/lib/phronomy/filter/{contract/filter_block_error.rb → filter_block_error.rb} +1 -1
- data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
- data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +2 -2
- data/lib/phronomy/generation/generator_verifier.rb +7 -7
- data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
- data/lib/phronomy/{llm_contract → llm_adapter}/authentication_error.rb +3 -1
- data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
- data/lib/phronomy/llm_adapter/base.rb +49 -68
- data/lib/phronomy/{llm_contract → llm_adapter}/context_length_error.rb +3 -1
- data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
- data/lib/phronomy/llm_adapter/message.rb +62 -0
- data/lib/phronomy/{llm_contract → llm_adapter}/rate_limit_error.rb +3 -1
- data/lib/phronomy/llm_adapter/request.rb +79 -0
- data/lib/phronomy/llm_adapter/response.rb +36 -0
- data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
- data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
- data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
- data/lib/phronomy/llm_adapter.rb +3 -16
- data/lib/phronomy/multi_agent/admission_registry.rb +1 -8
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
- data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
- data/lib/phronomy/{agent/handoff → multi_agent}/handoff.rb +2 -2
- data/lib/phronomy/{agent/handoff → multi_agent}/handoff_capability_factory.rb +21 -2
- data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
- data/lib/phronomy/{agent/handoff → multi_agent}/handoff_policy.rb +1 -1
- data/lib/phronomy/multi_agent/handoff_runner.rb +77 -100
- data/lib/phronomy/{agent/handoff → multi_agent}/handoff_state.rb +1 -1
- data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
- data/lib/phronomy/multi_agent/persistence/codec.rb +20 -0
- data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
- data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
- data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +58 -42
- data/lib/phronomy/multi_agent/persistence/team_repository.rb +29 -21
- data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
- data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
- data/lib/phronomy/multi_agent/shared_state.rb +2 -2
- data/lib/phronomy/multi_agent/store.rb +156 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +83 -74
- data/lib/phronomy/multi_agent/team_ownership_registry.rb +2 -12
- data/lib/phronomy/output_parser/parse_error.rb +7 -0
- data/lib/phronomy/persistence/content_repository.rb +26 -0
- data/lib/phronomy/persistence/errors.rb +16 -0
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +2 -2
- data/lib/phronomy/persistence/persistence.rb +23 -0
- data/lib/phronomy/persistence/save_outcome.rb +24 -0
- data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
- data/lib/phronomy/persistence/storage_boundary.rb +32 -0
- data/lib/phronomy/persistence/transaction.rb +86 -0
- data/lib/phronomy/persistence_composition/storage_schema.rb +9 -4
- data/lib/phronomy/persistence_composition/stores.rb +34 -0
- data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
- data/lib/phronomy/runtime_composition/agent_defaults.rb +5 -1
- data/lib/phronomy/runtime_composition/configuration.rb +146 -0
- data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
- data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
- data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
- data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
- data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
- data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
- data/lib/phronomy/storage/async/async_client.rb +60 -0
- data/lib/phronomy/storage/backend.rb +8 -2
- data/lib/phronomy/testing/eval/metrics.rb +1 -1
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +55 -52
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +7 -7
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +11 -11
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +57 -51
- data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +23 -22
- data/lib/phronomy/tool/authorization.rb +77 -0
- data/lib/phronomy/tool/base.rb +549 -9
- data/lib/phronomy/tool/call_request.rb +40 -0
- data/lib/phronomy/{agent/tool_execution/tool_definition_set.rb → tool/definition_set.rb} +7 -8
- data/lib/phronomy/tool/operation.rb +88 -0
- data/lib/phronomy/tool/schema.rb +214 -0
- data/lib/phronomy/tool/settings.rb +31 -0
- data/lib/phronomy/tool/tool_error.rb +7 -0
- data/lib/phronomy/tool/tool_executor.rb +58 -0
- data/lib/phronomy/tools/agent.rb +22 -90
- data/lib/phronomy/tools/mcp.rb +21 -14
- data/lib/phronomy/tools/vector_search.rb +4 -4
- data/lib/phronomy/tracing/automatic.rb +11 -6
- data/lib/phronomy/tracing/base.rb +1 -1
- data/lib/phronomy/tracing/observation.rb +37 -0
- data/lib/phronomy/tracing/settings.rb +36 -0
- data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
- data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
- data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
- data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
- data/lib/phronomy/vector_store/base.rb +118 -49
- data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
- data/lib/phronomy/vector_store.rb +2 -17
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/completion.rb +11 -0
- data/lib/phronomy/workflow/execution/workflow.rb +7 -7
- data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
- data/lib/phronomy/workflow/execution/workflow_context.rb +2 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +3 -5
- data/lib/phronomy/workflow/execution/workflow_runner.rb +58 -79
- data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
- data/lib/phronomy/workflow/persistence/state_repository.rb +28 -22
- data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
- data/lib/phronomy/workflow/{execution → runtime_binding}/workflow_terminal_policy.rb +4 -4
- data/lib/phronomy.rb +57 -16
- data/scripts/api_snapshot.rb +35 -15
- data/sig/_private/phronomy/agent/settings.rbs +19 -0
- data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
- data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
- data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
- data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
- data/sig/_private/phronomy/tool/settings.rbs +13 -0
- data/sig/_private/phronomy/tracing/settings.rbs +15 -0
- data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
- data/sig/phronomy/agent/admission.rbs +19 -0
- data/sig/phronomy/agent/execution_contract.rbs +132 -0
- data/sig/phronomy/agent/execution_environment.rbs +20 -0
- data/sig/phronomy/agent/store.rbs +73 -0
- data/sig/phronomy/agent.rbs +14 -138
- data/sig/phronomy/common.rbs +7 -0
- data/sig/phronomy/content_store/contracts.rbs +13 -0
- data/sig/phronomy/context.rbs +152 -0
- data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
- data/sig/phronomy/embeddings/base.rbs +17 -0
- data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
- data/sig/phronomy/engine/control_bindings.rbs +11 -0
- data/sig/phronomy/engine/control_protocols.rbs +23 -0
- data/sig/phronomy/engine/execution_binding.rbs +6 -0
- data/sig/phronomy/engine/execution_receiver.rbs +15 -0
- data/sig/phronomy/engine/observability.rbs +13 -0
- data/sig/phronomy/execution/async_operation.rbs +7 -0
- data/sig/phronomy/execution/blocking.rbs +5 -0
- data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
- data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
- data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
- data/sig/phronomy/execution/control_protocols.rbs +18 -0
- data/sig/phronomy/execution/errors.rbs +7 -0
- data/sig/phronomy/execution/execution.rbs +41 -0
- data/sig/phronomy/execution/execution_receiver.rbs +40 -0
- data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
- data/sig/phronomy/execution/invocation_context.rbs +18 -0
- data/sig/phronomy/execution/invocation_controls.rbs +13 -0
- data/sig/phronomy/execution/task_result.rbs +29 -0
- data/sig/phronomy/extensions.rbs +0 -21
- data/sig/phronomy/handoff.rbs +10 -4
- data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
- data/sig/phronomy/llm_adapter/base.rbs +14 -0
- data/sig/phronomy/llm_adapter/usage.rbs +14 -0
- data/sig/phronomy/llm_adapter/values.rbs +43 -0
- data/sig/phronomy/llm_context_window.rbs +10 -0
- data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
- data/sig/phronomy/multi_agent/store.rbs +54 -0
- data/sig/phronomy/multi_agent.rbs +6 -3
- data/sig/phronomy/persistence/errors.rbs +18 -0
- data/sig/phronomy/persistence/migration.rbs +13 -0
- data/sig/phronomy/persistence/transaction.rbs +19 -0
- data/sig/phronomy/persistence.rbs +1 -99
- data/sig/phronomy/persistence_composition.rbs +19 -0
- data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
- data/sig/phronomy/storage/async/async_client.rbs +8 -0
- data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
- data/sig/phronomy/{storage.rbs → storage/contracts.rbs} +2 -16
- data/sig/phronomy/tool.rbs +48 -4
- data/sig/phronomy/tracing/observation.rbs +7 -0
- data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
- data/sig/phronomy/vector_store/base.rbs +23 -0
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
- data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
- data/sig/phronomy/workflow/store.rbs +8 -0
- data/sig/phronomy/workflow.rbs +1 -1
- data/sig/phronomy.rbs +0 -49
- metadata +318 -117
- data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
- data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
- data/lib/phronomy/agent/concerns/filterable.rb +0 -184
- data/lib/phronomy/agent/context/capability/base.rb +0 -548
- data/lib/phronomy/agent/context/capability/tool_executor.rb +0 -62
- data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
- data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +0 -36
- data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +0 -22
- data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
- data/lib/phronomy/agent/context_policies/default.rb +0 -142
- data/lib/phronomy/agent/execution/phase_machine_builder.rb +0 -101
- data/lib/phronomy/agent/execution/provider_call_outcome.rb +0 -106
- data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +0 -15
- data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +0 -131
- data/lib/phronomy/agent/handoff/handoff_request.rb +0 -45
- data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +0 -7
- data/lib/phronomy/agent/persistence/handoff_state_repository.rb +0 -58
- data/lib/phronomy/agent/persistence/queries.rb +0 -61
- data/lib/phronomy/agent/selection/candidate.rb +0 -53
- data/lib/phronomy/agent/selection/constraint.rb +0 -49
- data/lib/phronomy/agent/tool_execution/tool_call_intercepted.rb +0 -29
- data/lib/phronomy/agent/tool_execution/tool_invocation_session_builder.rb +0 -276
- data/lib/phronomy/configuration/configuration.rb +0 -68
- data/lib/phronomy/configuration/global_configuration.rb +0 -26
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
- data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
- data/lib/phronomy/engine/execution_receiver.rb +0 -65
- data/lib/phronomy/engine/runnable.rb +0 -54
- data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
- data/lib/phronomy/llm_contract/token_usage.rb +0 -85
- data/lib/phronomy/multi_agent/persistence/queries.rb +0 -30
- data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +0 -15
- data/lib/phronomy/output_parser/contract/parse_error.rb +0 -7
- data/lib/phronomy/persistence/api/persistence.rb +0 -140
- data/lib/phronomy/persistence_composition/repositories.rb +0 -77
- data/lib/phronomy/recovery/recovery.rb +0 -186
- data/lib/phronomy/tool/contract/tool_error.rb +0 -7
- data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
- data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
- data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
- data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +0 -9
- data/sig/phronomy/execution_receiver.rbs +0 -34
- data/sig/phronomy/llm_adapter.rbs +0 -11
- data/sig/phronomy/runtime.rbs +0 -76
- /data/lib/phronomy/agent/{context_assembly/context_importer.rb → context_importer.rb} +0 -0
- /data/lib/phronomy/{engine → execution}/backpressure_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/cancellation_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/concurrency/worker_input_restricted.rb +0 -0
- /data/lib/phronomy/{engine → execution}/event_loop_reentrancy_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/execution_cancellation_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/execution_timeout_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/invalid_async_entry_action_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/invalid_async_transition_action_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/invalid_async_workflow_action_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/pool_shutdown_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/recursion_limit_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/runtime_shutdown_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/runtime_shutdown_reentrancy_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/scheduler_reentrancy_error.rb +0 -0
- /data/lib/phronomy/{engine → execution}/timeout_error.rb +0 -0
|
@@ -19,9 +19,9 @@ module Phronomy
|
|
|
19
19
|
|
|
20
20
|
def start(name, input: nil, **metadata)
|
|
21
21
|
trace_pii = false
|
|
22
|
-
|
|
23
|
-
trace_pii = !!
|
|
24
|
-
tracer =
|
|
22
|
+
settings = Settings.current
|
|
23
|
+
trace_pii = !!settings.trace_pii
|
|
24
|
+
tracer = settings.tracer
|
|
25
25
|
span = tracer.start_span(
|
|
26
26
|
name,
|
|
27
27
|
input: traced_payload(input, trace_pii: trace_pii),
|
|
@@ -47,7 +47,7 @@ module Phronomy
|
|
|
47
47
|
error: traced_error(error, trace_pii: handle.trace_pii)
|
|
48
48
|
}.freeze
|
|
49
49
|
|
|
50
|
-
if Phronomy::
|
|
50
|
+
if Phronomy::WaitPolicy.blocking_forbidden?
|
|
51
51
|
submit_finish(handle, attributes)
|
|
52
52
|
else
|
|
53
53
|
finish_now(handle, attributes)
|
|
@@ -130,8 +130,13 @@ module Phronomy
|
|
|
130
130
|
private_class_method :task_usage
|
|
131
131
|
|
|
132
132
|
def submit_finish(handle, attributes)
|
|
133
|
-
Phronomy::
|
|
133
|
+
Phronomy::Blocking.call_async do
|
|
134
134
|
finish_now(handle, attributes)
|
|
135
|
+
end.on_complete do |_value, error|
|
|
136
|
+
if error
|
|
137
|
+
report_failure(:finish, handle.name, error,
|
|
138
|
+
include_message: handle.trace_pii)
|
|
139
|
+
end
|
|
135
140
|
end
|
|
136
141
|
nil
|
|
137
142
|
rescue => error
|
|
@@ -164,7 +169,7 @@ module Phronomy
|
|
|
164
169
|
message =
|
|
165
170
|
"[Phronomy] automatic tracing #{stage} failed for " \
|
|
166
171
|
"#{name}: #{error.class}#{detail}"
|
|
167
|
-
logger = Phronomy.
|
|
172
|
+
logger = Phronomy::RuntimeSettings.current.logger
|
|
168
173
|
logger ? logger.warn(message) : Kernel.warn(message)
|
|
169
174
|
nil
|
|
170
175
|
rescue
|
|
@@ -57,7 +57,7 @@ module Phronomy
|
|
|
57
57
|
#
|
|
58
58
|
# @param span [Object] the span returned by #start_span
|
|
59
59
|
# @param output [Object, nil] successful output value
|
|
60
|
-
# @param usage [Phronomy::TokenUsage, nil] token usage for this span
|
|
60
|
+
# @param usage [Phronomy::LLMAdapter::TokenUsage, nil] token usage for this span
|
|
61
61
|
# @param error [Exception, nil] exception if the block raised
|
|
62
62
|
# @api public
|
|
63
63
|
def finish_span(span, output: nil, usage: nil, error: nil)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Tracing
|
|
5
|
+
module Observation
|
|
6
|
+
# Convenience wrapper that delegates to the global tracer.
|
|
7
|
+
# Yields a span; the block must return [result, usage] where usage is a
|
|
8
|
+
# Phronomy::LLMAdapter::TokenUsage or nil. Returns only the result value.
|
|
9
|
+
#
|
|
10
|
+
# When +trace_pii+ is disabled, both the input and the output (LLM response,
|
|
11
|
+
# tool result) are replaced with the literal string "[REDACTED]" before being
|
|
12
|
+
# forwarded to the tracing backend. The actual result is still returned to
|
|
13
|
+
# the caller — only the copy sent to the tracer is redacted.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# trace("my_chain", input: input) { [invoke(input), nil] }
|
|
17
|
+
# @api public
|
|
18
|
+
def self.trace(name, input: nil, **meta, &block)
|
|
19
|
+
traced_input = Settings.current.trace_pii ? input : "[REDACTED]"
|
|
20
|
+
|
|
21
|
+
if Settings.current.trace_pii
|
|
22
|
+
# PII recording is allowed: pass through unchanged.
|
|
23
|
+
Settings.current.tracer.trace(name, input: traced_input, **meta, &block)
|
|
24
|
+
else
|
|
25
|
+
# Redact both input (above) and output before forwarding to the tracer.
|
|
26
|
+
# Capture the real result so callers receive the unredacted value.
|
|
27
|
+
real_result = nil
|
|
28
|
+
Settings.current.tracer.trace(name, input: traced_input, **meta) do |span|
|
|
29
|
+
real_result, usage = block.call(span)
|
|
30
|
+
["[REDACTED]", usage]
|
|
31
|
+
end
|
|
32
|
+
real_result
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../common/configuration_error"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Tracing
|
|
7
|
+
# Tracing owns recording options; application composition selects the tracer.
|
|
8
|
+
# No concrete backend, execution resource or application facade belongs here.
|
|
9
|
+
# @api private
|
|
10
|
+
class Settings
|
|
11
|
+
def self.install_provider(&provider)
|
|
12
|
+
raise ArgumentError, "Tracing settings provider requires a block" unless provider
|
|
13
|
+
|
|
14
|
+
@provider = provider
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Follow reset and scoped restoration only when a consumer already reads.
|
|
19
|
+
def self.current
|
|
20
|
+
settings = @provider&.call
|
|
21
|
+
unless settings.is_a?(Settings)
|
|
22
|
+
raise ConfigurationError, "Tracing settings provider must return Tracing::Settings"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
settings
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_accessor :tracer, :trace_pii
|
|
29
|
+
|
|
30
|
+
def initialize(tracer:)
|
|
31
|
+
@tracer = tracer
|
|
32
|
+
@trace_pii = false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module VectorStore
|
|
5
|
-
# Framework-owned
|
|
5
|
+
# Framework-owned asynchronous execution of a synchronous vector backend.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# Backend authors implement only Base's synchronous operations. Applications
|
|
8
|
+
# use this client for asynchronous calls. Admission never waits for queue
|
|
9
|
+
# capacity; accepted calls return the pool's original TaskResult. Timeout
|
|
10
|
+
# includes queue wait and settles the result without interrupting running I/O.
|
|
11
|
+
# Construction does not start Runtime; the current default pool is resolved
|
|
12
|
+
# for each operation. An injected pool remains owned by its caller.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
# @api public
|
|
15
|
+
class AsyncClient
|
|
16
|
+
# @param backend [VectorStore::Base] synchronous vector store
|
|
17
|
+
# @param pool [Concurrency::OffloadPool, nil] optional execution pool
|
|
18
|
+
# @api public
|
|
19
|
+
def initialize(backend:, pool: nil)
|
|
20
|
+
@backend = backend
|
|
21
|
+
@pool = pool
|
|
22
|
+
end
|
|
23
|
+
|
|
20
24
|
# Async variant of {VectorStore::Base#add}.
|
|
21
25
|
#
|
|
22
26
|
# @param id [String]
|
|
@@ -27,12 +31,11 @@ module Phronomy
|
|
|
27
31
|
# @return [Phronomy::TaskResult]
|
|
28
32
|
# @api public
|
|
29
33
|
def add_async(id:, embedding:, metadata: {}, cancellation_token: nil, timeout: nil)
|
|
30
|
-
Phronomy::
|
|
34
|
+
Phronomy::Execution.submit(pool: default_pool,
|
|
31
35
|
timeout: timeout,
|
|
32
36
|
cancellation_token: cancellation_token,
|
|
33
|
-
on_full: :raise
|
|
34
|
-
|
|
35
|
-
add(id: id, embedding: embedding, metadata: metadata, cancellation_token: cancellation_token)
|
|
37
|
+
on_full: :raise) do
|
|
38
|
+
@backend.add(id: id, embedding: embedding, metadata: metadata, cancellation_token: cancellation_token)
|
|
36
39
|
end
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -45,12 +48,11 @@ module Phronomy
|
|
|
45
48
|
# @return [Phronomy::TaskResult]
|
|
46
49
|
# @api public
|
|
47
50
|
def search_async(query_embedding:, k: 5, cancellation_token: nil, timeout: nil)
|
|
48
|
-
Phronomy::
|
|
51
|
+
Phronomy::Execution.submit(pool: default_pool,
|
|
49
52
|
timeout: timeout,
|
|
50
53
|
cancellation_token: cancellation_token,
|
|
51
|
-
on_full: :raise
|
|
52
|
-
|
|
53
|
-
search(query_embedding: query_embedding, k: k, cancellation_token: cancellation_token)
|
|
54
|
+
on_full: :raise) do
|
|
55
|
+
@backend.search(query_embedding: query_embedding, k: k, cancellation_token: cancellation_token)
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
|
|
@@ -62,12 +64,11 @@ module Phronomy
|
|
|
62
64
|
# @return [Phronomy::TaskResult]
|
|
63
65
|
# @api public
|
|
64
66
|
def remove_async(id:, cancellation_token: nil, timeout: nil)
|
|
65
|
-
Phronomy::
|
|
67
|
+
Phronomy::Execution.submit(pool: default_pool,
|
|
66
68
|
timeout: timeout,
|
|
67
69
|
cancellation_token: cancellation_token,
|
|
68
|
-
on_full: :raise
|
|
69
|
-
|
|
70
|
-
remove(id: id)
|
|
70
|
+
on_full: :raise) do
|
|
71
|
+
@backend.remove(id: id)
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -78,14 +79,19 @@ module Phronomy
|
|
|
78
79
|
# @return [Phronomy::TaskResult]
|
|
79
80
|
# @api public
|
|
80
81
|
def clear_async(cancellation_token: nil, timeout: nil)
|
|
81
|
-
Phronomy::
|
|
82
|
+
Phronomy::Execution.submit(pool: default_pool,
|
|
82
83
|
timeout: timeout,
|
|
83
84
|
cancellation_token: cancellation_token,
|
|
84
|
-
on_full: :raise
|
|
85
|
-
|
|
86
|
-
clear
|
|
85
|
+
on_full: :raise) do
|
|
86
|
+
@backend.clear
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def default_pool
|
|
93
|
+
@pool
|
|
94
|
+
end
|
|
89
95
|
end
|
|
90
96
|
end
|
|
91
97
|
end
|
|
@@ -22,16 +22,16 @@ module Phronomy
|
|
|
22
22
|
@expected_dimension = dimension
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
protected
|
|
26
|
+
|
|
25
27
|
# @param id [String]
|
|
26
28
|
# @param embedding [Array<Float>]
|
|
27
29
|
# @param metadata [Hash]
|
|
28
30
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
29
31
|
# @api public
|
|
30
|
-
def
|
|
31
|
-
cancellation_token&.raise_if_cancelled!
|
|
32
|
+
def perform_add(id:, embedding:, metadata: {}, cancellation_token: nil)
|
|
32
33
|
# Establish expected dimension on first add, then validate.
|
|
33
34
|
@expected_dimension ||= embedding.size
|
|
34
|
-
validate_embedding_dimension!(embedding, @expected_dimension)
|
|
35
35
|
@documents[id] = {embedding: embedding, metadata: metadata}
|
|
36
36
|
self
|
|
37
37
|
end
|
|
@@ -46,11 +46,8 @@ module Phronomy
|
|
|
46
46
|
# (key always present); -r.fetch(:score) vs -r[:score] (key always present); snapshot = @documents
|
|
47
47
|
# vs .dup is equivalent in single-threaded tests (GVL makes Hash#dup atomic, no behaviour
|
|
48
48
|
# difference under test isolation)
|
|
49
|
-
def
|
|
50
|
-
cancellation_token&.raise_if_cancelled!
|
|
51
|
-
k = validate_k!(k)
|
|
49
|
+
def perform_search(query_embedding:, k: 5, cancellation_token: nil)
|
|
52
50
|
# search never establishes dimension; validate only when dimension is known.
|
|
53
|
-
validate_embedding_dimension!(query_embedding, @expected_dimension)
|
|
54
51
|
# Take an atomic snapshot before iterating. Hash#dup is a C-level
|
|
55
52
|
# call that completes without releasing the GVL, so it is atomic with
|
|
56
53
|
# respect to any other Ruby thread. Iterating the copy instead of
|
|
@@ -64,22 +61,27 @@ module Phronomy
|
|
|
64
61
|
results.sort_by { |r| -r[:score] }.first(k)
|
|
65
62
|
end
|
|
66
63
|
|
|
67
|
-
def
|
|
64
|
+
def perform_remove(id:)
|
|
68
65
|
@documents.delete(id)
|
|
69
66
|
self
|
|
70
67
|
end
|
|
71
68
|
|
|
72
|
-
def
|
|
69
|
+
def perform_clear
|
|
73
70
|
@documents.clear
|
|
74
71
|
self
|
|
75
72
|
end
|
|
76
73
|
|
|
77
74
|
# @return [Integer] number of documents stored
|
|
78
75
|
# @api public
|
|
79
|
-
def
|
|
76
|
+
def perform_size
|
|
80
77
|
@documents.size
|
|
81
78
|
end
|
|
82
79
|
|
|
80
|
+
# @api public
|
|
81
|
+
def embedding_dimension
|
|
82
|
+
@expected_dimension
|
|
83
|
+
end
|
|
84
|
+
|
|
83
85
|
private
|
|
84
86
|
|
|
85
87
|
# mutant:disable - empty-vector early-return condition variants (if false, if nil, if a.empty?,
|
|
@@ -35,14 +35,14 @@ module Phronomy
|
|
|
35
35
|
@dimension = dimension
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
protected
|
|
39
|
+
|
|
38
40
|
# @param id [String]
|
|
39
41
|
# @param embedding [Array<Float>]
|
|
40
42
|
# @param metadata [Hash]
|
|
41
43
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
42
44
|
# @api public
|
|
43
|
-
def
|
|
44
|
-
cancellation_token&.raise_if_cancelled!
|
|
45
|
-
validate_embedding_dimension!(embedding, @dimension)
|
|
45
|
+
def perform_add(id:, embedding:, metadata: {}, cancellation_token: nil)
|
|
46
46
|
@model_class.upsert(
|
|
47
47
|
{id: id, embedding: safe_vector(embedding), metadata: metadata.to_json},
|
|
48
48
|
unique_by: :id
|
|
@@ -55,10 +55,7 @@ module Phronomy
|
|
|
55
55
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
56
56
|
# @return [Array<Hash>] sorted by descending similarity score
|
|
57
57
|
# @api public
|
|
58
|
-
def
|
|
59
|
-
cancellation_token&.raise_if_cancelled!
|
|
60
|
-
k_safe = validate_k!(k)
|
|
61
|
-
validate_embedding_dimension!(query_embedding, @dimension)
|
|
58
|
+
def perform_search(query_embedding:, k: 5, cancellation_token: nil)
|
|
62
59
|
vec = safe_vector_literal(query_embedding)
|
|
63
60
|
conn = @model_class.connection
|
|
64
61
|
quoted_vec = "#{conn.quote(vec)}::vector"
|
|
@@ -66,48 +63,79 @@ module Phronomy
|
|
|
66
63
|
@model_class
|
|
67
64
|
.select("id, metadata, 1 - (embedding <=> #{quoted_vec}) AS score")
|
|
68
65
|
.order("embedding <=> #{quoted_vec}")
|
|
69
|
-
.limit(
|
|
66
|
+
.limit(k)
|
|
70
67
|
.map do |r|
|
|
71
68
|
{
|
|
72
|
-
id: r.id
|
|
73
|
-
score: r.score
|
|
69
|
+
id: parse_id(r.id),
|
|
70
|
+
score: parse_score(r.score),
|
|
74
71
|
metadata: parse_metadata(r.metadata)
|
|
75
72
|
}
|
|
76
73
|
end
|
|
77
74
|
end
|
|
78
75
|
|
|
79
|
-
def
|
|
76
|
+
def perform_remove(id:)
|
|
80
77
|
@model_class.where(id: id).delete_all
|
|
81
78
|
self
|
|
82
79
|
end
|
|
83
80
|
|
|
84
|
-
def
|
|
81
|
+
def perform_clear
|
|
85
82
|
@model_class.delete_all
|
|
86
83
|
self
|
|
87
84
|
end
|
|
88
85
|
|
|
89
86
|
# Returns the number of documents in the backing table.
|
|
90
|
-
def
|
|
87
|
+
def perform_size
|
|
91
88
|
@model_class.count
|
|
92
89
|
end
|
|
93
90
|
|
|
91
|
+
# @api public
|
|
92
|
+
def embedding_dimension
|
|
93
|
+
@dimension
|
|
94
|
+
end
|
|
95
|
+
|
|
94
96
|
private
|
|
95
97
|
|
|
98
|
+
def parse_id(raw)
|
|
99
|
+
unless raw.is_a?(String) || raw.is_a?(Integer)
|
|
100
|
+
raise InvalidResultError, "Pgvector returned an invalid document id"
|
|
101
|
+
end
|
|
102
|
+
raw.to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_score(raw)
|
|
106
|
+
valid = (raw.is_a?(String) && raw.match?(/\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/)) ||
|
|
107
|
+
(raw.is_a?(Numeric) && raw.real?)
|
|
108
|
+
value = Float(raw, exception: false) if valid
|
|
109
|
+
unless value&.finite?
|
|
110
|
+
raise InvalidResultError, "Pgvector requires a finite numeric score"
|
|
111
|
+
end
|
|
112
|
+
value
|
|
113
|
+
end
|
|
114
|
+
|
|
96
115
|
# Parses a metadata value returned by the pg driver.
|
|
97
116
|
# Handles NULL (nil), already-parsed Hash, and JSON string forms.
|
|
98
117
|
def parse_metadata(raw)
|
|
99
118
|
return {} if raw.nil?
|
|
100
119
|
return symbolize_hash_keys(raw) if raw.is_a?(Hash)
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
unless raw.is_a?(String)
|
|
122
|
+
raise InvalidResultError, "Pgvector metadata must contain a JSON object"
|
|
123
|
+
end
|
|
124
|
+
parsed = JSON.parse(raw, symbolize_names: true)
|
|
125
|
+
unless parsed.is_a?(Hash)
|
|
126
|
+
raise InvalidResultError, "Pgvector metadata must contain a JSON object"
|
|
127
|
+
end
|
|
128
|
+
parsed
|
|
104
129
|
rescue JSON::ParserError
|
|
105
|
-
|
|
130
|
+
raise InvalidResultError, "Pgvector metadata contains invalid JSON"
|
|
106
131
|
end
|
|
107
132
|
|
|
108
133
|
# Recursively symbolizes keys for an already-parsed Hash.
|
|
109
134
|
def symbolize_hash_keys(hash)
|
|
110
135
|
hash.each_with_object({}) do |(k, v), h|
|
|
136
|
+
unless k.is_a?(String) || k.is_a?(Symbol)
|
|
137
|
+
raise InvalidResultError, "Pgvector metadata contains an invalid object key"
|
|
138
|
+
end
|
|
111
139
|
h[k.to_sym] = v.is_a?(Hash) ? symbolize_hash_keys(v) : v
|
|
112
140
|
end
|
|
113
141
|
end
|
|
@@ -8,7 +8,7 @@ module Phronomy
|
|
|
8
8
|
#
|
|
9
9
|
# Requires:
|
|
10
10
|
# - The +redis+ gem (add to your Gemfile)
|
|
11
|
-
# - A Redis server with the RediSearch
|
|
11
|
+
# - A RESP2 Redis client and a server with the RediSearch module enabled
|
|
12
12
|
# (or Redis Stack which bundles RediSearch)
|
|
13
13
|
#
|
|
14
14
|
# Vectors are stored as FLOAT32 binary blobs in Redis Hash fields and
|
|
@@ -46,17 +46,17 @@ module Phronomy
|
|
|
46
46
|
@mutex = Mutex.new
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
protected
|
|
50
|
+
|
|
49
51
|
# @param id [String]
|
|
50
52
|
# @param embedding [Array<Float>]
|
|
51
53
|
# @param metadata [Hash]
|
|
52
54
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
53
55
|
# @api public
|
|
54
|
-
def
|
|
55
|
-
cancellation_token&.raise_if_cancelled!
|
|
56
|
+
def perform_add(id:, embedding:, metadata: {}, cancellation_token: nil)
|
|
56
57
|
# Establish expected dimension on first add (not race-free for concurrent
|
|
57
|
-
# first adds), then
|
|
58
|
+
# first adds), then create/reuse the index. Base has validated the input.
|
|
58
59
|
@dimension ||= embedding.size
|
|
59
|
-
validate_embedding_dimension!(embedding, @dimension)
|
|
60
60
|
ensure_index!(@dimension)
|
|
61
61
|
@redis.call(
|
|
62
62
|
"HSET", "#{DOC_PREFIX}#{id}",
|
|
@@ -71,20 +71,17 @@ module Phronomy
|
|
|
71
71
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
72
72
|
# @return [Array<Hash>] sorted by descending similarity score
|
|
73
73
|
# @api public
|
|
74
|
-
def
|
|
75
|
-
cancellation_token&.raise_if_cancelled!
|
|
74
|
+
def perform_search(query_embedding:, k: 5, cancellation_token: nil)
|
|
76
75
|
# search never establishes dimension. If dimension is unknown and the
|
|
77
76
|
# index has not been created yet, there are no documents to return.
|
|
78
77
|
return [] if @dimension.nil? && !@index_created
|
|
79
78
|
|
|
80
|
-
validate_embedding_dimension!(query_embedding, @dimension)
|
|
81
79
|
ensure_index!(@dimension)
|
|
82
|
-
k_safe = validate_k!(k)
|
|
83
80
|
blob = pack_vector(query_embedding)
|
|
84
81
|
|
|
85
82
|
raw = @redis.call(
|
|
86
83
|
"FT.SEARCH", @index_name,
|
|
87
|
-
"*=>[KNN #{
|
|
84
|
+
"*=>[KNN #{k} @embedding $BLOB AS score]",
|
|
88
85
|
"PARAMS", 2, "BLOB", blob,
|
|
89
86
|
"SORTBY", "score",
|
|
90
87
|
"RETURN", 2, "score", "metadata",
|
|
@@ -94,26 +91,26 @@ module Phronomy
|
|
|
94
91
|
parse_results(raw)
|
|
95
92
|
end
|
|
96
93
|
|
|
97
|
-
def
|
|
94
|
+
def perform_remove(id:)
|
|
98
95
|
@redis.call("DEL", "#{DOC_PREFIX}#{id}")
|
|
99
96
|
self
|
|
100
97
|
end
|
|
101
98
|
|
|
102
99
|
# Returns the number of documents indexed.
|
|
103
100
|
# Queries FT.INFO when the index has been created; returns 0 otherwise.
|
|
104
|
-
def
|
|
101
|
+
def perform_size
|
|
105
102
|
return 0 unless @index_created
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
fields = parse_fields(@redis.call("FT.INFO", @index_name), "FT.INFO")
|
|
105
|
+
count = fields["num_docs"]
|
|
106
|
+
count = Integer(count, 10) if count.is_a?(String) && count.match?(/\A\d+\z/)
|
|
107
|
+
unless count.is_a?(Integer) && count >= 0
|
|
108
|
+
raise InvalidResultError, "Redis FT.INFO requires a non-negative integer num_docs"
|
|
109
|
+
end
|
|
110
|
+
count
|
|
114
111
|
end
|
|
115
112
|
|
|
116
|
-
def
|
|
113
|
+
def perform_clear
|
|
117
114
|
@mutex.synchronize do
|
|
118
115
|
begin
|
|
119
116
|
@redis.call("FT.DROPINDEX", @index_name, "DD")
|
|
@@ -125,6 +122,11 @@ module Phronomy
|
|
|
125
122
|
self
|
|
126
123
|
end
|
|
127
124
|
|
|
125
|
+
# @api public
|
|
126
|
+
def embedding_dimension
|
|
127
|
+
@dimension
|
|
128
|
+
end
|
|
129
|
+
|
|
128
130
|
private
|
|
129
131
|
|
|
130
132
|
def ensure_index!(dim)
|
|
@@ -160,33 +162,68 @@ module Phronomy
|
|
|
160
162
|
#
|
|
161
163
|
# Redis FT.SEARCH returns: [count, key1, [field, value, ...], key2, ...]
|
|
162
164
|
def parse_results(raw)
|
|
163
|
-
|
|
165
|
+
unless raw.is_a?(Array) && raw.size.odd? && raw.first.is_a?(Integer) &&
|
|
166
|
+
raw.first >= (raw.size - 1) / 2 && raw.first.zero? == (raw.size == 1)
|
|
167
|
+
raise InvalidResultError, "Redis FT.SEARCH requires a count and document/field pairs"
|
|
168
|
+
end
|
|
164
169
|
|
|
165
170
|
results = []
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
next
|
|
173
|
-
|
|
174
|
-
field_hash = fields.each_slice(2).to_h
|
|
175
|
-
score_str = field_hash["score"]
|
|
176
|
-
metadata_str = field_hash["metadata"]
|
|
177
|
-
|
|
178
|
-
next if score_str.nil?
|
|
171
|
+
raw.drop(1).each_slice(2) do |key, fields|
|
|
172
|
+
unless key.is_a?(String) && key.start_with?(DOC_PREFIX)
|
|
173
|
+
raise InvalidResultError, "Redis FT.SEARCH returned an invalid document key"
|
|
174
|
+
end
|
|
175
|
+
# Redis documents this null content when a key expires/is updated
|
|
176
|
+
# during the search. The key remains in the total count.
|
|
177
|
+
next if fields.nil?
|
|
179
178
|
|
|
180
|
-
|
|
179
|
+
field_hash = parse_fields(fields, "FT.SEARCH")
|
|
180
|
+
id = key.delete_prefix(DOC_PREFIX)
|
|
181
181
|
# RediSearch returns cosine distance (0=identical, 2=opposite);
|
|
182
182
|
# convert to cosine similarity for consistency with other backends.
|
|
183
|
-
score = 1.0 -
|
|
184
|
-
metadata =
|
|
183
|
+
score = 1.0 - parse_distance(field_hash["score"])
|
|
184
|
+
metadata = parse_metadata(field_hash["metadata"])
|
|
185
185
|
|
|
186
186
|
results << {id: id, score: score, metadata: metadata}
|
|
187
187
|
end
|
|
188
188
|
results
|
|
189
189
|
end
|
|
190
|
+
|
|
191
|
+
# RESP2 field/value decoding belongs to this backend, not the Contract.
|
|
192
|
+
def parse_fields(raw, command)
|
|
193
|
+
unless raw.is_a?(Array) && raw.size.even?
|
|
194
|
+
raise InvalidResultError, "Redis #{command} requires field/value pairs"
|
|
195
|
+
end
|
|
196
|
+
raw.each_slice(2).each_with_object({}) do |(key, value), fields|
|
|
197
|
+
unless key.is_a?(String) && !fields.key?(key)
|
|
198
|
+
raise InvalidResultError, "Redis #{command} returned an invalid or duplicate field"
|
|
199
|
+
end
|
|
200
|
+
fields[key] = value
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_distance(raw)
|
|
205
|
+
valid = (raw.is_a?(String) && raw.match?(/\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/)) ||
|
|
206
|
+
(raw.is_a?(Numeric) && raw.real?)
|
|
207
|
+
value = Float(raw, exception: false) if valid
|
|
208
|
+
unless value&.finite?
|
|
209
|
+
raise InvalidResultError, "Redis FT.SEARCH requires a finite numeric score"
|
|
210
|
+
end
|
|
211
|
+
value
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def parse_metadata(raw)
|
|
215
|
+
return {} if raw.nil?
|
|
216
|
+
unless raw.is_a?(String)
|
|
217
|
+
raise InvalidResultError, "Redis metadata must contain a JSON object"
|
|
218
|
+
end
|
|
219
|
+
parsed = JSON.parse(raw, symbolize_names: true)
|
|
220
|
+
unless parsed.is_a?(Hash)
|
|
221
|
+
raise InvalidResultError, "Redis metadata must contain a JSON object"
|
|
222
|
+
end
|
|
223
|
+
parsed
|
|
224
|
+
rescue JSON::ParserError
|
|
225
|
+
raise InvalidResultError, "Redis metadata contains invalid JSON"
|
|
226
|
+
end
|
|
190
227
|
end
|
|
191
228
|
end
|
|
192
229
|
end
|