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,690 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "monitor"
|
|
4
|
-
require "digest"
|
|
5
|
-
|
|
6
|
-
module Phronomy
|
|
7
|
-
class Persistence
|
|
8
|
-
class InMemory < Persistence
|
|
9
|
-
class Contents < Phronomy::ContentStore::Base
|
|
10
|
-
def initialize(owner)
|
|
11
|
-
@owner = owner
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def put(bytes, canonicalization_version:)
|
|
15
|
-
value = String(bytes).b.freeze
|
|
16
|
-
id = content_id_for(value)
|
|
17
|
-
@owner.synchronize do
|
|
18
|
-
current = @owner.state[:contents][id]
|
|
19
|
-
if current && current[:bytes] != value
|
|
20
|
-
raise Phronomy::ContentStore::IntegrityError, "digest collision for #{id}"
|
|
21
|
-
end
|
|
22
|
-
@owner.state[:contents][id] ||= {
|
|
23
|
-
bytes: value,
|
|
24
|
-
canonicalization_version: Integer(canonicalization_version)
|
|
25
|
-
}
|
|
26
|
-
end
|
|
27
|
-
id
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def fetch(content_id)
|
|
31
|
-
@owner.synchronize do
|
|
32
|
-
record = @owner.state[:contents].fetch(content_id.to_s) do
|
|
33
|
-
raise NotFoundError, "content not found: #{content_id}"
|
|
34
|
-
end
|
|
35
|
-
bytes = record[:bytes]
|
|
36
|
-
unless content_id_for(bytes) == content_id.to_s
|
|
37
|
-
raise Phronomy::ContentStore::IntegrityError, "content digest mismatch: #{content_id}"
|
|
38
|
-
end
|
|
39
|
-
bytes.dup
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def exist?(content_id)
|
|
44
|
-
@owner.synchronize { @owner.state[:contents].key?(content_id.to_s) }
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Backend-side Agent record repository. DurableRecord is opaque here;
|
|
49
|
-
# identity/revision metadata is supplied explicitly by RepositoryFacades.
|
|
50
|
-
class Agents
|
|
51
|
-
def initialize(owner) = @owner = owner
|
|
52
|
-
|
|
53
|
-
def create(agent_id:, agent_revision:, record:)
|
|
54
|
-
record = @owner.require_durable_record!(record)
|
|
55
|
-
key = agent_id.to_s
|
|
56
|
-
revision = Integer(agent_revision)
|
|
57
|
-
raise ConflictError, "agent_id must not be empty" if key.empty?
|
|
58
|
-
raise ConflictError, "agent_revision must be non-negative" if revision.negative?
|
|
59
|
-
|
|
60
|
-
@owner.synchronize do
|
|
61
|
-
raise ConflictError, "agent already exists: #{key}" if @owner.state[:agents].key?(key)
|
|
62
|
-
@owner.state[:agents][key] = record.copy
|
|
63
|
-
@owner.state[:agent_revisions][key] = revision
|
|
64
|
-
end
|
|
65
|
-
record.copy
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def load(agent_id)
|
|
69
|
-
@owner.synchronize do
|
|
70
|
-
@owner.state[:agents].fetch(agent_id.to_s) do
|
|
71
|
-
raise NotFoundError, "agent not found: #{agent_id}"
|
|
72
|
-
end.copy
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def save(agent_id, expected_revision:, next_revision:, record:)
|
|
77
|
-
record = @owner.require_durable_record!(record)
|
|
78
|
-
key = agent_id.to_s
|
|
79
|
-
expected = Integer(expected_revision)
|
|
80
|
-
next_value = Integer(next_revision)
|
|
81
|
-
@owner.synchronize do
|
|
82
|
-
unless @owner.state[:agents].key?(key)
|
|
83
|
-
raise NotFoundError, "agent not found: #{agent_id}"
|
|
84
|
-
end
|
|
85
|
-
actual_revision = @owner.state[:agent_revisions].fetch(key)
|
|
86
|
-
unless actual_revision == expected
|
|
87
|
-
raise ConflictError,
|
|
88
|
-
"agent revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
89
|
-
end
|
|
90
|
-
unless next_value == expected + 1
|
|
91
|
-
raise ConflictError,
|
|
92
|
-
"agent save must advance revision exactly once: " \
|
|
93
|
-
"expected #{expected + 1}, got #{next_value}"
|
|
94
|
-
end
|
|
95
|
-
@owner.state[:agents][key] = record.copy
|
|
96
|
-
@owner.state[:agent_revisions][key] = next_value
|
|
97
|
-
end
|
|
98
|
-
record.copy
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def delete(agent_id)
|
|
102
|
-
@owner.synchronize do
|
|
103
|
-
key = agent_id.to_s
|
|
104
|
-
@owner.state[:agent_revisions].delete(key)
|
|
105
|
-
@owner.state[:agents].delete(key)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
class Journals
|
|
111
|
-
def initialize(owner) = @owner = owner
|
|
112
|
-
|
|
113
|
-
def append(agent_id, expected_position:, records:, record_ids:)
|
|
114
|
-
encoded = Array(records).map { |record| @owner.require_durable_record!(record) }
|
|
115
|
-
ids = Array(record_ids).map(&:to_s)
|
|
116
|
-
unless encoded.length == ids.length
|
|
117
|
-
raise ConflictError,
|
|
118
|
-
"Journal records/record_ids length mismatch: #{encoded.length} != #{ids.length}"
|
|
119
|
-
end
|
|
120
|
-
if ids.any?(&:empty?)
|
|
121
|
-
raise ConflictError, "Journal record_id must not be empty"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
@owner.synchronize do
|
|
125
|
-
key = agent_id.to_s
|
|
126
|
-
target = (@owner.state[:journals][key] ||= [])
|
|
127
|
-
known_ids = (@owner.state[:journal_record_ids][key] ||= {})
|
|
128
|
-
expected = Integer(expected_position)
|
|
129
|
-
unless target.length == expected
|
|
130
|
-
raise ConflictError,
|
|
131
|
-
"journal position conflict: expected #{expected}, actual #{target.length}"
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
incoming_ids = {}
|
|
135
|
-
ids.each do |record_id|
|
|
136
|
-
if known_ids[record_id] || incoming_ids[record_id]
|
|
137
|
-
raise ConflictError, "duplicate Journal record_id: #{record_id}"
|
|
138
|
-
end
|
|
139
|
-
incoming_ids[record_id] = true
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
stored = encoded.map(&:copy)
|
|
143
|
-
target.concat(stored)
|
|
144
|
-
incoming_ids.each_key { |record_id| known_ids[record_id] = true }
|
|
145
|
-
stored.map(&:copy).freeze
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def read(agent_id, after: nil, limit: nil)
|
|
150
|
-
@owner.synchronize do
|
|
151
|
-
result = Array(@owner.state[:journals][agent_id.to_s])
|
|
152
|
-
result = result.drop(Integer(after)) if after
|
|
153
|
-
result = result.first(Integer(limit)) if limit
|
|
154
|
-
result.map(&:copy).freeze
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def head(agent_id)
|
|
159
|
-
@owner.synchronize { Array(@owner.state[:journals][agent_id.to_s]).length }
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def delete(agent_id)
|
|
163
|
-
@owner.synchronize do
|
|
164
|
-
key = agent_id.to_s
|
|
165
|
-
@owner.state[:journal_record_ids].delete(key)
|
|
166
|
-
@owner.state[:journals].delete(key)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
class Executions
|
|
172
|
-
def initialize(owner) = @owner = owner
|
|
173
|
-
|
|
174
|
-
def create_active(execution_id:, agent_id:, execution_revision:, record:)
|
|
175
|
-
record = @owner.require_durable_record!(record)
|
|
176
|
-
execution_key = execution_id.to_s
|
|
177
|
-
agent_key = agent_id.to_s
|
|
178
|
-
revision = Integer(execution_revision)
|
|
179
|
-
raise ConflictError, "execution_id must not be empty" if execution_key.empty?
|
|
180
|
-
raise ConflictError, "agent_id must not be empty" if agent_key.empty?
|
|
181
|
-
raise ConflictError, "execution_revision must be non-negative" if revision.negative?
|
|
182
|
-
|
|
183
|
-
@owner.synchronize do
|
|
184
|
-
if @owner.state[:executions].key?(execution_key)
|
|
185
|
-
raise ConflictError, "execution already exists: #{execution_key}"
|
|
186
|
-
end
|
|
187
|
-
active = @owner.state[:execution_metadata].values.find do |metadata|
|
|
188
|
-
metadata.fetch(:agent_id) == agent_key && metadata.fetch(:active)
|
|
189
|
-
end
|
|
190
|
-
raise Phronomy::AgentBusyError, "agent is busy: #{agent_key}" if active
|
|
191
|
-
|
|
192
|
-
@owner.state[:executions][execution_key] = record.copy
|
|
193
|
-
@owner.state[:execution_metadata][execution_key] = {
|
|
194
|
-
agent_id: agent_key,
|
|
195
|
-
revision: revision,
|
|
196
|
-
active: true
|
|
197
|
-
}.freeze
|
|
198
|
-
end
|
|
199
|
-
record.copy
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def load(execution_id)
|
|
203
|
-
@owner.synchronize do
|
|
204
|
-
@owner.state[:executions].fetch(execution_id.to_s) do
|
|
205
|
-
raise NotFoundError, "execution not found: #{execution_id}"
|
|
206
|
-
end.copy
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def save(execution_id, expected_revision:, next_revision:, agent_id:, active:, record:)
|
|
211
|
-
record = @owner.require_durable_record!(record)
|
|
212
|
-
execution_key = execution_id.to_s
|
|
213
|
-
agent_key = agent_id.to_s
|
|
214
|
-
expected = Integer(expected_revision)
|
|
215
|
-
next_value = Integer(next_revision)
|
|
216
|
-
unless active.equal?(true) || active.equal?(false)
|
|
217
|
-
raise ConflictError, "execution active metadata must be true or false"
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
@owner.synchronize do
|
|
221
|
-
unless @owner.state[:executions].key?(execution_key)
|
|
222
|
-
raise NotFoundError, "execution not found: #{execution_id}"
|
|
223
|
-
end
|
|
224
|
-
current = @owner.state[:execution_metadata].fetch(execution_key)
|
|
225
|
-
actual_revision = current.fetch(:revision)
|
|
226
|
-
unless actual_revision == expected
|
|
227
|
-
raise ConflictError,
|
|
228
|
-
"execution revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
229
|
-
end
|
|
230
|
-
unless current.fetch(:agent_id) == agent_key
|
|
231
|
-
raise ConflictError,
|
|
232
|
-
"Execution Agent identity mismatch: #{agent_key} != #{current.fetch(:agent_id)}"
|
|
233
|
-
end
|
|
234
|
-
unless next_value == expected + 1
|
|
235
|
-
raise ConflictError,
|
|
236
|
-
"execution save must advance revision exactly once: " \
|
|
237
|
-
"expected #{expected + 1}, got #{next_value}"
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
@owner.state[:executions][execution_key] = record.copy
|
|
241
|
-
@owner.state[:execution_metadata][execution_key] = {
|
|
242
|
-
agent_id: agent_key,
|
|
243
|
-
revision: next_value,
|
|
244
|
-
active: active
|
|
245
|
-
}.freeze
|
|
246
|
-
end
|
|
247
|
-
record.copy
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def list_active(agent_id)
|
|
251
|
-
@owner.synchronize do
|
|
252
|
-
agent_key = agent_id.to_s
|
|
253
|
-
ids = @owner.state[:execution_metadata].filter_map do |execution_id, metadata|
|
|
254
|
-
execution_id if metadata.fetch(:agent_id) == agent_key && metadata.fetch(:active)
|
|
255
|
-
end
|
|
256
|
-
ids.map { |execution_id| @owner.state[:executions].fetch(execution_id).copy }.freeze
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def list(agent_id, after: nil, limit: 100)
|
|
261
|
-
@owner.synchronize do
|
|
262
|
-
ids = @owner.state[:execution_metadata].filter_map do |id, metadata|
|
|
263
|
-
id if metadata.fetch(:agent_id) == agent_id.to_s && (!after || id > after.to_s)
|
|
264
|
-
end.sort.first(limit)
|
|
265
|
-
ids.map { |id| @owner.state[:executions].fetch(id).copy }.freeze
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def delete(execution_id)
|
|
270
|
-
@owner.synchronize do
|
|
271
|
-
key = execution_id.to_s
|
|
272
|
-
@owner.state[:execution_metadata].delete(key)
|
|
273
|
-
@owner.state[:executions].delete(key)
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def delete_for_agent(agent_id)
|
|
278
|
-
@owner.synchronize do
|
|
279
|
-
agent_key = agent_id.to_s
|
|
280
|
-
ids = @owner.state[:execution_metadata].filter_map do |execution_id, metadata|
|
|
281
|
-
execution_id if metadata.fetch(:agent_id) == agent_key
|
|
282
|
-
end
|
|
283
|
-
ids.each do |execution_id|
|
|
284
|
-
@owner.state[:execution_metadata].delete(execution_id)
|
|
285
|
-
@owner.state[:executions].delete(execution_id)
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def assert_idle!(agent_id)
|
|
291
|
-
@owner.synchronize do
|
|
292
|
-
active = @owner.state[:execution_metadata].values.find do |metadata|
|
|
293
|
-
metadata.fetch(:agent_id) == agent_id.to_s && metadata.fetch(:active)
|
|
294
|
-
end
|
|
295
|
-
if active
|
|
296
|
-
raise Phronomy::AgentBusyError,
|
|
297
|
-
"agent has an active or suspended execution: #{agent_id}"
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
true
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
class Teams
|
|
305
|
-
def initialize(owner) = @owner = owner
|
|
306
|
-
|
|
307
|
-
def create(team_id:, team_revision:, record:)
|
|
308
|
-
record = @owner.require_durable_record!(record)
|
|
309
|
-
key = team_id.to_s
|
|
310
|
-
revision = Integer(team_revision)
|
|
311
|
-
raise ConflictError, "team_id must not be empty" if key.empty?
|
|
312
|
-
raise ConflictError, "team_revision must be non-negative" if revision.negative?
|
|
313
|
-
|
|
314
|
-
@owner.synchronize do
|
|
315
|
-
raise ConflictError, "agent already exists: #{key}" if @owner.state[:teams].key?(key)
|
|
316
|
-
@owner.state[:teams][key] = record.copy
|
|
317
|
-
@owner.state[:team_revisions][key] = revision
|
|
318
|
-
end
|
|
319
|
-
record.copy
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def load(team_id)
|
|
323
|
-
@owner.synchronize do
|
|
324
|
-
@owner.state[:teams].fetch(team_id.to_s) do
|
|
325
|
-
raise NotFoundError, "agent not found: #{team_id}"
|
|
326
|
-
end.copy
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def save(team_id, expected_revision:, next_revision:, record:)
|
|
331
|
-
record = @owner.require_durable_record!(record)
|
|
332
|
-
key = team_id.to_s
|
|
333
|
-
expected = Integer(expected_revision)
|
|
334
|
-
next_value = Integer(next_revision)
|
|
335
|
-
@owner.synchronize do
|
|
336
|
-
unless @owner.state[:teams].key?(key)
|
|
337
|
-
raise NotFoundError, "agent not found: #{team_id}"
|
|
338
|
-
end
|
|
339
|
-
actual_revision = @owner.state[:team_revisions].fetch(key)
|
|
340
|
-
unless actual_revision == expected
|
|
341
|
-
raise ConflictError,
|
|
342
|
-
"agent revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
343
|
-
end
|
|
344
|
-
unless next_value == expected + 1
|
|
345
|
-
raise ConflictError,
|
|
346
|
-
"agent save must advance revision exactly once: " \
|
|
347
|
-
"expected #{expected + 1}, got #{next_value}"
|
|
348
|
-
end
|
|
349
|
-
@owner.state[:teams][key] = record.copy
|
|
350
|
-
@owner.state[:team_revisions][key] = next_value
|
|
351
|
-
end
|
|
352
|
-
record.copy
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def delete(team_id)
|
|
356
|
-
@owner.synchronize do
|
|
357
|
-
key = team_id.to_s
|
|
358
|
-
@owner.state[:team_revisions].delete(key)
|
|
359
|
-
@owner.state[:teams].delete(key)
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
class TeamExecutions
|
|
365
|
-
def initialize(owner) = @owner = owner
|
|
366
|
-
|
|
367
|
-
def create_active(team_execution_id:, team_id:, execution_revision:, record:)
|
|
368
|
-
record = @owner.require_durable_record!(record)
|
|
369
|
-
execution_key = team_execution_id.to_s
|
|
370
|
-
agent_key = team_id.to_s
|
|
371
|
-
revision = Integer(execution_revision)
|
|
372
|
-
raise ConflictError, "team_execution_id must not be empty" if execution_key.empty?
|
|
373
|
-
raise ConflictError, "team_id must not be empty" if agent_key.empty?
|
|
374
|
-
raise ConflictError, "execution_revision must be non-negative" if revision.negative?
|
|
375
|
-
|
|
376
|
-
@owner.synchronize do
|
|
377
|
-
if @owner.state[:team_executions].key?(execution_key)
|
|
378
|
-
raise ConflictError, "execution already exists: #{execution_key}"
|
|
379
|
-
end
|
|
380
|
-
active = @owner.state[:team_execution_metadata].values.find do |metadata|
|
|
381
|
-
metadata.fetch(:team_id) == agent_key && metadata.fetch(:active)
|
|
382
|
-
end
|
|
383
|
-
raise Phronomy::AgentBusyError, "agent is busy: #{agent_key}" if active
|
|
384
|
-
|
|
385
|
-
@owner.state[:team_executions][execution_key] = record.copy
|
|
386
|
-
@owner.state[:team_execution_metadata][execution_key] = {
|
|
387
|
-
team_id: agent_key,
|
|
388
|
-
revision: revision,
|
|
389
|
-
active: true
|
|
390
|
-
}.freeze
|
|
391
|
-
end
|
|
392
|
-
record.copy
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def load(team_execution_id)
|
|
396
|
-
@owner.synchronize do
|
|
397
|
-
@owner.state[:team_executions].fetch(team_execution_id.to_s) do
|
|
398
|
-
raise NotFoundError, "execution not found: #{team_execution_id}"
|
|
399
|
-
end.copy
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def save(team_execution_id, expected_revision:, next_revision:, team_id:, active:, record:)
|
|
404
|
-
record = @owner.require_durable_record!(record)
|
|
405
|
-
execution_key = team_execution_id.to_s
|
|
406
|
-
agent_key = team_id.to_s
|
|
407
|
-
expected = Integer(expected_revision)
|
|
408
|
-
next_value = Integer(next_revision)
|
|
409
|
-
unless active.equal?(true) || active.equal?(false)
|
|
410
|
-
raise ConflictError, "execution active metadata must be true or false"
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
@owner.synchronize do
|
|
414
|
-
unless @owner.state[:team_executions].key?(execution_key)
|
|
415
|
-
raise NotFoundError, "execution not found: #{team_execution_id}"
|
|
416
|
-
end
|
|
417
|
-
current = @owner.state[:team_execution_metadata].fetch(execution_key)
|
|
418
|
-
actual_revision = current.fetch(:revision)
|
|
419
|
-
unless actual_revision == expected
|
|
420
|
-
raise ConflictError,
|
|
421
|
-
"execution revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
422
|
-
end
|
|
423
|
-
unless current.fetch(:team_id) == agent_key
|
|
424
|
-
raise ConflictError,
|
|
425
|
-
"Execution Team identity mismatch: #{agent_key} != #{current.fetch(:team_id)}"
|
|
426
|
-
end
|
|
427
|
-
if active && !current.fetch(:active)
|
|
428
|
-
raise ConflictError, "A terminal Team execution cannot become active"
|
|
429
|
-
end
|
|
430
|
-
unless next_value == expected + 1
|
|
431
|
-
raise ConflictError,
|
|
432
|
-
"execution save must advance revision exactly once: " \
|
|
433
|
-
"expected #{expected + 1}, got #{next_value}"
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
@owner.state[:team_executions][execution_key] = record.copy
|
|
437
|
-
@owner.state[:team_execution_metadata][execution_key] = {
|
|
438
|
-
team_id: agent_key,
|
|
439
|
-
revision: next_value,
|
|
440
|
-
active: active
|
|
441
|
-
}.freeze
|
|
442
|
-
end
|
|
443
|
-
record.copy
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def list_active(team_id)
|
|
447
|
-
@owner.synchronize do
|
|
448
|
-
agent_key = team_id.to_s
|
|
449
|
-
ids = @owner.state[:team_execution_metadata].filter_map do |team_execution_id, metadata|
|
|
450
|
-
team_execution_id if metadata.fetch(:team_id) == agent_key && metadata.fetch(:active)
|
|
451
|
-
end
|
|
452
|
-
ids.map { |team_execution_id| @owner.state[:team_executions].fetch(team_execution_id).copy }.freeze
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def list(team_id, after: nil, limit: 100)
|
|
457
|
-
@owner.synchronize do
|
|
458
|
-
ids = @owner.state[:team_execution_metadata].filter_map do |id, metadata|
|
|
459
|
-
id if metadata.fetch(:team_id) == team_id.to_s && (!after || id > after.to_s)
|
|
460
|
-
end.sort.first(limit)
|
|
461
|
-
ids.map { |id| @owner.state[:team_executions].fetch(id).copy }.freeze
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def delete(team_execution_id)
|
|
466
|
-
@owner.synchronize do
|
|
467
|
-
key = team_execution_id.to_s
|
|
468
|
-
@owner.state[:team_execution_metadata].delete(key)
|
|
469
|
-
@owner.state[:team_executions].delete(key)
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
def delete_for_team(team_id)
|
|
474
|
-
@owner.synchronize do
|
|
475
|
-
agent_key = team_id.to_s
|
|
476
|
-
ids = @owner.state[:team_execution_metadata].filter_map do |team_execution_id, metadata|
|
|
477
|
-
team_execution_id if metadata.fetch(:team_id) == agent_key
|
|
478
|
-
end
|
|
479
|
-
ids.each do |team_execution_id|
|
|
480
|
-
@owner.state[:team_execution_metadata].delete(team_execution_id)
|
|
481
|
-
@owner.state[:team_executions].delete(team_execution_id)
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def assert_idle!(team_id)
|
|
487
|
-
@owner.synchronize do
|
|
488
|
-
active = @owner.state[:team_execution_metadata].values.find do |metadata|
|
|
489
|
-
metadata.fetch(:team_id) == team_id.to_s && metadata.fetch(:active)
|
|
490
|
-
end
|
|
491
|
-
if active
|
|
492
|
-
raise Phronomy::AgentBusyError,
|
|
493
|
-
"agent has an active or suspended execution: #{team_id}"
|
|
494
|
-
end
|
|
495
|
-
end
|
|
496
|
-
true
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
class HandoffStates
|
|
501
|
-
def initialize(owner) = @owner = owner
|
|
502
|
-
|
|
503
|
-
def load(main_agent_id)
|
|
504
|
-
@owner.synchronize do
|
|
505
|
-
record = @owner.state[:handoff_states][main_agent_id.to_s]
|
|
506
|
-
record&.copy
|
|
507
|
-
end
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def save(main_agent_id, expected_revision:, next_revision:, active_agent_id:, record:)
|
|
511
|
-
record = @owner.require_durable_record!(record)
|
|
512
|
-
raise ArgumentError, "active_agent_id is required" if active_agent_id.to_s.empty?
|
|
513
|
-
key = main_agent_id.to_s
|
|
514
|
-
expected = expected_revision.nil? ? nil : Integer(expected_revision)
|
|
515
|
-
next_value = Integer(next_revision)
|
|
516
|
-
@owner.synchronize do
|
|
517
|
-
actual_revision = @owner.state[:handoff_revisions][key]
|
|
518
|
-
unless actual_revision == expected
|
|
519
|
-
raise ConflictError,
|
|
520
|
-
"workflow state revision conflict for #{key}: " \
|
|
521
|
-
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
522
|
-
end
|
|
523
|
-
expected_next = expected.nil? ? 1 : expected + 1
|
|
524
|
-
unless next_value == expected_next
|
|
525
|
-
raise ConflictError,
|
|
526
|
-
"workflow state save must advance revision exactly once: " \
|
|
527
|
-
"expected #{expected_next}, got #{next_value}"
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
@owner.state[:handoff_states][key] = record.copy
|
|
531
|
-
@owner.state[:handoff_revisions][key] = next_value
|
|
532
|
-
end
|
|
533
|
-
record.copy
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
def delete(main_agent_id, expected_revision:)
|
|
537
|
-
@owner.synchronize do
|
|
538
|
-
key = main_agent_id.to_s
|
|
539
|
-
expected = Integer(expected_revision)
|
|
540
|
-
actual_revision = @owner.state[:handoff_revisions][key]
|
|
541
|
-
unless actual_revision == expected
|
|
542
|
-
raise ConflictError,
|
|
543
|
-
"workflow state revision conflict for #{key}: " \
|
|
544
|
-
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
545
|
-
end
|
|
546
|
-
@owner.state[:handoff_revisions].delete(key)
|
|
547
|
-
@owner.state[:handoff_states].delete(key)
|
|
548
|
-
end
|
|
549
|
-
nil
|
|
550
|
-
end
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
class WorkflowStates
|
|
554
|
-
def initialize(owner) = @owner = owner
|
|
555
|
-
|
|
556
|
-
def load(workflow_instance_id)
|
|
557
|
-
@owner.synchronize do
|
|
558
|
-
record = @owner.state[:workflow_states][workflow_instance_id.to_s]
|
|
559
|
-
record&.copy
|
|
560
|
-
end
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
def save(workflow_instance_id, expected_revision:, next_revision:, record:)
|
|
564
|
-
record = @owner.require_durable_record!(record)
|
|
565
|
-
key = workflow_instance_id.to_s
|
|
566
|
-
expected = expected_revision.nil? ? nil : Integer(expected_revision)
|
|
567
|
-
next_value = Integer(next_revision)
|
|
568
|
-
@owner.synchronize do
|
|
569
|
-
actual_revision = @owner.state[:workflow_revisions][key]
|
|
570
|
-
unless actual_revision == expected
|
|
571
|
-
raise ConflictError,
|
|
572
|
-
"workflow state revision conflict for #{key}: " \
|
|
573
|
-
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
574
|
-
end
|
|
575
|
-
expected_next = expected.nil? ? 1 : expected + 1
|
|
576
|
-
unless next_value == expected_next
|
|
577
|
-
raise ConflictError,
|
|
578
|
-
"workflow state save must advance revision exactly once: " \
|
|
579
|
-
"expected #{expected_next}, got #{next_value}"
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
@owner.state[:workflow_states][key] = record.copy
|
|
583
|
-
@owner.state[:workflow_revisions][key] = next_value
|
|
584
|
-
end
|
|
585
|
-
record.copy
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
def delete(workflow_instance_id, expected_revision:)
|
|
589
|
-
@owner.synchronize do
|
|
590
|
-
key = workflow_instance_id.to_s
|
|
591
|
-
expected = Integer(expected_revision)
|
|
592
|
-
actual_revision = @owner.state[:workflow_revisions][key]
|
|
593
|
-
unless actual_revision == expected
|
|
594
|
-
raise ConflictError,
|
|
595
|
-
"workflow state revision conflict for #{key}: " \
|
|
596
|
-
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
597
|
-
end
|
|
598
|
-
@owner.state[:workflow_revisions].delete(key)
|
|
599
|
-
@owner.state[:workflow_states].delete(key)
|
|
600
|
-
end
|
|
601
|
-
nil
|
|
602
|
-
end
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
attr_reader :state
|
|
606
|
-
|
|
607
|
-
def initialize
|
|
608
|
-
@monitor = Monitor.new
|
|
609
|
-
@state = {
|
|
610
|
-
contents: {},
|
|
611
|
-
agents: {},
|
|
612
|
-
agent_revisions: {},
|
|
613
|
-
journals: {},
|
|
614
|
-
journal_record_ids: {},
|
|
615
|
-
executions: {},
|
|
616
|
-
execution_metadata: {},
|
|
617
|
-
workflow_states: {},
|
|
618
|
-
workflow_revisions: {},
|
|
619
|
-
handoff_states: {}, handoff_revisions: {},
|
|
620
|
-
teams: {}, team_revisions: {}, team_executions: {}, team_execution_metadata: {}
|
|
621
|
-
}
|
|
622
|
-
@contents_backend = Contents.new(self)
|
|
623
|
-
@agents_backend = Agents.new(self)
|
|
624
|
-
@journals_backend = Journals.new(self)
|
|
625
|
-
@executions_backend = Executions.new(self)
|
|
626
|
-
@workflow_states_backend = WorkflowStates.new(self)
|
|
627
|
-
super(
|
|
628
|
-
contents: @contents_backend,
|
|
629
|
-
agents: @agents_backend,
|
|
630
|
-
journals: @journals_backend,
|
|
631
|
-
executions: @executions_backend,
|
|
632
|
-
workflow_states: @workflow_states_backend,
|
|
633
|
-
handoff_states: HandoffStates.new(self),
|
|
634
|
-
teams: Teams.new(self), team_executions: TeamExecutions.new(self)
|
|
635
|
-
)
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
def capabilities
|
|
639
|
-
{atomic_all: true, atomic_admission: true, optimistic_revision: true}.freeze
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
def assert_agent_watermark!(agent_id:, agent_revision:, journal_position:)
|
|
643
|
-
synchronize do
|
|
644
|
-
key = agent_id.to_s
|
|
645
|
-
unless @state[:agents].key?(key)
|
|
646
|
-
raise NotFoundError, "Agent not found: #{agent_id}"
|
|
647
|
-
end
|
|
648
|
-
|
|
649
|
-
actual_revision = @state[:agent_revisions].fetch(key)
|
|
650
|
-
if actual_revision != agent_revision
|
|
651
|
-
raise ConflictError,
|
|
652
|
-
"agent revision conflict: expected #{agent_revision}, actual #{actual_revision}"
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
actual_position = Array(@state[:journals][key]).length
|
|
656
|
-
if actual_position != journal_position
|
|
657
|
-
raise ConflictError,
|
|
658
|
-
"journal position conflict: expected #{journal_position}, actual #{actual_position}"
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
true
|
|
662
|
-
end
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
def transaction
|
|
666
|
-
synchronize do
|
|
667
|
-
state_snapshot = Marshal.load(Marshal.dump(@state))
|
|
668
|
-
begin
|
|
669
|
-
yield self
|
|
670
|
-
rescue
|
|
671
|
-
@state.replace(state_snapshot)
|
|
672
|
-
raise
|
|
673
|
-
end
|
|
674
|
-
end
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
def synchronize(&block)
|
|
678
|
-
@monitor.synchronize(&block)
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
# @api private
|
|
682
|
-
def require_durable_record!(record)
|
|
683
|
-
return record if record.is_a?(Phronomy::Persistence::DurableRecord)
|
|
684
|
-
|
|
685
|
-
raise Phronomy::Persistence::SerializationError,
|
|
686
|
-
"backend repository expected Persistence::DurableRecord, got #{record.class}"
|
|
687
|
-
end
|
|
688
|
-
end
|
|
689
|
-
end
|
|
690
|
-
end
|