phronomy 0.25.0 → 0.27.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/CHANGELOG.md +71 -0
- data/CONTRIBUTING.md +4 -4
- data/README.md +14 -7
- data/VERIFY.sh +27 -27
- data/benchmark/bench_agent_invoke.rb +26 -22
- data/benchmark/bench_context_assembler.rb +4 -5
- data/benchmark/bench_regression.rb +1 -1
- data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
- data/docs/architecture/agent-context.md +5 -3
- data/docs/architecture/agent-transition-ownership.md +89 -0
- data/docs/architecture/before-llm-input.md +6 -0
- data/docs/architecture/context-management.md +30 -0
- data/docs/architecture/context-preparation-steps.md +90 -0
- data/docs/architecture/entry-action-and-team-wording.md +72 -0
- data/docs/architecture/execution-metadata-and-values.md +80 -0
- data/docs/architecture/generator-verifier-ownership.md +111 -0
- data/docs/architecture/multi-agent-handoff.md +8 -2
- data/docs/architecture/persistence-refactoring-plan.md +50 -0
- data/docs/architecture/persistence.md +29 -6
- data/docs/architecture/refactoring-closure.md +334 -0
- data/docs/architecture/remaining-refactoring-plan.md +374 -0
- data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
- data/docs/architecture/tool-schema-recording-gap.md +50 -0
- data/docs/architecture/tracing.md +4 -4
- data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
- data/docs/architecture.md +209 -0
- data/docs/async-composition.md +205 -0
- data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
- data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
- data/docs/decisions/032-storage-backend-composition.md +80 -0
- data/docs/decisions/033-domain-persistence-ownership.md +79 -0
- data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
- data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
- data/docs/decisions/036-context-contract-ownership.md +86 -0
- data/docs/decisions/037-common-definition-ownership.md +62 -0
- data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
- data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
- data/docs/decisions/040-configuration-default-composition.md +94 -0
- data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
- data/docs/decisions/042-feature-owned-execution-state.md +106 -0
- data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
- data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
- data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
- data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
- data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
- data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
- data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
- data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
- data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
- data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
- data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
- data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
- data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
- data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
- data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
- data/docs/decisions/058-neutral-storage-primitives.md +78 -0
- data/docs/decisions/README.md +35 -6
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
- data/docs/features.md +21 -12
- data/docs/getting-started.md +10 -10
- data/docs/migrations/0.15.md +5 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +7 -1
- data/docs/migrations/handoff-runner-multi-agent.md +35 -0
- data/docs/migrations/neutral-storage-spi.md +43 -0
- data/docs/migrations/parallel-tool-chat-removal.md +49 -0
- data/docs/migrations/shared-state-multi-agent.md +44 -0
- data/docs/migrations/storage-backend-composition.md +117 -0
- data/docs/migrations/storage-transaction-boundaries.md +74 -0
- data/docs/persistence-backends.md +162 -204
- data/docs/runtime-and-concurrency.md +205 -60
- data/lib/phronomy/agent/api/agent.rb +17 -0
- data/lib/phronomy/agent/async_event_api.rb +2 -2
- data/lib/phronomy/agent/base.rb +55 -251
- data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
- data/lib/phronomy/agent/context/capability/base.rb +67 -26
- data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
- data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
- data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
- data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
- data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
- data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
- data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
- data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
- data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
- data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
- data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
- data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
- data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
- data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
- data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
- data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +20 -96
- data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
- data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +12 -13
- data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
- data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
- data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
- data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
- data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
- data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
- data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
- data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
- data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +22 -73
- data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
- data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
- data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
- data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
- data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
- data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
- data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
- data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
- data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
- data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
- data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
- data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
- data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
- data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
- data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
- data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
- data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
- data/lib/phronomy/agent/persistence/codec.rb +358 -0
- data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
- data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
- data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
- data/lib/phronomy/agent/persistence/queries.rb +61 -0
- data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
- data/lib/phronomy/agent/persistence/watermark.rb +27 -0
- data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +36 -41
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
- data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +9 -13
- data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
- data/lib/phronomy/agent/selection/candidate.rb +1 -1
- data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
- data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
- data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
- data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
- data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
- data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
- data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +2 -2
- data/lib/phronomy/common/configuration_error.rb +7 -0
- data/lib/phronomy/common/error.rb +5 -0
- data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
- data/lib/phronomy/common/values/serializable.rb +32 -0
- data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
- data/lib/phronomy/configuration/global_configuration.rb +26 -0
- data/lib/phronomy/content_store/storage_schema.rb +11 -0
- data/lib/phronomy/content_store/stored_contents.rb +43 -0
- data/lib/phronomy/engine/backpressure_error.rb +7 -0
- data/lib/phronomy/{blocking.rb → engine/blocking.rb} +15 -8
- data/lib/phronomy/engine/cancellation_error.rb +7 -0
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +29 -16
- data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
- data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
- data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
- data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
- data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
- data/lib/phronomy/engine/event_loop.rb +160 -626
- data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
- data/lib/phronomy/engine/execution.rb +229 -0
- data/lib/phronomy/engine/execution_cancellation_error.rb +14 -0
- data/lib/phronomy/engine/execution_receiver.rb +65 -0
- data/lib/phronomy/engine/execution_timeout_error.rb +14 -0
- data/lib/phronomy/engine/fsm_protocol.rb +14 -0
- data/lib/phronomy/engine/fsm_session.rb +38 -31
- data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +3 -1
- data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +3 -1
- data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +3 -1
- data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +13 -1
- data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
- data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
- data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
- data/lib/phronomy/engine/runtime.rb +80 -84
- data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
- data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
- data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
- data/lib/phronomy/engine/{task.rb → task_result.rb} +101 -42
- data/lib/phronomy/engine/timeout_error.rb +7 -0
- data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
- data/lib/phronomy/generation/generator_verifier.rb +118 -0
- data/lib/phronomy/generation/low_confidence_error.rb +14 -0
- data/lib/phronomy/llm_adapter/base.rb +2 -2
- data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
- data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
- data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
- data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
- data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
- data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
- data/lib/phronomy/llm_contract/transport_error.rb +7 -0
- data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +8 -8
- data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
- data/lib/phronomy/multi_agent/orchestrator.rb +36 -68
- data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
- data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
- data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
- data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
- data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
- data/lib/phronomy/multi_agent/team_execution.rb +1 -1
- data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
- data/lib/phronomy/multi_agent/team_root.rb +1 -1
- data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
- data/lib/phronomy/persistence/api/persistence.rb +140 -0
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
- data/lib/phronomy/persistence_composition/repositories.rb +77 -0
- data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
- data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
- data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
- data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
- data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
- data/lib/phronomy/storage/backend.rb +101 -0
- data/lib/phronomy/storage/backends/in_memory.rb +157 -0
- data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
- data/lib/phronomy/storage/blobs.rb +31 -0
- data/lib/phronomy/storage/condition.rb +25 -0
- data/lib/phronomy/storage/condition_failed_error.rb +16 -0
- data/lib/phronomy/storage/conflict_error.rb +9 -0
- data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
- data/lib/phronomy/storage/entry.rb +33 -0
- data/lib/phronomy/storage/guard_ref.rb +13 -0
- data/lib/phronomy/storage/not_found_error.rb +9 -0
- data/lib/phronomy/storage/record_codec.rb +177 -0
- data/lib/phronomy/storage/records.rb +61 -0
- data/lib/phronomy/storage/resource.rb +126 -0
- data/lib/phronomy/storage/scope.rb +25 -0
- data/lib/phronomy/storage/serialization_error.rb +9 -0
- data/lib/phronomy/storage/streams.rb +44 -0
- data/lib/phronomy/storage/transaction_error.rb +10 -0
- data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
- data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
- data/lib/phronomy/storage/validation.rb +53 -0
- data/lib/phronomy/storage/view.rb +131 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
- data/lib/phronomy/testing/fake_clock.rb +13 -9
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
- data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
- data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
- data/lib/phronomy/testing/persistence_contract.rb +4 -0
- data/lib/phronomy/tool/contract/tool_error.rb +7 -0
- data/lib/phronomy/tools/agent.rb +6 -6
- data/lib/phronomy/vector_store/async_backend.rb +5 -5
- data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
- data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
- data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
- data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +126 -75
- data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
- data/lib/phronomy/workflow/persistence/codec.rb +153 -0
- data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
- data/lib/phronomy/workflow/phase_machine_builder.rb +8 -8
- data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
- data/lib/phronomy.rb +52 -94
- data/scripts/api_snapshot.rb +4 -2
- data/scripts/storage_spi_snapshot.rb +36 -0
- data/sig/phronomy/agent.rbs +4 -5
- data/sig/phronomy/execution_receiver.rbs +34 -0
- data/sig/phronomy/extensions.rbs +5 -5
- data/sig/phronomy/handoff.rbs +4 -2
- data/sig/phronomy/multi_agent.rbs +19 -3
- data/sig/phronomy/persistence.rbs +7 -91
- data/sig/phronomy/runtime.rbs +34 -7
- data/sig/phronomy/storage.rbs +174 -0
- data/sig/phronomy/tool.rbs +11 -2
- data/sig/phronomy/workflow.rbs +1 -1
- metadata +236 -99
- data/lib/phronomy/agent/execution_coordinator.rb +0 -3138
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
- data/lib/phronomy/agent/recovery_support.rb +0 -504
- data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
- data/lib/phronomy/agent/tool_executor.rb +0 -55
- data/lib/phronomy/generator_verifier.rb +0 -369
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
- data/lib/phronomy/persistence/durable_codec.rb +0 -706
- data/lib/phronomy/persistence/in_memory.rb +0 -690
- data/lib/phronomy/persistence/repository_facades.rb +0 -535
- data/lib/phronomy/persistence.rb +0 -276
- data/lib/phronomy/ruby_llm_patches.rb +0 -24
- data/lib/phronomy/workflow_recovery.rb +0 -123
- /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
- /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
- /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
- /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
- /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
- /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
- /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
- /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
- /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
- /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
- /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../engine/concurrency/worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
require "securerandom"
|
|
4
6
|
|
|
5
7
|
module Phronomy
|
|
@@ -16,9 +18,9 @@ module Phronomy
|
|
|
16
18
|
# to the same FSMSession before that session becomes halted/completed.
|
|
17
19
|
# @api private
|
|
18
20
|
class WorkflowRunner
|
|
19
|
-
include Phronomy::
|
|
21
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
include Phronomy::Runnable
|
|
22
24
|
|
|
23
25
|
Execution = Data.define(
|
|
24
26
|
:context,
|
|
@@ -138,7 +140,7 @@ module Phronomy
|
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
# Posts an application-defined event to the currently live Workflow owner.
|
|
141
|
-
#
|
|
143
|
+
# WorkflowExecutionRegistry resolves workflow_instance_id to the current concrete
|
|
142
144
|
# fsm_session_id. The opaque admission owner token is deliberately not a
|
|
143
145
|
# routing identity and is not exposed here.
|
|
144
146
|
def signal(workflow_instance_id:, event:, payload: nil)
|
|
@@ -151,7 +153,10 @@ module Phronomy
|
|
|
151
153
|
"Valid events: #{@external_events.keys.inspect}"
|
|
152
154
|
end
|
|
153
155
|
|
|
154
|
-
Phronomy::Runtime.instance
|
|
156
|
+
registry = Phronomy::WorkflowExecutionRegistry.existing_for(Phronomy::Runtime.instance)
|
|
157
|
+
return false unless registry
|
|
158
|
+
|
|
159
|
+
registry.post_to_workflow(
|
|
155
160
|
workflow_instance_id: workflow_instance_id,
|
|
156
161
|
event: event_name,
|
|
157
162
|
payload: payload
|
|
@@ -216,7 +221,7 @@ module Phronomy
|
|
|
216
221
|
def start_new_execution(input, config, stable_observer: nil)
|
|
217
222
|
reject_legacy_workflow_identity_key!(config)
|
|
218
223
|
runtime = Phronomy::Runtime.instance
|
|
219
|
-
result_task = Phronomy::
|
|
224
|
+
result_task = Phronomy::TaskResult.deferred(name: "workflow:preparing")
|
|
220
225
|
explicit_workflow_instance_id = !config[:workflow_instance_id].nil?
|
|
221
226
|
workflow_instance_id = (config[:workflow_instance_id] || SecureRandom.uuid).to_s.freeze
|
|
222
227
|
Phronomy::Tracing::Automatic.observe_task(
|
|
@@ -247,7 +252,7 @@ module Phronomy
|
|
|
247
252
|
def start_resume_execution(state, input:, event_name:, current_phase:)
|
|
248
253
|
runtime = Phronomy::Runtime.instance
|
|
249
254
|
workflow_instance_id = state.workflow_instance_id.to_s.freeze
|
|
250
|
-
result_task = Phronomy::
|
|
255
|
+
result_task = Phronomy::TaskResult.deferred(name: "workflow-resume:#{workflow_instance_id}")
|
|
251
256
|
Phronomy::Tracing::Automatic.observe_task(
|
|
252
257
|
result_task,
|
|
253
258
|
"workflow.execution",
|
|
@@ -277,7 +282,7 @@ module Phronomy
|
|
|
277
282
|
runtime = Phronomy::Runtime.instance
|
|
278
283
|
event_loop = runtime.event_loop
|
|
279
284
|
admitted = false
|
|
280
|
-
event_loop.admit_workflow(
|
|
285
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).admit_workflow(
|
|
281
286
|
request.workflow_instance_id,
|
|
282
287
|
owner_token: request.owner_token
|
|
283
288
|
)
|
|
@@ -318,7 +323,7 @@ module Phronomy
|
|
|
318
323
|
)
|
|
319
324
|
rescue => error
|
|
320
325
|
if admitted
|
|
321
|
-
event_loop.release_workflow(
|
|
326
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
|
|
322
327
|
request.workflow_instance_id,
|
|
323
328
|
owner_token: request.owner_token
|
|
324
329
|
)
|
|
@@ -330,7 +335,7 @@ module Phronomy
|
|
|
330
335
|
runtime = Phronomy::Runtime.instance
|
|
331
336
|
event_loop = runtime.event_loop
|
|
332
337
|
admitted = false
|
|
333
|
-
event_loop.admit_workflow(
|
|
338
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).admit_workflow(
|
|
334
339
|
request.workflow_instance_id,
|
|
335
340
|
owner_token: request.owner_token
|
|
336
341
|
)
|
|
@@ -366,7 +371,7 @@ module Phronomy
|
|
|
366
371
|
)
|
|
367
372
|
rescue => error
|
|
368
373
|
if admitted
|
|
369
|
-
event_loop.release_workflow(
|
|
374
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
|
|
370
375
|
request.workflow_instance_id,
|
|
371
376
|
owner_token: request.owner_token
|
|
372
377
|
)
|
|
@@ -503,7 +508,7 @@ module Phronomy
|
|
|
503
508
|
local_snapshot = normalize_snapshot(snapshot_for(state))
|
|
504
509
|
return durable_revision if normalized_durable == local_snapshot
|
|
505
510
|
|
|
506
|
-
raise Phronomy::
|
|
511
|
+
raise Phronomy::Storage::ConflictError,
|
|
507
512
|
"Workflow state changed since the supplied halted context for " \
|
|
508
513
|
"workflow_instance_id #{state.workflow_instance_id.inspect}; explicit reload/reconciliation is required"
|
|
509
514
|
end
|
|
@@ -576,13 +581,13 @@ module Phronomy
|
|
|
576
581
|
resume_phase: resume_phase,
|
|
577
582
|
stable_observer: stable_observer
|
|
578
583
|
)
|
|
579
|
-
event_loop.bind_workflow_session(
|
|
584
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).bind_workflow_session(
|
|
580
585
|
execution.workflow_instance_id,
|
|
581
586
|
owner_token: execution.owner_token,
|
|
582
587
|
fsm_session_id: session.id
|
|
583
588
|
)
|
|
584
589
|
|
|
585
|
-
source_task = Phronomy::
|
|
590
|
+
source_task = Phronomy::TaskResult.deferred(name: "workflow-source:#{session.id}")
|
|
586
591
|
source_task.on_complete do |result, error|
|
|
587
592
|
finalize_execution(
|
|
588
593
|
execution: execution,
|
|
@@ -591,11 +596,12 @@ module Phronomy
|
|
|
591
596
|
error: error
|
|
592
597
|
)
|
|
593
598
|
end
|
|
594
|
-
event_loop.register(session, completion: source_task
|
|
599
|
+
event_loop.register(session, completion: source_task,
|
|
600
|
+
receiver: Phronomy::WorkflowExecutionRegistry.for(event_loop))
|
|
595
601
|
result_task
|
|
596
602
|
rescue => error
|
|
597
603
|
if event_loop&.current?
|
|
598
|
-
event_loop
|
|
604
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
|
|
599
605
|
execution.workflow_instance_id,
|
|
600
606
|
owner_token: execution.owner_token
|
|
601
607
|
)
|
|
@@ -608,7 +614,7 @@ module Phronomy
|
|
|
608
614
|
def finalize_execution(execution:, result_task:, result:, error:)
|
|
609
615
|
event_loop = Phronomy::Runtime.instance.event_loop
|
|
610
616
|
assert_event_loop!(event_loop)
|
|
611
|
-
event_loop.release_workflow(
|
|
617
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
|
|
612
618
|
execution.workflow_instance_id,
|
|
613
619
|
owner_token: execution.owner_token
|
|
614
620
|
)
|
|
@@ -624,73 +630,117 @@ module Phronomy
|
|
|
624
630
|
runtime = Phronomy::Runtime.instance
|
|
625
631
|
event_loop = runtime.event_loop
|
|
626
632
|
assert_event_loop!(event_loop)
|
|
627
|
-
event_loop.mark_workflow_admission(
|
|
633
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).mark_workflow_admission(
|
|
628
634
|
execution.workflow_instance_id,
|
|
629
635
|
owner_token: execution.owner_token,
|
|
630
636
|
state: :persisting_terminal
|
|
631
637
|
)
|
|
632
638
|
|
|
633
|
-
operation = WorkflowTerminalPersistenceCommand.new(
|
|
639
|
+
operation = Phronomy::WorkflowRunner::WorkflowTerminalPersistenceCommand.new(
|
|
634
640
|
repository: execution.repository,
|
|
635
641
|
workflow_instance_id: execution.workflow_instance_id.to_s.freeze,
|
|
636
642
|
expected_revision: execution.expected_revision,
|
|
637
643
|
snapshot: deep_immutable_copy(snapshot_for(context))
|
|
638
644
|
)
|
|
645
|
+
|
|
639
646
|
task = runtime.offload.submit(on_full: :raise) do
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
647
|
+
persist_terminal_snapshot(operation)
|
|
648
|
+
end
|
|
649
|
+
task.on_complete do |result, operation_error|
|
|
650
|
+
deliver_terminal_persistence_result(execution, event_sink, result, operation_error)
|
|
651
|
+
end
|
|
652
|
+
terminal_type
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def persist_terminal_snapshot(operation)
|
|
656
|
+
revision = operation.repository.save(
|
|
657
|
+
operation.workflow_instance_id,
|
|
658
|
+
expected_revision: operation.expected_revision,
|
|
659
|
+
snapshot: operation.snapshot
|
|
660
|
+
)
|
|
661
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
662
|
+
outcome: :success,
|
|
663
|
+
revision: revision,
|
|
664
|
+
error: nil
|
|
665
|
+
)
|
|
666
|
+
rescue Phronomy::Storage::ConflictError,
|
|
667
|
+
Phronomy::Storage::NotFoundError,
|
|
668
|
+
Phronomy::Storage::SerializationError,
|
|
669
|
+
Phronomy::Storage::UnsupportedBackendError => error
|
|
670
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
671
|
+
outcome: :known_failure,
|
|
672
|
+
revision: nil,
|
|
673
|
+
error: error
|
|
674
|
+
)
|
|
675
|
+
rescue => error
|
|
676
|
+
reconcile_workflow_terminal_f1(operation, error)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def deliver_terminal_persistence_result(execution, event_sink, result, operation_error)
|
|
680
|
+
delivery = if operation_error
|
|
681
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
682
|
+
outcome: :outcome_unknown,
|
|
683
|
+
revision: nil,
|
|
684
|
+
error: operation_error
|
|
644
685
|
)
|
|
645
|
-
|
|
686
|
+
else
|
|
687
|
+
result
|
|
688
|
+
end
|
|
689
|
+
accepted = event_sink.post(:workflow_terminal_persistence_result, delivery)
|
|
690
|
+
unless accepted
|
|
691
|
+
Phronomy.configuration.logger&.warn(
|
|
692
|
+
"[Phronomy] EventLoop rejected Workflow terminal persistence result " \
|
|
693
|
+
"for #{execution.workflow_instance_id.inspect}"
|
|
694
|
+
)
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def reconcile_workflow_terminal_f1(operation, original_error)
|
|
699
|
+
record = operation.repository.load(operation.workflow_instance_id)
|
|
700
|
+
case Phronomy::Recovery.compare_revisioned_snapshot(
|
|
701
|
+
record: record,
|
|
702
|
+
expected_pre_revision: operation.expected_revision,
|
|
703
|
+
intended_snapshot: operation.snapshot
|
|
704
|
+
)
|
|
705
|
+
when :post_state
|
|
706
|
+
revision = Phronomy::Recovery.fetch_value(record, :revision)
|
|
707
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
646
708
|
outcome: :success,
|
|
647
709
|
revision: revision,
|
|
648
710
|
error: nil
|
|
649
711
|
)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
WorkflowTerminalPersistenceResult.new(
|
|
712
|
+
when :pre_state
|
|
713
|
+
# The uncertain save is now known not to have committed. Do not blindly
|
|
714
|
+
# retry it here; surface a known failure through the ordinary terminal
|
|
715
|
+
# barrier semantics.
|
|
716
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
655
717
|
outcome: :known_failure,
|
|
656
718
|
revision: nil,
|
|
657
|
-
error:
|
|
719
|
+
error: original_error
|
|
658
720
|
)
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
721
|
+
else
|
|
722
|
+
conflict = Phronomy::Storage::ConflictError.new(
|
|
723
|
+
"Workflow terminal Persistence outcome conflicts with both expected " \
|
|
724
|
+
"pre-state and intended post-state for #{operation.workflow_instance_id.inspect}"
|
|
725
|
+
)
|
|
726
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
664
727
|
outcome: :outcome_unknown,
|
|
665
728
|
revision: nil,
|
|
666
|
-
error:
|
|
729
|
+
error: conflict
|
|
667
730
|
)
|
|
668
731
|
end
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
)
|
|
676
|
-
else
|
|
677
|
-
result
|
|
678
|
-
end
|
|
679
|
-
accepted = event_sink.post(:workflow_terminal_persistence_result, delivery)
|
|
680
|
-
unless accepted
|
|
681
|
-
Phronomy.configuration.logger&.warn(
|
|
682
|
-
"[Phronomy] EventLoop rejected Workflow terminal persistence result " \
|
|
683
|
-
"for #{execution.workflow_instance_id.inspect}"
|
|
684
|
-
)
|
|
685
|
-
end
|
|
686
|
-
end
|
|
687
|
-
terminal_type
|
|
732
|
+
rescue => reconciliation_error
|
|
733
|
+
Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
|
|
734
|
+
outcome: :outcome_unknown,
|
|
735
|
+
revision: nil,
|
|
736
|
+
error: reconciliation_error
|
|
737
|
+
)
|
|
688
738
|
end
|
|
689
739
|
|
|
690
740
|
def release_and_fail(request, error)
|
|
691
741
|
event_loop = Phronomy::Runtime.instance.event_loop
|
|
692
742
|
assert_event_loop!(event_loop)
|
|
693
|
-
event_loop.release_workflow(
|
|
743
|
+
Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
|
|
694
744
|
request.workflow_instance_id,
|
|
695
745
|
owner_token: request.owner_token
|
|
696
746
|
)
|
|
@@ -711,17 +761,6 @@ module Phronomy
|
|
|
711
761
|
resume_phase: nil,
|
|
712
762
|
stable_observer: nil
|
|
713
763
|
)
|
|
714
|
-
terminal_barrier = if execution.repository && execution.persist
|
|
715
|
-
->(terminal_type:, context:, event_sink:) {
|
|
716
|
-
begin_terminal_persistence_on_event_loop(
|
|
717
|
-
execution,
|
|
718
|
-
terminal_type: terminal_type,
|
|
719
|
-
context: context,
|
|
720
|
-
event_sink: event_sink
|
|
721
|
-
)
|
|
722
|
-
}
|
|
723
|
-
end
|
|
724
|
-
|
|
725
764
|
Phronomy::FSMSession.new(
|
|
726
765
|
context: execution.context,
|
|
727
766
|
context_metadata: {
|
|
@@ -739,7 +778,22 @@ module Phronomy
|
|
|
739
778
|
resume_event: resume_event,
|
|
740
779
|
resume_phase: resume_phase,
|
|
741
780
|
stable_observer: stable_observer,
|
|
742
|
-
|
|
781
|
+
terminal_policy: build_terminal_policy(execution)
|
|
782
|
+
)
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def build_terminal_policy(execution)
|
|
786
|
+
return unless execution.repository && execution.persist
|
|
787
|
+
|
|
788
|
+
Phronomy::WorkflowTerminalPolicy.new(
|
|
789
|
+
persist: ->(terminal_type:, context:, event_sink:) {
|
|
790
|
+
begin_terminal_persistence_on_event_loop(
|
|
791
|
+
execution,
|
|
792
|
+
terminal_type: terminal_type,
|
|
793
|
+
context: context,
|
|
794
|
+
event_sink: event_sink
|
|
795
|
+
)
|
|
796
|
+
}
|
|
743
797
|
)
|
|
744
798
|
end
|
|
745
799
|
|
|
@@ -766,13 +820,10 @@ module Phronomy
|
|
|
766
820
|
end
|
|
767
821
|
|
|
768
822
|
def post_control(runtime, command)
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
payload: {command: command}
|
|
774
|
-
)
|
|
775
|
-
)
|
|
823
|
+
admission = command.is_a?(StartCommand) || command.is_a?(ResumeCommand)
|
|
824
|
+
completion = admission ? command.result_task : command.request.result_task
|
|
825
|
+
Phronomy::WorkflowExecutionRegistry.for(runtime.event_loop).post(command,
|
|
826
|
+
admission: admission, completion: completion)
|
|
776
827
|
end
|
|
777
828
|
|
|
778
829
|
def runtime_rejected_error(action)
|
|
@@ -797,7 +848,7 @@ module Phronomy
|
|
|
797
848
|
end
|
|
798
849
|
|
|
799
850
|
def failed_task(name, error)
|
|
800
|
-
task = Phronomy::
|
|
851
|
+
task = Phronomy::TaskResult.deferred(name: name)
|
|
801
852
|
fail_task(task, error)
|
|
802
853
|
task
|
|
803
854
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Interprets durable Workflow results for the shared FSM terminal gate.
|
|
5
|
+
# The session owns pending state; Runner owns persistence and reconciliation.
|
|
6
|
+
# @api private
|
|
7
|
+
class WorkflowTerminalPolicy
|
|
8
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
9
|
+
|
|
10
|
+
def initialize(persist:)
|
|
11
|
+
@persist = persist
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start(terminal_type:, context:, event_sink:)
|
|
15
|
+
@persist.call(terminal_type: terminal_type, context: context, event_sink: event_sink)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def handles?(event)
|
|
19
|
+
event.type == :workflow_terminal_persistence_result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decision_for(event)
|
|
23
|
+
result = event.payload
|
|
24
|
+
case result.outcome
|
|
25
|
+
when :success
|
|
26
|
+
FSMProtocol::TerminalDecision.new(action: :complete, error: nil)
|
|
27
|
+
when :known_failure
|
|
28
|
+
FSMProtocol::TerminalDecision.new(
|
|
29
|
+
action: :fail,
|
|
30
|
+
error: result.error || Phronomy::Error.new("Workflow terminal persistence failed")
|
|
31
|
+
)
|
|
32
|
+
when :outcome_unknown
|
|
33
|
+
FSMProtocol::TerminalDecision.new(action: :retire, error: result.error)
|
|
34
|
+
else
|
|
35
|
+
raise Phronomy::Error,
|
|
36
|
+
"unknown Workflow terminal persistence outcome: #{result.outcome.inspect}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
class Workflow
|
|
5
|
+
module Persistence
|
|
6
|
+
# Current-format record schemas and conversion for this domain.
|
|
7
|
+
# @api private
|
|
8
|
+
module Codec
|
|
9
|
+
extend Phronomy::Storage::RecordCodec
|
|
10
|
+
|
|
11
|
+
WORKFLOW_STATE_RECORD_TYPE = "phronomy.workflow_state"
|
|
12
|
+
WORKFLOW_STATE_FORMAT_VERSION = "0.1"
|
|
13
|
+
WORKFLOW_STATE_KEYS = %w[
|
|
14
|
+
workflow_instance_id workflow_revision snapshot
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
WORKFLOW_SNAPSHOT_KEYS = %w[fields phase].freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def encode_workflow_state(workflow_instance_id:, workflow_revision:, snapshot:)
|
|
22
|
+
normalized_snapshot = canonicalize_workflow_snapshot(snapshot)
|
|
23
|
+
validate_workflow_snapshot!(normalized_snapshot)
|
|
24
|
+
revision = Integer(workflow_revision)
|
|
25
|
+
unless revision.positive?
|
|
26
|
+
raise Phronomy::Storage::SerializationError,
|
|
27
|
+
"Workflow durable revision must be positive"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
payload = {
|
|
31
|
+
"workflow_instance_id" => String(workflow_instance_id),
|
|
32
|
+
"workflow_revision" => revision,
|
|
33
|
+
"snapshot" => normalized_snapshot
|
|
34
|
+
}
|
|
35
|
+
validate_workflow_state_payload!(payload)
|
|
36
|
+
build_record(WORKFLOW_STATE_RECORD_TYPE, WORKFLOW_STATE_FORMAT_VERSION, payload)
|
|
37
|
+
rescue Phronomy::Storage::SerializationError
|
|
38
|
+
raise
|
|
39
|
+
rescue => error
|
|
40
|
+
serialization_error("cannot encode Workflow state", error)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decode_workflow_state(record, expected_workflow_instance_id: nil)
|
|
44
|
+
payload = current_payload!(
|
|
45
|
+
record,
|
|
46
|
+
record_type: WORKFLOW_STATE_RECORD_TYPE,
|
|
47
|
+
format_version: WORKFLOW_STATE_FORMAT_VERSION,
|
|
48
|
+
keys: WORKFLOW_STATE_KEYS,
|
|
49
|
+
label: "Workflow state payload"
|
|
50
|
+
)
|
|
51
|
+
validate_workflow_state_payload!(payload)
|
|
52
|
+
workflow_instance_id = payload.fetch("workflow_instance_id")
|
|
53
|
+
if expected_workflow_instance_id &&
|
|
54
|
+
workflow_instance_id != expected_workflow_instance_id.to_s
|
|
55
|
+
raise Phronomy::Storage::SerializationError,
|
|
56
|
+
"Workflow state identity mismatch: #{workflow_instance_id.inspect} != " \
|
|
57
|
+
"#{expected_workflow_instance_id.to_s.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
snapshot: immutable_copy(payload.fetch("snapshot")),
|
|
62
|
+
revision: payload.fetch("workflow_revision")
|
|
63
|
+
}.freeze
|
|
64
|
+
rescue Phronomy::Storage::SerializationError
|
|
65
|
+
raise
|
|
66
|
+
rescue => error
|
|
67
|
+
serialization_error("cannot decode Workflow state", error)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_workflow_state_payload!(payload)
|
|
71
|
+
validate_exact_keys!(payload, WORKFLOW_STATE_KEYS, label: "Workflow state payload")
|
|
72
|
+
require_nonempty_string!(payload, "workflow_instance_id", label: "Workflow state payload")
|
|
73
|
+
require_positive_integer!(payload, "workflow_revision", label: "Workflow state payload")
|
|
74
|
+
validate_workflow_snapshot!(payload.fetch("snapshot"))
|
|
75
|
+
payload
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_workflow_snapshot!(snapshot)
|
|
79
|
+
validate_exact_keys!(snapshot, WORKFLOW_SNAPSHOT_KEYS, label: "Workflow snapshot")
|
|
80
|
+
unless snapshot.fetch("fields").is_a?(Hash)
|
|
81
|
+
raise Phronomy::Storage::SerializationError,
|
|
82
|
+
"Workflow snapshot fields must be a Hash"
|
|
83
|
+
end
|
|
84
|
+
phase = snapshot.fetch("phase")
|
|
85
|
+
unless phase.nil? || phase.is_a?(String)
|
|
86
|
+
raise Phronomy::Storage::SerializationError,
|
|
87
|
+
"Workflow snapshot phase must be a String or nil"
|
|
88
|
+
end
|
|
89
|
+
Phronomy::CanonicalJSON.dump(snapshot)
|
|
90
|
+
snapshot
|
|
91
|
+
rescue ArgumentError => error
|
|
92
|
+
raise Phronomy::Storage::SerializationError,
|
|
93
|
+
"Workflow snapshot is not canonical JSON compatible: #{error.message}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def canonicalize_workflow_snapshot(snapshot)
|
|
97
|
+
source = top_level_string_keys(snapshot, label: "Workflow snapshot")
|
|
98
|
+
fields = source.fetch("fields")
|
|
99
|
+
unless fields.is_a?(Hash)
|
|
100
|
+
raise Phronomy::Storage::SerializationError,
|
|
101
|
+
"Workflow snapshot fields must be a Hash"
|
|
102
|
+
end
|
|
103
|
+
{
|
|
104
|
+
"fields" => canonicalize_workflow_value(fields),
|
|
105
|
+
"phase" => source["phase"]&.to_s
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def canonicalize_workflow_value(value)
|
|
110
|
+
case value
|
|
111
|
+
when Hash
|
|
112
|
+
value.each_with_object({}) do |(key, child), result|
|
|
113
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
114
|
+
raise Phronomy::Storage::SerializationError,
|
|
115
|
+
"Workflow field key must be String or Symbol, got #{key.class}"
|
|
116
|
+
end
|
|
117
|
+
string_key = key.to_s
|
|
118
|
+
if result.key?(string_key)
|
|
119
|
+
raise Phronomy::Storage::SerializationError,
|
|
120
|
+
"duplicate Workflow field key after normalization: #{string_key.inspect}"
|
|
121
|
+
end
|
|
122
|
+
result[string_key] = canonicalize_workflow_value(child)
|
|
123
|
+
end
|
|
124
|
+
when Array
|
|
125
|
+
value.map { |child| canonicalize_workflow_value(child) }
|
|
126
|
+
when Symbol
|
|
127
|
+
value.to_s
|
|
128
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
129
|
+
value
|
|
130
|
+
else
|
|
131
|
+
raise Phronomy::Storage::SerializationError,
|
|
132
|
+
"unsupported Workflow durable value: #{value.class}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def immutable_copy(value)
|
|
137
|
+
case value
|
|
138
|
+
when Hash
|
|
139
|
+
value.each_with_object({}) do |(key, child), result|
|
|
140
|
+
result[key.dup.freeze] = immutable_copy(child)
|
|
141
|
+
end.freeze
|
|
142
|
+
when Array
|
|
143
|
+
value.map { |child| immutable_copy(child) }.freeze
|
|
144
|
+
when String
|
|
145
|
+
value.dup.freeze
|
|
146
|
+
else
|
|
147
|
+
value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
class Workflow
|
|
5
|
+
module Persistence
|
|
6
|
+
# Owns Workflow snapshot encoding and the initial revision.
|
|
7
|
+
# @api private
|
|
8
|
+
class StateRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def load(workflow_instance_id)
|
|
12
|
+
@view.atomic do |bound|
|
|
13
|
+
entry = bound.records(Phronomy::WorkflowStorageSchema::STATES).read(workflow_instance_id.to_s)
|
|
14
|
+
entry && decode(entry, workflow_instance_id)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save(workflow_instance_id, expected_revision:, snapshot:)
|
|
19
|
+
expected = expected_revision.nil? ? nil : Integer(expected_revision)
|
|
20
|
+
revision = expected.nil? ? 1 : expected + 1
|
|
21
|
+
record = Codec.encode_workflow_state(workflow_instance_id: workflow_instance_id,
|
|
22
|
+
workflow_revision: revision, snapshot: snapshot)
|
|
23
|
+
@view.atomic do |bound|
|
|
24
|
+
records = bound.records(Phronomy::WorkflowStorageSchema::STATES)
|
|
25
|
+
values = {key: workflow_instance_id.to_s, attributes: {}, record: record}
|
|
26
|
+
entry = if expected.nil?
|
|
27
|
+
records.insert(**values, revision: revision)
|
|
28
|
+
else
|
|
29
|
+
records.replace(**values, expected_revision: expected, next_revision: revision)
|
|
30
|
+
end
|
|
31
|
+
decoded = decode(entry, workflow_instance_id)
|
|
32
|
+
unless decoded.fetch(:revision) == revision
|
|
33
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Workflow revision"
|
|
34
|
+
end
|
|
35
|
+
revision
|
|
36
|
+
end
|
|
37
|
+
rescue Phronomy::Storage::NotFoundError => error
|
|
38
|
+
raise Phronomy::Storage::ConflictError, error.message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(workflow_instance_id, expected_revision:)
|
|
42
|
+
@view.records(Phronomy::WorkflowStorageSchema::STATES).delete(key: workflow_instance_id.to_s, expected_revision: Integer(expected_revision))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def decode(entry, workflow_instance_id)
|
|
48
|
+
decoded = Codec.decode_workflow_state(entry.record, expected_workflow_instance_id: workflow_instance_id)
|
|
49
|
+
unless entry.key == workflow_instance_id.to_s && decoded.fetch(:revision) == entry.revision
|
|
50
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Workflow identity/revision"
|
|
51
|
+
end
|
|
52
|
+
decoded
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -32,7 +32,7 @@ module Phronomy
|
|
|
32
32
|
|
|
33
33
|
def build
|
|
34
34
|
entry = @entry_point
|
|
35
|
-
all_states = (@declared_states + @wait_state_names + [
|
|
35
|
+
all_states = (@declared_states + @wait_state_names + [FSMProtocol::FINISH]).uniq
|
|
36
36
|
auto_transitions = @auto_transitions
|
|
37
37
|
external_events = @external_events
|
|
38
38
|
entry_actions = @entry_actions
|
|
@@ -139,7 +139,7 @@ module Phronomy
|
|
|
139
139
|
machine.context,
|
|
140
140
|
machine.current_event
|
|
141
141
|
)
|
|
142
|
-
if result.is_a?(Phronomy::
|
|
142
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
143
143
|
raise Phronomy::InvalidAsyncTransitionActionError,
|
|
144
144
|
transition_task_error_message(metadata)
|
|
145
145
|
end
|
|
@@ -151,9 +151,9 @@ module Phronomy
|
|
|
151
151
|
def build_entry_callback(callable, state_name)
|
|
152
152
|
->(machine) {
|
|
153
153
|
result = callable.call(machine.context)
|
|
154
|
-
if result.is_a?(Phronomy::
|
|
154
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
155
155
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
156
|
-
"Entry action for state #{state_name.inspect} returned Phronomy::
|
|
156
|
+
"Entry action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
|
|
157
157
|
"Start the asynchronous operation, register its callback/listener, " \
|
|
158
158
|
"and return the WorkflowContext or nil."
|
|
159
159
|
end
|
|
@@ -164,9 +164,9 @@ module Phronomy
|
|
|
164
164
|
def build_exit_callback(callable, state_name)
|
|
165
165
|
->(machine) {
|
|
166
166
|
result = callable.call(machine.context)
|
|
167
|
-
if result.is_a?(Phronomy::
|
|
167
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
168
168
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
169
|
-
"Exit action for state #{state_name.inspect} returned Phronomy::
|
|
169
|
+
"Exit action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
|
|
170
170
|
"Exit actions are synchronous Run-to-Completion callbacks."
|
|
171
171
|
end
|
|
172
172
|
}
|
|
@@ -188,7 +188,7 @@ module Phronomy
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
def public_destination(destination)
|
|
191
|
-
return :__finish__ if destination ==
|
|
191
|
+
return :__finish__ if destination == FSMProtocol::FINISH
|
|
192
192
|
|
|
193
193
|
destination
|
|
194
194
|
end
|
|
@@ -196,7 +196,7 @@ module Phronomy
|
|
|
196
196
|
def transition_task_error_message(metadata)
|
|
197
197
|
"Transition action " \
|
|
198
198
|
"#{metadata[:from].inspect} --#{metadata[:event].inspect}--> " \
|
|
199
|
-
"#{metadata[:to].inspect} returned Phronomy::
|
|
199
|
+
"#{metadata[:to].inspect} returned Phronomy::TaskResult. " \
|
|
200
200
|
"Start the asynchronous operation, register its callback/listener, " \
|
|
201
201
|
"and return the WorkflowContext or nil."
|
|
202
202
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Domain-owned Workflow metadata, loadable without the Workflow runtime.
|
|
5
|
+
# @api private
|
|
6
|
+
module WorkflowStorageSchema
|
|
7
|
+
STATES = Phronomy::Storage::Resource.new(id: "workflow.states", kind: :records)
|
|
8
|
+
end
|
|
9
|
+
end
|