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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Thread-free fan-out/fan-in. Each input block returns the final TaskResult for
|
|
5
|
+
# that JOB; its application-defined intermediate work is not discovered here.
|
|
6
|
+
# @api public
|
|
7
|
+
class Execution
|
|
8
|
+
# @return [InvocationContext] this execution's explicit operation context
|
|
9
|
+
# @api public
|
|
10
|
+
attr_reader :invocation_context
|
|
11
|
+
|
|
12
|
+
# @param inputs [Array] shallowly snapshotted JOB inputs
|
|
13
|
+
# @param timeout [Numeric, nil] whole-execution deadline in seconds
|
|
14
|
+
# @param cancellation_token [Concurrency::CancellationToken, nil]
|
|
15
|
+
# @param invocation_context [InvocationContext, nil] existing information/controls
|
|
16
|
+
# @yield [input, execution] starts one JOB and returns its final TaskResult
|
|
17
|
+
# @return [TaskResult<Array<TaskResult::Outcome>>]
|
|
18
|
+
# @api public
|
|
19
|
+
def self.run_async(inputs, timeout: nil, cancellation_token: nil,
|
|
20
|
+
invocation_context: nil, &block)
|
|
21
|
+
__run_async(inputs, timeout: timeout, cancellation_token: cancellation_token,
|
|
22
|
+
invocation_context: invocation_context, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Starts the same execution once and waits outside EventLoop.
|
|
26
|
+
# @return [Array<TaskResult::Outcome>]
|
|
27
|
+
# @see run_async
|
|
28
|
+
# @api public
|
|
29
|
+
def self.run(inputs, timeout: nil, cancellation_token: nil,
|
|
30
|
+
invocation_context: nil, &block)
|
|
31
|
+
if Runtime.in_event_loop_context?
|
|
32
|
+
raise EventLoopReentrancyError, "Execution.run cannot run on EventLoop; use run_async"
|
|
33
|
+
end
|
|
34
|
+
run_async(inputs, timeout: timeout, cancellation_token: cancellation_token,
|
|
35
|
+
invocation_context: invocation_context, &block).wait_result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Existing Orchestrator concurrency policy uses the same execution engine.
|
|
39
|
+
# This is not an additional public execution entrance or scheduling mode.
|
|
40
|
+
# @api private
|
|
41
|
+
def self.__run_async(inputs, timeout: nil, cancellation_token: nil,
|
|
42
|
+
invocation_context: nil, concurrency_limit: nil, &block)
|
|
43
|
+
raise TypeError, "inputs must be an Array" unless inputs.is_a?(Array)
|
|
44
|
+
raise ArgumentError, "Execution.run_async requires a block" unless block
|
|
45
|
+
if !cancellation_token.nil? && !cancellation_token.is_a?(Concurrency::CancellationToken)
|
|
46
|
+
raise TypeError, "cancellation_token must be a Phronomy::Concurrency::CancellationToken or nil"
|
|
47
|
+
end
|
|
48
|
+
if !invocation_context.nil? && !invocation_context.is_a?(InvocationContext)
|
|
49
|
+
raise TypeError, "invocation_context must be a Phronomy::InvocationContext or nil"
|
|
50
|
+
end
|
|
51
|
+
unless timeout.nil?
|
|
52
|
+
raise TypeError, "timeout must be Numeric or nil" unless timeout.is_a?(Numeric)
|
|
53
|
+
if timeout.is_a?(Complex) || !timeout.finite? || !timeout.to_f.finite?
|
|
54
|
+
raise ArgumentError, "timeout must be a finite real number"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
new(inputs.dup, timeout: timeout, cancellation_token: cancellation_token,
|
|
58
|
+
invocation_context: invocation_context, concurrency_limit: concurrency_limit,
|
|
59
|
+
&block).send(:start)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :__run_async
|
|
62
|
+
|
|
63
|
+
# A scoped observation copy. The source, its controls and its physical work
|
|
64
|
+
# keep their original ownership. Bind before adding this run's map/flat_map.
|
|
65
|
+
# @param source_result [TaskResult]
|
|
66
|
+
# @return [TaskResult] a distinct scoped observation result
|
|
67
|
+
# @raise [TypeError] for a non-TaskResult source
|
|
68
|
+
# @api public
|
|
69
|
+
def observe(source_result)
|
|
70
|
+
unless source_result.is_a?(TaskResult)
|
|
71
|
+
raise TypeError, "source_result must be a Phronomy::TaskResult"
|
|
72
|
+
end
|
|
73
|
+
observed = TaskResult.deferred(name: "execution-observation").__bind_execution(self)
|
|
74
|
+
subscriptions = Concurrency::Subscriptions.new
|
|
75
|
+
subscriptions.result(observed) { subscriptions.close }
|
|
76
|
+
unless __open?
|
|
77
|
+
observed.cancel!(__cancellation_error)
|
|
78
|
+
return observed
|
|
79
|
+
end
|
|
80
|
+
subscriptions.result(source_result) do
|
|
81
|
+
status, value, error = source_result.__snapshot
|
|
82
|
+
if !__open? && status == :completed
|
|
83
|
+
observed.cancel!(__cancellation_error)
|
|
84
|
+
elsif status == :completed
|
|
85
|
+
observed.complete(value)
|
|
86
|
+
elsif status == :cancelled
|
|
87
|
+
observed.cancel!(error)
|
|
88
|
+
else
|
|
89
|
+
observed.fail(error)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
subscriptions.cancellation(@token) { observed.cancel!(__cancellation_error) }
|
|
93
|
+
observed
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Scope/admission hooks shared with framework-owned operations.
|
|
97
|
+
# @api private
|
|
98
|
+
def __while_open(&block)
|
|
99
|
+
@collector.while_open(&block)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @api private
|
|
103
|
+
def __open?
|
|
104
|
+
@collector.open?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @api private
|
|
108
|
+
def __cancellation_token
|
|
109
|
+
@token
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @api private
|
|
113
|
+
def __cancellation_error
|
|
114
|
+
@cancellation_error
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def initialize(inputs, timeout:, cancellation_token:, invocation_context:,
|
|
120
|
+
concurrency_limit:, &block)
|
|
121
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
122
|
+
@inputs = inputs
|
|
123
|
+
@block = block
|
|
124
|
+
@timeout = timeout
|
|
125
|
+
@parent_context = invocation_context
|
|
126
|
+
@parent_token = cancellation_token
|
|
127
|
+
@token = Concurrency::CancellationToken.new
|
|
128
|
+
@subscriptions = Concurrency::Subscriptions.new
|
|
129
|
+
@cancellation_error = CancellationError.new("Execution scope is closed")
|
|
130
|
+
@invocation_context = (invocation_context || InvocationContext.new)
|
|
131
|
+
.merge(cancellation_token: @token).__bind_execution(self)
|
|
132
|
+
@result = TaskResult.deferred(name: "execution")
|
|
133
|
+
@pump_mutex = Mutex.new
|
|
134
|
+
@next_index = 0
|
|
135
|
+
@active = 0
|
|
136
|
+
@draining = false
|
|
137
|
+
@limit = concurrency_limit || [inputs.length, 1].max
|
|
138
|
+
@collector = Concurrency::ResultCollector.new(inputs.length) do |kind, outcomes|
|
|
139
|
+
finish(kind, outcomes)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def start
|
|
144
|
+
# All slots and control state exist before any registration can fire.
|
|
145
|
+
[@parent_token, @parent_context&.cancellation_token].compact.uniq.each do |token|
|
|
146
|
+
@subscriptions.cancellation(token) { @collector.finish(:cancelled) }
|
|
147
|
+
end
|
|
148
|
+
if @parent_context&.__execution_scope && !@parent_context.__execution_scope.__open?
|
|
149
|
+
@collector.finish(:cancelled)
|
|
150
|
+
end
|
|
151
|
+
if @parent_context&.deadline
|
|
152
|
+
@subscriptions.after(@parent_context.deadline.remaining_seconds) { @collector.finish(:cancelled) }
|
|
153
|
+
end
|
|
154
|
+
if @timeout
|
|
155
|
+
remaining = @timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at)
|
|
156
|
+
@subscriptions.after(remaining) { @collector.finish(:timeout) }
|
|
157
|
+
end
|
|
158
|
+
@collector.finish(:completed) if @inputs.empty?
|
|
159
|
+
drain
|
|
160
|
+
@result
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def drain
|
|
164
|
+
claimed = @pump_mutex.synchronize do
|
|
165
|
+
next false if @draining
|
|
166
|
+
@draining = true
|
|
167
|
+
end
|
|
168
|
+
return unless claimed
|
|
169
|
+
|
|
170
|
+
loop do
|
|
171
|
+
work = @collector.while_open do
|
|
172
|
+
@pump_mutex.synchronize do
|
|
173
|
+
if @active < @limit && @next_index < @inputs.length
|
|
174
|
+
index = @next_index
|
|
175
|
+
@next_index += 1
|
|
176
|
+
@active += 1
|
|
177
|
+
[index, @inputs[index], @block]
|
|
178
|
+
else
|
|
179
|
+
@draining = false
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
unless work
|
|
185
|
+
@pump_mutex.synchronize { @draining = false } unless __open?
|
|
186
|
+
break
|
|
187
|
+
end
|
|
188
|
+
index, input, start_block = work
|
|
189
|
+
begin
|
|
190
|
+
result = start_block.call(input, self)
|
|
191
|
+
unless result.is_a?(TaskResult)
|
|
192
|
+
raise TypeError, "JOB at inputs[#{index}] must return a Phronomy::TaskResult"
|
|
193
|
+
end
|
|
194
|
+
rescue => error
|
|
195
|
+
result = TaskResult.failed(error, name: "execution-job-#{index}")
|
|
196
|
+
end
|
|
197
|
+
@collector.watch(index, result) do
|
|
198
|
+
@pump_mutex.synchronize { @active -= 1 }
|
|
199
|
+
drain
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def finish(kind, outcomes)
|
|
205
|
+
@subscriptions.close
|
|
206
|
+
# A retained scoped result needs the closed-scope gate, not all original
|
|
207
|
+
# inputs and the application start closure. A claimed start keeps its own
|
|
208
|
+
# local copy so cancellation during admission does not invalidate it.
|
|
209
|
+
@pump_mutex.synchronize do
|
|
210
|
+
@inputs = []
|
|
211
|
+
@block = nil
|
|
212
|
+
end
|
|
213
|
+
case kind
|
|
214
|
+
when :completed
|
|
215
|
+
@result.complete(outcomes)
|
|
216
|
+
when :timeout
|
|
217
|
+
@cancellation_error = CancellationError.new("Execution deadline exceeded")
|
|
218
|
+
error = ExecutionTimeoutError.new(outcomes: outcomes)
|
|
219
|
+
@token.cancel!
|
|
220
|
+
@result.fail(error)
|
|
221
|
+
when :cancelled
|
|
222
|
+
@cancellation_error = CancellationError.new("Execution cancelled")
|
|
223
|
+
error = ExecutionCancellationError.new(outcomes: outcomes)
|
|
224
|
+
@token.cancel!
|
|
225
|
+
@result.cancel!(error)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cancellation_error"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
class ExecutionCancellationError < CancellationError
|
|
7
|
+
attr_reader :outcomes
|
|
8
|
+
|
|
9
|
+
def initialize(message = "Execution cancelled", outcomes:)
|
|
10
|
+
@outcomes = outcomes
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Internal lifecycle/delivery contract for feature-owned execution state.
|
|
5
|
+
# Receivers are retained by one EventLoop, not an application callback SPI.
|
|
6
|
+
# Normal mutation and delivery run only on that loop. idle? runs with its
|
|
7
|
+
# lifecycle lock held; it must not call Runtime or acquire that lock again.
|
|
8
|
+
# shutdown runs on a failing loop, or exclusively after a clean loop joins.
|
|
9
|
+
# @api private
|
|
10
|
+
class ExecutionReceiver
|
|
11
|
+
def self.for(event_loop)
|
|
12
|
+
event_loop.__execution_receiver(key: self) ||
|
|
13
|
+
event_loop.__register_execution_receiver(key: self, receiver: new(event_loop: event_loop))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Lookup only: neither a loop nor a receiver is created during inspection.
|
|
17
|
+
def self.existing_for(runtime)
|
|
18
|
+
runtime.__event_loop_if_initialized&.__execution_receiver(key: self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(event_loop:)
|
|
22
|
+
@event_loop = event_loop
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def __bound_to?(event_loop)
|
|
26
|
+
@event_loop.equal?(event_loop)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def idle?
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def deliver(message)
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def shutdown(error:)
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def session_retired(fsm_session_id, reason:)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def synchronize(&block)
|
|
47
|
+
@event_loop.__synchronize_execution_state(&block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def admit(&block)
|
|
51
|
+
@event_loop.__admit_execution(self, &block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def post_message(message, admission: false, completion: nil)
|
|
55
|
+
@event_loop.__post_execution(self, message, admission: admission, completion: completion)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def assert_event_loop_thread!
|
|
59
|
+
return if @event_loop.current?
|
|
60
|
+
|
|
61
|
+
raise Phronomy::Error,
|
|
62
|
+
"Phronomy-managed live execution state may only be mutated on EventLoop"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "timeout_error"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
class ExecutionTimeoutError < TimeoutError
|
|
7
|
+
attr_reader :outcomes
|
|
8
|
+
|
|
9
|
+
def initialize(message = "Execution timed out", outcomes:)
|
|
10
|
+
@outcomes = outcomes
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Shared terminal-state vocabulary and domain decisions for FSM execution.
|
|
5
|
+
# This protocol does not depend on a particular runner or session instance.
|
|
6
|
+
# @api private
|
|
7
|
+
module FSMProtocol
|
|
8
|
+
FINISH = :__end__
|
|
9
|
+
|
|
10
|
+
# A domain policy permits completion, reports failure, or retires a session
|
|
11
|
+
# without settling its caller. It never carries live session authority.
|
|
12
|
+
TerminalDecision = Data.define(:action, :error)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "concurrency/worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
require "securerandom"
|
|
4
6
|
|
|
5
7
|
module Phronomy
|
|
6
8
|
# Event-driven execution wrapper for a single FSM session.
|
|
7
9
|
#
|
|
8
10
|
# All public methods are called from the Runtime-owned EventLoop thread.
|
|
9
|
-
# FSMSession owns FSM execution only; it does not own external
|
|
11
|
+
# FSMSession owns FSM execution only; it does not own external TaskResult handles,
|
|
10
12
|
# activity tokens, callback correlation, or domain-specific stale-event policy.
|
|
11
13
|
class FSMSession
|
|
14
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
15
|
+
|
|
12
16
|
class IdentityReservation
|
|
13
17
|
attr_reader :fsm_session_id
|
|
14
18
|
|
|
@@ -30,6 +34,8 @@ module Phronomy
|
|
|
30
34
|
private_constant :IdentityReservation
|
|
31
35
|
|
|
32
36
|
class EventSink
|
|
37
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
38
|
+
|
|
33
39
|
attr_reader :fsm_session_id
|
|
34
40
|
|
|
35
41
|
def initialize(event_loop:)
|
|
@@ -60,8 +66,6 @@ module Phronomy
|
|
|
60
66
|
end
|
|
61
67
|
end
|
|
62
68
|
|
|
63
|
-
FINISH = WorkflowRunner::FINISH
|
|
64
|
-
|
|
65
69
|
attr_reader :id, :context, :event_sink
|
|
66
70
|
|
|
67
71
|
# Returns the live current Workflow phase. During an active FSM transition
|
|
@@ -95,7 +99,7 @@ module Phronomy
|
|
|
95
99
|
context_metadata: {},
|
|
96
100
|
event_sink: nil,
|
|
97
101
|
identity_reservation: nil,
|
|
98
|
-
|
|
102
|
+
terminal_policy: nil
|
|
99
103
|
)
|
|
100
104
|
@id = if identity_reservation
|
|
101
105
|
unless identity_reservation.is_a?(IdentityReservation)
|
|
@@ -123,7 +127,7 @@ module Phronomy
|
|
|
123
127
|
@resume_event = resume_event
|
|
124
128
|
@resume_phase = resume_phase
|
|
125
129
|
@stable_observer = stable_observer
|
|
126
|
-
@
|
|
130
|
+
@terminal_policy = terminal_policy
|
|
127
131
|
@terminal_lifecycle_state = :running
|
|
128
132
|
@pending_terminal_type = nil
|
|
129
133
|
@pending_terminal_notify_stable = false
|
|
@@ -160,14 +164,13 @@ module Phronomy
|
|
|
160
164
|
def handle(event)
|
|
161
165
|
return if @done
|
|
162
166
|
|
|
163
|
-
if event
|
|
164
|
-
|
|
167
|
+
if @terminal_policy&.handles?(event)
|
|
168
|
+
handle_terminal_event(event)
|
|
165
169
|
return
|
|
166
170
|
end
|
|
167
171
|
|
|
168
|
-
# Once terminal
|
|
169
|
-
#
|
|
170
|
-
# FSMSession can advance its terminal lifecycle.
|
|
172
|
+
# Once a terminal decision is pending, ordinary events cannot advance
|
|
173
|
+
# the state machine. Only the injected policy can permit terminalization.
|
|
171
174
|
return unless @terminal_lifecycle_state == :running
|
|
172
175
|
|
|
173
176
|
context_disposition = apply_context_event(event)
|
|
@@ -195,9 +198,9 @@ module Phronomy
|
|
|
195
198
|
end
|
|
196
199
|
|
|
197
200
|
def apply_synchronous_action_result!(result, state_name)
|
|
198
|
-
if result.is_a?(Phronomy::
|
|
201
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
199
202
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
200
|
-
"Entry action for state #{state_name.inspect} returned Phronomy::
|
|
203
|
+
"Entry action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
|
|
201
204
|
"Start the asynchronous operation, register its callback/listener, " \
|
|
202
205
|
"and return the WorkflowContext or nil."
|
|
203
206
|
end
|
|
@@ -261,7 +264,7 @@ module Phronomy
|
|
|
261
264
|
end
|
|
262
265
|
|
|
263
266
|
def advance_or_halt
|
|
264
|
-
return finish! if @current_state == FINISH
|
|
267
|
+
return finish! if @current_state == FSMProtocol::FINISH
|
|
265
268
|
|
|
266
269
|
# A wait state is the logical end of this Workflow execution segment. Its
|
|
267
270
|
# public stable-state notification is therefore part of terminalization
|
|
@@ -324,7 +327,7 @@ module Phronomy
|
|
|
324
327
|
def finish!(notify_stable: false)
|
|
325
328
|
request_terminal!(
|
|
326
329
|
:finished,
|
|
327
|
-
phase:
|
|
330
|
+
phase: FSMProtocol::FINISH,
|
|
328
331
|
notify_stable: notify_stable
|
|
329
332
|
)
|
|
330
333
|
end
|
|
@@ -339,13 +342,13 @@ module Phronomy
|
|
|
339
342
|
apply_context_metadata!(phase: phase)
|
|
340
343
|
@pending_terminal_type = terminal_type
|
|
341
344
|
@pending_terminal_notify_stable = notify_stable
|
|
342
|
-
unless @
|
|
345
|
+
unless @terminal_policy
|
|
343
346
|
complete_terminal!(terminal_type)
|
|
344
347
|
return
|
|
345
348
|
end
|
|
346
349
|
|
|
347
|
-
@terminal_lifecycle_state = :
|
|
348
|
-
@
|
|
350
|
+
@terminal_lifecycle_state = :awaiting_terminal
|
|
351
|
+
@terminal_policy.start(
|
|
349
352
|
terminal_type: terminal_type,
|
|
350
353
|
context: @ctx,
|
|
351
354
|
event_sink: @event_sink
|
|
@@ -354,31 +357,35 @@ module Phronomy
|
|
|
354
357
|
finish_with_error(error)
|
|
355
358
|
end
|
|
356
359
|
|
|
357
|
-
def
|
|
358
|
-
return unless @terminal_lifecycle_state == :
|
|
360
|
+
def handle_terminal_event(event)
|
|
361
|
+
return unless @terminal_lifecycle_state == :awaiting_terminal
|
|
359
362
|
|
|
360
|
-
|
|
361
|
-
|
|
363
|
+
decision = @terminal_policy.decision_for(event)
|
|
364
|
+
case decision.action
|
|
365
|
+
when :complete
|
|
362
366
|
complete_terminal!(@pending_terminal_type)
|
|
363
|
-
when :
|
|
364
|
-
finish_with_error(
|
|
365
|
-
|
|
366
|
-
)
|
|
367
|
-
when :outcome_unknown
|
|
368
|
-
@done = true
|
|
369
|
-
@terminal_lifecycle_state = :recovery_required
|
|
370
|
-
post_recovery_required_event(result.error)
|
|
367
|
+
when :fail
|
|
368
|
+
finish_with_error(decision.error)
|
|
369
|
+
when :retire
|
|
370
|
+
retire_without_result!(decision.error)
|
|
371
371
|
else
|
|
372
372
|
raise Phronomy::Error,
|
|
373
|
-
"unknown
|
|
373
|
+
"unknown FSM terminal decision: #{decision.action.inspect}"
|
|
374
374
|
end
|
|
375
375
|
end
|
|
376
376
|
|
|
377
|
-
def
|
|
377
|
+
def retire_without_result!(error)
|
|
378
378
|
@done = true
|
|
379
|
+
@terminal_lifecycle_state = :retired
|
|
380
|
+
post_recovery_required_event(error)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def complete_terminal!(terminal_type)
|
|
379
384
|
@terminal_lifecycle_state =
|
|
380
385
|
(terminal_type == :halted) ? :halted : :completed
|
|
386
|
+
# Keep the error path open until the observer returns successfully.
|
|
381
387
|
notify_stable_state! if @pending_terminal_notify_stable
|
|
388
|
+
@done = true
|
|
382
389
|
post_terminal_event(terminal_type, @ctx)
|
|
383
390
|
end
|
|
384
391
|
|
data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb}
RENAMED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "invalid_async_workflow_action_error"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
|
-
# Raised when a synchronous FSM entry action returns Phronomy::
|
|
6
|
+
# Raised when a synchronous FSM entry action returns Phronomy::TaskResult.
|
|
5
7
|
#
|
|
6
8
|
# Entry actions are Run-to-Completion callbacks. They may start asynchronous
|
|
7
9
|
# work, but completion must return through a later explicit event.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "invalid_async_workflow_action_error"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
|
-
# Raised when a synchronous Workflow transition action returns Phronomy::
|
|
6
|
+
# Raised when a synchronous Workflow transition action returns Phronomy::TaskResult.
|
|
5
7
|
#
|
|
6
8
|
# Transition actions are Run-to-Completion callbacks. They may start
|
|
7
9
|
# asynchronous work, but completion must return through a later explicit event.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../common/error"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
|
-
# Base error for synchronous Workflow callbacks that return Phronomy::
|
|
6
|
+
# Base error for synchronous Workflow callbacks that return Phronomy::TaskResult.
|
|
5
7
|
#
|
|
6
8
|
# Workflow callbacks may start asynchronous work, but they must return
|
|
7
9
|
# synchronously and deliver completion through a later explicit event.
|
|
@@ -56,7 +56,19 @@ module Phronomy
|
|
|
56
56
|
redaction_policy: overrides.fetch(:redaction_policy, @redaction_policy),
|
|
57
57
|
task_id: overrides.fetch(:task_id, @task_id),
|
|
58
58
|
parent_task_id: overrides.fetch(:parent_task_id, @parent_task_id)
|
|
59
|
-
)
|
|
59
|
+
).__bind_execution(@execution_scope)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Framework-owned metadata; not inferred from the executing Ruby thread.
|
|
63
|
+
# @api private
|
|
64
|
+
def __bind_execution(execution)
|
|
65
|
+
@execution_scope = execution
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
def __execution_scope
|
|
71
|
+
@execution_scope
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
# @api private
|
|
@@ -44,6 +44,17 @@ module Phronomy
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Remove this registration without changing another user's timers. A
|
|
48
|
+
# callback already taken by fire_due may still run; owners arbitrate once.
|
|
49
|
+
# @api private
|
|
50
|
+
def cancel(callback)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
before = @heap.length
|
|
53
|
+
@heap.delete_if { |(_, registered)| registered == callback }
|
|
54
|
+
before != @heap.length
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
# Executes all callbacks whose deadline is due. Must be called by EventLoop.
|
|
48
59
|
def fire_due
|
|
49
60
|
callbacks = @mutex.synchronize do
|