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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
5
|
module Phronomy
|
|
6
|
-
module
|
|
7
|
-
#
|
|
6
|
+
module MultiAgent
|
|
7
|
+
# Coordinates durable handoffs across Agents using one Persistence instance.
|
|
8
8
|
# @api public
|
|
9
9
|
class HandoffRunner
|
|
10
10
|
MAX_HANDOFFS = 20
|
|
@@ -16,7 +16,7 @@ module Phronomy
|
|
|
16
16
|
def initialize(main_agent:, handoffs: [])
|
|
17
17
|
@main_agent, @handoffs = main_agent, Array(handoffs).freeze
|
|
18
18
|
@persistence = main_agent.persistence
|
|
19
|
-
raise ArgumentError, "handoffs must contain Agent::Handoff" unless @handoffs.all? { |h| h.is_a?(Handoff) }
|
|
19
|
+
raise ArgumentError, "handoffs must contain Agent::Handoff" unless @handoffs.all? { |h| h.is_a?(Phronomy::Agent::Handoff) }
|
|
20
20
|
@agents = ([main_agent] + @handoffs.flat_map { |edge| [edge.source_agent, edge.target_agent] }).uniq.to_h { |a| [a.agent_id, a] }.freeze
|
|
21
21
|
unless @agents.values.all? { |a| a.persistence.equal?(@persistence) }
|
|
22
22
|
raise Phronomy::ConfigurationError, "Durable Handoff graph requires one Persistence instance"
|
|
@@ -24,8 +24,9 @@ module Phronomy
|
|
|
24
24
|
if @handoffs.group_by { |h| [h.source_agent.agent_id, h.target_agent.agent_id] }.any? { |_, edges| edges.size > 1 }
|
|
25
25
|
raise ArgumentError, "Duplicate Source/Target Handoff edges"
|
|
26
26
|
end
|
|
27
|
-
@bindings = @handoffs.group_by { |h| h.source_agent.agent_id }.transform_values { |edges| edges.map { |h| HandoffCapabilityFactory.build(h) }.freeze }.freeze
|
|
27
|
+
@bindings = @handoffs.group_by { |h| h.source_agent.agent_id }.transform_values { |edges| edges.map { |h| Phronomy::Agent::HandoffCapabilityFactory.build(h) }.freeze }.freeze
|
|
28
28
|
@runtime = Phronomy::Runtime.instance
|
|
29
|
+
@admissions = Phronomy::MultiAgent::AdmissionRegistry.for(@runtime)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# @api public
|
|
@@ -41,7 +42,7 @@ module Phronomy
|
|
|
41
42
|
raise Phronomy::RuntimeShutdownError, "HandoffRunner belongs to a previous Runtime"
|
|
42
43
|
end
|
|
43
44
|
config = config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new)
|
|
44
|
-
@
|
|
45
|
+
@admissions.admit!(main_agent)
|
|
45
46
|
admitted = true
|
|
46
47
|
state = load_state
|
|
47
48
|
count = 0
|
|
@@ -49,7 +50,7 @@ module Phronomy
|
|
|
49
50
|
active = @agents.fetch(state.active_agent_id) do
|
|
50
51
|
raise Phronomy::ExecutionRehydrationRequiredError, "Handoff graph lacks active Agent #{state.active_agent_id}"
|
|
51
52
|
end
|
|
52
|
-
context = state.active_handoff_context_ref && HandoffContext.from_h(@persistence.contents.fetch_json(state.active_handoff_context_ref))
|
|
53
|
+
context = state.active_handoff_context_ref && Phronomy::Agent::HandoffContext.from_h(@persistence.contents.fetch_json(state.active_handoff_context_ref))
|
|
53
54
|
wiring = config.merge(phronomy_handoff_bindings: @bindings.fetch(active.agent_id, []),
|
|
54
55
|
phronomy_handoff_context: context,
|
|
55
56
|
phronomy_coordination: {"kind" => "handoff", "main_agent_id" => main_agent.agent_id, "handoff_revision" => state.handoff_revision}).freeze
|
|
@@ -61,11 +62,11 @@ module Phronomy
|
|
|
61
62
|
else
|
|
62
63
|
exact = unfinished.fetch(0)
|
|
63
64
|
unless unfinished.size == 1 && exact.metadata.dig("coordination", "main_agent_id") == main_agent.agent_id
|
|
64
|
-
raise Phronomy::
|
|
65
|
+
raise Phronomy::Storage::ConflictError, "Active Agent execution belongs to another coordination turn"
|
|
65
66
|
end
|
|
66
67
|
wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(exact.execution_id)
|
|
67
68
|
stored_input = @persistence.contents.fetch_text(exact.metadata.fetch("current_input_ref"))
|
|
68
|
-
ExactExecution.start(agent: active, execution_id: exact.execution_id, input: stored_input, config: wiring).wait_result
|
|
69
|
+
Phronomy::Agent::ExactExecution.start(agent: active, execution_id: exact.execution_id, input: stored_input, config: wiring).wait_result
|
|
69
70
|
end
|
|
70
71
|
else
|
|
71
72
|
source = @persistence.executions.load(state.pending_source_execution_id)
|
|
@@ -77,7 +78,7 @@ module Phronomy
|
|
|
77
78
|
raise Phronomy::ConfigurationError, "Handoff Target definition mismatch"
|
|
78
79
|
end
|
|
79
80
|
wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(state.pending_target_execution_id)
|
|
80
|
-
ExactExecution.start(agent: active, execution_id: state.pending_target_execution_id,
|
|
81
|
+
Phronomy::Agent::ExactExecution.start(agent: active, execution_id: state.pending_target_execution_id,
|
|
81
82
|
input: context.responsibility, config: wiring).wait_result
|
|
82
83
|
end
|
|
83
84
|
execution = @persistence.executions.load(result.fetch(:execution_id))
|
|
@@ -90,7 +91,7 @@ module Phronomy
|
|
|
90
91
|
if execution.active?
|
|
91
92
|
raise Phronomy::ExecutionRehydrationRequiredError, "Handoff execution requires approval or recovery"
|
|
92
93
|
end
|
|
93
|
-
raise RecoverySupport.error_from_failure(result[:error]) if result[:error]
|
|
94
|
+
raise Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]) if result[:error]
|
|
94
95
|
return result.reject { |key, _| key.to_s.start_with?("_phronomy_") || key == :handoff_request }.merge(agent: active)
|
|
95
96
|
end
|
|
96
97
|
rescue Phronomy::CancellationError => error
|
|
@@ -101,7 +102,7 @@ module Phronomy
|
|
|
101
102
|
operation_error = error
|
|
102
103
|
raise
|
|
103
104
|
ensure
|
|
104
|
-
@
|
|
105
|
+
@admissions.release!(main_agent) if admitted
|
|
105
106
|
Phronomy::Tracing::Automatic.finish(trace_handle, output: result && result[:output], error: operation_error)
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -117,25 +118,25 @@ module Phronomy
|
|
|
117
118
|
@persistence.transaction do |tx|
|
|
118
119
|
first = tx.executions.load(execution_id)
|
|
119
120
|
unless first.metadata.dig("coordination", "main_agent_id") == main_agent.agent_id
|
|
120
|
-
raise Phronomy::
|
|
121
|
+
raise Phronomy::Storage::ConflictError, "Execution does not belong to this Handoff anchor"
|
|
121
122
|
end
|
|
122
123
|
leaf = first
|
|
123
124
|
leaf_id = first.execution_id
|
|
124
125
|
seen = {}
|
|
125
126
|
while leaf&.status == :handed_off
|
|
126
|
-
raise Phronomy::
|
|
127
|
+
raise Phronomy::Storage::SerializationError, "Cyclic Handoff chain" if seen[leaf_id]
|
|
127
128
|
seen[leaf_id] = true
|
|
128
129
|
leaf_id = leaf.metadata.fetch("handoff_target_execution_id")
|
|
129
130
|
begin
|
|
130
131
|
leaf = tx.executions.load(leaf_id)
|
|
131
|
-
rescue Phronomy::
|
|
132
|
+
rescue Phronomy::Storage::NotFoundError
|
|
132
133
|
leaf = nil
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
136
|
next if leaf&.terminal?
|
|
136
137
|
routing = tx.handoff_states.load(main_agent.agent_id)
|
|
137
138
|
unless routing && (leaf ? routing.active_agent_id == leaf.agent_id : routing.pending_target_execution_id == leaf_id)
|
|
138
|
-
raise Phronomy::
|
|
139
|
+
raise Phronomy::Storage::ConflictError, "Handoff routing no longer owns the requested turn"
|
|
139
140
|
end
|
|
140
141
|
ids = (Array(routing.metadata["cancelled_execution_ids"]) + [leaf_id]).uniq
|
|
141
142
|
intended = routing.with(phase: leaf ? routing.phase : "stable",
|
|
@@ -147,7 +148,7 @@ module Phronomy
|
|
|
147
148
|
raise error unless intended && confirmed && Array(confirmed.metadata["cancelled_execution_ids"]).include?(leaf_id)
|
|
148
149
|
end
|
|
149
150
|
return @persistence.execution_result(leaf_id) if leaf&.terminal?
|
|
150
|
-
ExecutionCancellation.signal(leaf_id, leaf.agent_id) if leaf&.active?
|
|
151
|
+
Phronomy::Agent::ExecutionCancellation.signal(leaf_id, leaf.agent_id) if leaf&.active?
|
|
151
152
|
{execution_id: leaf_id, cancellation_requested: !leaf&.terminal?}.freeze
|
|
152
153
|
end
|
|
153
154
|
|
|
@@ -163,7 +164,7 @@ module Phronomy
|
|
|
163
164
|
state = @persistence.handoff_states.load(main_agent.agent_id)
|
|
164
165
|
return state if state
|
|
165
166
|
now = Time.now.utc.iso8601(6)
|
|
166
|
-
initial = HandoffState.new(main_agent_id: main_agent.agent_id, handoff_revision: 1,
|
|
167
|
+
initial = Phronomy::Agent::HandoffState.new(main_agent_id: main_agent.agent_id, handoff_revision: 1,
|
|
167
168
|
active_agent_id: main_agent.agent_id, active_handoff_context_ref: nil,
|
|
168
169
|
phase: "stable", pending_source_execution_id: nil, pending_target_execution_id: nil,
|
|
169
170
|
created_at: now, updated_at: now, metadata: {})
|
|
@@ -23,7 +23,7 @@ module Phronomy
|
|
|
23
23
|
raise Phronomy::EventLoopReentrancyError, "Orchestrator#resume cannot block EventLoop"
|
|
24
24
|
end
|
|
25
25
|
execution = persistence.executions.load(execution_id)
|
|
26
|
-
raise Phronomy::
|
|
26
|
+
raise Phronomy::Storage::ConflictError, "Parent execution owner mismatch" unless execution.agent_id == agent_id
|
|
27
27
|
input = persistence.contents.fetch_text(execution.metadata.fetch("current_input_ref"))
|
|
28
28
|
result = Phronomy::Agent::ExactExecution.start(agent: self, execution_id: execution_id, input: input, config: config).wait_result
|
|
29
29
|
raise Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]) if result[:error]
|
|
@@ -174,7 +174,7 @@ module Phronomy
|
|
|
174
174
|
invocation_context: invocation_context, concurrency_limit: max_concurrency) do |child, execution|
|
|
175
175
|
# Preserve a child-specific context as well as this dispatch's controls.
|
|
176
176
|
# Agent admission adds that context's constraints to the supplied token.
|
|
177
|
-
binding = Phronomy::
|
|
177
|
+
binding = Phronomy::Concurrency::OperationBinding.new(
|
|
178
178
|
invocation_context: execution.invocation_context,
|
|
179
179
|
cancellation_token: child.config[:cancellation_token]
|
|
180
180
|
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module MultiAgent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Current-format record schemas and conversion for this domain.
|
|
7
|
+
# @api private
|
|
8
|
+
module Codec
|
|
9
|
+
extend Phronomy::Storage::RecordCodec
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def encode_team_root(value)
|
|
14
|
+
payload = value.to_h
|
|
15
|
+
Phronomy::MultiAgent::TeamRoot.from_h(payload)
|
|
16
|
+
build_record("phronomy.team_root", "0.1", payload)
|
|
17
|
+
rescue Phronomy::Storage::SerializationError
|
|
18
|
+
raise
|
|
19
|
+
rescue => error
|
|
20
|
+
serialization_error("cannot encode TeamRoot", error)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decode_team_root(record)
|
|
24
|
+
payload = current_payload!(record, record_type: "phronomy.team_root",
|
|
25
|
+
format_version: "0.1", keys: Phronomy::MultiAgent::TeamRoot::ATTRIBUTES, label: "TeamRoot")
|
|
26
|
+
Phronomy::MultiAgent::TeamRoot.from_h(payload)
|
|
27
|
+
rescue Phronomy::Storage::SerializationError
|
|
28
|
+
raise
|
|
29
|
+
rescue => error
|
|
30
|
+
serialization_error("cannot decode TeamRoot", error)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def encode_team_execution(value)
|
|
34
|
+
payload = value.to_h
|
|
35
|
+
Phronomy::MultiAgent::TeamExecution.from_h(payload)
|
|
36
|
+
build_record("phronomy.team_execution", "0.1", payload)
|
|
37
|
+
rescue Phronomy::Storage::SerializationError
|
|
38
|
+
raise
|
|
39
|
+
rescue => error
|
|
40
|
+
serialization_error("cannot encode TeamExecution", error)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decode_team_execution(record)
|
|
44
|
+
payload = current_payload!(record, record_type: "phronomy.team_execution",
|
|
45
|
+
format_version: "0.1", keys: Phronomy::MultiAgent::TeamExecution::ATTRIBUTES, label: "TeamExecution")
|
|
46
|
+
Phronomy::MultiAgent::TeamExecution.from_h(payload)
|
|
47
|
+
rescue Phronomy::Storage::SerializationError
|
|
48
|
+
raise
|
|
49
|
+
rescue => error
|
|
50
|
+
serialization_error("cannot decode TeamExecution", error)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module MultiAgent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Read and assemble domain results; the public entry point owns the execution-thread guard.
|
|
7
|
+
# @api private
|
|
8
|
+
class Queries
|
|
9
|
+
def initialize(repositories)
|
|
10
|
+
@repositories = repositories
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def team_execution_result(team_execution_id)
|
|
14
|
+
execution = @repositories.team_executions.load(team_execution_id)
|
|
15
|
+
{
|
|
16
|
+
team_execution_id: execution.team_execution_id, team_id: execution.team_id,
|
|
17
|
+
status: execution.status, phase: execution.phase,
|
|
18
|
+
result_ref: execution.result_ref, error_ref: execution.error_ref,
|
|
19
|
+
result: execution.result_ref && @repositories.contents.fetch_json(execution.result_ref),
|
|
20
|
+
error: execution.error_ref && @repositories.contents.fetch_json(execution.error_ref)
|
|
21
|
+
}.then { |value| Phronomy::Values::Immutable.copy(value) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def list_team_executions(team_id, after: nil, limit: 100)
|
|
25
|
+
@repositories.team_executions.list(team_id, after: after, limit: limit)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module MultiAgent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Owns execution activity, ownership, admission and durable record encoding.
|
|
7
|
+
# @api private
|
|
8
|
+
class TeamExecutionRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def create_active(execution)
|
|
12
|
+
raise Phronomy::Storage::SerializationError, "create_active requires an active TeamExecution" unless execution.active?
|
|
13
|
+
record = Codec.encode_team_execution(execution)
|
|
14
|
+
admission do
|
|
15
|
+
@view.atomic do |bound|
|
|
16
|
+
entry = bound.records(Phronomy::TeamStorageSchema::EXECUTIONS).insert(key: execution.team_execution_id.to_s,
|
|
17
|
+
revision: Integer(execution.execution_revision), attributes: attributes(execution), record: record)
|
|
18
|
+
decode(entry, execution.team_execution_id, owner: execution.team_id,
|
|
19
|
+
revision: execution.execution_revision, active: true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load(team_execution_id)
|
|
25
|
+
value = @view.atomic do |bound|
|
|
26
|
+
entry = bound.records(Phronomy::TeamStorageSchema::EXECUTIONS).read(team_execution_id.to_s)
|
|
27
|
+
entry && decode(entry, team_execution_id)
|
|
28
|
+
end
|
|
29
|
+
value || raise(Phronomy::Storage::NotFoundError, "TeamExecution not found: #{team_execution_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save(team_execution_id, expected_revision:, execution:)
|
|
33
|
+
expected = Integer(expected_revision)
|
|
34
|
+
revision = Integer(execution.execution_revision)
|
|
35
|
+
raise Phronomy::Storage::ConflictError, "execution save must advance revision exactly once" unless revision == expected + 1
|
|
36
|
+
unless execution.team_execution_id.to_s == team_execution_id.to_s
|
|
37
|
+
raise Phronomy::Storage::SerializationError, "Execution identity mismatch"
|
|
38
|
+
end
|
|
39
|
+
record = Codec.encode_team_execution(execution)
|
|
40
|
+
admission do
|
|
41
|
+
@view.atomic do |bound|
|
|
42
|
+
entry = bound.records(Phronomy::TeamStorageSchema::EXECUTIONS).replace(key: team_execution_id.to_s,
|
|
43
|
+
expected_revision: expected, next_revision: revision, attributes: attributes(execution),
|
|
44
|
+
expected_attributes: execution.active? ? {active: true} : {}, record: record)
|
|
45
|
+
decode(entry, team_execution_id, owner: execution.team_id, revision: revision, active: execution.active?)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def list_active(team_id)
|
|
51
|
+
@view.atomic do |bound|
|
|
52
|
+
bound.records(Phronomy::TeamStorageSchema::EXECUTIONS).scan(index: :owner_active,
|
|
53
|
+
equals: {owner: team_id.to_s, active: true}).map { |entry| decode(entry, entry.key, owner: team_id, active: true) }.freeze
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def list(team_id, after: nil, limit: 100)
|
|
58
|
+
raise ArgumentError, "limit must be a positive Integer" unless limit.is_a?(Integer) && limit.positive?
|
|
59
|
+
@view.atomic do |bound|
|
|
60
|
+
bound.records(Phronomy::TeamStorageSchema::EXECUTIONS).scan(index: :owner, equals: {owner: team_id.to_s},
|
|
61
|
+
after: after&.to_s, limit: limit).map { |entry| decode(entry, entry.key, owner: team_id) }.freeze
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def delete(team_execution_id)
|
|
66
|
+
@view.records(Phronomy::TeamStorageSchema::EXECUTIONS).delete(key: team_execution_id.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete_for_team(team_id)
|
|
70
|
+
@view.records(Phronomy::TeamStorageSchema::EXECUTIONS).delete_matching(index: :owner, equals: {owner: team_id.to_s})
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def assert_idle!(team_id)
|
|
74
|
+
condition = Phronomy::Storage::Condition::NoRows.new(resource: Phronomy::TeamStorageSchema::EXECUTIONS,
|
|
75
|
+
index: :owner_active, equals: {owner: team_id.to_s, active: true})
|
|
76
|
+
@view.check!(guards: [Phronomy::Storage::GuardRef.new(resource: Phronomy::TeamStorageSchema::ROOTS, key: team_id.to_s)],
|
|
77
|
+
conditions: [condition])
|
|
78
|
+
rescue Phronomy::Storage::ConditionFailedError => error
|
|
79
|
+
raise unless error.condition.equal?(condition)
|
|
80
|
+
raise Phronomy::AgentBusyError, error.message
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def attributes(execution) = {owner: execution.team_id.to_s, active: execution.active?}
|
|
86
|
+
|
|
87
|
+
def admission
|
|
88
|
+
yield
|
|
89
|
+
rescue Phronomy::Storage::UniqueConstraintError => error
|
|
90
|
+
raise unless error.resource == Phronomy::TeamStorageSchema::EXECUTIONS.id && error.constraint == :one_active_owner
|
|
91
|
+
raise Phronomy::AgentBusyError, error.message
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def decode(entry, team_execution_id, owner: nil, revision: nil, active: nil)
|
|
95
|
+
execution = Codec.decode_team_execution(entry.record)
|
|
96
|
+
valid = entry.key == team_execution_id.to_s && execution.team_execution_id == entry.key &&
|
|
97
|
+
execution.execution_revision == entry.revision && attributes(execution) == entry.attributes &&
|
|
98
|
+
(!owner || execution.team_id == owner.to_s) && (!revision || execution.execution_revision == revision) &&
|
|
99
|
+
(active.nil? || execution.active? == active)
|
|
100
|
+
unless valid
|
|
101
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Team Execution identity/metadata"
|
|
102
|
+
end
|
|
103
|
+
execution
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module MultiAgent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Owns Team root encoding and validates returned storage metadata.
|
|
7
|
+
# @api private
|
|
8
|
+
class TeamRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def create(root)
|
|
12
|
+
record = Codec.encode_team_root(root)
|
|
13
|
+
@view.atomic do |bound|
|
|
14
|
+
entry = bound.records(Phronomy::TeamStorageSchema::ROOTS).insert(key: root.team_id.to_s,
|
|
15
|
+
revision: Integer(root.team_revision), attributes: {}, record: record)
|
|
16
|
+
decode(entry, root.team_id, revision: root.team_revision)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load(team_id)
|
|
21
|
+
value = @view.atomic do |bound|
|
|
22
|
+
entry = bound.records(Phronomy::TeamStorageSchema::ROOTS).read(team_id.to_s)
|
|
23
|
+
entry && decode(entry, team_id)
|
|
24
|
+
end
|
|
25
|
+
value || raise(Phronomy::Storage::NotFoundError, "Team not found: #{team_id}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save(team_id, expected_revision:, root:)
|
|
29
|
+
expected = Integer(expected_revision)
|
|
30
|
+
revision = Integer(root.team_revision)
|
|
31
|
+
raise Phronomy::Storage::ConflictError, "root save must advance revision exactly once" unless revision == expected + 1
|
|
32
|
+
unless root.team_id.to_s == team_id.to_s
|
|
33
|
+
raise Phronomy::Storage::SerializationError, "Team root identity mismatch"
|
|
34
|
+
end
|
|
35
|
+
record = Codec.encode_team_root(root)
|
|
36
|
+
@view.atomic do |bound|
|
|
37
|
+
entry = bound.records(Phronomy::TeamStorageSchema::ROOTS).replace(key: team_id.to_s,
|
|
38
|
+
expected_revision: expected, next_revision: revision, attributes: {}, record: record)
|
|
39
|
+
decode(entry, team_id, revision: revision)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(team_id)
|
|
44
|
+
@view.records(Phronomy::TeamStorageSchema::ROOTS).delete(key: team_id.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def decode(entry, team_id, revision: nil)
|
|
50
|
+
root = Codec.decode_team_root(entry.record)
|
|
51
|
+
valid = entry.key == team_id.to_s && root.team_id == entry.key &&
|
|
52
|
+
root.team_revision == entry.revision && (!revision || revision == entry.revision)
|
|
53
|
+
unless valid
|
|
54
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Team identity/revision"
|
|
55
|
+
end
|
|
56
|
+
root
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
module
|
|
5
|
-
# Implements peer coordination through
|
|
4
|
+
module MultiAgent
|
|
5
|
+
# Implements peer coordination through an invocation-local KnowledgeStore.
|
|
6
|
+
# @api public
|
|
6
7
|
class SharedState
|
|
7
8
|
# Semantic revision of the framework-owned SharedState instrumentation
|
|
8
9
|
# applied on top of one specific researcher definition revision.
|
|
@@ -13,6 +14,8 @@ module Phronomy
|
|
|
13
14
|
INSTRUMENTATION_DEFINITION_VERSION = 1
|
|
14
15
|
private_constant :INSTRUMENTATION_DEFINITION_VERSION
|
|
15
16
|
|
|
17
|
+
# Findings shared by the sequential members of one invocation.
|
|
18
|
+
# @api public
|
|
16
19
|
class KnowledgeStore
|
|
17
20
|
def initialize
|
|
18
21
|
@findings = []
|
|
@@ -78,8 +81,15 @@ module Phronomy
|
|
|
78
81
|
# @api public
|
|
79
82
|
def invoke(input, config: {})
|
|
80
83
|
validate_termination!
|
|
81
|
-
|
|
82
84
|
store = KnowledgeStore.new
|
|
85
|
+
cycles, terminated_by = coordinate_members(input, store)
|
|
86
|
+
|
|
87
|
+
{output: aggregate_findings(store), cycles: cycles, terminated_by: terminated_by}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def coordinate_members(input, store)
|
|
83
93
|
max_cycles = self.class._max_cycles
|
|
84
94
|
deadline = self.class._timeout ? Time.now + self.class._timeout : nil
|
|
85
95
|
terminated_by = :max_cycles
|
|
@@ -87,15 +97,7 @@ module Phronomy
|
|
|
87
97
|
cycle_limit = max_cycles || Float::INFINITY
|
|
88
98
|
|
|
89
99
|
(1..cycle_limit).each do |cycle|
|
|
90
|
-
|
|
91
|
-
invoke_researcher(
|
|
92
|
-
member_config[:klass],
|
|
93
|
-
store,
|
|
94
|
-
cycle,
|
|
95
|
-
input,
|
|
96
|
-
member_config[:instruction]
|
|
97
|
-
)
|
|
98
|
-
end
|
|
100
|
+
invoke_members(input, store, cycle)
|
|
99
101
|
completed_cycles = cycle
|
|
100
102
|
|
|
101
103
|
if self.class._terminate_when&.call(store)
|
|
@@ -109,17 +111,25 @@ module Phronomy
|
|
|
109
111
|
end
|
|
110
112
|
end
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
[completed_cycles, terminated_by]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def invoke_members(input, store, cycle)
|
|
118
|
+
self.class._members.each do |member_config|
|
|
119
|
+
invoke_researcher(
|
|
120
|
+
member_config[:klass], store, cycle, input, member_config[:instruction]
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def aggregate_findings(store)
|
|
126
|
+
if self.class._aggregator
|
|
113
127
|
self.class._aggregator.call(store)
|
|
114
128
|
else
|
|
115
129
|
store.read_all
|
|
116
130
|
end
|
|
117
|
-
|
|
118
|
-
{output: output, cycles: completed_cycles, terminated_by: terminated_by}
|
|
119
131
|
end
|
|
120
132
|
|
|
121
|
-
private
|
|
122
|
-
|
|
123
133
|
def validate_termination!
|
|
124
134
|
return if self.class._max_cycles || self.class._timeout
|
|
125
135
|
|
|
@@ -148,28 +158,8 @@ module Phronomy
|
|
|
148
158
|
|
|
149
159
|
def build_instrumented_researcher(researcher_class, store, cycle)
|
|
150
160
|
agent_key = researcher_class.name&.to_sym || researcher_class.object_id.to_s.to_sym
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
tool_name "read_store"
|
|
154
|
-
description "Read all current findings from the shared knowledge store. " \
|
|
155
|
-
"Call this to see what other researchers have discovered."
|
|
156
|
-
execution_mode :cooperative
|
|
157
|
-
|
|
158
|
-
define_method(:execute) { store.read_all.to_json }
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
write_tool = Class.new(Phronomy::Agent::Context::Capability::Base) do
|
|
162
|
-
tool_name "write_finding"
|
|
163
|
-
description "Record a new finding into the shared knowledge store so " \
|
|
164
|
-
"that other researchers can build on your discovery."
|
|
165
|
-
execution_mode :cooperative
|
|
166
|
-
param :content, type: :string, desc: "The finding to record"
|
|
167
|
-
|
|
168
|
-
define_method(:execute) do |content:|
|
|
169
|
-
store.write(agent: agent_key, content: content, cycle: cycle)
|
|
170
|
-
"Finding recorded."
|
|
171
|
-
end
|
|
172
|
-
end
|
|
161
|
+
read_tool = build_read_tool(store)
|
|
162
|
+
write_tool = build_write_tool(store, cycle, agent_key)
|
|
173
163
|
|
|
174
164
|
definitions = researcher_class.tools.to_h do |tool_class|
|
|
175
165
|
[tool_class, researcher_class.tool_aliases[tool_class]]
|
|
@@ -197,12 +187,39 @@ module Phronomy
|
|
|
197
187
|
end
|
|
198
188
|
end
|
|
199
189
|
|
|
190
|
+
def build_read_tool(store)
|
|
191
|
+
Class.new(Phronomy::Agent::Context::Capability::Base) do
|
|
192
|
+
tool_name "read_store"
|
|
193
|
+
description "Read all current findings from the shared knowledge store. " \
|
|
194
|
+
"Call this to see what other researchers have discovered."
|
|
195
|
+
execution_mode :cooperative
|
|
196
|
+
|
|
197
|
+
define_method(:execute) { store.read_all.to_json }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def build_write_tool(store, cycle, agent_key)
|
|
202
|
+
Class.new(Phronomy::Agent::Context::Capability::Base) do
|
|
203
|
+
tool_name "write_finding"
|
|
204
|
+
description "Record a new finding into the shared knowledge store so " \
|
|
205
|
+
"that other researchers can build on your discovery."
|
|
206
|
+
execution_mode :cooperative
|
|
207
|
+
param :content, type: :string, desc: "The finding to record"
|
|
208
|
+
|
|
209
|
+
define_method(:execute) do |content:|
|
|
210
|
+
store.write(agent: agent_key, content: content, cycle: cycle)
|
|
211
|
+
"Finding recorded."
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
200
216
|
def instrumented_definition_for(researcher_class)
|
|
201
217
|
parent_def = researcher_class.agent_definition
|
|
202
218
|
parent_id = parent_def.fetch(:id)
|
|
203
219
|
parent_version = parent_def.fetch(:version)
|
|
204
220
|
|
|
205
221
|
{
|
|
222
|
+
# Keep the existing semantic ID across the Ruby namespace migration.
|
|
206
223
|
id: "Phronomy::Agent::SharedState::Instrumented/#{parent_id}@#{parent_version}".freeze,
|
|
207
224
|
version: INSTRUMENTATION_DEFINITION_VERSION
|
|
208
225
|
}.freeze
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Domain-owned Team metadata, independent of coordination runtime loading.
|
|
5
|
+
# @api private
|
|
6
|
+
module TeamStorageSchema
|
|
7
|
+
ROOTS = Phronomy::Storage::Resource.new(id: "team.roots", kind: :records,
|
|
8
|
+
guard: {resource: "team.roots", via: :key})
|
|
9
|
+
EXECUTIONS = Phronomy::Storage::Resource.new(id: "team.executions", kind: :records,
|
|
10
|
+
attributes: {owner: :string, active: :boolean}, immutable_attributes: [:owner],
|
|
11
|
+
indexes: {owner: [:owner], owner_active: [:owner, :active]},
|
|
12
|
+
unique: [{name: :one_active_owner, fields: [:owner], where: {active: true}}],
|
|
13
|
+
guard: {resource: ROOTS.id, via: :owner})
|
|
14
|
+
end
|
|
15
|
+
end
|