phronomy 0.25.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 +71 -0
- data/CONTRIBUTING.md +4 -4
- data/README.md +14 -7
- data/VERIFY.sh +27 -27
- data/benchmark/bench_agent_invoke.rb +26 -22
- data/benchmark/bench_context_assembler.rb +4 -5
- data/benchmark/bench_regression.rb +1 -1
- 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 +5 -3
- 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 +29 -6
- 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 +4 -4
- data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
- data/docs/architecture.md +209 -0
- data/docs/async-composition.md +205 -0
- data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
- 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 +21 -12
- data/docs/getting-started.md +10 -10
- data/docs/migrations/0.15.md +5 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +7 -1
- 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 +205 -60
- 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 +55 -251
- 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} +20 -96
- 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} +12 -13
- 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} +22 -73
- 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 +36 -41
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
- data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +9 -13
- 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/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +2 -2
- 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} +15 -8
- 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 +29 -16
- data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
- data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
- data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
- data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
- data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
- data/lib/phronomy/engine/event_loop.rb +160 -626
- data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
- data/lib/phronomy/engine/execution.rb +229 -0
- data/lib/phronomy/engine/execution_cancellation_error.rb +14 -0
- data/lib/phronomy/engine/execution_receiver.rb +65 -0
- data/lib/phronomy/engine/execution_timeout_error.rb +14 -0
- data/lib/phronomy/engine/fsm_protocol.rb +14 -0
- data/lib/phronomy/engine/fsm_session.rb +38 -31
- data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +3 -1
- data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +3 -1
- data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +3 -1
- data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +13 -1
- 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/timer_queue.rb +11 -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.rb → task_result.rb} +101 -42
- 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_adapter/base.rb +2 -2
- 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 +8 -8
- data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
- data/lib/phronomy/multi_agent/orchestrator.rb +36 -68
- 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/eval/scorer/llm_judge.rb +5 -3
- data/lib/phronomy/testing/fake_clock.rb +13 -9
- 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 +6 -6
- data/lib/phronomy/vector_store/async_backend.rb +5 -5
- data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
- 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} +126 -75
- 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 +8 -8
- data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
- data/lib/phronomy.rb +52 -94
- data/scripts/api_snapshot.rb +4 -2
- data/scripts/storage_spi_snapshot.rb +36 -0
- data/sig/phronomy/agent.rbs +4 -5
- data/sig/phronomy/execution_receiver.rbs +34 -0
- data/sig/phronomy/extensions.rbs +5 -5
- data/sig/phronomy/handoff.rbs +4 -2
- data/sig/phronomy/multi_agent.rbs +19 -3
- data/sig/phronomy/persistence.rbs +7 -91
- data/sig/phronomy/runtime.rbs +34 -7
- data/sig/phronomy/storage.rbs +174 -0
- data/sig/phronomy/tool.rbs +11 -2
- data/sig/phronomy/workflow.rbs +1 -1
- metadata +236 -99
- data/lib/phronomy/agent/execution_coordinator.rb +0 -3138
- 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/fan_out_invocation.rb +0 -137
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
- 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/{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/{metrics.rb → engine/metrics.rb} +0 -0
- /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
class GeneratorVerifier
|
|
5
|
+
class PipelineState
|
|
6
|
+
include Phronomy::WorkflowContext
|
|
7
|
+
|
|
8
|
+
field :input, type: :replace, default: -> { "" }
|
|
9
|
+
field :draft, type: :replace, default: -> { {} }
|
|
10
|
+
field :self_score, type: :replace, default: -> { 0.0 }
|
|
11
|
+
field :review_score, type: :replace, default: -> { 0.0 }
|
|
12
|
+
field :citations, type: :replace, default: -> { [] }
|
|
13
|
+
field :review_notes, type: :append, default: -> { [] }
|
|
14
|
+
field :iteration, type: :replace, default: -> { 0 }
|
|
15
|
+
field :approved, type: :replace, default: -> { false }
|
|
16
|
+
field :output, type: :replace, default: -> { {} }
|
|
17
|
+
field :draft_request_id, type: :replace, default: nil
|
|
18
|
+
field :review_request_id, type: :replace, default: nil
|
|
19
|
+
field :pipeline_error, type: :replace, default: nil
|
|
20
|
+
|
|
21
|
+
# Application-level event interpretation. Correlation IDs belong to this
|
|
22
|
+
# Pipeline rather than to the generic FSMSession.
|
|
23
|
+
def handle_fsm_event(event)
|
|
24
|
+
case event.type
|
|
25
|
+
when :draft_completed
|
|
26
|
+
return :consume unless event.payload[:request_id] == draft_request_id
|
|
27
|
+
|
|
28
|
+
self.draft = event.payload[:draft]
|
|
29
|
+
self.self_score = event.payload[:self_score]
|
|
30
|
+
self.citations = event.payload[:citations]
|
|
31
|
+
self.iteration = iteration + 1
|
|
32
|
+
self.draft_request_id = nil
|
|
33
|
+
self.pipeline_error = nil
|
|
34
|
+
when :review_completed
|
|
35
|
+
return :consume unless event.payload[:request_id] == review_request_id
|
|
36
|
+
|
|
37
|
+
self.review_score = event.payload[:review_score]
|
|
38
|
+
self.approved = event.payload[:approved]
|
|
39
|
+
self.review_notes = review_notes + [event.payload[:feedback]]
|
|
40
|
+
self.review_request_id = nil
|
|
41
|
+
self.pipeline_error = nil
|
|
42
|
+
when :draft_failed
|
|
43
|
+
return :consume unless event.payload[:request_id] == draft_request_id
|
|
44
|
+
|
|
45
|
+
self.pipeline_error = event.payload[:error]
|
|
46
|
+
self.draft_request_id = nil
|
|
47
|
+
when :review_failed
|
|
48
|
+
return :consume unless event.payload[:request_id] == review_request_id
|
|
49
|
+
|
|
50
|
+
self.pipeline_error = event.payload[:error]
|
|
51
|
+
self.review_request_id = nil
|
|
52
|
+
end
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
class GeneratorVerifier
|
|
7
|
+
# Builds the generation/review loop; result reception does not mutate state.
|
|
8
|
+
class WorkflowBuilder
|
|
9
|
+
def initialize(
|
|
10
|
+
draft_agent:, review_agent:,
|
|
11
|
+
draft_prompt_builder:, review_prompt_builder:,
|
|
12
|
+
draft_result_parser:, review_result_parser:,
|
|
13
|
+
threshold:, max_iterations:
|
|
14
|
+
)
|
|
15
|
+
@draft_agent = draft_agent
|
|
16
|
+
@review_agent = review_agent
|
|
17
|
+
@draft_prompt_builder = draft_prompt_builder
|
|
18
|
+
@review_prompt_builder = review_prompt_builder
|
|
19
|
+
@threshold = threshold
|
|
20
|
+
@max_iterations = max_iterations
|
|
21
|
+
@result_receiver = AgentResultReceiver.new(
|
|
22
|
+
draft_result_parser: draft_result_parser,
|
|
23
|
+
review_result_parser: review_result_parser
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build
|
|
28
|
+
# The entry closures see the Workflow assigned after definition.
|
|
29
|
+
workflow = nil
|
|
30
|
+
draft_entry = ->(state) { start_draft(state, workflow) }
|
|
31
|
+
review_entry = ->(state) { start_review(state, workflow) }
|
|
32
|
+
finalize_entry = method(:finalize)
|
|
33
|
+
failed_entry = method(:fail_pipeline)
|
|
34
|
+
ready_to_finalize = method(:ready_to_finalize?)
|
|
35
|
+
|
|
36
|
+
workflow = Phronomy::Workflow.define(PipelineState) do
|
|
37
|
+
initial :draft
|
|
38
|
+
|
|
39
|
+
state :draft
|
|
40
|
+
state :review
|
|
41
|
+
state :finalize
|
|
42
|
+
state :failed
|
|
43
|
+
|
|
44
|
+
entry :draft, draft_entry
|
|
45
|
+
entry :review, review_entry
|
|
46
|
+
entry :finalize, finalize_entry
|
|
47
|
+
entry :failed, failed_entry
|
|
48
|
+
|
|
49
|
+
transition from: :draft, on: :draft_completed, to: :review
|
|
50
|
+
transition from: :draft, on: :draft_failed, to: :failed
|
|
51
|
+
transition from: :review,
|
|
52
|
+
on: :review_completed,
|
|
53
|
+
guard: ready_to_finalize,
|
|
54
|
+
to: :finalize
|
|
55
|
+
transition from: :review, on: :review_completed, to: :draft
|
|
56
|
+
transition from: :review, on: :review_failed, to: :failed
|
|
57
|
+
|
|
58
|
+
transition from: :finalize, to: :__finish__
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def start_draft(state, workflow)
|
|
65
|
+
request_id = SecureRandom.uuid
|
|
66
|
+
next_state = state.merge(draft_request_id: request_id)
|
|
67
|
+
feedback = next_state.review_notes.last
|
|
68
|
+
prompt = @draft_prompt_builder.call(next_state.input, feedback)
|
|
69
|
+
listener = @result_receiver.draft_listener(
|
|
70
|
+
workflow: workflow,
|
|
71
|
+
workflow_instance_id: next_state.workflow_instance_id,
|
|
72
|
+
request_id: request_id
|
|
73
|
+
)
|
|
74
|
+
@draft_agent.send(:__invoke_async_with_event_sink, prompt, on_event: listener)
|
|
75
|
+
next_state
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def start_review(state, workflow)
|
|
79
|
+
request_id = SecureRandom.uuid
|
|
80
|
+
next_state = state.merge(review_request_id: request_id)
|
|
81
|
+
prompt = @review_prompt_builder.call(
|
|
82
|
+
next_state.input, next_state.draft, next_state.citations
|
|
83
|
+
)
|
|
84
|
+
listener = @result_receiver.review_listener(
|
|
85
|
+
workflow: workflow,
|
|
86
|
+
workflow_instance_id: next_state.workflow_instance_id,
|
|
87
|
+
request_id: request_id
|
|
88
|
+
)
|
|
89
|
+
@review_agent.send(:__invoke_async_with_event_sink, prompt, on_event: listener)
|
|
90
|
+
next_state
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ready_to_finalize?(state)
|
|
94
|
+
confidence = [state.self_score || 0.0, state.review_score || 0.0].min
|
|
95
|
+
(confidence >= @threshold && state.approved) ||
|
|
96
|
+
state.iteration >= @max_iterations
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def finalize(state)
|
|
100
|
+
state.output = state.draft
|
|
101
|
+
state
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def fail_pipeline(state)
|
|
105
|
+
raise(
|
|
106
|
+
state.pipeline_error ||
|
|
107
|
+
Phronomy::Error.new("GeneratorVerifier Agent operation failed")
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Implements the Generator-Verifier multi-agent coordination pattern.
|
|
5
|
+
#
|
|
6
|
+
# Agent completion is integrated through application-defined Workflow events;
|
|
7
|
+
# Workflow entry actions do not return or await Agent Tasks.
|
|
8
|
+
class GeneratorVerifier
|
|
9
|
+
DEFAULT_CONFIDENCE_THRESHOLD = 0.7
|
|
10
|
+
DEFAULT_MAX_ITERATIONS = 3
|
|
11
|
+
|
|
12
|
+
Result = Struct.new(
|
|
13
|
+
:output,
|
|
14
|
+
:confidence,
|
|
15
|
+
:citations,
|
|
16
|
+
:iterations,
|
|
17
|
+
:review_notes,
|
|
18
|
+
:trusted
|
|
19
|
+
) do
|
|
20
|
+
alias_method :trusted?, :trusted
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private_constant :PipelineState, :WorkflowBuilder, :AgentResultReceiver
|
|
24
|
+
|
|
25
|
+
def initialize(
|
|
26
|
+
draft_agent:,
|
|
27
|
+
review_agent:,
|
|
28
|
+
draft_prompt_builder:,
|
|
29
|
+
review_prompt_builder:,
|
|
30
|
+
draft_result_parser: nil,
|
|
31
|
+
review_result_parser: nil,
|
|
32
|
+
confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
|
|
33
|
+
max_iterations: DEFAULT_MAX_ITERATIONS,
|
|
34
|
+
raise_if_untrusted: false
|
|
35
|
+
)
|
|
36
|
+
@draft_agent_class = draft_agent
|
|
37
|
+
@review_agent_class = review_agent
|
|
38
|
+
@draft_prompt_builder = draft_prompt_builder
|
|
39
|
+
@review_prompt_builder = review_prompt_builder
|
|
40
|
+
@draft_result_parser =
|
|
41
|
+
draft_result_parser || method(:default_parse_draft)
|
|
42
|
+
@review_result_parser =
|
|
43
|
+
review_result_parser || method(:default_parse_review)
|
|
44
|
+
@threshold = confidence_threshold.to_f
|
|
45
|
+
@max_iterations = max_iterations.to_i
|
|
46
|
+
@raise_if_untrusted = raise_if_untrusted
|
|
47
|
+
@compiled_workflow = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def invoke(input, config: {})
|
|
51
|
+
state = compiled_workflow.invoke({input: input}, config: config)
|
|
52
|
+
confidence = combined_confidence(state)
|
|
53
|
+
trusted = confidence >= @threshold
|
|
54
|
+
result = Result.new(
|
|
55
|
+
output: state.output || state.draft.to_s,
|
|
56
|
+
confidence: confidence,
|
|
57
|
+
citations: state.citations,
|
|
58
|
+
iterations: state.iteration,
|
|
59
|
+
review_notes: state.review_notes,
|
|
60
|
+
trusted: trusted
|
|
61
|
+
)
|
|
62
|
+
if @raise_if_untrusted && !trusted
|
|
63
|
+
raise LowConfidenceError.new(result)
|
|
64
|
+
end
|
|
65
|
+
result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def combined_confidence(state)
|
|
71
|
+
[
|
|
72
|
+
(state.self_score || 0.0).to_f,
|
|
73
|
+
(state.review_score || 0.0).to_f
|
|
74
|
+
].min
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def compiled_workflow
|
|
78
|
+
@compiled_workflow ||= build_workflow
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_workflow
|
|
82
|
+
WorkflowBuilder.new(
|
|
83
|
+
draft_agent: @draft_agent_class.new,
|
|
84
|
+
review_agent: @review_agent_class.new,
|
|
85
|
+
draft_prompt_builder: @draft_prompt_builder,
|
|
86
|
+
review_prompt_builder: @review_prompt_builder,
|
|
87
|
+
draft_result_parser: @draft_result_parser,
|
|
88
|
+
review_result_parser: @review_result_parser,
|
|
89
|
+
threshold: @threshold,
|
|
90
|
+
max_iterations: @max_iterations
|
|
91
|
+
).build
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def default_parse_draft(text)
|
|
95
|
+
json_parser.parse(text)
|
|
96
|
+
rescue Phronomy::ParseError
|
|
97
|
+
{
|
|
98
|
+
answer: text.to_s,
|
|
99
|
+
confidence: 0.0,
|
|
100
|
+
citations: []
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def default_parse_review(text)
|
|
105
|
+
json_parser.parse(text)
|
|
106
|
+
rescue Phronomy::ParseError
|
|
107
|
+
{
|
|
108
|
+
approved: false,
|
|
109
|
+
score: 0.0,
|
|
110
|
+
feedback: "Review output could not be parsed: #{text}"
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def json_parser
|
|
115
|
+
@json_parser ||= Phronomy::OutputParser::JsonParser.new
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../common/error"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
class LowConfidenceError < Error
|
|
7
|
+
attr_reader :result
|
|
8
|
+
|
|
9
|
+
def initialize(result)
|
|
10
|
+
@result = result
|
|
11
|
+
super("Answer confidence #{result.confidence} is below the required threshold")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -52,7 +52,7 @@ module Phronomy
|
|
|
52
52
|
# Transport timeout and retry remain the responsibility of the adapter or
|
|
53
53
|
# provider client; Phronomy does not attach an additional operation timeout.
|
|
54
54
|
#
|
|
55
|
-
# @return [Phronomy::
|
|
55
|
+
# @return [Phronomy::TaskResult] caller-facing completion handle
|
|
56
56
|
# @api private
|
|
57
57
|
def complete_async(chat, message, config: {}, pool: default_pool)
|
|
58
58
|
token = config[:cancellation_token]
|
|
@@ -68,7 +68,7 @@ module Phronomy
|
|
|
68
68
|
# application callbacks must never be passed directly to this method.
|
|
69
69
|
#
|
|
70
70
|
# @yield [chunk] streaming chunk on the worker thread
|
|
71
|
-
# @return [Phronomy::
|
|
71
|
+
# @return [Phronomy::TaskResult] caller-facing completion handle
|
|
72
72
|
# @api private
|
|
73
73
|
def stream_async(chat, message, config: {}, pool: default_pool, &block)
|
|
74
74
|
raise ArgumentError, "stream_async requires a block" unless block
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module LlmContextWindow
|
|
5
|
-
# Immutable arithmetic
|
|
6
|
-
# Model-registry lookup belongs to Agent::TokenBudgetResolver.
|
|
5
|
+
# Immutable input-budget arithmetic. RubyLLM owns model capabilities.
|
|
7
6
|
class TokenBudget
|
|
8
|
-
attr_reader :
|
|
7
|
+
attr_reader :max_input_tokens
|
|
9
8
|
|
|
10
9
|
# @api private
|
|
11
|
-
def initialize(
|
|
12
|
-
@
|
|
13
|
-
|
|
10
|
+
def initialize(max_input_tokens:)
|
|
11
|
+
@max_input_tokens = Integer(max_input_tokens)
|
|
12
|
+
raise ArgumentError, "max_input_tokens must be positive" unless @max_input_tokens.positive?
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
# @api private
|
|
17
16
|
def effective_input_limit
|
|
18
|
-
|
|
17
|
+
@max_input_tokens
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
# @api private
|
|
@@ -2,18 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module MultiAgent
|
|
5
|
-
# Runtime-local admission for one
|
|
6
|
-
#
|
|
5
|
+
# Runtime-local admission for one synchronous coordination call per owner
|
|
6
|
+
# (a Handoff main Agent or TeamCoordinator). This does not own durable runs
|
|
7
|
+
# or active-agent mutation; those remain in their existing domains.
|
|
8
|
+
# @api private
|
|
7
9
|
class AdmissionRegistry
|
|
10
|
+
# Construction has no side effects. Runtime returns the registered instance
|
|
11
|
+
# when callers concurrently supply candidates for the same key.
|
|
12
|
+
# @api private
|
|
13
|
+
def self.for(runtime)
|
|
14
|
+
runtime.__register_shutdown_participant(key: self, participant: new)
|
|
15
|
+
end
|
|
16
|
+
|
|
8
17
|
def initialize
|
|
9
18
|
@mutex = Mutex.new
|
|
10
19
|
@cond = ConditionVariable.new
|
|
11
20
|
@owners = {}
|
|
21
|
+
@draining = false
|
|
12
22
|
end
|
|
13
23
|
|
|
14
24
|
def admit!(coordinator)
|
|
15
25
|
key = coordinator.object_id
|
|
16
26
|
@mutex.synchronize do
|
|
27
|
+
if @draining
|
|
28
|
+
raise Phronomy::RuntimeShutdownError,
|
|
29
|
+
"Runtime is shutting down or failed; new Multi-Agent turns are not accepted"
|
|
30
|
+
end
|
|
17
31
|
if @owners.key?(key)
|
|
18
32
|
raise Phronomy::HandoffError,
|
|
19
33
|
"a Multi-Agent turn is already active for this main Agent instance"
|
|
@@ -36,6 +50,12 @@ module Phronomy
|
|
|
36
50
|
@mutex.synchronize { @owners.empty? }
|
|
37
51
|
end
|
|
38
52
|
|
|
53
|
+
# Close admission atomically with respect to admit!, without waiting for
|
|
54
|
+
# admitted callers or calling back into Runtime. Repeated closure is safe.
|
|
55
|
+
def begin_draining
|
|
56
|
+
@mutex.synchronize { @draining = true }
|
|
57
|
+
end
|
|
58
|
+
|
|
39
59
|
def wait_until_idle(deadline)
|
|
40
60
|
@mutex.synchronize do
|
|
41
61
|
until @owners.empty?
|
|
@@ -4,7 +4,7 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
module MultiAgent
|
|
7
|
-
# Agent-owned transactions reserve child work;
|
|
7
|
+
# Agent-owned transactions reserve child work; TaskResult callbacks only wake observers.
|
|
8
8
|
# @api private
|
|
9
9
|
class DurableSubagentCoordinator
|
|
10
10
|
KEY = "multi_agent_coordination_ref"
|
|
@@ -16,13 +16,13 @@ module Phronomy
|
|
|
16
16
|
children.each do |child|
|
|
17
17
|
begin
|
|
18
18
|
exact = tx.executions.load(child.fetch("execution_id"))
|
|
19
|
-
rescue Phronomy::
|
|
19
|
+
rescue Phronomy::Storage::NotFoundError
|
|
20
20
|
next
|
|
21
21
|
end
|
|
22
|
-
raise Phronomy::
|
|
22
|
+
raise Phronomy::Storage::ConflictError, "Child owner mismatch" unless exact.agent_id == child.fetch("agent_id")
|
|
23
23
|
child.merge!("state" => exact.status.to_s, "result_ref" => exact.result_ref, "error_ref" => exact.error_ref)
|
|
24
24
|
end
|
|
25
|
-
Array(execution.metadata[Phronomy::Agent::
|
|
25
|
+
Array(execution.metadata[Phronomy::Agent::ExecutionMetadata::TOOL_BATCH_METADATA_KEY]).each do |tool|
|
|
26
26
|
registration = parent.class.registered_subagents.find { |name, _| "dispatch_to_#{name}" == tool.fetch("tool_name") }
|
|
27
27
|
next unless registration
|
|
28
28
|
next if children.any? { |child| child.fetch("slot") == tool.fetch("tool_invocation_id") }
|
|
@@ -47,10 +47,10 @@ module Phronomy
|
|
|
47
47
|
|
|
48
48
|
def self.start(parent:, tool_invocation_id:, parent_execution_id:, config:)
|
|
49
49
|
runtime = Phronomy::Runtime.instance
|
|
50
|
-
completion = Phronomy::
|
|
50
|
+
completion = Phronomy::TaskResult.deferred(name: "durable-subagent:#{tool_invocation_id}")
|
|
51
51
|
preparation = runtime.offload.submit(on_full: :raise) do
|
|
52
52
|
current = parent.persistence.executions.load(parent_execution_id)
|
|
53
|
-
raise Phronomy::
|
|
53
|
+
raise Phronomy::Storage::ConflictError, "Parent owner mismatch" unless current.agent_id == parent.agent_id
|
|
54
54
|
snapshot = parent.persistence.contents.fetch_json(current.metadata.fetch(KEY))
|
|
55
55
|
child = snapshot.fetch("children").find { |entry| entry.fetch("slot") == tool_invocation_id }
|
|
56
56
|
raise Phronomy::ExecutionRehydrationRequiredError, "Missing reserved child slot" unless child
|
|
@@ -65,7 +65,7 @@ module Phronomy
|
|
|
65
65
|
begin
|
|
66
66
|
parent.persistence.agents.load(id)
|
|
67
67
|
exists = true
|
|
68
|
-
rescue Phronomy::
|
|
68
|
+
rescue Phronomy::Storage::NotFoundError
|
|
69
69
|
exists = false
|
|
70
70
|
end
|
|
71
71
|
listener = parent.send(:_phronomy_event_listener)
|
|
@@ -125,7 +125,7 @@ module Phronomy
|
|
|
125
125
|
end
|
|
126
126
|
completion
|
|
127
127
|
rescue => error
|
|
128
|
-
completion ||= Phronomy::
|
|
128
|
+
completion ||= Phronomy::TaskResult.deferred(name: "durable-subagent")
|
|
129
129
|
completion.fail(error)
|
|
130
130
|
completion
|
|
131
131
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
5
|
module Phronomy
|
|
6
|
-
module
|
|
7
|
-
#
|
|
6
|
+
module MultiAgent
|
|
7
|
+
# Coordinates durable handoffs across Agents using one Persistence instance.
|
|
8
8
|
# @api public
|
|
9
9
|
class HandoffRunner
|
|
10
10
|
MAX_HANDOFFS = 20
|
|
@@ -16,7 +16,7 @@ module Phronomy
|
|
|
16
16
|
def initialize(main_agent:, handoffs: [])
|
|
17
17
|
@main_agent, @handoffs = main_agent, Array(handoffs).freeze
|
|
18
18
|
@persistence = main_agent.persistence
|
|
19
|
-
raise ArgumentError, "handoffs must contain Agent::Handoff" unless @handoffs.all? { |h| h.is_a?(Handoff) }
|
|
19
|
+
raise ArgumentError, "handoffs must contain Agent::Handoff" unless @handoffs.all? { |h| h.is_a?(Phronomy::Agent::Handoff) }
|
|
20
20
|
@agents = ([main_agent] + @handoffs.flat_map { |edge| [edge.source_agent, edge.target_agent] }).uniq.to_h { |a| [a.agent_id, a] }.freeze
|
|
21
21
|
unless @agents.values.all? { |a| a.persistence.equal?(@persistence) }
|
|
22
22
|
raise Phronomy::ConfigurationError, "Durable Handoff graph requires one Persistence instance"
|
|
@@ -24,8 +24,9 @@ module Phronomy
|
|
|
24
24
|
if @handoffs.group_by { |h| [h.source_agent.agent_id, h.target_agent.agent_id] }.any? { |_, edges| edges.size > 1 }
|
|
25
25
|
raise ArgumentError, "Duplicate Source/Target Handoff edges"
|
|
26
26
|
end
|
|
27
|
-
@bindings = @handoffs.group_by { |h| h.source_agent.agent_id }.transform_values { |edges| edges.map { |h| HandoffCapabilityFactory.build(h) }.freeze }.freeze
|
|
27
|
+
@bindings = @handoffs.group_by { |h| h.source_agent.agent_id }.transform_values { |edges| edges.map { |h| Phronomy::Agent::HandoffCapabilityFactory.build(h) }.freeze }.freeze
|
|
28
28
|
@runtime = Phronomy::Runtime.instance
|
|
29
|
+
@admissions = Phronomy::MultiAgent::AdmissionRegistry.for(@runtime)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# @api public
|
|
@@ -41,7 +42,7 @@ module Phronomy
|
|
|
41
42
|
raise Phronomy::RuntimeShutdownError, "HandoffRunner belongs to a previous Runtime"
|
|
42
43
|
end
|
|
43
44
|
config = config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new)
|
|
44
|
-
@
|
|
45
|
+
@admissions.admit!(main_agent)
|
|
45
46
|
admitted = true
|
|
46
47
|
state = load_state
|
|
47
48
|
count = 0
|
|
@@ -49,7 +50,7 @@ module Phronomy
|
|
|
49
50
|
active = @agents.fetch(state.active_agent_id) do
|
|
50
51
|
raise Phronomy::ExecutionRehydrationRequiredError, "Handoff graph lacks active Agent #{state.active_agent_id}"
|
|
51
52
|
end
|
|
52
|
-
context = state.active_handoff_context_ref && HandoffContext.from_h(@persistence.contents.fetch_json(state.active_handoff_context_ref))
|
|
53
|
+
context = state.active_handoff_context_ref && Phronomy::Agent::HandoffContext.from_h(@persistence.contents.fetch_json(state.active_handoff_context_ref))
|
|
53
54
|
wiring = config.merge(phronomy_handoff_bindings: @bindings.fetch(active.agent_id, []),
|
|
54
55
|
phronomy_handoff_context: context,
|
|
55
56
|
phronomy_coordination: {"kind" => "handoff", "main_agent_id" => main_agent.agent_id, "handoff_revision" => state.handoff_revision}).freeze
|
|
@@ -61,11 +62,11 @@ module Phronomy
|
|
|
61
62
|
else
|
|
62
63
|
exact = unfinished.fetch(0)
|
|
63
64
|
unless unfinished.size == 1 && exact.metadata.dig("coordination", "main_agent_id") == main_agent.agent_id
|
|
64
|
-
raise Phronomy::
|
|
65
|
+
raise Phronomy::Storage::ConflictError, "Active Agent execution belongs to another coordination turn"
|
|
65
66
|
end
|
|
66
67
|
wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(exact.execution_id)
|
|
67
68
|
stored_input = @persistence.contents.fetch_text(exact.metadata.fetch("current_input_ref"))
|
|
68
|
-
ExactExecution.start(agent: active, execution_id: exact.execution_id, input: stored_input, config: wiring).wait_result
|
|
69
|
+
Phronomy::Agent::ExactExecution.start(agent: active, execution_id: exact.execution_id, input: stored_input, config: wiring).wait_result
|
|
69
70
|
end
|
|
70
71
|
else
|
|
71
72
|
source = @persistence.executions.load(state.pending_source_execution_id)
|
|
@@ -77,7 +78,7 @@ module Phronomy
|
|
|
77
78
|
raise Phronomy::ConfigurationError, "Handoff Target definition mismatch"
|
|
78
79
|
end
|
|
79
80
|
wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(state.pending_target_execution_id)
|
|
80
|
-
ExactExecution.start(agent: active, execution_id: state.pending_target_execution_id,
|
|
81
|
+
Phronomy::Agent::ExactExecution.start(agent: active, execution_id: state.pending_target_execution_id,
|
|
81
82
|
input: context.responsibility, config: wiring).wait_result
|
|
82
83
|
end
|
|
83
84
|
execution = @persistence.executions.load(result.fetch(:execution_id))
|
|
@@ -90,7 +91,7 @@ module Phronomy
|
|
|
90
91
|
if execution.active?
|
|
91
92
|
raise Phronomy::ExecutionRehydrationRequiredError, "Handoff execution requires approval or recovery"
|
|
92
93
|
end
|
|
93
|
-
raise RecoverySupport.error_from_failure(result[:error]) if result[:error]
|
|
94
|
+
raise Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]) if result[:error]
|
|
94
95
|
return result.reject { |key, _| key.to_s.start_with?("_phronomy_") || key == :handoff_request }.merge(agent: active)
|
|
95
96
|
end
|
|
96
97
|
rescue Phronomy::CancellationError => error
|
|
@@ -101,7 +102,7 @@ module Phronomy
|
|
|
101
102
|
operation_error = error
|
|
102
103
|
raise
|
|
103
104
|
ensure
|
|
104
|
-
@
|
|
105
|
+
@admissions.release!(main_agent) if admitted
|
|
105
106
|
Phronomy::Tracing::Automatic.finish(trace_handle, output: result && result[:output], error: operation_error)
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -117,25 +118,25 @@ module Phronomy
|
|
|
117
118
|
@persistence.transaction do |tx|
|
|
118
119
|
first = tx.executions.load(execution_id)
|
|
119
120
|
unless first.metadata.dig("coordination", "main_agent_id") == main_agent.agent_id
|
|
120
|
-
raise Phronomy::
|
|
121
|
+
raise Phronomy::Storage::ConflictError, "Execution does not belong to this Handoff anchor"
|
|
121
122
|
end
|
|
122
123
|
leaf = first
|
|
123
124
|
leaf_id = first.execution_id
|
|
124
125
|
seen = {}
|
|
125
126
|
while leaf&.status == :handed_off
|
|
126
|
-
raise Phronomy::
|
|
127
|
+
raise Phronomy::Storage::SerializationError, "Cyclic Handoff chain" if seen[leaf_id]
|
|
127
128
|
seen[leaf_id] = true
|
|
128
129
|
leaf_id = leaf.metadata.fetch("handoff_target_execution_id")
|
|
129
130
|
begin
|
|
130
131
|
leaf = tx.executions.load(leaf_id)
|
|
131
|
-
rescue Phronomy::
|
|
132
|
+
rescue Phronomy::Storage::NotFoundError
|
|
132
133
|
leaf = nil
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
136
|
next if leaf&.terminal?
|
|
136
137
|
routing = tx.handoff_states.load(main_agent.agent_id)
|
|
137
138
|
unless routing && (leaf ? routing.active_agent_id == leaf.agent_id : routing.pending_target_execution_id == leaf_id)
|
|
138
|
-
raise Phronomy::
|
|
139
|
+
raise Phronomy::Storage::ConflictError, "Handoff routing no longer owns the requested turn"
|
|
139
140
|
end
|
|
140
141
|
ids = (Array(routing.metadata["cancelled_execution_ids"]) + [leaf_id]).uniq
|
|
141
142
|
intended = routing.with(phase: leaf ? routing.phase : "stable",
|
|
@@ -147,7 +148,7 @@ module Phronomy
|
|
|
147
148
|
raise error unless intended && confirmed && Array(confirmed.metadata["cancelled_execution_ids"]).include?(leaf_id)
|
|
148
149
|
end
|
|
149
150
|
return @persistence.execution_result(leaf_id) if leaf&.terminal?
|
|
150
|
-
ExecutionCancellation.signal(leaf_id, leaf.agent_id) if leaf&.active?
|
|
151
|
+
Phronomy::Agent::ExecutionCancellation.signal(leaf_id, leaf.agent_id) if leaf&.active?
|
|
151
152
|
{execution_id: leaf_id, cancellation_requested: !leaf&.terminal?}.freeze
|
|
152
153
|
end
|
|
153
154
|
|
|
@@ -163,7 +164,7 @@ module Phronomy
|
|
|
163
164
|
state = @persistence.handoff_states.load(main_agent.agent_id)
|
|
164
165
|
return state if state
|
|
165
166
|
now = Time.now.utc.iso8601(6)
|
|
166
|
-
initial = HandoffState.new(main_agent_id: main_agent.agent_id, handoff_revision: 1,
|
|
167
|
+
initial = Phronomy::Agent::HandoffState.new(main_agent_id: main_agent.agent_id, handoff_revision: 1,
|
|
167
168
|
active_agent_id: main_agent.agent_id, active_handoff_context_ref: nil,
|
|
168
169
|
phase: "stable", pending_source_execution_id: nil, pending_target_execution_id: nil,
|
|
169
170
|
created_at: now, updated_at: now, metadata: {})
|