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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Encodes execution facts using the caller's existing content transaction.
|
|
6
|
+
# It neither opens a transaction nor changes durable or live execution state.
|
|
7
|
+
# @api private
|
|
8
|
+
module RuntimeRecordEncoder
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def encode(
|
|
12
|
+
execution,
|
|
13
|
+
agent_id:,
|
|
14
|
+
tx:,
|
|
15
|
+
snapshot:,
|
|
16
|
+
context_candidate:,
|
|
17
|
+
agent_root:
|
|
18
|
+
)
|
|
19
|
+
root = agent_root
|
|
20
|
+
records = []
|
|
21
|
+
calls = []
|
|
22
|
+
|
|
23
|
+
snapshot.fetch(:llm_results).each_with_index do |item, index|
|
|
24
|
+
outcome = item[:response]
|
|
25
|
+
error = item[:error]
|
|
26
|
+
llm_call_id = item.fetch(:llm_call_id).to_s
|
|
27
|
+
intercepted = error.is_a?(ToolCallIntercepted)
|
|
28
|
+
call_error = intercepted ? nil : error
|
|
29
|
+
output_ref = assistant_output_ref(tx, outcome)
|
|
30
|
+
assistant_ref = assistant_message_ref(tx, outcome)
|
|
31
|
+
error_ref = if call_error
|
|
32
|
+
tx.contents.put_json(
|
|
33
|
+
"class" => call_error.class.name,
|
|
34
|
+
"message" => call_error.message
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
usage_ref = if outcome && !outcome.usage.empty?
|
|
38
|
+
tx.contents.put_json(json_value(outcome.usage))
|
|
39
|
+
end
|
|
40
|
+
call = LLMCallRecord.new(
|
|
41
|
+
llm_call_id: llm_call_id,
|
|
42
|
+
execution_id: execution.execution_id,
|
|
43
|
+
sequence: execution.llm_calls.length + index + 1,
|
|
44
|
+
status: call_error ? :failed : :completed,
|
|
45
|
+
manifest_ref: item.fetch(:manifest_ref),
|
|
46
|
+
output_ref: output_ref,
|
|
47
|
+
error_ref: error_ref,
|
|
48
|
+
usage_ref: usage_ref,
|
|
49
|
+
started_at: item.fetch(:started_at),
|
|
50
|
+
completed_at: Time.now.utc.iso8601(6),
|
|
51
|
+
metadata: {
|
|
52
|
+
"streaming" => item[:streaming],
|
|
53
|
+
"tool_call_intercepted" => intercepted,
|
|
54
|
+
"assistant_outcome_captured" => !outcome.nil?,
|
|
55
|
+
"tool_call_count" => outcome ? outcome.tool_calls.length : 0
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
calls << call
|
|
59
|
+
call_ref = tx.contents.put_json(call.to_h)
|
|
60
|
+
records << JournalRecord.new(
|
|
61
|
+
agent_id: agent_id,
|
|
62
|
+
execution_id: execution.execution_id,
|
|
63
|
+
llm_call_id: llm_call_id,
|
|
64
|
+
kind: :llm_call_recorded,
|
|
65
|
+
channel: :audit,
|
|
66
|
+
content_ref: call_ref,
|
|
67
|
+
context_generation: root.transcript_generation,
|
|
68
|
+
context_candidate: false
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if assistant_ref
|
|
72
|
+
records << JournalRecord.new(
|
|
73
|
+
agent_id: agent_id,
|
|
74
|
+
execution_id: execution.execution_id,
|
|
75
|
+
llm_call_id: llm_call_id,
|
|
76
|
+
kind: :assistant_message,
|
|
77
|
+
channel: :llm,
|
|
78
|
+
role: :assistant,
|
|
79
|
+
content_ref: assistant_ref,
|
|
80
|
+
context_generation: root.transcript_generation,
|
|
81
|
+
context_candidate: context_candidate,
|
|
82
|
+
metadata: assistant_message_metadata(outcome)
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if (active = snapshot[:active_call])
|
|
88
|
+
abandoned = LLMCallRecord.new(
|
|
89
|
+
llm_call_id: active.fetch(:llm_call_id),
|
|
90
|
+
execution_id: execution.execution_id,
|
|
91
|
+
sequence: execution.llm_calls.length + calls.length + 1,
|
|
92
|
+
status: :cancelled,
|
|
93
|
+
manifest_ref: active.fetch(:manifest_ref),
|
|
94
|
+
started_at: active.fetch(:started_at),
|
|
95
|
+
completed_at: Time.now.utc.iso8601(6),
|
|
96
|
+
metadata: {
|
|
97
|
+
"reason" => "execution_terminalized_before_provider_settlement"
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
calls << abandoned
|
|
101
|
+
records << JournalRecord.new(
|
|
102
|
+
agent_id: agent_id,
|
|
103
|
+
execution_id: execution.execution_id,
|
|
104
|
+
llm_call_id: abandoned.llm_call_id,
|
|
105
|
+
kind: :llm_call_recorded,
|
|
106
|
+
channel: :audit,
|
|
107
|
+
content_ref: tx.contents.put_json(abandoned.to_h),
|
|
108
|
+
context_generation: root.transcript_generation,
|
|
109
|
+
context_candidate: false
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
snapshot.fetch(:runtime_events).each do |event|
|
|
114
|
+
case event.type
|
|
115
|
+
when :tool_call
|
|
116
|
+
next
|
|
117
|
+
when :tool_result
|
|
118
|
+
payload = event.payload
|
|
119
|
+
llm_call_id = payload[:llm_call_id] || payload["llm_call_id"]
|
|
120
|
+
tool_call_id = payload.fetch(:tool_call_id).to_s
|
|
121
|
+
tool_name = payload.fetch(:tool_name).to_s
|
|
122
|
+
next if execution.working_records.any? do |record|
|
|
123
|
+
record.kind == :tool_message && record.causation_id.to_s == tool_call_id && record.llm_call_id.to_s == llm_call_id.to_s
|
|
124
|
+
end
|
|
125
|
+
result_ref = put_runtime_content(tx, payload.fetch(:tool_result))
|
|
126
|
+
records << JournalRecord.new(
|
|
127
|
+
agent_id: agent_id,
|
|
128
|
+
execution_id: execution.execution_id,
|
|
129
|
+
llm_call_id: llm_call_id,
|
|
130
|
+
kind: :tool_result,
|
|
131
|
+
channel: :tool,
|
|
132
|
+
content_ref: result_ref,
|
|
133
|
+
causation_id: tool_call_id,
|
|
134
|
+
context_generation: root.transcript_generation,
|
|
135
|
+
context_candidate: false,
|
|
136
|
+
metadata: {
|
|
137
|
+
"tool_call_id" => tool_call_id,
|
|
138
|
+
"tool_name" => tool_name
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
message_payload = if payload.key?(:tool_message)
|
|
143
|
+
payload.fetch(:tool_message)
|
|
144
|
+
else
|
|
145
|
+
payload.fetch("tool_message")
|
|
146
|
+
end
|
|
147
|
+
records << JournalRecord.new(
|
|
148
|
+
agent_id: agent_id,
|
|
149
|
+
execution_id: execution.execution_id,
|
|
150
|
+
llm_call_id: llm_call_id,
|
|
151
|
+
kind: :tool_message,
|
|
152
|
+
channel: :tool,
|
|
153
|
+
role: :tool,
|
|
154
|
+
content_ref: tx.contents.put_json(json_value(message_payload)),
|
|
155
|
+
causation_id: tool_call_id,
|
|
156
|
+
context_generation: root.transcript_generation,
|
|
157
|
+
context_candidate: context_candidate,
|
|
158
|
+
metadata: {
|
|
159
|
+
"tool_call_id" => tool_call_id,
|
|
160
|
+
"tool_name" => tool_name
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
[records, calls]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def assistant_message_ref(tx, outcome)
|
|
169
|
+
return unless outcome
|
|
170
|
+
|
|
171
|
+
payload = {
|
|
172
|
+
"role" => "assistant",
|
|
173
|
+
"content" => json_value(outcome.content),
|
|
174
|
+
"tool_calls" => json_value(Array(outcome.tool_calls)),
|
|
175
|
+
"model_id" => outcome.metadata["model_id"]
|
|
176
|
+
}.compact
|
|
177
|
+
tx.contents.put_json(payload)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def assistant_message_metadata(outcome)
|
|
181
|
+
calls = Array(outcome&.tool_calls)
|
|
182
|
+
{
|
|
183
|
+
"tool_call_ids" => calls.map { |call| call.fetch("id").to_s },
|
|
184
|
+
"tool_names" => calls.map { |call| call.fetch("name").to_s }
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def assistant_output_ref(tx, outcome)
|
|
189
|
+
return unless outcome&.content_present?
|
|
190
|
+
|
|
191
|
+
content = outcome.content
|
|
192
|
+
text = content.is_a?(String) ? content : Phronomy::CanonicalJSON.dump(content)
|
|
193
|
+
tx.contents.put_text(text)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def put_runtime_content(tx, value)
|
|
197
|
+
value.is_a?(String) ?
|
|
198
|
+
tx.contents.put_text(value) : tx.contents.put_json(json_value(value))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def json_value(value)
|
|
202
|
+
Phronomy::Values::Serializable.convert(value,
|
|
203
|
+
unsupported_message: "unsupported canonical runtime value")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private_class_method :assistant_message_ref, :assistant_message_metadata,
|
|
207
|
+
:assistant_output_ref
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -69,11 +69,11 @@ module Phronomy
|
|
|
69
69
|
candidate_category: values.fetch(:candidate_category).to_sym,
|
|
70
70
|
policy_category: values.fetch(:policy_category).to_sym,
|
|
71
71
|
role: values[:role]&.to_sym,
|
|
72
|
-
content: Phronomy::
|
|
72
|
+
content: Phronomy::Values::Immutable.copy(values.fetch(:content)),
|
|
73
73
|
content_format: format,
|
|
74
74
|
tool_call_id: values[:tool_call_id]&.to_s&.freeze,
|
|
75
75
|
provenance: provenance,
|
|
76
|
-
metadata: Phronomy::
|
|
76
|
+
metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})
|
|
77
77
|
))
|
|
78
78
|
freeze
|
|
79
79
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Selects Handoff persistence; execution ownership and delivery remain in
|
|
6
|
+
# ExecutionCoordinator. This is not a public subclass extension contract.
|
|
7
|
+
class HandoffExecutionCoordinator < Phronomy::Agent::ExecutionCoordinator
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def build_outcome_committer
|
|
11
|
+
HandoffOutcomeCommitter.new(agent: @agent, persistence: @agent.persistence)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Agent
|
|
7
|
+
# Adds atomic Source transfer while sharing ordinary result persistence.
|
|
8
|
+
# Outcome selection deliberately retains Handoff-specific precedence.
|
|
9
|
+
# @api private
|
|
10
|
+
class HandoffOutcomeCommitter < ExecutionOutcomeCommitter
|
|
11
|
+
# @api private
|
|
12
|
+
def commit_outcome(operation)
|
|
13
|
+
view = operation.terminal_view
|
|
14
|
+
if view.callback_failure
|
|
15
|
+
return commit_failed_outcome(
|
|
16
|
+
operation,
|
|
17
|
+
view.callback_failure.to_stream_callback_error
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
error = view.source_error || view.block_error || view.invocation_error
|
|
21
|
+
raise error if error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
|
|
22
|
+
return commit_failed_outcome(operation, error) if error
|
|
23
|
+
return commit_suspended(operation) if view.phase == :suspended
|
|
24
|
+
|
|
25
|
+
raise view.block_error if view.input_blocked || view.output_blocked
|
|
26
|
+
raise view.invocation_error if view.invocation_error
|
|
27
|
+
|
|
28
|
+
return commit_handed_off(operation) if view.handoff
|
|
29
|
+
|
|
30
|
+
commit_completed(operation)
|
|
31
|
+
rescue => caught
|
|
32
|
+
reconcile_terminal_error(operation, caught)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def commit_handed_off(operation)
|
|
38
|
+
handed_off = next_root = appended = nil
|
|
39
|
+
@persistence.transaction do |tx|
|
|
40
|
+
routing = validated_source_routing(tx, operation.execution)
|
|
41
|
+
context_ref = persist_handoff_context(tx, operation)
|
|
42
|
+
target_id = transfer_routing(tx, operation, routing, context_ref)
|
|
43
|
+
records, handed_off = encode_handoff(tx, operation, target_id, context_ref)
|
|
44
|
+
appended = append_journal(tx, operation.root, records)
|
|
45
|
+
save_execution(tx, operation.execution, handed_off)
|
|
46
|
+
next_root = save_terminal_root(tx, operation.root, appended)
|
|
47
|
+
end
|
|
48
|
+
Outcome.new(type: :handed_off, execution: handed_off, root: next_root,
|
|
49
|
+
appended_records: Array(appended).freeze,
|
|
50
|
+
result: result_base(handed_off, next_root).freeze,
|
|
51
|
+
error: nil, approval_request: nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validated_source_routing(tx, current)
|
|
55
|
+
coordination = current.metadata.fetch("coordination")
|
|
56
|
+
routing = tx.handoff_states.load(coordination.fetch("main_agent_id"))
|
|
57
|
+
unless routing && routing.active_agent_id == @agent.agent_id && routing.handoff_revision == coordination.fetch("handoff_revision")
|
|
58
|
+
raise Phronomy::Storage::ConflictError, "Handoff routing changed before Source transfer"
|
|
59
|
+
end
|
|
60
|
+
if Array(routing.metadata["cancelled_execution_ids"]).include?(current.execution_id)
|
|
61
|
+
raise Phronomy::CancellationError, "Handoff Source turn was cancelled"
|
|
62
|
+
end
|
|
63
|
+
routing
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def persist_handoff_context(tx, operation)
|
|
67
|
+
manifest = SavedContextReader.manifest_from_ref(@agent, operation.execution.metadata.fetch("manifest_ref"))
|
|
68
|
+
context = HandoffProjection.new.build_terminal(view: operation.terminal_view.handoff,
|
|
69
|
+
manifest: manifest, persistence: tx, source_agent_id: @agent.agent_id)
|
|
70
|
+
tx.contents.put_json(context.to_h)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def transfer_routing(tx, operation, routing, context_ref)
|
|
74
|
+
request = operation.terminal_view.handoff
|
|
75
|
+
target_id = "handoff-target-#{Digest::SHA256.hexdigest([operation.execution.execution_id, request.target_agent_id].join("\0"))}"
|
|
76
|
+
target_root = tx.agents.load(request.target_agent_id)
|
|
77
|
+
target_definition = {"id" => target_root.agent_definition_id, "version" => target_root.agent_definition_version}
|
|
78
|
+
transfer = routing.with(active_agent_id: request.target_agent_id,
|
|
79
|
+
active_handoff_context_ref: context_ref, phase: "target_pending",
|
|
80
|
+
pending_source_execution_id: operation.execution.execution_id, pending_target_execution_id: target_id,
|
|
81
|
+
metadata: routing.metadata.merge("target_definition" => target_definition))
|
|
82
|
+
tx.handoff_states.save(routing.main_agent_id, expected_revision: routing.handoff_revision, state: transfer)
|
|
83
|
+
target_id
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def encode_handoff(tx, operation, target_id, context_ref)
|
|
87
|
+
current = operation.execution
|
|
88
|
+
request = operation.terminal_view.handoff
|
|
89
|
+
encoded_records, call_records = encode_runtime_records(tx, operation, context_candidate: false)
|
|
90
|
+
audit_record = encode_handoff_audit(tx, operation)
|
|
91
|
+
records = current.working_records + encoded_records + [audit_record]
|
|
92
|
+
handed_off = current.with(
|
|
93
|
+
status: :handed_off,
|
|
94
|
+
phase: :handed_off,
|
|
95
|
+
working_records: [],
|
|
96
|
+
llm_calls: current.llm_calls + call_records,
|
|
97
|
+
approval_request: nil,
|
|
98
|
+
terminal_reason: "handed_off",
|
|
99
|
+
metadata: current.metadata.merge("handoff_target_agent_id" => request.target_agent_id,
|
|
100
|
+
"handoff_target_execution_id" => target_id, "handoff_context_ref" => context_ref)
|
|
101
|
+
)
|
|
102
|
+
[records, handed_off]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def encode_handoff_audit(tx, operation)
|
|
106
|
+
request = operation.terminal_view.handoff
|
|
107
|
+
audit_ref = tx.contents.put_json(
|
|
108
|
+
"target_agent_id" => request.target_agent_id,
|
|
109
|
+
"responsibility" => request.responsibility,
|
|
110
|
+
"selection_intent" => request.selection_intent.to_h do |category, included|
|
|
111
|
+
[category.to_s, included]
|
|
112
|
+
end
|
|
113
|
+
)
|
|
114
|
+
JournalRecord.new(
|
|
115
|
+
agent_id: @agent.agent_id,
|
|
116
|
+
execution_id: operation.execution.execution_id,
|
|
117
|
+
llm_call_id: request.llm_call_id,
|
|
118
|
+
kind: :execution_handed_off,
|
|
119
|
+
channel: :audit,
|
|
120
|
+
content_ref: audit_ref,
|
|
121
|
+
context_generation: operation.root.transcript_generation,
|
|
122
|
+
context_candidate: false,
|
|
123
|
+
metadata: {
|
|
124
|
+
"target_agent_id" => request.target_agent_id,
|
|
125
|
+
"handoff_tool_call_id" => request.tool_call_id
|
|
126
|
+
}.compact
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -14,7 +14,7 @@ module Phronomy
|
|
|
14
14
|
source = values.transform_keys(&:to_s)
|
|
15
15
|
raise ArgumentError, "HandoffState schema mismatch" unless source.keys.sort == ATTRIBUTES.sort
|
|
16
16
|
canonical = Phronomy::CanonicalJSON.load(Phronomy::CanonicalJSON.dump(source))
|
|
17
|
-
ATTRIBUTES.each { |key| instance_variable_set("@#{key}", Phronomy::
|
|
17
|
+
ATTRIBUTES.each { |key| instance_variable_set("@#{key}", Phronomy::Values::Immutable.copy(canonical.fetch(key))) }
|
|
18
18
|
raise ArgumentError, "missing main_agent_id" if main_agent_id.to_s.empty?
|
|
19
19
|
raise ArgumentError, "invalid handoff_revision" unless handoff_revision.is_a?(Integer) && handoff_revision >= 0
|
|
20
20
|
raise ArgumentError, "invalid metadata" unless metadata.is_a?(Hash)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../engine/concurrency/worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
6
|
module Agent
|
|
5
7
|
class JournalProjection
|
|
8
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
9
|
+
|
|
6
10
|
MESSAGE_KINDS = %i[
|
|
7
11
|
external_message
|
|
8
12
|
assistant_message
|
|
@@ -41,9 +41,9 @@ module Phronomy
|
|
|
41
41
|
ATTRIBUTES.each do |name|
|
|
42
42
|
value = values.fetch(name)
|
|
43
43
|
value = value.to_sym if %i[kind channel role visibility].include?(name) && value
|
|
44
|
-
instance_variable_set("@#{name}", Immutable.copy(value))
|
|
44
|
+
instance_variable_set("@#{name}", Phronomy::Values::Immutable.copy(value))
|
|
45
45
|
end
|
|
46
|
-
Immutable.validate_canonical_json!(metadata, label: "Journal metadata")
|
|
46
|
+
Phronomy::Values::Immutable.validate_canonical_json!(metadata, label: "Journal metadata")
|
|
47
47
|
freeze
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -52,7 +52,7 @@ module Phronomy
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Current semantic payload representation. Persistence format identity and
|
|
55
|
-
# compatibility validation are owned by Persistence::
|
|
55
|
+
# compatibility validation are owned by Agent::Persistence::Codec.
|
|
56
56
|
def to_h
|
|
57
57
|
ATTRIBUTES.to_h { |name| [name.to_s, public_send(name)] }
|
|
58
58
|
end
|
|
@@ -31,11 +31,11 @@ module Phronomy
|
|
|
31
31
|
ATTRIBUTES.each do |name|
|
|
32
32
|
value = values.fetch(name)
|
|
33
33
|
value = value.to_sym if name == :status
|
|
34
|
-
instance_variable_set("@#{name}", Immutable.copy(value))
|
|
34
|
+
instance_variable_set("@#{name}", Phronomy::Values::Immutable.copy(value))
|
|
35
35
|
end
|
|
36
36
|
raise ArgumentError, "unknown LLM Call status: #{status.inspect}" unless STATUSES.include?(status)
|
|
37
37
|
raise ArgumentError, "LLM Call sequence must be positive" unless sequence.positive?
|
|
38
|
-
Immutable.validate_canonical_json!(metadata, label: "LLM Call metadata")
|
|
38
|
+
Phronomy::Values::Immutable.validate_canonical_json!(metadata, label: "LLM Call metadata")
|
|
39
39
|
freeze
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../engine/concurrency/worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
require "time"
|
|
4
6
|
|
|
5
7
|
module Phronomy
|
|
6
8
|
module Agent
|
|
7
9
|
class AgentRoot
|
|
10
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
11
|
+
|
|
8
12
|
LIFECYCLE_STATUSES = %i[idle active suspended closed invalidated].freeze
|
|
9
13
|
|
|
10
14
|
ATTRIBUTES = %i[
|
|
@@ -36,7 +40,7 @@ module Phronomy
|
|
|
36
40
|
ATTRIBUTES.each do |name|
|
|
37
41
|
value = attributes.fetch(name)
|
|
38
42
|
value = value.to_sym if name == :lifecycle_status
|
|
39
|
-
instance_variable_set("@#{name}", Immutable.copy(value))
|
|
43
|
+
instance_variable_set("@#{name}", Phronomy::Values::Immutable.copy(value))
|
|
40
44
|
end
|
|
41
45
|
unless LIFECYCLE_STATUSES.include?(lifecycle_status)
|
|
42
46
|
raise ArgumentError, "unknown Agent lifecycle status: #{lifecycle_status.inspect}"
|
|
@@ -44,7 +48,7 @@ module Phronomy
|
|
|
44
48
|
raise ArgumentError, "agent_revision must be non-negative" if agent_revision.negative?
|
|
45
49
|
raise ArgumentError, "context_revision must be non-negative" if context_revision.negative?
|
|
46
50
|
raise ArgumentError, "journal_position must be non-negative" if journal_position.negative?
|
|
47
|
-
Immutable.validate_canonical_json!(metadata, label: "Agent metadata")
|
|
51
|
+
Phronomy::Values::Immutable.validate_canonical_json!(metadata, label: "Agent metadata")
|
|
48
52
|
freeze
|
|
49
53
|
end
|
|
50
54
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Agent owns this fresh-instance construction contract. Application
|
|
6
|
+
# composition supplies the concrete default once during loading.
|
|
7
|
+
# @api private
|
|
8
|
+
module DefaultPersistence
|
|
9
|
+
FACTORIES = {}
|
|
10
|
+
private_constant :FACTORIES
|
|
11
|
+
|
|
12
|
+
# A private boot binding, not an application extension registry.
|
|
13
|
+
# @api private
|
|
14
|
+
def self.install_factory(factory)
|
|
15
|
+
raise ArgumentError, "factory must respond to call" unless factory.respond_to?(:call)
|
|
16
|
+
|
|
17
|
+
FACTORIES.replace(persistence: factory).freeze
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Invoke only after explicit and configured instances have been excluded.
|
|
22
|
+
# No instance is cached, shared, or installed in global configuration.
|
|
23
|
+
# @api private
|
|
24
|
+
def self.build
|
|
25
|
+
FACTORIES.fetch(:persistence).call
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
|
|
4
|
+
module Agent
|
|
5
5
|
# Process-local authoritative owner registry for mutable Agent instances.
|
|
6
6
|
#
|
|
7
7
|
# This is not a cache. While a Runtime is alive, one agent_id maps to at most
|
|
8
8
|
# one mutable live Agent object. Construction is reserved before durable
|
|
9
9
|
# create/load so concurrent callers cannot materialize independent objects.
|
|
10
|
-
|
|
10
|
+
# @api private
|
|
11
|
+
class OwnershipRegistry
|
|
12
|
+
# Candidates have no side effects. Runtime retains the first registration.
|
|
13
|
+
# Existing instances remain available for lookup and transition completion
|
|
14
|
+
# after admission closes; their own gate rejects new ownership changes.
|
|
15
|
+
def self.for(runtime)
|
|
16
|
+
existing_for(runtime) || runtime.__register_shutdown_participant(
|
|
17
|
+
key: self, participant: new(runtime: runtime)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.existing_for(runtime)
|
|
22
|
+
runtime.__shutdown_participant(key: self)
|
|
23
|
+
end
|
|
24
|
+
|
|
11
25
|
Entry = Data.define(:state, :agent, :token)
|
|
12
26
|
private_constant :Entry
|
|
13
27
|
|
|
@@ -26,9 +40,9 @@ module Phronomy
|
|
|
26
40
|
begin
|
|
27
41
|
agent = yield(@runtime)
|
|
28
42
|
rescue Phronomy::AgentAlreadyExistsError,
|
|
29
|
-
Phronomy::
|
|
30
|
-
Phronomy::
|
|
31
|
-
Phronomy::
|
|
43
|
+
Phronomy::Storage::ConflictError,
|
|
44
|
+
Phronomy::Storage::NotFoundError,
|
|
45
|
+
Phronomy::Storage::SerializationError,
|
|
32
46
|
ArgumentError,
|
|
33
47
|
Phronomy::ConfigurationError
|
|
34
48
|
release_construction!(key, token)
|
|
@@ -156,10 +170,10 @@ module Phronomy
|
|
|
156
170
|
@mutex.synchronize do
|
|
157
171
|
entry = @entries[key]
|
|
158
172
|
validate_purge_entry!(entry, agent, token, key)
|
|
173
|
+
agent.send(:__mark_purged!, @runtime)
|
|
159
174
|
@entries.delete(key)
|
|
160
175
|
@condition.broadcast
|
|
161
176
|
end
|
|
162
|
-
agent.send(:__mark_purged!, @runtime)
|
|
163
177
|
true
|
|
164
178
|
end
|
|
165
179
|
|
|
@@ -169,10 +183,10 @@ module Phronomy
|
|
|
169
183
|
@mutex.synchronize do
|
|
170
184
|
entry = @entries[key]
|
|
171
185
|
validate_purge_entry!(entry, agent, token, key)
|
|
186
|
+
agent.send(:__restore_live_after_purge_abort!, @runtime)
|
|
172
187
|
@entries[key] = Entry.new(state: :live, agent: agent, token: nil)
|
|
173
188
|
@condition.broadcast
|
|
174
189
|
end
|
|
175
|
-
agent.send(:__restore_live_after_purge_abort!, @runtime)
|
|
176
190
|
true
|
|
177
191
|
end
|
|
178
192
|
|
|
@@ -183,6 +197,7 @@ module Phronomy
|
|
|
183
197
|
key = normalize_agent_id(agent.agent_id)
|
|
184
198
|
@mutex.synchronize do
|
|
185
199
|
validate_purge_entry!(@entries[key], agent, token, key)
|
|
200
|
+
agent.send(:__mark_ownership_recovery_required!, @runtime)
|
|
186
201
|
@entries[key] = Entry.new(
|
|
187
202
|
state: :recovery_required,
|
|
188
203
|
agent: agent,
|
|
@@ -190,7 +205,6 @@ module Phronomy
|
|
|
190
205
|
)
|
|
191
206
|
@condition.broadcast
|
|
192
207
|
end
|
|
193
|
-
agent.send(:__mark_ownership_recovery_required!, @runtime)
|
|
194
208
|
true
|
|
195
209
|
end
|
|
196
210
|
|
|
@@ -204,7 +218,7 @@ module Phronomy
|
|
|
204
218
|
|
|
205
219
|
# Live/recovery-required entries do not prevent Runtime shutdown. Only an
|
|
206
220
|
# ownership transition that is still actively changing state must settle.
|
|
207
|
-
def
|
|
221
|
+
def wait_until_idle(deadline)
|
|
208
222
|
@mutex.synchronize do
|
|
209
223
|
while @entries.values.any? { |entry| %i[constructing purging].include?(entry.state) }
|
|
210
224
|
remaining = deadline - monotonic_now
|
|
@@ -217,7 +231,7 @@ module Phronomy
|
|
|
217
231
|
end
|
|
218
232
|
end
|
|
219
233
|
|
|
220
|
-
def
|
|
234
|
+
def after_runtime_shutdown
|
|
221
235
|
agents = @mutex.synchronize do
|
|
222
236
|
@state = :terminated
|
|
223
237
|
owned = @entries.values.filter_map(&:agent).uniq
|