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,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Owns Agent root encoding and validates returned storage metadata.
|
|
7
|
+
# @api private
|
|
8
|
+
class AgentRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def create(root)
|
|
12
|
+
record = Codec.encode_agent_root(root)
|
|
13
|
+
@view.atomic do |bound|
|
|
14
|
+
entry = bound.records(StorageSchema::ROOTS).insert(key: root.agent_id.to_s,
|
|
15
|
+
revision: Integer(root.agent_revision), attributes: {}, record: record)
|
|
16
|
+
decode(entry, root.agent_id, revision: root.agent_revision)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load(agent_id)
|
|
21
|
+
value = @view.atomic do |bound|
|
|
22
|
+
entry = bound.records(StorageSchema::ROOTS).read(agent_id.to_s)
|
|
23
|
+
entry && decode(entry, agent_id)
|
|
24
|
+
end
|
|
25
|
+
value || raise(Phronomy::Storage::NotFoundError, "Agent not found: #{agent_id}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save(agent_id, expected_revision:, root:)
|
|
29
|
+
expected = Integer(expected_revision)
|
|
30
|
+
revision = Integer(root.agent_revision)
|
|
31
|
+
raise Phronomy::Storage::ConflictError, "root save must advance revision exactly once" unless revision == expected + 1
|
|
32
|
+
unless root.agent_id.to_s == agent_id.to_s
|
|
33
|
+
raise Phronomy::Storage::SerializationError, "Agent root identity mismatch"
|
|
34
|
+
end
|
|
35
|
+
record = Codec.encode_agent_root(root)
|
|
36
|
+
@view.atomic do |bound|
|
|
37
|
+
entry = bound.records(StorageSchema::ROOTS).replace(key: agent_id.to_s,
|
|
38
|
+
expected_revision: expected, next_revision: revision, attributes: {}, record: record)
|
|
39
|
+
decode(entry, agent_id, revision: revision)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(agent_id)
|
|
44
|
+
@view.records(StorageSchema::ROOTS).delete(key: agent_id.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def decode(entry, agent_id, revision: nil)
|
|
50
|
+
root = Codec.decode_agent_root(entry.record)
|
|
51
|
+
valid = entry.key == agent_id.to_s && root.agent_id == entry.key &&
|
|
52
|
+
root.agent_revision == entry.revision && (!revision || revision == entry.revision)
|
|
53
|
+
unless valid
|
|
54
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Agent identity/revision"
|
|
55
|
+
end
|
|
56
|
+
root
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
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
|
+
AGENT_ROOT_RECORD_TYPE = "phronomy.agent_root"
|
|
12
|
+
AGENT_ROOT_FORMAT_VERSION = "0.1"
|
|
13
|
+
AGENT_EXECUTION_RECORD_TYPE = "phronomy.agent_execution"
|
|
14
|
+
AGENT_EXECUTION_FORMAT_VERSION = "0.1"
|
|
15
|
+
JOURNAL_RECORD_TYPE = "phronomy.journal_record"
|
|
16
|
+
JOURNAL_FORMAT_VERSION = "0.1"
|
|
17
|
+
AGENT_ROOT_KEYS = %w[
|
|
18
|
+
agent_id agent_definition_id agent_definition_version agent_revision
|
|
19
|
+
context_revision journal_position lifecycle_status transcript_generation
|
|
20
|
+
created_at updated_at metadata
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
AGENT_EXECUTION_KEYS = %w[
|
|
24
|
+
execution_id agent_id execution_revision status phase
|
|
25
|
+
base_agent_revision base_context_revision base_journal_position
|
|
26
|
+
working_records llm_calls approval_request result_ref error_ref
|
|
27
|
+
created_at updated_at terminal_reason metadata
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
JOURNAL_RECORD_KEYS = %w[
|
|
31
|
+
record_id agent_id sequence execution_id llm_call_id kind channel role
|
|
32
|
+
content_ref parent_id causation_id visibility context_generation
|
|
33
|
+
context_candidate occurred_at metadata
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
LLM_CALL_RECORD_KEYS = %w[
|
|
37
|
+
llm_call_id execution_id sequence status manifest_ref output_ref
|
|
38
|
+
error_ref usage_ref started_at completed_at metadata
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
APPROVAL_REQUEST_KEYS = %w[id execution_id items created_at].freeze
|
|
42
|
+
|
|
43
|
+
APPROVAL_REQUEST_OPTIONAL_KEYS = %w[approved].freeze
|
|
44
|
+
|
|
45
|
+
APPROVAL_ITEM_KEYS = %w[
|
|
46
|
+
tool_invocation_id tool_call_id tool_name arguments facts reason origin metadata
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
def encode_handoff_state(value)
|
|
52
|
+
payload = value.to_h
|
|
53
|
+
Phronomy::Agent::HandoffState.from_h(payload)
|
|
54
|
+
build_record("phronomy.handoff_state", "0.1", payload)
|
|
55
|
+
rescue Phronomy::Storage::SerializationError
|
|
56
|
+
raise
|
|
57
|
+
rescue => error
|
|
58
|
+
serialization_error("cannot encode HandoffState", error)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def decode_handoff_state(record)
|
|
62
|
+
payload = current_payload!(record, record_type: "phronomy.handoff_state",
|
|
63
|
+
format_version: "0.1", keys: Phronomy::Agent::HandoffState::ATTRIBUTES, label: "HandoffState")
|
|
64
|
+
Phronomy::Agent::HandoffState.from_h(payload)
|
|
65
|
+
rescue Phronomy::Storage::SerializationError
|
|
66
|
+
raise
|
|
67
|
+
rescue => error
|
|
68
|
+
serialization_error("cannot decode HandoffState", error)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def encode_agent_root(root)
|
|
72
|
+
payload = top_level_string_keys(root.to_h, label: "AgentRoot payload")
|
|
73
|
+
payload["lifecycle_status"] = root.lifecycle_status.to_s
|
|
74
|
+
validate_agent_root_payload!(payload)
|
|
75
|
+
build_record(AGENT_ROOT_RECORD_TYPE, AGENT_ROOT_FORMAT_VERSION, payload)
|
|
76
|
+
rescue Phronomy::Storage::SerializationError
|
|
77
|
+
raise
|
|
78
|
+
rescue => error
|
|
79
|
+
serialization_error("cannot encode AgentRoot", error)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def decode_agent_root(record)
|
|
83
|
+
payload = current_payload!(
|
|
84
|
+
record,
|
|
85
|
+
record_type: AGENT_ROOT_RECORD_TYPE,
|
|
86
|
+
format_version: AGENT_ROOT_FORMAT_VERSION,
|
|
87
|
+
keys: AGENT_ROOT_KEYS,
|
|
88
|
+
label: "AgentRoot payload"
|
|
89
|
+
)
|
|
90
|
+
validate_agent_root_payload!(payload)
|
|
91
|
+
Phronomy::Agent::AgentRoot.from_h(payload)
|
|
92
|
+
rescue Phronomy::Storage::SerializationError
|
|
93
|
+
raise
|
|
94
|
+
rescue => error
|
|
95
|
+
serialization_error("cannot decode AgentRoot", error)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def encode_agent_execution(execution)
|
|
99
|
+
payload = top_level_string_keys(execution.to_h, label: "AgentExecution payload")
|
|
100
|
+
payload["status"] = execution.status.to_s
|
|
101
|
+
payload["phase"] = execution.phase.to_s
|
|
102
|
+
payload["working_records"] = execution.working_records.map do |record|
|
|
103
|
+
journal_payload(record, require_sequence: false)
|
|
104
|
+
end
|
|
105
|
+
payload["llm_calls"] = execution.llm_calls.map do |call|
|
|
106
|
+
llm_call_payload(call)
|
|
107
|
+
end
|
|
108
|
+
validate_agent_execution_payload!(payload)
|
|
109
|
+
build_record(
|
|
110
|
+
AGENT_EXECUTION_RECORD_TYPE,
|
|
111
|
+
AGENT_EXECUTION_FORMAT_VERSION,
|
|
112
|
+
payload
|
|
113
|
+
)
|
|
114
|
+
rescue Phronomy::Storage::SerializationError
|
|
115
|
+
raise
|
|
116
|
+
rescue => error
|
|
117
|
+
serialization_error("cannot encode AgentExecution", error)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def decode_agent_execution(record)
|
|
121
|
+
payload = current_payload!(
|
|
122
|
+
record,
|
|
123
|
+
record_type: AGENT_EXECUTION_RECORD_TYPE,
|
|
124
|
+
format_version: AGENT_EXECUTION_FORMAT_VERSION,
|
|
125
|
+
keys: AGENT_EXECUTION_KEYS,
|
|
126
|
+
label: "AgentExecution payload"
|
|
127
|
+
)
|
|
128
|
+
validate_agent_execution_payload!(payload)
|
|
129
|
+
Phronomy::Agent::AgentExecution.from_h(payload)
|
|
130
|
+
rescue Phronomy::Storage::SerializationError
|
|
131
|
+
raise
|
|
132
|
+
rescue => error
|
|
133
|
+
serialization_error("cannot decode AgentExecution", error)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def encode_journal_record(journal_record)
|
|
137
|
+
payload = journal_payload(journal_record, require_sequence: true)
|
|
138
|
+
build_record(JOURNAL_RECORD_TYPE, JOURNAL_FORMAT_VERSION, payload)
|
|
139
|
+
rescue Phronomy::Storage::SerializationError
|
|
140
|
+
raise
|
|
141
|
+
rescue => error
|
|
142
|
+
serialization_error("cannot encode JournalRecord", error)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def decode_journal_record(record)
|
|
146
|
+
payload = current_payload!(
|
|
147
|
+
record,
|
|
148
|
+
record_type: JOURNAL_RECORD_TYPE,
|
|
149
|
+
format_version: JOURNAL_FORMAT_VERSION,
|
|
150
|
+
keys: JOURNAL_RECORD_KEYS,
|
|
151
|
+
label: "JournalRecord payload"
|
|
152
|
+
)
|
|
153
|
+
validate_journal_payload!(payload, label: "JournalRecord payload", require_sequence: true)
|
|
154
|
+
Phronomy::Agent::JournalRecord.from_h(payload)
|
|
155
|
+
rescue Phronomy::Storage::SerializationError
|
|
156
|
+
raise
|
|
157
|
+
rescue => error
|
|
158
|
+
serialization_error("cannot decode JournalRecord", error)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def validate_agent_root_payload!(payload)
|
|
162
|
+
validate_exact_keys!(payload, AGENT_ROOT_KEYS, label: "AgentRoot payload")
|
|
163
|
+
require_nonempty_string!(payload, "agent_id", label: "AgentRoot payload")
|
|
164
|
+
require_nonempty_string!(payload, "agent_definition_id", label: "AgentRoot payload")
|
|
165
|
+
require_integer!(payload, "agent_definition_version", label: "AgentRoot payload")
|
|
166
|
+
require_nonnegative_integer!(payload, "agent_revision", label: "AgentRoot payload")
|
|
167
|
+
require_nonnegative_integer!(payload, "context_revision", label: "AgentRoot payload")
|
|
168
|
+
require_nonnegative_integer!(payload, "journal_position", label: "AgentRoot payload")
|
|
169
|
+
require_enum_string!(
|
|
170
|
+
payload,
|
|
171
|
+
"lifecycle_status",
|
|
172
|
+
Phronomy::Agent::AgentRoot::LIFECYCLE_STATUSES.map(&:to_s),
|
|
173
|
+
label: "AgentRoot payload"
|
|
174
|
+
)
|
|
175
|
+
require_nonnegative_integer!(payload, "transcript_generation", label: "AgentRoot payload")
|
|
176
|
+
require_nonempty_string!(payload, "created_at", label: "AgentRoot payload")
|
|
177
|
+
require_nonempty_string!(payload, "updated_at", label: "AgentRoot payload")
|
|
178
|
+
require_canonical_hash!(payload, "metadata", label: "AgentRoot payload")
|
|
179
|
+
payload
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_agent_execution_payload!(payload)
|
|
183
|
+
validate_exact_keys!(payload, AGENT_EXECUTION_KEYS, label: "AgentExecution payload")
|
|
184
|
+
require_nonempty_string!(payload, "execution_id", label: "AgentExecution payload")
|
|
185
|
+
require_nonempty_string!(payload, "agent_id", label: "AgentExecution payload")
|
|
186
|
+
require_nonnegative_integer!(payload, "execution_revision", label: "AgentExecution payload")
|
|
187
|
+
require_enum_string!(
|
|
188
|
+
payload,
|
|
189
|
+
"status",
|
|
190
|
+
Phronomy::Agent::AgentExecution::TRANSITIONS.keys.map(&:to_s),
|
|
191
|
+
label: "AgentExecution payload"
|
|
192
|
+
)
|
|
193
|
+
require_nonempty_string!(payload, "phase", label: "AgentExecution payload")
|
|
194
|
+
require_nonnegative_integer!(payload, "base_agent_revision", label: "AgentExecution payload")
|
|
195
|
+
require_nonnegative_integer!(payload, "base_context_revision", label: "AgentExecution payload")
|
|
196
|
+
require_nonnegative_integer!(payload, "base_journal_position", label: "AgentExecution payload")
|
|
197
|
+
require_optional_string!(payload, "result_ref", label: "AgentExecution payload")
|
|
198
|
+
require_optional_string!(payload, "error_ref", label: "AgentExecution payload")
|
|
199
|
+
require_nonempty_string!(payload, "created_at", label: "AgentExecution payload")
|
|
200
|
+
require_nonempty_string!(payload, "updated_at", label: "AgentExecution payload")
|
|
201
|
+
require_optional_string!(payload, "terminal_reason", label: "AgentExecution payload")
|
|
202
|
+
require_canonical_hash!(payload, "metadata", label: "AgentExecution payload")
|
|
203
|
+
|
|
204
|
+
working_records = payload.fetch("working_records")
|
|
205
|
+
unless working_records.is_a?(Array)
|
|
206
|
+
raise Phronomy::Storage::SerializationError,
|
|
207
|
+
"AgentExecution payload working_records must be an Array"
|
|
208
|
+
end
|
|
209
|
+
working_records.each_with_index do |record, index|
|
|
210
|
+
validate_journal_payload!(
|
|
211
|
+
record,
|
|
212
|
+
label: "AgentExecution working_records[#{index}]",
|
|
213
|
+
require_sequence: false
|
|
214
|
+
)
|
|
215
|
+
record_agent_id = record.fetch("agent_id")
|
|
216
|
+
unless record_agent_id == payload.fetch("agent_id")
|
|
217
|
+
raise Phronomy::Storage::SerializationError,
|
|
218
|
+
"AgentExecution working_records[#{index}] agent_id mismatch"
|
|
219
|
+
end
|
|
220
|
+
record_execution_id = record.fetch("execution_id")
|
|
221
|
+
if record_execution_id && record_execution_id != payload.fetch("execution_id")
|
|
222
|
+
raise Phronomy::Storage::SerializationError,
|
|
223
|
+
"AgentExecution working_records[#{index}] execution_id mismatch"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
llm_calls = payload.fetch("llm_calls")
|
|
228
|
+
unless llm_calls.is_a?(Array)
|
|
229
|
+
raise Phronomy::Storage::SerializationError,
|
|
230
|
+
"AgentExecution payload llm_calls must be an Array"
|
|
231
|
+
end
|
|
232
|
+
llm_calls.each_with_index do |call, index|
|
|
233
|
+
validate_llm_call_payload!(call, label: "AgentExecution llm_calls[#{index}]")
|
|
234
|
+
unless call.fetch("execution_id") == payload.fetch("execution_id")
|
|
235
|
+
raise Phronomy::Storage::SerializationError,
|
|
236
|
+
"AgentExecution llm_calls[#{index}] execution_id mismatch"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
validate_approval_request!(
|
|
241
|
+
payload.fetch("approval_request"),
|
|
242
|
+
execution_id: payload.fetch("execution_id")
|
|
243
|
+
)
|
|
244
|
+
payload
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def validate_journal_payload!(payload, label:, require_sequence:)
|
|
248
|
+
validate_exact_keys!(payload, JOURNAL_RECORD_KEYS, label: label)
|
|
249
|
+
require_nonempty_string!(payload, "record_id", label: label)
|
|
250
|
+
require_nonempty_string!(payload, "agent_id", label: label)
|
|
251
|
+
sequence = payload.fetch("sequence")
|
|
252
|
+
if require_sequence
|
|
253
|
+
unless sequence.is_a?(Integer) && sequence.positive?
|
|
254
|
+
raise Phronomy::Storage::SerializationError,
|
|
255
|
+
"#{label} sequence must be a positive Integer"
|
|
256
|
+
end
|
|
257
|
+
elsif !(sequence.nil? || (sequence.is_a?(Integer) && sequence.positive?))
|
|
258
|
+
raise Phronomy::Storage::SerializationError,
|
|
259
|
+
"#{label} sequence must be nil or a positive Integer"
|
|
260
|
+
end
|
|
261
|
+
require_optional_string!(payload, "execution_id", label: label)
|
|
262
|
+
require_optional_string!(payload, "llm_call_id", label: label)
|
|
263
|
+
require_nonempty_string!(payload, "kind", label: label)
|
|
264
|
+
require_nonempty_string!(payload, "channel", label: label)
|
|
265
|
+
require_optional_string!(payload, "role", label: label)
|
|
266
|
+
require_optional_string!(payload, "content_ref", label: label)
|
|
267
|
+
require_optional_string!(payload, "parent_id", label: label)
|
|
268
|
+
require_optional_string!(payload, "causation_id", label: label)
|
|
269
|
+
require_nonempty_string!(payload, "visibility", label: label)
|
|
270
|
+
require_nonnegative_integer!(payload, "context_generation", label: label)
|
|
271
|
+
require_boolean!(payload, "context_candidate", label: label)
|
|
272
|
+
require_nonempty_string!(payload, "occurred_at", label: label)
|
|
273
|
+
require_canonical_hash!(payload, "metadata", label: label)
|
|
274
|
+
payload
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def validate_llm_call_payload!(payload, label:)
|
|
278
|
+
validate_exact_keys!(payload, LLM_CALL_RECORD_KEYS, label: label)
|
|
279
|
+
require_nonempty_string!(payload, "llm_call_id", label: label)
|
|
280
|
+
require_nonempty_string!(payload, "execution_id", label: label)
|
|
281
|
+
require_positive_integer!(payload, "sequence", label: label)
|
|
282
|
+
require_enum_string!(
|
|
283
|
+
payload,
|
|
284
|
+
"status",
|
|
285
|
+
Phronomy::Agent::LLMCallRecord::STATUSES.map(&:to_s),
|
|
286
|
+
label: label
|
|
287
|
+
)
|
|
288
|
+
require_nonempty_string!(payload, "manifest_ref", label: label)
|
|
289
|
+
require_optional_string!(payload, "output_ref", label: label)
|
|
290
|
+
require_optional_string!(payload, "error_ref", label: label)
|
|
291
|
+
require_optional_string!(payload, "usage_ref", label: label)
|
|
292
|
+
require_nonempty_string!(payload, "started_at", label: label)
|
|
293
|
+
require_optional_string!(payload, "completed_at", label: label)
|
|
294
|
+
require_canonical_hash!(payload, "metadata", label: label)
|
|
295
|
+
payload
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def validate_approval_request!(request, execution_id:)
|
|
299
|
+
return if request.nil?
|
|
300
|
+
|
|
301
|
+
validate_allowed_keys!(
|
|
302
|
+
request,
|
|
303
|
+
required_keys: APPROVAL_REQUEST_KEYS,
|
|
304
|
+
optional_keys: APPROVAL_REQUEST_OPTIONAL_KEYS,
|
|
305
|
+
label: "approval_request"
|
|
306
|
+
)
|
|
307
|
+
require_nonempty_string!(request, "id", label: "approval_request")
|
|
308
|
+
require_nonempty_string!(request, "execution_id", label: "approval_request")
|
|
309
|
+
unless request.fetch("execution_id") == execution_id
|
|
310
|
+
raise Phronomy::Storage::SerializationError,
|
|
311
|
+
"approval_request execution_id mismatch"
|
|
312
|
+
end
|
|
313
|
+
require_nonempty_string!(request, "created_at", label: "approval_request")
|
|
314
|
+
if request.key?("approved") && !boolean?(request.fetch("approved"))
|
|
315
|
+
raise Phronomy::Storage::SerializationError,
|
|
316
|
+
"approval_request approved must be true or false"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
items = request.fetch("items")
|
|
320
|
+
unless items.is_a?(Array) && !items.empty?
|
|
321
|
+
raise Phronomy::Storage::SerializationError,
|
|
322
|
+
"approval_request items must be a non-empty Array"
|
|
323
|
+
end
|
|
324
|
+
items.each_with_index do |item, index|
|
|
325
|
+
item_label = "approval_request items[#{index}]"
|
|
326
|
+
validate_exact_keys!(item, APPROVAL_ITEM_KEYS, label: item_label)
|
|
327
|
+
require_nonempty_string!(item, "tool_invocation_id", label: item_label)
|
|
328
|
+
require_optional_string!(item, "tool_call_id", label: item_label)
|
|
329
|
+
require_nonempty_string!(item, "tool_name", label: item_label)
|
|
330
|
+
require_canonical_hash!(item, "arguments", label: item_label)
|
|
331
|
+
require_canonical_hash!(item, "facts", label: item_label)
|
|
332
|
+
require_optional_string!(item, "reason", label: item_label)
|
|
333
|
+
require_nonempty_string!(item, "origin", label: item_label)
|
|
334
|
+
require_canonical_hash!(item, "metadata", label: item_label)
|
|
335
|
+
end
|
|
336
|
+
request
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def journal_payload(record, require_sequence:)
|
|
340
|
+
payload = top_level_string_keys(record.to_h, label: "JournalRecord payload")
|
|
341
|
+
%w[kind channel role visibility].each do |key|
|
|
342
|
+
value = payload[key]
|
|
343
|
+
payload[key] = value.to_s if value
|
|
344
|
+
end
|
|
345
|
+
validate_journal_payload!(payload, label: "JournalRecord payload", require_sequence: require_sequence)
|
|
346
|
+
payload
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def llm_call_payload(call)
|
|
350
|
+
payload = top_level_string_keys(call.to_h, label: "LLMCallRecord payload")
|
|
351
|
+
payload["status"] = call.status.to_s
|
|
352
|
+
validate_llm_call_payload!(payload, label: "LLMCallRecord payload")
|
|
353
|
+
payload
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Owns execution activity, ownership, admission and durable record encoding.
|
|
7
|
+
# @api private
|
|
8
|
+
class ExecutionRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def create_active(execution)
|
|
12
|
+
raise Phronomy::Storage::SerializationError, "create_active requires an active AgentExecution" unless execution.active?
|
|
13
|
+
record = Codec.encode_agent_execution(execution)
|
|
14
|
+
admission do
|
|
15
|
+
@view.atomic do |bound|
|
|
16
|
+
entry = bound.records(StorageSchema::EXECUTIONS).insert(key: execution.execution_id.to_s,
|
|
17
|
+
revision: Integer(execution.execution_revision), attributes: attributes(execution), record: record)
|
|
18
|
+
decode(entry, execution.execution_id, owner: execution.agent_id,
|
|
19
|
+
revision: execution.execution_revision, active: true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load(execution_id)
|
|
25
|
+
value = @view.atomic do |bound|
|
|
26
|
+
entry = bound.records(StorageSchema::EXECUTIONS).read(execution_id.to_s)
|
|
27
|
+
entry && decode(entry, execution_id)
|
|
28
|
+
end
|
|
29
|
+
value || raise(Phronomy::Storage::NotFoundError, "Execution not found: #{execution_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save(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.execution_id.to_s == execution_id.to_s
|
|
37
|
+
raise Phronomy::Storage::SerializationError, "Execution identity mismatch"
|
|
38
|
+
end
|
|
39
|
+
record = Codec.encode_agent_execution(execution)
|
|
40
|
+
admission do
|
|
41
|
+
@view.atomic do |bound|
|
|
42
|
+
entry = bound.records(StorageSchema::EXECUTIONS).replace(key: execution_id.to_s,
|
|
43
|
+
expected_revision: expected, next_revision: revision, attributes: attributes(execution),
|
|
44
|
+
expected_attributes: {}, record: record)
|
|
45
|
+
decode(entry, execution_id, owner: execution.agent_id, revision: revision, active: execution.active?)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def list_active(agent_id)
|
|
51
|
+
@view.atomic do |bound|
|
|
52
|
+
bound.records(StorageSchema::EXECUTIONS).scan(index: :owner_active,
|
|
53
|
+
equals: {owner: agent_id.to_s, active: true}).map { |entry| decode(entry, entry.key, owner: agent_id, active: true) }.freeze
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def list(agent_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(StorageSchema::EXECUTIONS).scan(index: :owner, equals: {owner: agent_id.to_s},
|
|
61
|
+
after: after&.to_s, limit: limit).map { |entry| decode(entry, entry.key, owner: agent_id) }.freeze
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def delete(execution_id)
|
|
66
|
+
@view.records(StorageSchema::EXECUTIONS).delete(key: execution_id.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete_for_agent(agent_id)
|
|
70
|
+
@view.records(StorageSchema::EXECUTIONS).delete_matching(index: :owner, equals: {owner: agent_id.to_s})
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def assert_idle!(agent_id)
|
|
74
|
+
condition = Phronomy::Storage::Condition::NoRows.new(resource: StorageSchema::EXECUTIONS,
|
|
75
|
+
index: :owner_active, equals: {owner: agent_id.to_s, active: true})
|
|
76
|
+
@view.check!(guards: [Phronomy::Storage::GuardRef.new(resource: StorageSchema::ROOTS, key: agent_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.agent_id.to_s, active: execution.active?}
|
|
86
|
+
|
|
87
|
+
def admission
|
|
88
|
+
yield
|
|
89
|
+
rescue Phronomy::Storage::UniqueConstraintError => error
|
|
90
|
+
raise unless error.resource == StorageSchema::EXECUTIONS.id && error.constraint == :one_active_owner
|
|
91
|
+
raise Phronomy::AgentBusyError, error.message
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def decode(entry, execution_id, owner: nil, revision: nil, active: nil)
|
|
95
|
+
execution = Codec.decode_agent_execution(entry.record)
|
|
96
|
+
valid = entry.key == execution_id.to_s && execution.execution_id == entry.key &&
|
|
97
|
+
execution.execution_revision == entry.revision && attributes(execution) == entry.attributes &&
|
|
98
|
+
(!owner || execution.agent_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 Agent Execution identity/metadata"
|
|
102
|
+
end
|
|
103
|
+
execution
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Owns Handoff state encoding and the initial revision.
|
|
7
|
+
# @api private
|
|
8
|
+
class HandoffStateRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def load(main_agent_id)
|
|
12
|
+
@view.atomic do |bound|
|
|
13
|
+
entry = bound.records(StorageSchema::HANDOFF_STATES).read(main_agent_id.to_s)
|
|
14
|
+
entry && decode(entry, main_agent_id)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save(main_agent_id, expected_revision:, state:)
|
|
19
|
+
expected = expected_revision.nil? ? nil : Integer(expected_revision)
|
|
20
|
+
revision = expected.nil? ? 1 : expected + 1
|
|
21
|
+
unless state.main_agent_id == main_agent_id.to_s && state.handoff_revision == revision
|
|
22
|
+
raise Phronomy::Storage::SerializationError, "Handoff identity/revision mismatch"
|
|
23
|
+
end
|
|
24
|
+
record = Codec.encode_handoff_state(state)
|
|
25
|
+
@view.atomic do |bound|
|
|
26
|
+
records = bound.records(StorageSchema::HANDOFF_STATES)
|
|
27
|
+
values = {key: main_agent_id.to_s, attributes: {active_agent_id: state.active_agent_id}, record: record}
|
|
28
|
+
entry = if expected.nil?
|
|
29
|
+
records.insert(**values, revision: revision)
|
|
30
|
+
else
|
|
31
|
+
records.replace(**values, expected_revision: expected, next_revision: revision)
|
|
32
|
+
end
|
|
33
|
+
decode(entry, main_agent_id, revision: revision)
|
|
34
|
+
end
|
|
35
|
+
rescue Phronomy::Storage::NotFoundError => error
|
|
36
|
+
raise Phronomy::Storage::ConflictError, error.message
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def delete(main_agent_id, expected_revision:)
|
|
40
|
+
@view.records(StorageSchema::HANDOFF_STATES).delete(key: main_agent_id.to_s, expected_revision: Integer(expected_revision))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def decode(entry, main_agent_id, revision: nil)
|
|
46
|
+
state = Codec.decode_handoff_state(entry.record)
|
|
47
|
+
valid = entry.key == main_agent_id.to_s && state.main_agent_id == entry.key &&
|
|
48
|
+
state.handoff_revision == entry.revision && (!revision || revision == entry.revision) &&
|
|
49
|
+
entry.attributes == {active_agent_id: state.active_agent_id}
|
|
50
|
+
unless valid
|
|
51
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Handoff identity/revision"
|
|
52
|
+
end
|
|
53
|
+
state
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module Persistence
|
|
6
|
+
# Assigns domain sequence numbers and validates opaque stream entries.
|
|
7
|
+
# @api private
|
|
8
|
+
class JournalRepository
|
|
9
|
+
def initialize(view) = @view = view
|
|
10
|
+
|
|
11
|
+
def append(agent_id, expected_position:, records:)
|
|
12
|
+
expected = Integer(expected_position)
|
|
13
|
+
entries = Array(records).each_with_index.map do |record, index|
|
|
14
|
+
unless record.agent_id.to_s == agent_id.to_s
|
|
15
|
+
raise Phronomy::Storage::SerializationError, "Journal record Agent mismatch"
|
|
16
|
+
end
|
|
17
|
+
sequenced = record.with_sequence(expected + index + 1)
|
|
18
|
+
Phronomy::Storage::Entry::Append.new(id: sequenced.record_id.to_s, record: Codec.encode_journal_record(sequenced))
|
|
19
|
+
end
|
|
20
|
+
@view.atomic do |bound|
|
|
21
|
+
stored = bound.streams(StorageSchema::JOURNAL).append(stream: agent_id.to_s,
|
|
22
|
+
expected_head: expected, entries: entries)
|
|
23
|
+
decode(stored, agent_id, after: expected)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def read(agent_id, after: nil, limit: nil)
|
|
28
|
+
position = after.nil? ? 0 : Integer(after)
|
|
29
|
+
count = limit.nil? ? nil : Integer(limit)
|
|
30
|
+
@view.atomic do |bound|
|
|
31
|
+
stored = (count == 0) ? [] : bound.streams(StorageSchema::JOURNAL).read(stream: agent_id.to_s, after: position, limit: count)
|
|
32
|
+
decode(stored, agent_id, after: position)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def head(agent_id) = @view.streams(StorageSchema::JOURNAL).head(stream: agent_id.to_s)
|
|
37
|
+
def delete(agent_id) = @view.streams(StorageSchema::JOURNAL).delete(stream: agent_id.to_s)
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def decode(entries, agent_id, after:)
|
|
42
|
+
entries.each_with_index.map do |entry, index|
|
|
43
|
+
record = Codec.decode_journal_record(entry.record)
|
|
44
|
+
unless record.agent_id == agent_id.to_s && record.record_id == entry.id &&
|
|
45
|
+
record.sequence == entry.position && entry.position == after + index + 1
|
|
46
|
+
raise Phronomy::Storage::SerializationError, "backend returned another Journal identity/sequence"
|
|
47
|
+
end
|
|
48
|
+
record
|
|
49
|
+
end.freeze
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|