phronomy 0.26.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 +40 -0
- data/README.md +4 -0
- data/VERIFY.sh +27 -27
- data/benchmark/bench_agent_invoke.rb +26 -22
- data/benchmark/bench_context_assembler.rb +4 -5
- 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 +4 -2
- 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 +27 -4
- 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 +1 -1
- data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
- data/docs/architecture.md +209 -0
- 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 +9 -3
- data/docs/getting-started.md +4 -4
- data/docs/migrations/0.15.md +5 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +6 -0
- 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 +117 -32
- 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 +51 -241
- 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} +19 -95
- 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} +9 -10
- 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} +20 -71
- 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 +32 -37
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
- data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +6 -10
- 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/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} +1 -1
- 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 +4 -0
- data/lib/phronomy/engine/concurrency/operation_binding.rb +1 -1
- data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
- data/lib/phronomy/engine/event_loop.rb +158 -624
- data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
- data/lib/phronomy/{execution.rb → engine/execution.rb} +0 -7
- data/lib/phronomy/{execution_cancellation_error.rb → engine/execution_cancellation_error.rb} +2 -0
- data/lib/phronomy/engine/execution_receiver.rb +65 -0
- data/lib/phronomy/{execution_timeout_error.rb → engine/execution_timeout_error.rb} +2 -0
- data/lib/phronomy/engine/fsm_protocol.rb +14 -0
- data/lib/phronomy/engine/fsm_session.rb +35 -28
- data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +2 -0
- 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.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_result.rb +4 -0
- 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_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 +5 -5
- data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
- data/lib/phronomy/multi_agent/orchestrator.rb +2 -2
- 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/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 +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +8 -6
- 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} +122 -71
- 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 +2 -2
- data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
- data/lib/phronomy.rb +52 -94
- data/scripts/api_snapshot.rb +1 -1
- data/scripts/storage_spi_snapshot.rb +36 -0
- data/sig/phronomy/agent.rbs +0 -1
- data/sig/phronomy/execution_receiver.rbs +34 -0
- data/sig/phronomy/handoff.rbs +4 -2
- data/sig/phronomy/multi_agent.rbs +18 -0
- data/sig/phronomy/persistence.rbs +7 -91
- data/sig/phronomy/storage.rbs +174 -0
- data/sig/phronomy/tool.rbs +10 -1
- metadata +230 -99
- data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
- 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/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/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.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/{invocation_context.rb → engine/invocation_context.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 "concurrency/worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
6
|
# Runtime-owned FIFO event loop for FSMSession instances.
|
|
5
7
|
#
|
|
@@ -7,6 +9,8 @@ module Phronomy
|
|
|
7
9
|
# lifecycle progression and Phronomy-managed live execution-state mutation
|
|
8
10
|
# happens by short event dispatches on this thread.
|
|
9
11
|
class EventLoop
|
|
12
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
13
|
+
|
|
10
14
|
SYSTEM_CHANNEL_ID = "__event_loop__"
|
|
11
15
|
|
|
12
16
|
QUEUE_BACKLOG_WARNING_THRESHOLD = 1_000
|
|
@@ -17,43 +21,7 @@ module Phronomy
|
|
|
17
21
|
|
|
18
22
|
STOP = Object.new.freeze
|
|
19
23
|
WAKE = Object.new.freeze
|
|
20
|
-
|
|
21
|
-
private_constant :STOP, :WAKE, :UNSET
|
|
22
|
-
|
|
23
|
-
# Immutable EventLoop-owned value. The map containing these records is the
|
|
24
|
-
# mutable authority; records are replaced rather than mutated in place.
|
|
25
|
-
AgentExecutionState = Data.define(
|
|
26
|
-
:execution_id,
|
|
27
|
-
:agent,
|
|
28
|
-
:coordinator,
|
|
29
|
-
:execution,
|
|
30
|
-
:runtime_projection,
|
|
31
|
-
:base_manifest,
|
|
32
|
-
:invocation,
|
|
33
|
-
:fsm_session_id
|
|
34
|
-
)
|
|
35
|
-
private_constant :AgentExecutionState
|
|
36
|
-
|
|
37
|
-
# Read-only process-local lookup view used by approval/live-owner APIs.
|
|
38
|
-
# It intentionally exposes no mutable execution, invocation, or projection.
|
|
39
|
-
AgentExecutionOwner = Data.define(:execution_id, :agent, :coordinator, :status)
|
|
40
|
-
private_constant :AgentExecutionOwner
|
|
41
|
-
|
|
42
|
-
# EventLoop-owned process-local top-level execution admission. This is
|
|
43
|
-
# separate from AgentExecutionState because admission begins before the
|
|
44
|
-
# durable AgentExecution exists. owner_token is coordination-only and is
|
|
45
|
-
# never a semantic result-authority identifier.
|
|
46
|
-
AgentAdmission = Data.define(:agent_id, :owner_token, :execution_id, :state)
|
|
47
|
-
private_constant :AgentAdmission
|
|
48
|
-
|
|
49
|
-
# EventLoop-owned process-local Workflow execution-segment admission. The
|
|
50
|
-
# owner token is a Runtime coordination capability, not a domain identity or
|
|
51
|
-
# routing identity. fsm_session_id is bound only after durable load/hydration
|
|
52
|
-
# completes and a concrete FSMSession has been constructed.
|
|
53
|
-
WorkflowAdmission = Data.define(
|
|
54
|
-
:workflow_instance_id, :owner_token, :fsm_session_id, :state
|
|
55
|
-
)
|
|
56
|
-
private_constant :WorkflowAdmission
|
|
24
|
+
private_constant :STOP, :WAKE
|
|
57
25
|
|
|
58
26
|
def initialize(runtime:)
|
|
59
27
|
@runtime = runtime
|
|
@@ -63,15 +31,14 @@ module Phronomy
|
|
|
63
31
|
@max_queue_depth = 0
|
|
64
32
|
@last_queue_backlog_warning_at = nil
|
|
65
33
|
|
|
34
|
+
@execution_receivers = {}
|
|
35
|
+
@execution_deliveries = {}
|
|
36
|
+
@session_receivers = {}
|
|
37
|
+
@dispatching_delivery = nil
|
|
38
|
+
@receiver_shutdown_error = nil
|
|
66
39
|
@fsms = {}
|
|
67
40
|
@waiting = {}
|
|
68
41
|
@admitted_fsm_session_ids = Set.new
|
|
69
|
-
@workflow_admissions = {}
|
|
70
|
-
@agent_admissions = {}
|
|
71
|
-
@agent_executions = {}
|
|
72
|
-
@agent_completion_waiters = Hash.new { |hash, key| hash[key] = [] }
|
|
73
|
-
@agent_inflight_work = Hash.new { |hash, key| hash[key] = {} }
|
|
74
|
-
@agent_deferred_terminals = {}
|
|
75
42
|
|
|
76
43
|
@lifecycle_mutex = Mutex.new
|
|
77
44
|
@idle_cond = ConditionVariable.new
|
|
@@ -113,7 +80,93 @@ module Phronomy
|
|
|
113
80
|
@queue_metrics_mutex.synchronize { @max_queue_depth }
|
|
114
81
|
end
|
|
115
82
|
|
|
116
|
-
|
|
83
|
+
# Internal receiver registration is closed before shutdown tests idleness.
|
|
84
|
+
# Feature constructors are side-effect free and run outside this lock.
|
|
85
|
+
# @api private
|
|
86
|
+
def __register_execution_receiver(key:, receiver:)
|
|
87
|
+
unless receiver.is_a?(Phronomy::ExecutionReceiver) && receiver.__bound_to?(self)
|
|
88
|
+
raise ArgumentError, "execution receiver must implement the Engine contract for this EventLoop"
|
|
89
|
+
end
|
|
90
|
+
@lifecycle_mutex.synchronize do
|
|
91
|
+
unless @state == :running
|
|
92
|
+
raise Phronomy::RuntimeShutdownError,
|
|
93
|
+
"EventLoop is #{@state}; execution receivers cannot be registered"
|
|
94
|
+
end
|
|
95
|
+
@execution_receivers[key] ||= receiver
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @api private
|
|
100
|
+
def __execution_receiver(key:)
|
|
101
|
+
@lifecycle_mutex.synchronize { @execution_receivers[key] }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The same lock covers feature state and Engine's idle decision. Blocks are
|
|
105
|
+
# short state reads/writes, never I/O, delivery callbacks or Runtime calls.
|
|
106
|
+
# @api private
|
|
107
|
+
def __synchronize_execution_state
|
|
108
|
+
@lifecycle_mutex.synchronize do
|
|
109
|
+
yield
|
|
110
|
+
ensure
|
|
111
|
+
@idle_cond.broadcast if runtime_idle_locked?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Admission already queued while running may establish its feature slot
|
|
116
|
+
# during drain. A newly requested admission may not do so.
|
|
117
|
+
# @api private
|
|
118
|
+
def __admit_execution(receiver)
|
|
119
|
+
assert_event_loop_thread!
|
|
120
|
+
__synchronize_execution_state do
|
|
121
|
+
accepted_delivery = @dispatching_delivery &&
|
|
122
|
+
@dispatching_delivery[:receiver].equal?(receiver) &&
|
|
123
|
+
@dispatching_delivery[:admission]
|
|
124
|
+
accepting = @execution_receivers.value?(receiver) &&
|
|
125
|
+
(@state == :running || (@state == :draining && accepted_delivery))
|
|
126
|
+
unless accepting
|
|
127
|
+
raise Phronomy::RuntimeShutdownError,
|
|
128
|
+
"EventLoop is #{@state}; new executions are not accepted"
|
|
129
|
+
end
|
|
130
|
+
yield
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Count accepted deliveries until dispatch finishes, including the interval
|
|
135
|
+
# before a feature admission or FSM exists. Continuations use registered
|
|
136
|
+
# receivers; new admissions require the running state.
|
|
137
|
+
# @api private
|
|
138
|
+
def __post_execution(receiver, message, admission: false, completion: nil)
|
|
139
|
+
token = Object.new.freeze
|
|
140
|
+
event = Phronomy::Event.new(type: :execution_control,
|
|
141
|
+
target_id: SYSTEM_CHANNEL_ID, payload: token)
|
|
142
|
+
queued_depth = nil
|
|
143
|
+
accepted = @lifecycle_mutex.synchronize do
|
|
144
|
+
next false unless accepting_events? && @execution_receivers.value?(receiver)
|
|
145
|
+
next false if admission && @state != :running
|
|
146
|
+
|
|
147
|
+
@execution_deliveries[token] = {
|
|
148
|
+
receiver: receiver, message: message, admission: admission, completion: completion
|
|
149
|
+
}.freeze
|
|
150
|
+
begin
|
|
151
|
+
queued_depth = enqueue([event, monotonic_nanoseconds])
|
|
152
|
+
rescue
|
|
153
|
+
@execution_deliveries.delete(token)
|
|
154
|
+
raise
|
|
155
|
+
end
|
|
156
|
+
true
|
|
157
|
+
end
|
|
158
|
+
return false unless accepted
|
|
159
|
+
|
|
160
|
+
check_queue_backlog(queued_depth, event)
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @api private
|
|
165
|
+
def __receiver_cleanup_complete?
|
|
166
|
+
@receiver_shutdown_error.nil?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def register(fsm_session, completion: nil, receiver: nil)
|
|
117
170
|
if current? && !completion.is_a?(Phronomy::TaskResult)
|
|
118
171
|
raise Phronomy::Error,
|
|
119
172
|
"Cannot call a synchronous invocation API from an EventLoop action. " \
|
|
@@ -135,11 +188,16 @@ module Phronomy
|
|
|
135
188
|
"FSMSession #{fsm_session.id.inspect} is already registered"
|
|
136
189
|
end
|
|
137
190
|
|
|
191
|
+
if receiver && !@execution_receivers.value?(receiver)
|
|
192
|
+
raise ArgumentError, "execution receiver is not registered on this EventLoop"
|
|
193
|
+
end
|
|
194
|
+
@session_receivers[fsm_session.id] = receiver if receiver
|
|
138
195
|
@admitted_fsm_session_ids.add(fsm_session.id)
|
|
139
196
|
@outstanding_sessions += 1
|
|
140
197
|
begin
|
|
141
198
|
queued_depth = enqueue([event, monotonic_nanoseconds])
|
|
142
199
|
rescue
|
|
200
|
+
@session_receivers.delete(fsm_session.id)
|
|
143
201
|
@admitted_fsm_session_ids.delete(fsm_session.id)
|
|
144
202
|
@outstanding_sessions -= 1
|
|
145
203
|
@idle_cond.broadcast if runtime_idle_locked?
|
|
@@ -195,504 +253,32 @@ module Phronomy
|
|
|
195
253
|
true
|
|
196
254
|
end
|
|
197
255
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
def agent_execution_admitted?(agent_id)
|
|
201
|
-
@lifecycle_mutex.synchronize { @agent_admissions.key?(agent_id.to_s) }
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Reserves the one top-level logical execution slot for agent_id before any
|
|
205
|
-
# Persistence execution admission is attempted.
|
|
256
|
+
# Resolve feature identity and enqueue to a live FSM under one lock. The
|
|
257
|
+
# receiver supplies only a short lookup; dispatch still uses the FIFO.
|
|
206
258
|
# @api private
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
ensure_accepting_registrations!
|
|
215
|
-
if @agent_admissions.key?(key)
|
|
216
|
-
raise Phronomy::AgentBusyError,
|
|
217
|
-
"Agent #{key.inspect} already has a nonterminal top-level execution"
|
|
218
|
-
end
|
|
219
|
-
@agent_admissions[key] = AgentAdmission.new(
|
|
220
|
-
agent_id: key.freeze,
|
|
221
|
-
owner_token: owner_token,
|
|
222
|
-
execution_id: nil,
|
|
223
|
-
state: :admitting
|
|
224
|
-
)
|
|
225
|
-
end
|
|
226
|
-
true
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Binds a successful durable AgentExecution identity to the earlier
|
|
230
|
-
# process-local admission.
|
|
231
|
-
# @api private
|
|
232
|
-
def bind_agent_execution_admission(agent_id, owner_token:, execution_id:)
|
|
233
|
-
assert_event_loop_thread!
|
|
234
|
-
key = agent_id.to_s
|
|
235
|
-
execution_key = execution_id.to_s
|
|
236
|
-
@lifecycle_mutex.synchronize do
|
|
237
|
-
current = @agent_admissions.fetch(key) do
|
|
238
|
-
raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
|
|
239
|
-
end
|
|
240
|
-
unless current.owner_token.equal?(owner_token) && current.execution_id.nil?
|
|
241
|
-
raise Phronomy::Error, "stale Agent admission bind for #{key.inspect}"
|
|
242
|
-
end
|
|
243
|
-
@agent_admissions[key] = AgentAdmission.new(
|
|
244
|
-
agent_id: current.agent_id,
|
|
245
|
-
owner_token: current.owner_token,
|
|
246
|
-
execution_id: execution_key.freeze,
|
|
247
|
-
state: :executing
|
|
248
|
-
)
|
|
249
|
-
end
|
|
250
|
-
true
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# @api private
|
|
254
|
-
def mark_agent_execution_admission(agent_id, execution_id:, state:)
|
|
255
|
-
assert_event_loop_thread!
|
|
256
|
-
key = agent_id.to_s
|
|
257
|
-
execution_key = execution_id.to_s
|
|
258
|
-
next_state = state.to_sym
|
|
259
|
-
unless %i[executing suspended resuming cancelling terminalizing recovery_required].include?(next_state)
|
|
260
|
-
raise ArgumentError, "unsupported Agent admission state: #{next_state.inspect}"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
@lifecycle_mutex.synchronize do
|
|
264
|
-
current = @agent_admissions.fetch(key) do
|
|
265
|
-
raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
|
|
266
|
-
end
|
|
267
|
-
unless current.execution_id.to_s == execution_key
|
|
268
|
-
raise Phronomy::Error, "stale Agent admission state update for #{key.inspect}"
|
|
269
|
-
end
|
|
270
|
-
@agent_admissions[key] = AgentAdmission.new(
|
|
271
|
-
agent_id: current.agent_id,
|
|
272
|
-
owner_token: current.owner_token,
|
|
273
|
-
execution_id: current.execution_id,
|
|
274
|
-
state: next_state
|
|
275
|
-
)
|
|
276
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
277
|
-
end
|
|
278
|
-
true
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
# @api private
|
|
282
|
-
def mark_agent_admission_recovery_required(agent_id, owner_token:)
|
|
283
|
-
assert_event_loop_thread!
|
|
284
|
-
key = agent_id.to_s
|
|
285
|
-
@lifecycle_mutex.synchronize do
|
|
286
|
-
current = @agent_admissions.fetch(key) do
|
|
287
|
-
raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
|
|
288
|
-
end
|
|
289
|
-
unless current.owner_token.equal?(owner_token)
|
|
290
|
-
raise Phronomy::Error, "stale Agent admission recovery update for #{key.inspect}"
|
|
291
|
-
end
|
|
292
|
-
@agent_admissions[key] = AgentAdmission.new(
|
|
293
|
-
agent_id: current.agent_id,
|
|
294
|
-
owner_token: current.owner_token,
|
|
295
|
-
execution_id: current.execution_id,
|
|
296
|
-
state: :recovery_required
|
|
297
|
-
)
|
|
298
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
299
|
-
end
|
|
300
|
-
true
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
# Owner-aware release. Pre-durable failures release by owner_token; durable
|
|
304
|
-
# terminal outcomes release by execution_id.
|
|
305
|
-
# @api private
|
|
306
|
-
def release_agent_execution_admission(agent_id, owner_token: nil, execution_id: nil)
|
|
307
|
-
assert_event_loop_thread!
|
|
308
|
-
key = agent_id.to_s
|
|
309
|
-
@lifecycle_mutex.synchronize do
|
|
310
|
-
current = @agent_admissions[key]
|
|
311
|
-
next false unless current
|
|
312
|
-
|
|
313
|
-
authoritative = if execution_id
|
|
314
|
-
current.execution_id.to_s == execution_id.to_s
|
|
315
|
-
elsif owner_token
|
|
316
|
-
current.owner_token.equal?(owner_token)
|
|
317
|
-
else
|
|
318
|
-
false
|
|
319
|
-
end
|
|
320
|
-
next false unless authoritative
|
|
259
|
+
def __route_execution_event(receiver)
|
|
260
|
+
event = nil
|
|
261
|
+
queued_depth = nil
|
|
262
|
+
accepted = @lifecycle_mutex.synchronize do
|
|
263
|
+
next false unless accepting_events? && @execution_receivers.value?(receiver)
|
|
264
|
+
event = yield
|
|
265
|
+
next false unless event && @admitted_fsm_session_ids.include?(event.target_id)
|
|
321
266
|
|
|
322
|
-
|
|
323
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
267
|
+
queued_depth = enqueue([event, monotonic_nanoseconds])
|
|
324
268
|
true
|
|
325
269
|
end
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# Process-local read-only owner lookup. Mutable Agent execution state never
|
|
329
|
-
# crosses this boundary; external callers receive only routing/ownership data.
|
|
330
|
-
def agent_execution_owner(execution_id)
|
|
331
|
-
key = execution_id.to_s
|
|
332
|
-
@lifecycle_mutex.synchronize do
|
|
333
|
-
state = @agent_executions[key]
|
|
334
|
-
next nil unless state
|
|
335
|
-
|
|
336
|
-
AgentExecutionOwner.new(
|
|
337
|
-
execution_id: key.freeze,
|
|
338
|
-
agent: state.agent,
|
|
339
|
-
coordinator: state.coordinator,
|
|
340
|
-
status: state.execution.status
|
|
341
|
-
)
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# EventLoop-only accessors below form the live Agent execution authority.
|
|
346
|
-
# Offload workers receive operation-specific immutable snapshots instead.
|
|
347
|
-
# @api private
|
|
348
|
-
def agent_execution_state(execution_id)
|
|
349
|
-
assert_event_loop_thread!
|
|
350
|
-
@agent_executions[execution_id.to_s]
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# @api private
|
|
354
|
-
def install_agent_execution(
|
|
355
|
-
execution_id:,
|
|
356
|
-
agent:,
|
|
357
|
-
coordinator:,
|
|
358
|
-
execution:,
|
|
359
|
-
runtime_projection:,
|
|
360
|
-
base_manifest:,
|
|
361
|
-
invocation:,
|
|
362
|
-
fsm_session_id:
|
|
363
|
-
)
|
|
364
|
-
assert_event_loop_thread!
|
|
365
|
-
key = execution_id.to_s
|
|
366
|
-
state = AgentExecutionState.new(
|
|
367
|
-
execution_id: key.freeze,
|
|
368
|
-
agent: agent,
|
|
369
|
-
coordinator: coordinator,
|
|
370
|
-
execution: execution,
|
|
371
|
-
runtime_projection: runtime_projection,
|
|
372
|
-
base_manifest: base_manifest,
|
|
373
|
-
invocation: invocation,
|
|
374
|
-
fsm_session_id: fsm_session_id&.to_s&.freeze
|
|
375
|
-
)
|
|
376
|
-
@lifecycle_mutex.synchronize do
|
|
377
|
-
if @agent_executions.key?(key)
|
|
378
|
-
raise Phronomy::Error, "Agent execution #{key.inspect} is already live"
|
|
379
|
-
end
|
|
380
|
-
@agent_executions[key] = state
|
|
381
|
-
end
|
|
382
|
-
state
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
# @api private
|
|
386
|
-
def replace_agent_execution(
|
|
387
|
-
execution_id,
|
|
388
|
-
execution: UNSET,
|
|
389
|
-
runtime_projection: UNSET,
|
|
390
|
-
invocation: UNSET,
|
|
391
|
-
fsm_session_id: UNSET
|
|
392
|
-
)
|
|
393
|
-
assert_event_loop_thread!
|
|
394
|
-
key = execution_id.to_s
|
|
395
|
-
@lifecycle_mutex.synchronize do
|
|
396
|
-
current = @agent_executions.fetch(key) do
|
|
397
|
-
raise Phronomy::Error, "Agent execution #{key.inspect} is not live"
|
|
398
|
-
end
|
|
399
|
-
updated = AgentExecutionState.new(
|
|
400
|
-
execution_id: current.execution_id,
|
|
401
|
-
agent: current.agent,
|
|
402
|
-
coordinator: current.coordinator,
|
|
403
|
-
execution: execution.equal?(UNSET) ? current.execution : execution,
|
|
404
|
-
runtime_projection: runtime_projection.equal?(UNSET) ?
|
|
405
|
-
current.runtime_projection : runtime_projection,
|
|
406
|
-
base_manifest: current.base_manifest,
|
|
407
|
-
invocation: invocation.equal?(UNSET) ? current.invocation : invocation,
|
|
408
|
-
fsm_session_id: fsm_session_id.equal?(UNSET) ?
|
|
409
|
-
current.fsm_session_id : fsm_session_id&.to_s&.freeze
|
|
410
|
-
)
|
|
411
|
-
@agent_executions[key] = updated
|
|
412
|
-
updated
|
|
413
|
-
end
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
# Registers a caller-facing TaskResult that observes the authoritative terminal
|
|
417
|
-
# outcome of one logical Agent execution. Waiters are Runtime-only and are
|
|
418
|
-
# never persisted or rehydrated.
|
|
419
|
-
# @api private
|
|
420
|
-
def register_agent_completion_waiter(execution_id, task)
|
|
421
|
-
assert_event_loop_thread!
|
|
422
|
-
unless task.is_a?(Phronomy::TaskResult)
|
|
423
|
-
raise ArgumentError, "Agent completion waiter must be a Phronomy::TaskResult"
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
key = execution_id.to_s
|
|
427
|
-
@lifecycle_mutex.synchronize do
|
|
428
|
-
waiters = @agent_completion_waiters[key]
|
|
429
|
-
waiters << task unless waiters.include?(task)
|
|
430
|
-
end
|
|
431
|
-
task
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
# Atomically detaches all process-local completion waiters at authoritative
|
|
435
|
-
# terminal delivery. A fallback TaskResult is included for pre-install terminal
|
|
436
|
-
# paths that never acquired a live execution directory entry.
|
|
437
|
-
# @api private
|
|
438
|
-
def take_agent_completion_waiters(execution_id, fallback: nil)
|
|
439
|
-
assert_event_loop_thread!
|
|
440
|
-
key = execution_id.to_s
|
|
441
|
-
@lifecycle_mutex.synchronize do
|
|
442
|
-
waiters = @agent_completion_waiters.delete(key) || []
|
|
443
|
-
waiters << fallback if fallback && !waiters.include?(fallback)
|
|
444
|
-
waiters
|
|
445
|
-
end
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
# Registers one execution-owned asynchronous operation for physical
|
|
449
|
-
# quiescence supervision. OffloadPool tasks expose a private physical
|
|
450
|
-
# completion signal; custom asynchronous handles are required to make their
|
|
451
|
-
# ordinary completion mean that no residual execution-affecting work remains.
|
|
452
|
-
# @api private
|
|
453
|
-
def supervise_agent_operation(execution_id, operation)
|
|
454
|
-
assert_event_loop_thread!
|
|
455
|
-
key = execution_id.to_s
|
|
456
|
-
unless operation.respond_to?(:on_complete)
|
|
457
|
-
raise ArgumentError, "supervised operation must expose on_complete"
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
physically_done = if operation.respond_to?(:physical_complete?)
|
|
461
|
-
operation.physical_complete?
|
|
462
|
-
elsif operation.respond_to?(:done?)
|
|
463
|
-
operation.done?
|
|
464
|
-
else
|
|
465
|
-
false
|
|
466
|
-
end
|
|
467
|
-
return operation if physically_done
|
|
468
|
-
|
|
469
|
-
token = Object.new.freeze
|
|
470
|
-
@lifecycle_mutex.synchronize do
|
|
471
|
-
unless @agent_executions.key?(key)
|
|
472
|
-
raise Phronomy::Error, "Agent execution #{key.inspect} is not live"
|
|
473
|
-
end
|
|
474
|
-
@agent_inflight_work[key][token] = true
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
callback = lambda do
|
|
478
|
-
accepted = post(
|
|
479
|
-
Phronomy::Event.new(
|
|
480
|
-
type: :agent_physical_work_completed,
|
|
481
|
-
target_id: SYSTEM_CHANNEL_ID,
|
|
482
|
-
payload: {execution_id: key, token: token}.freeze
|
|
483
|
-
)
|
|
484
|
-
)
|
|
485
|
-
unless accepted
|
|
486
|
-
Phronomy.configuration.logger&.warn(
|
|
487
|
-
"[Phronomy] EventLoop rejected physical-completion delivery for #{key}"
|
|
488
|
-
)
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
if operation.respond_to?(:on_physical_complete)
|
|
493
|
-
operation.on_physical_complete(&callback)
|
|
494
|
-
else
|
|
495
|
-
operation.on_complete { |_value, _error| callback.call }
|
|
496
|
-
end
|
|
497
|
-
operation
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# @api private
|
|
501
|
-
def agent_execution_quiescent?(execution_id)
|
|
502
|
-
assert_event_loop_thread!
|
|
503
|
-
key = execution_id.to_s
|
|
504
|
-
@lifecycle_mutex.synchronize do
|
|
505
|
-
work = @agent_inflight_work.fetch(key, nil)
|
|
506
|
-
work.nil? || work.empty?
|
|
507
|
-
end
|
|
508
|
-
end
|
|
270
|
+
return false unless accepted
|
|
509
271
|
|
|
510
|
-
|
|
511
|
-
# revoked result authority but execution-owned physical work is still live.
|
|
512
|
-
# @api private
|
|
513
|
-
def defer_agent_terminal_until_quiescent(execution_id, command)
|
|
514
|
-
assert_event_loop_thread!
|
|
515
|
-
key = execution_id.to_s
|
|
516
|
-
@lifecycle_mutex.synchronize do
|
|
517
|
-
if @agent_deferred_terminals.key?(key)
|
|
518
|
-
raise Phronomy::Error, "Agent execution #{key.inspect} already has a deferred terminal"
|
|
519
|
-
end
|
|
520
|
-
@agent_deferred_terminals[key] = command
|
|
521
|
-
end
|
|
272
|
+
check_queue_backlog(queued_depth, event)
|
|
522
273
|
true
|
|
523
274
|
end
|
|
524
275
|
|
|
525
|
-
# @api private
|
|
526
|
-
def agent_inflight_work_count(execution_id)
|
|
527
|
-
key = execution_id.to_s
|
|
528
|
-
@lifecycle_mutex.synchronize do
|
|
529
|
-
(@agent_inflight_work.fetch(key, nil) || {}).size
|
|
530
|
-
end
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
# @api private
|
|
534
|
-
def release_agent_execution(execution_id)
|
|
535
|
-
assert_event_loop_thread!
|
|
536
|
-
key = execution_id.to_s
|
|
537
|
-
@lifecycle_mutex.synchronize do
|
|
538
|
-
work = @agent_inflight_work.fetch(key, nil)
|
|
539
|
-
unless work.nil? || work.empty?
|
|
540
|
-
raise Phronomy::Error,
|
|
541
|
-
"cannot release non-quiescent Agent execution #{key.inspect}"
|
|
542
|
-
end
|
|
543
|
-
if @agent_deferred_terminals.key?(key)
|
|
544
|
-
raise Phronomy::Error,
|
|
545
|
-
"cannot release Agent execution #{key.inspect} with deferred terminal work"
|
|
546
|
-
end
|
|
547
|
-
@agent_inflight_work.delete(key)
|
|
548
|
-
@agent_executions.delete(key)
|
|
549
|
-
end
|
|
550
|
-
end
|
|
551
|
-
|
|
552
276
|
# @api private
|
|
553
277
|
def fsm_session_state(fsm_session_id)
|
|
554
278
|
assert_event_loop_thread!
|
|
555
279
|
@fsms[fsm_session_id.to_s]&.current_state
|
|
556
280
|
end
|
|
557
281
|
|
|
558
|
-
# Reserves one logical Workflow execution segment before durable load or
|
|
559
|
-
# hydration. The owner token is independent from any later FSMSession id.
|
|
560
|
-
# @api private
|
|
561
|
-
def admit_workflow(workflow_instance_id, owner_token:)
|
|
562
|
-
assert_event_loop_thread!
|
|
563
|
-
key = workflow_instance_id.to_s
|
|
564
|
-
raise ArgumentError, "workflow_instance_id must not be empty" if key.empty?
|
|
565
|
-
raise ArgumentError, "owner_token is required" unless owner_token
|
|
566
|
-
|
|
567
|
-
@lifecycle_mutex.synchronize do
|
|
568
|
-
ensure_accepting_registrations!
|
|
569
|
-
if @workflow_admissions.key?(key)
|
|
570
|
-
raise Phronomy::Error,
|
|
571
|
-
"Workflow instance #{key.inspect} already has a live execution segment"
|
|
572
|
-
end
|
|
573
|
-
@workflow_admissions[key] = WorkflowAdmission.new(
|
|
574
|
-
workflow_instance_id: key.freeze,
|
|
575
|
-
owner_token: owner_token,
|
|
576
|
-
fsm_session_id: nil,
|
|
577
|
-
state: :admitting
|
|
578
|
-
)
|
|
579
|
-
end
|
|
580
|
-
true
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
# Binds the concrete routing identity after admission and durable hydration.
|
|
584
|
-
# @api private
|
|
585
|
-
def bind_workflow_session(workflow_instance_id, owner_token:, fsm_session_id:)
|
|
586
|
-
assert_event_loop_thread!
|
|
587
|
-
key = workflow_instance_id.to_s
|
|
588
|
-
fsm_key = fsm_session_id.to_s
|
|
589
|
-
raise ArgumentError, "fsm_session_id must not be empty" if fsm_key.empty?
|
|
590
|
-
|
|
591
|
-
@lifecycle_mutex.synchronize do
|
|
592
|
-
current = @workflow_admissions.fetch(key) do
|
|
593
|
-
raise Phronomy::Error, "Workflow instance #{key.inspect} has no Runtime admission"
|
|
594
|
-
end
|
|
595
|
-
unless current.owner_token.equal?(owner_token) && current.fsm_session_id.nil?
|
|
596
|
-
raise Phronomy::Error, "stale Workflow admission bind for #{key.inspect}"
|
|
597
|
-
end
|
|
598
|
-
@workflow_admissions[key] = WorkflowAdmission.new(
|
|
599
|
-
workflow_instance_id: current.workflow_instance_id,
|
|
600
|
-
owner_token: current.owner_token,
|
|
601
|
-
fsm_session_id: fsm_key.freeze,
|
|
602
|
-
state: :executing
|
|
603
|
-
)
|
|
604
|
-
end
|
|
605
|
-
true
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
# @api private
|
|
609
|
-
def mark_workflow_admission(workflow_instance_id, owner_token:, state:)
|
|
610
|
-
assert_event_loop_thread!
|
|
611
|
-
key = workflow_instance_id.to_s
|
|
612
|
-
next_state = state.to_sym
|
|
613
|
-
unless %i[executing persisting_terminal recovery_required].include?(next_state)
|
|
614
|
-
raise ArgumentError, "unsupported Workflow admission state: #{next_state.inspect}"
|
|
615
|
-
end
|
|
616
|
-
|
|
617
|
-
@lifecycle_mutex.synchronize do
|
|
618
|
-
current = @workflow_admissions.fetch(key) do
|
|
619
|
-
raise Phronomy::Error, "Workflow instance #{key.inspect} has no Runtime admission"
|
|
620
|
-
end
|
|
621
|
-
unless current.owner_token.equal?(owner_token)
|
|
622
|
-
raise Phronomy::Error, "stale Workflow admission update for #{key.inspect}"
|
|
623
|
-
end
|
|
624
|
-
@workflow_admissions[key] = WorkflowAdmission.new(
|
|
625
|
-
workflow_instance_id: current.workflow_instance_id,
|
|
626
|
-
owner_token: current.owner_token,
|
|
627
|
-
fsm_session_id: current.fsm_session_id,
|
|
628
|
-
state: next_state
|
|
629
|
-
)
|
|
630
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
631
|
-
end
|
|
632
|
-
true
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
# Owner-aware release. A competing or stale attempt cannot release the
|
|
636
|
-
# current Workflow execution segment.
|
|
637
|
-
# @api private
|
|
638
|
-
def release_workflow(workflow_instance_id, owner_token:)
|
|
639
|
-
assert_event_loop_thread!
|
|
640
|
-
key = workflow_instance_id.to_s
|
|
641
|
-
@lifecycle_mutex.synchronize do
|
|
642
|
-
current = @workflow_admissions[key]
|
|
643
|
-
next false unless current&.owner_token&.equal?(owner_token)
|
|
644
|
-
|
|
645
|
-
@workflow_admissions.delete(key)
|
|
646
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
647
|
-
true
|
|
648
|
-
end
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
# Read-only diagnostics used by internal tests and routing assertions.
|
|
652
|
-
def workflow_admission_owner(workflow_instance_id)
|
|
653
|
-
@lifecycle_mutex.synchronize do
|
|
654
|
-
@workflow_admissions[workflow_instance_id.to_s]&.owner_token
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
def workflow_admission_fsm_session_id(workflow_instance_id)
|
|
659
|
-
@lifecycle_mutex.synchronize do
|
|
660
|
-
@workflow_admissions[workflow_instance_id.to_s]&.fsm_session_id
|
|
661
|
-
end
|
|
662
|
-
end
|
|
663
|
-
|
|
664
|
-
def workflow_admission_state(workflow_instance_id)
|
|
665
|
-
@lifecycle_mutex.synchronize do
|
|
666
|
-
@workflow_admissions[workflow_instance_id.to_s]&.state
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
def post_to_workflow(workflow_instance_id:, event:, payload: nil)
|
|
671
|
-
queued_depth = nil
|
|
672
|
-
posted_event = nil
|
|
673
|
-
accepted = @lifecycle_mutex.synchronize do
|
|
674
|
-
next false unless accepting_events?
|
|
675
|
-
|
|
676
|
-
admission = @workflow_admissions[workflow_instance_id.to_s]
|
|
677
|
-
next false unless admission&.state == :executing
|
|
678
|
-
fsm_session_id = admission.fsm_session_id
|
|
679
|
-
next false unless fsm_session_id
|
|
680
|
-
next false unless @admitted_fsm_session_ids.include?(fsm_session_id)
|
|
681
|
-
|
|
682
|
-
posted_event = Phronomy::Event.new(
|
|
683
|
-
type: event.to_sym,
|
|
684
|
-
target_id: fsm_session_id,
|
|
685
|
-
payload: payload
|
|
686
|
-
)
|
|
687
|
-
queued_depth = enqueue([posted_event, monotonic_nanoseconds])
|
|
688
|
-
true
|
|
689
|
-
end
|
|
690
|
-
return false unless accepted
|
|
691
|
-
|
|
692
|
-
check_queue_backlog(queued_depth, posted_event)
|
|
693
|
-
true
|
|
694
|
-
end
|
|
695
|
-
|
|
696
282
|
def wake
|
|
697
283
|
@queue.push(WAKE)
|
|
698
284
|
true
|
|
@@ -770,6 +356,23 @@ module Phronomy
|
|
|
770
356
|
|
|
771
357
|
private
|
|
772
358
|
|
|
359
|
+
def dispatch_execution(token)
|
|
360
|
+
delivery = @lifecycle_mutex.synchronize { @execution_deliveries[token] }
|
|
361
|
+
return unless delivery
|
|
362
|
+
|
|
363
|
+
@dispatching_delivery = delivery
|
|
364
|
+
delivery[:receiver].deliver(delivery[:message])
|
|
365
|
+
rescue => error
|
|
366
|
+
complete_waiter(delivery[:completion], error) if delivery
|
|
367
|
+
raise
|
|
368
|
+
ensure
|
|
369
|
+
@dispatching_delivery = nil
|
|
370
|
+
@lifecycle_mutex.synchronize do
|
|
371
|
+
@execution_deliveries.delete(token)
|
|
372
|
+
@idle_cond.broadcast if runtime_idle_locked?
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
773
376
|
def run_loop
|
|
774
377
|
loop do
|
|
775
378
|
fire_due_timers
|
|
@@ -824,6 +427,7 @@ module Phronomy
|
|
|
824
427
|
session = @fsms.delete(fsm_session_id)
|
|
825
428
|
waiter = @waiting.delete(fsm_session_id)
|
|
826
429
|
decrement_outstanding if session
|
|
430
|
+
@lifecycle_mutex.synchronize { @session_receivers.delete(fsm_session_id) }
|
|
827
431
|
complete_waiter(waiter, event.payload.fetch(:result))
|
|
828
432
|
when :start
|
|
829
433
|
session = event.payload.fetch(:session)
|
|
@@ -831,21 +435,14 @@ module Phronomy
|
|
|
831
435
|
@fsms[session.id] = session
|
|
832
436
|
@waiting[session.id] = waiter if waiter
|
|
833
437
|
session.start
|
|
834
|
-
when :
|
|
835
|
-
|
|
836
|
-
# dispatch name; ACS-11 emits the operation-neutral :agent_control event.
|
|
837
|
-
cmd = event.payload.fetch(:command)
|
|
838
|
-
cmd.coordinator.deliver_on_event_loop(cmd)
|
|
839
|
-
when :agent_physical_work_completed
|
|
840
|
-
complete_agent_physical_work(event.payload)
|
|
841
|
-
when :workflow_control
|
|
842
|
-
cmd = event.payload.fetch(:command)
|
|
843
|
-
cmd.runner.deliver_on_event_loop(cmd)
|
|
438
|
+
when :execution_control
|
|
439
|
+
dispatch_execution(event.payload)
|
|
844
440
|
when :recovery_required
|
|
845
441
|
fsm_session_id = event.payload.fetch(:fsm_session_id)
|
|
846
442
|
session = @fsms.delete(fsm_session_id)
|
|
847
443
|
decrement_outstanding if session
|
|
848
|
-
|
|
444
|
+
receiver = @lifecycle_mutex.synchronize { @session_receivers.delete(fsm_session_id) }
|
|
445
|
+
receiver&.session_retired(fsm_session_id, reason: :recovery_required)
|
|
849
446
|
end
|
|
850
447
|
end
|
|
851
448
|
|
|
@@ -856,42 +453,6 @@ module Phronomy
|
|
|
856
453
|
event.payload.key?(:fsm_session_id)
|
|
857
454
|
end
|
|
858
455
|
|
|
859
|
-
def complete_agent_physical_work(payload)
|
|
860
|
-
key = payload.fetch(:execution_id).to_s
|
|
861
|
-
token = payload.fetch(:token)
|
|
862
|
-
deferred = @lifecycle_mutex.synchronize do
|
|
863
|
-
work = @agent_inflight_work.fetch(key, nil)
|
|
864
|
-
next nil unless work&.delete(token)
|
|
865
|
-
|
|
866
|
-
if work.empty?
|
|
867
|
-
@agent_inflight_work.delete(key)
|
|
868
|
-
command = @agent_deferred_terminals.delete(key)
|
|
869
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
870
|
-
command
|
|
871
|
-
end
|
|
872
|
-
end
|
|
873
|
-
deferred&.coordinator&.deliver_on_event_loop(deferred)
|
|
874
|
-
true
|
|
875
|
-
end
|
|
876
|
-
|
|
877
|
-
def mark_workflow_recovery_required_for_session(fsm_session_id)
|
|
878
|
-
@lifecycle_mutex.synchronize do
|
|
879
|
-
key, admission = @workflow_admissions.find do |_workflow_instance_id, candidate|
|
|
880
|
-
candidate.fsm_session_id == fsm_session_id.to_s
|
|
881
|
-
end
|
|
882
|
-
return false unless admission
|
|
883
|
-
|
|
884
|
-
@workflow_admissions[key] = WorkflowAdmission.new(
|
|
885
|
-
workflow_instance_id: admission.workflow_instance_id,
|
|
886
|
-
owner_token: admission.owner_token,
|
|
887
|
-
fsm_session_id: nil,
|
|
888
|
-
state: :recovery_required
|
|
889
|
-
)
|
|
890
|
-
@idle_cond.broadcast if runtime_idle_locked?
|
|
891
|
-
end
|
|
892
|
-
true
|
|
893
|
-
end
|
|
894
|
-
|
|
895
456
|
def begin_stopping_if_idle
|
|
896
457
|
@lifecycle_mutex.synchronize do
|
|
897
458
|
return false unless @state == :draining
|
|
@@ -916,22 +477,17 @@ module Phronomy
|
|
|
916
477
|
@waiting.values.each { |waiter| complete_waiter(waiter, error) }
|
|
917
478
|
@waiting.clear
|
|
918
479
|
@fsms.clear
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
@
|
|
922
|
-
@
|
|
923
|
-
@agent_deferred_terminals.clear
|
|
924
|
-
waiters
|
|
925
|
-
end
|
|
926
|
-
completion_waiters.each { |waiter| complete_waiter(waiter, error) }
|
|
927
|
-
@lifecycle_mutex.synchronize do
|
|
480
|
+
deliveries = @lifecycle_mutex.synchronize do
|
|
481
|
+
pending = @execution_deliveries.values
|
|
482
|
+
@execution_deliveries.clear
|
|
483
|
+
@session_receivers.clear
|
|
928
484
|
@admitted_fsm_session_ids.clear
|
|
929
|
-
@workflow_admissions.clear
|
|
930
|
-
@agent_admissions.clear
|
|
931
|
-
@agent_executions.clear
|
|
932
485
|
@outstanding_sessions = 0
|
|
933
486
|
@idle_cond.broadcast
|
|
487
|
+
pending
|
|
934
488
|
end
|
|
489
|
+
deliveries.each { |delivery| complete_waiter(delivery[:completion], error) }
|
|
490
|
+
shutdown_receivers(error)
|
|
935
491
|
end
|
|
936
492
|
|
|
937
493
|
def drain_queued_items
|
|
@@ -948,9 +504,6 @@ module Phronomy
|
|
|
948
504
|
@lifecycle_mutex.synchronize do
|
|
949
505
|
@state = :failed
|
|
950
506
|
@admitted_fsm_session_ids.clear
|
|
951
|
-
@workflow_admissions.clear
|
|
952
|
-
@agent_admissions.clear
|
|
953
|
-
@agent_executions.clear
|
|
954
507
|
@idle_cond.broadcast
|
|
955
508
|
end
|
|
956
509
|
cleanup_abandoned_work(error)
|
|
@@ -982,23 +535,8 @@ module Phronomy
|
|
|
982
535
|
end
|
|
983
536
|
|
|
984
537
|
def runtime_idle_locked?
|
|
985
|
-
@outstanding_sessions.zero? &&
|
|
986
|
-
@
|
|
987
|
-
@agent_deferred_terminals.empty? &&
|
|
988
|
-
!workflow_admission_transition_in_progress_locked? &&
|
|
989
|
-
!agent_admission_transition_in_progress_locked?
|
|
990
|
-
end
|
|
991
|
-
|
|
992
|
-
def workflow_admission_transition_in_progress_locked?
|
|
993
|
-
@workflow_admissions.values.any? do |admission|
|
|
994
|
-
%i[admitting executing persisting_terminal].include?(admission.state)
|
|
995
|
-
end
|
|
996
|
-
end
|
|
997
|
-
|
|
998
|
-
def agent_admission_transition_in_progress_locked?
|
|
999
|
-
@agent_admissions.values.any? do |admission|
|
|
1000
|
-
%i[admitting executing resuming cancelling terminalizing].include?(admission.state)
|
|
1001
|
-
end
|
|
538
|
+
@outstanding_sessions.zero? && @execution_deliveries.empty? &&
|
|
539
|
+
@execution_receivers.values.all?(&:idle?)
|
|
1002
540
|
end
|
|
1003
541
|
|
|
1004
542
|
def join_until(deadline)
|
|
@@ -1010,28 +548,24 @@ module Phronomy
|
|
|
1010
548
|
end
|
|
1011
549
|
|
|
1012
550
|
def finalize_terminated(status)
|
|
1013
|
-
|
|
1014
|
-
waiters = @agent_completion_waiters.values.flatten
|
|
1015
|
-
@agent_completion_waiters.clear
|
|
1016
|
-
@agent_inflight_work.clear
|
|
1017
|
-
@agent_deferred_terminals.clear
|
|
551
|
+
@lifecycle_mutex.synchronize do
|
|
1018
552
|
@state = :terminated
|
|
1019
553
|
@admitted_fsm_session_ids.clear
|
|
1020
|
-
@
|
|
1021
|
-
@
|
|
1022
|
-
@agent_executions.clear
|
|
1023
|
-
@thread = nil unless @thread&.alive?
|
|
554
|
+
@session_receivers.clear
|
|
555
|
+
@thread = nil
|
|
1024
556
|
@idle_cond.broadcast
|
|
1025
|
-
waiters
|
|
1026
557
|
end
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
558
|
+
shutdown_receivers(nil)
|
|
559
|
+
@receiver_shutdown_error ? :failed : status
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def shutdown_receivers(error)
|
|
563
|
+
@execution_receivers.each_value do |receiver|
|
|
564
|
+
receiver.shutdown(error: error)
|
|
565
|
+
rescue => caught
|
|
566
|
+
@receiver_shutdown_error ||= caught
|
|
1033
567
|
end
|
|
1034
|
-
|
|
568
|
+
@runtime.__event_loop_failed(@receiver_shutdown_error) if @receiver_shutdown_error
|
|
1035
569
|
end
|
|
1036
570
|
|
|
1037
571
|
def complete_waiter(waiter, payload)
|