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
data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb}
RENAMED
|
@@ -4,8 +4,8 @@ module Phronomy
|
|
|
4
4
|
module Agent
|
|
5
5
|
# Normal control-transfer signal raised before RubyLLM executes a Tool Call.
|
|
6
6
|
#
|
|
7
|
-
# RubyLLM
|
|
8
|
-
# been added to Chat#messages before
|
|
7
|
+
# RubyLLM 2.0 guarantees that the complete assistant message has already
|
|
8
|
+
# been added to Chat#messages before after_message runs. Keeping that
|
|
9
9
|
# message here allows Phronomy to persist the complete Provider outcome even
|
|
10
10
|
# though the RubyLLM call itself unwinds through this exception.
|
|
11
11
|
# @api private
|
|
@@ -14,10 +14,10 @@ module Phronomy
|
|
|
14
14
|
"name" => tool.name.to_s,
|
|
15
15
|
"description" => tool.description.to_s,
|
|
16
16
|
"parameters_schema" => normalize(
|
|
17
|
-
tool.
|
|
17
|
+
tool.parameters_schema
|
|
18
18
|
),
|
|
19
19
|
"provider_options" => normalize(
|
|
20
|
-
tool.
|
|
20
|
+
tool.provider_options
|
|
21
21
|
)
|
|
22
22
|
}
|
|
23
23
|
end.freeze
|
|
@@ -47,13 +47,13 @@ module Phronomy
|
|
|
47
47
|
|
|
48
48
|
def initialize(runtime_tools:, definitions:)
|
|
49
49
|
@runtime_tools = Array(runtime_tools).freeze
|
|
50
|
-
@definitions = Immutable.copy(definitions)
|
|
50
|
+
@definitions = Phronomy::Values::Immutable.copy(definitions)
|
|
51
51
|
validate_unique_names!
|
|
52
52
|
freeze
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def select_definitions(expected_definitions)
|
|
56
|
-
expected = Immutable.copy(Array(expected_definitions))
|
|
56
|
+
expected = Phronomy::Values::Immutable.copy(Array(expected_definitions))
|
|
57
57
|
current_by_name = definitions.each_with_index.to_h do |definition, index|
|
|
58
58
|
[definition.fetch("name"), [definition, runtime_tools.fetch(index)]]
|
|
59
59
|
end
|
|
@@ -70,6 +70,11 @@ module Phronomy
|
|
|
70
70
|
raise Phronomy::ConfigurationError,
|
|
71
71
|
"ContextPolicy selected Tool not present in current Agent configuration: #{name}"
|
|
72
72
|
end
|
|
73
|
+
if definition["parameters_schema"] == {} && current_definition["parameters_schema"] != {}
|
|
74
|
+
raise Phronomy::ConfigurationError,
|
|
75
|
+
"Saved Tool definition lacks a verifiable parameters_schema: #{name}; " \
|
|
76
|
+
"finish this in-flight execution on its original version before upgrading"
|
|
77
|
+
end
|
|
73
78
|
unless Phronomy::CanonicalJSON.dump(current_definition) ==
|
|
74
79
|
Phronomy::CanonicalJSON.dump(definition)
|
|
75
80
|
raise Phronomy::ConfigurationError,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../engine/concurrency/worker_input_restricted"
|
|
4
|
+
|
|
5
|
+
require "digest"
|
|
3
6
|
require "securerandom"
|
|
4
7
|
|
|
5
8
|
module Phronomy
|
|
6
9
|
module Agent
|
|
7
10
|
class ToolInvocation
|
|
11
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
12
|
+
|
|
8
13
|
AuthorizationOutcome = Data.define(
|
|
9
14
|
:tool_invocation_id, :decision, :facts, :reason, :error, :cancelled
|
|
10
15
|
) do
|
|
@@ -74,6 +79,20 @@ module Phronomy
|
|
|
74
79
|
:origin,
|
|
75
80
|
:metadata
|
|
76
81
|
|
|
82
|
+
# Stable identity used both before dispatch and when rebuilding facts.
|
|
83
|
+
# This does not construct, authorize, or execute a Tool invocation.
|
|
84
|
+
# @api private
|
|
85
|
+
def self.semantic_id(execution_id:, llm_call_id:, tool_call_id:, tool_name:)
|
|
86
|
+
source = [
|
|
87
|
+
"tool_invocation",
|
|
88
|
+
execution_id.to_s,
|
|
89
|
+
llm_call_id.to_s,
|
|
90
|
+
tool_call_id.to_s,
|
|
91
|
+
tool_name.to_s
|
|
92
|
+
].join("\0")
|
|
93
|
+
"tool_invocation-#{Digest::SHA256.hexdigest(source)}".freeze
|
|
94
|
+
end
|
|
95
|
+
|
|
77
96
|
def self.missing(
|
|
78
97
|
execution_id:,
|
|
79
98
|
agent:,
|
|
@@ -135,6 +154,16 @@ module Phronomy
|
|
|
135
154
|
@phase = phase
|
|
136
155
|
end
|
|
137
156
|
|
|
157
|
+
# Restores a newly constructed invocation from materialized saved facts.
|
|
158
|
+
# Recovery owns snapshot decoding and identity matching. This operation
|
|
159
|
+
# owns state application; it neither dispatches nor reevaluates approval.
|
|
160
|
+
# @api private
|
|
161
|
+
def restore_state!(status:, result: nil, approval_item: nil)
|
|
162
|
+
restore_saved_state!(status, result)
|
|
163
|
+
restore_approval_evidence!(approval_item)
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
138
167
|
def handle_fsm_event(event)
|
|
139
168
|
case event.type
|
|
140
169
|
when :authorization_completed
|
|
@@ -215,7 +244,7 @@ module Phronomy
|
|
|
215
244
|
cancellation_token: @config[:cancellation_token],
|
|
216
245
|
on_full: :raise
|
|
217
246
|
) { evaluator.send(:evaluate_authorization_command, command) }
|
|
218
|
-
runtime.event_loop.supervise_agent_operation(@execution_id, operation)
|
|
247
|
+
Phronomy::Agent::ExecutionRegistry.for(runtime.event_loop).supervise_agent_operation(@execution_id, operation)
|
|
219
248
|
operation.on_complete do |outcome, error|
|
|
220
249
|
callback.call(
|
|
221
250
|
error ? evaluator.send(:authorization_failure_result, tool_invocation_id, error) : outcome
|
|
@@ -259,7 +288,7 @@ module Phronomy
|
|
|
259
288
|
raise Phronomy::ToolError,
|
|
260
289
|
"Tool #{@tool.class.name}#call_async must return a completion handle"
|
|
261
290
|
end
|
|
262
|
-
runtime.event_loop.supervise_agent_operation(@execution_id, operation)
|
|
291
|
+
Phronomy::Agent::ExecutionRegistry.for(runtime.event_loop).supervise_agent_operation(@execution_id, operation)
|
|
263
292
|
|
|
264
293
|
evaluator = self.class
|
|
265
294
|
tool_invocation_id = @id.to_s.freeze
|
|
@@ -332,7 +361,7 @@ module Phronomy
|
|
|
332
361
|
end
|
|
333
362
|
|
|
334
363
|
def tool_schema
|
|
335
|
-
@tool&.respond_to?(:
|
|
364
|
+
@tool&.respond_to?(:parameters_schema) ? @tool.parameters_schema : {}
|
|
336
365
|
end
|
|
337
366
|
|
|
338
367
|
def display_arguments
|
|
@@ -345,6 +374,40 @@ module Phronomy
|
|
|
345
374
|
|
|
346
375
|
private
|
|
347
376
|
|
|
377
|
+
def restore_saved_state!(status, result)
|
|
378
|
+
case status
|
|
379
|
+
when :awaiting_approval
|
|
380
|
+
validate! unless terminal?
|
|
381
|
+
@final_decision = :require_approval
|
|
382
|
+
mark_awaiting_approval!
|
|
383
|
+
when :authorized
|
|
384
|
+
validate! unless terminal?
|
|
385
|
+
@final_decision = :allow
|
|
386
|
+
mark_authorized!
|
|
387
|
+
when :completed
|
|
388
|
+
@result = result
|
|
389
|
+
@status = :completed
|
|
390
|
+
when :rejected
|
|
391
|
+
mark_rejected!
|
|
392
|
+
when :failed
|
|
393
|
+
mark_framework_failed!(
|
|
394
|
+
Phronomy::ToolError.new("durably restored Tool preflight failure")
|
|
395
|
+
)
|
|
396
|
+
when :cancelled
|
|
397
|
+
mark_cancelled!
|
|
398
|
+
else
|
|
399
|
+
raise Phronomy::ExecutionRehydrationRequiredError,
|
|
400
|
+
"unsupported durable Tool snapshot state: #{status.inspect}"
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def restore_approval_evidence!(item)
|
|
405
|
+
return unless item
|
|
406
|
+
|
|
407
|
+
@facts = immutable_copy(item.facts)
|
|
408
|
+
@authorization_reason = item.reason
|
|
409
|
+
end
|
|
410
|
+
|
|
348
411
|
def authorization_command
|
|
349
412
|
definition = @agent.class.agent_definition
|
|
350
413
|
|
|
@@ -490,24 +553,7 @@ module Phronomy
|
|
|
490
553
|
private_class_method :immutable_command_copy
|
|
491
554
|
|
|
492
555
|
def self.phronomy_managed_live_domain_object?(value)
|
|
493
|
-
value.is_a?(Phronomy::
|
|
494
|
-
value.is_a?(Phronomy::Agent::AgentRoot) ||
|
|
495
|
-
value.is_a?(Phronomy::Agent::AgentExecution) ||
|
|
496
|
-
value.is_a?(Phronomy::Agent::AgentInvocation) ||
|
|
497
|
-
value.is_a?(Phronomy::Agent::ToolInvocation) ||
|
|
498
|
-
value.is_a?(Phronomy::Agent::JournalProjection) ||
|
|
499
|
-
value.is_a?(Phronomy::Agent::ExecutionCoordinator) ||
|
|
500
|
-
value.is_a?(Phronomy::Agent::Context::Capability::Base) ||
|
|
501
|
-
value.is_a?(Phronomy::Workflow) ||
|
|
502
|
-
value.is_a?(Phronomy::WorkflowRunner) ||
|
|
503
|
-
value.is_a?(Phronomy::WorkflowContext) ||
|
|
504
|
-
value.is_a?(Phronomy::Runtime) ||
|
|
505
|
-
value.is_a?(Phronomy::Task) ||
|
|
506
|
-
value.is_a?(Phronomy::EventLoop) ||
|
|
507
|
-
value.is_a?(Phronomy::FSMSession) ||
|
|
508
|
-
value.is_a?(Phronomy::FSMSession::EventSink) ||
|
|
509
|
-
value.is_a?(Phronomy::Concurrency::CancellationToken) ||
|
|
510
|
-
value.is_a?(Phronomy::Concurrency::OffloadPool)
|
|
556
|
+
value.is_a?(Phronomy::Concurrency::WorkerInputRestricted)
|
|
511
557
|
end
|
|
512
558
|
private_class_method :phronomy_managed_live_domain_object?
|
|
513
559
|
|
|
@@ -551,7 +597,7 @@ module Phronomy
|
|
|
551
597
|
|
|
552
598
|
def start_async_tool_operation(runtime)
|
|
553
599
|
if uses_default_call_async?
|
|
554
|
-
Phronomy::Agent::ToolExecutor.call_async(
|
|
600
|
+
Phronomy::Agent::Context::Capability::ToolExecutor.call_async(
|
|
555
601
|
tool: @tool,
|
|
556
602
|
args: @arguments,
|
|
557
603
|
cancellation_token: @config[:cancellation_token],
|
|
@@ -665,18 +711,7 @@ module Phronomy
|
|
|
665
711
|
end
|
|
666
712
|
|
|
667
713
|
def immutable_copy(value)
|
|
668
|
-
|
|
669
|
-
when Hash
|
|
670
|
-
value.each_with_object({}) do |(key, item), result|
|
|
671
|
-
result[immutable_copy(key)] = immutable_copy(item)
|
|
672
|
-
end.freeze
|
|
673
|
-
when Array
|
|
674
|
-
value.map { |item| immutable_copy(item) }.freeze
|
|
675
|
-
when String
|
|
676
|
-
value.dup.freeze
|
|
677
|
-
else
|
|
678
|
-
value
|
|
679
|
-
end
|
|
714
|
+
Phronomy::Values::Immutable.copy(value)
|
|
680
715
|
end
|
|
681
716
|
|
|
682
717
|
def redact_for_display(value)
|
|
@@ -181,9 +181,9 @@ module Phronomy
|
|
|
181
181
|
def self.build_entry_callback(callable, state_name)
|
|
182
182
|
->(machine) {
|
|
183
183
|
result = callable.call(machine.context)
|
|
184
|
-
if result.is_a?(Phronomy::
|
|
184
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
185
185
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
186
|
-
"Tool entry action for #{state_name.inspect} returned Phronomy::
|
|
186
|
+
"Tool entry action for #{state_name.inspect} returned Phronomy::TaskResult"
|
|
187
187
|
end
|
|
188
188
|
machine.context = result if result.respond_to?(:set_graph_metadata)
|
|
189
189
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../canonical_json"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
|
-
module
|
|
6
|
+
module Values
|
|
7
|
+
# Shared value operations for snapshots, recovery facts, and result views.
|
|
8
|
+
# Hash, Array, and String trees are copied and frozen; other values retain
|
|
9
|
+
# their identity. Canonical JSON validation is a separate operation.
|
|
10
|
+
# @api private
|
|
5
11
|
module Immutable
|
|
6
12
|
module_function
|
|
7
13
|
|
|
14
|
+
# @api private
|
|
8
15
|
def copy(value)
|
|
9
16
|
case value
|
|
10
17
|
when Hash
|
|
@@ -20,6 +27,7 @@ module Phronomy
|
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
|
|
30
|
+
# @api private
|
|
23
31
|
def validate_canonical_json!(value, label: "value")
|
|
24
32
|
Phronomy::CanonicalJSON.dump(value)
|
|
25
33
|
true
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Values
|
|
5
|
+
# Prepares a Ruby value tree for JSON serialization. Hash keys and Symbol
|
|
6
|
+
# values become Strings; containers are rebuilt and scalar objects retained.
|
|
7
|
+
# This neither freezes values nor validates canonical JSON numbers/encoding.
|
|
8
|
+
# @api private
|
|
9
|
+
module Serializable
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def convert(value, unsupported_message:)
|
|
13
|
+
case value
|
|
14
|
+
when Hash
|
|
15
|
+
value.to_h { |key, child| [key.to_s, convert(child, unsupported_message: unsupported_message)] }
|
|
16
|
+
when Array
|
|
17
|
+
value.map { |child| convert(child, unsupported_message: unsupported_message) }
|
|
18
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
19
|
+
value
|
|
20
|
+
when Symbol
|
|
21
|
+
value.to_s
|
|
22
|
+
else
|
|
23
|
+
if value.respond_to?(:to_h)
|
|
24
|
+
convert(value.to_h, unsupported_message: unsupported_message)
|
|
25
|
+
else
|
|
26
|
+
raise ArgumentError, "#{unsupported_message}: #{value.class}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -6,13 +6,23 @@ module Phronomy
|
|
|
6
6
|
STREAM_CALLBACK_ERROR_POLICIES = %i[report fail_task].freeze
|
|
7
7
|
private_constant :STREAM_CALLBACK_ERROR_POLICIES
|
|
8
8
|
|
|
9
|
+
DEFAULT_FACTORIES = {}
|
|
10
|
+
private_constant :DEFAULT_FACTORIES
|
|
11
|
+
|
|
12
|
+
# Bind fresh-instance factories once during application loading. Concrete
|
|
13
|
+
# component selection belongs to runtime_composition, not configuration.
|
|
14
|
+
# Applications replace instances through the existing configuration writers.
|
|
15
|
+
# @api private
|
|
16
|
+
def self.install_default_factories(tracer:, llm_adapter:)
|
|
17
|
+
DEFAULT_FACTORIES.replace(tracer: tracer, llm_adapter: llm_adapter).freeze
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
9
21
|
attr_accessor :default_model
|
|
10
22
|
attr_accessor :default_embedding_model
|
|
11
23
|
attr_accessor :tracer
|
|
12
24
|
attr_accessor :before_llm_input
|
|
13
|
-
attr_accessor :default_output_reserve
|
|
14
25
|
attr_accessor :recursion_limit
|
|
15
|
-
attr_accessor :parallel_tool_execution
|
|
16
26
|
attr_accessor :trace_pii
|
|
17
27
|
attr_accessor :logger
|
|
18
28
|
attr_accessor :event_loop_stop_grace_seconds
|
|
@@ -40,11 +50,10 @@ module Phronomy
|
|
|
40
50
|
|
|
41
51
|
def initialize
|
|
42
52
|
@recursion_limit = 25
|
|
43
|
-
@tracer =
|
|
53
|
+
@tracer = DEFAULT_FACTORIES.fetch(:tracer).call
|
|
44
54
|
@trace_pii = false
|
|
45
|
-
@parallel_tool_execution = false
|
|
46
55
|
@event_loop_stop_grace_seconds = 5
|
|
47
|
-
@llm_adapter =
|
|
56
|
+
@llm_adapter = DEFAULT_FACTORIES.fetch(:llm_adapter).call
|
|
48
57
|
@event_loop_starvation_threshold_seconds = nil
|
|
49
58
|
@event_loop_dispatch_threshold_seconds = nil
|
|
50
59
|
@stream_callback_error_policy = :report
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Global configuration access, replacement, and scoped overrides.
|
|
4
|
+
# Explicitly loaded by the application entry point; not a common definition.
|
|
5
|
+
module Phronomy
|
|
6
|
+
class << self
|
|
7
|
+
def configuration
|
|
8
|
+
@configuration ||= Configuration.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def configure
|
|
12
|
+
yield configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset_configuration!
|
|
16
|
+
@configuration = Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_configuration
|
|
20
|
+
original = @configuration&.dup
|
|
21
|
+
yield configuration
|
|
22
|
+
ensure
|
|
23
|
+
@configuration = original
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module ContentStore
|
|
5
|
+
# @api private
|
|
6
|
+
module StorageSchema
|
|
7
|
+
CONTENTS = Phronomy::Storage::Resource.new(id: "content.blobs", kind: :blobs,
|
|
8
|
+
attributes: {canonicalization_version: :integer})
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module ContentStore
|
|
5
|
+
# Content identity and digest validation belong to ContentStore, not Blobs.
|
|
6
|
+
# @api private
|
|
7
|
+
class StoredContents < Base
|
|
8
|
+
def initialize(view) = @view = view
|
|
9
|
+
|
|
10
|
+
def put(bytes, canonicalization_version:)
|
|
11
|
+
value = String(bytes).b
|
|
12
|
+
id = content_id_for(value)
|
|
13
|
+
@view.atomic do |bound|
|
|
14
|
+
blob = bound.blobs(StorageSchema::CONTENTS).put_if_absent(key: id, bytes: value,
|
|
15
|
+
attributes: {canonicalization_version: Integer(canonicalization_version)})
|
|
16
|
+
verify!(blob, id)
|
|
17
|
+
id
|
|
18
|
+
end
|
|
19
|
+
rescue Phronomy::Storage::BlobConflictError => error
|
|
20
|
+
raise IntegrityError, error.message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch(content_id)
|
|
24
|
+
id = content_id.to_s
|
|
25
|
+
@view.atomic do |bound|
|
|
26
|
+
blob = bound.blobs(StorageSchema::CONTENTS).fetch(id)
|
|
27
|
+
verify!(blob, id)
|
|
28
|
+
blob.bytes.dup
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def exist?(content_id) = @view.blobs(StorageSchema::CONTENTS).exist?(content_id.to_s)
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def verify!(blob, id)
|
|
37
|
+
unless blob.key == id && content_id_for(blob.bytes) == id
|
|
38
|
+
raise IntegrityError, "content digest mismatch: #{id}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -7,27 +7,34 @@ module Phronomy
|
|
|
7
7
|
module Blocking
|
|
8
8
|
# Submits synchronous application work to the default Runtime OffloadPool.
|
|
9
9
|
# Admission never waits for queue space. Admission StandardError failures
|
|
10
|
-
# become failed
|
|
11
|
-
# A timeout/cancellation can settle that
|
|
12
|
-
# The block must not wait for another Agent, Workflow, or
|
|
10
|
+
# become failed results; accepted work retains the pool's TaskResult unchanged.
|
|
11
|
+
# A timeout/cancellation can settle that TaskResult before a running worker exits.
|
|
12
|
+
# The block must not wait for another Agent, Workflow, or TaskResult.
|
|
13
13
|
# @param timeout [Numeric, nil] operation deadline, including queue time
|
|
14
14
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
15
|
+
# @param invocation_context [Phronomy::InvocationContext, nil] explicit context/scope
|
|
15
16
|
# @yield synchronous application work
|
|
16
|
-
# @return [Phronomy::
|
|
17
|
+
# @return [Phronomy::TaskResult] original accepted TaskResult or failed admission TaskResult
|
|
17
18
|
# @raise [ArgumentError] if no block is supplied
|
|
18
19
|
# @api public
|
|
19
|
-
def self.call_async(timeout: nil, cancellation_token: nil, &block)
|
|
20
|
+
def self.call_async(timeout: nil, cancellation_token: nil, invocation_context: nil, &block)
|
|
20
21
|
raise ArgumentError, "Blocking.call_async requires a block" unless block
|
|
21
22
|
|
|
22
23
|
begin
|
|
23
|
-
|
|
24
|
+
unless invocation_context.nil?
|
|
25
|
+
binding = Concurrency::OperationBinding.new(invocation_context: invocation_context,
|
|
26
|
+
cancellation_token: cancellation_token)
|
|
27
|
+
end
|
|
28
|
+
result = Phronomy::Runtime.instance.offload.submit(
|
|
24
29
|
on_full: :raise,
|
|
25
30
|
timeout: timeout,
|
|
26
|
-
cancellation_token: cancellation_token,
|
|
31
|
+
cancellation_token: binding ? binding.token : cancellation_token,
|
|
27
32
|
&block
|
|
28
33
|
)
|
|
34
|
+
binding ? binding.bind(result) : result
|
|
29
35
|
rescue => error
|
|
30
|
-
Phronomy::
|
|
36
|
+
result = Phronomy::TaskResult.failed(error, name: "blocking-admission-failed")
|
|
37
|
+
binding ? binding.bind(result) : result
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
6
|
module Concurrency
|
|
5
7
|
# Cooperative cancellation token for Agent/Tool work.
|
|
6
8
|
class CancellationToken
|
|
9
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
10
|
+
|
|
7
11
|
# Creates a token that expires after +seconds+ measured with the monotonic clock.
|
|
8
12
|
# @api public
|
|
9
13
|
def self.timeout_after(seconds)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "worker_input_restricted"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
6
|
module Concurrency
|
|
5
7
|
# A bounded, observable thread pool for synchronous work that must not run on
|
|
@@ -13,7 +15,7 @@ module Phronomy
|
|
|
13
15
|
# synchronous call. Phronomy deliberately does not classify the workload by
|
|
14
16
|
# cause; the application decides whether a unit of work is EventLoop-safe.
|
|
15
17
|
#
|
|
16
|
-
# Logical waits are different. Waiting for another Phronomy
|
|
18
|
+
# Logical waits are different. Waiting for another Phronomy TaskResult, Agent,
|
|
17
19
|
# Workflow, ToolInvocation, timer, or FSMSession must remain an explicit
|
|
18
20
|
# EventLoop/FSMSession continuation and must not consume an OffloadPool worker.
|
|
19
21
|
# See ADR-010.
|
|
@@ -22,7 +24,7 @@ module Phronomy
|
|
|
22
24
|
#
|
|
23
25
|
# 1. The total number of worker OS threads is capped.
|
|
24
26
|
# 2. Queue depth is bounded (backpressure when the pool is saturated).
|
|
25
|
-
# 3. Per-operation timeouts and cancellation settle the caller-facing
|
|
27
|
+
# 3. Per-operation timeouts and cancellation settle the caller-facing TaskResult.
|
|
26
28
|
# 4. Operations that settle after worker execution has started are tracked as
|
|
27
29
|
# abandoned until that worker returns.
|
|
28
30
|
# 5. Metrics expose active work, queue depth, cumulative abandonment,
|
|
@@ -41,9 +43,11 @@ module Phronomy
|
|
|
41
43
|
# task = pool.submit(timeout: 30, cancellation_token: token) { expensive_call }
|
|
42
44
|
# result = task.wait_result
|
|
43
45
|
class OffloadPool
|
|
46
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
47
|
+
|
|
44
48
|
# Private execution record for one submitted synchronous operation.
|
|
45
49
|
#
|
|
46
|
-
# Caller-facing completion is represented exclusively by {Phronomy::
|
|
50
|
+
# Caller-facing completion is represented exclusively by {Phronomy::TaskResult}.
|
|
47
51
|
# This object owns only OffloadPool-specific execution state: queue timing,
|
|
48
52
|
# worker-start linearization, submit timeout/cancellation, abandonment, and
|
|
49
53
|
# the submitted block itself.
|
|
@@ -77,6 +81,7 @@ module Phronomy
|
|
|
77
81
|
@submitted_at = submitted_at ||
|
|
78
82
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
79
83
|
@mutex = Mutex.new
|
|
84
|
+
@timer_subscriptions = Subscriptions.new
|
|
80
85
|
|
|
81
86
|
# Explicit submit cancellation is operation-wide. Deadline-only tokens are
|
|
82
87
|
# promoted to cancel! by OffloadPool#submit using the Runtime timer queue.
|
|
@@ -92,19 +97,26 @@ module Phronomy
|
|
|
92
97
|
@mutex.synchronize { @settled }
|
|
93
98
|
end
|
|
94
99
|
|
|
95
|
-
#
|
|
100
|
+
# Registration can race logical completion. Dispose the timer even when
|
|
101
|
+
# settlement occurred while schedule was returning.
|
|
102
|
+
def schedule(timer_queue, seconds:, &callback)
|
|
103
|
+
timer_queue.schedule(seconds: seconds, &callback)
|
|
104
|
+
@timer_subscriptions.add { timer_queue.cancel(callback) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] true when the submit-time deadline settled the TaskResult
|
|
96
108
|
# @api private
|
|
97
109
|
def timed_out?
|
|
98
110
|
@mutex.synchronize { @timed_out }
|
|
99
111
|
end
|
|
100
112
|
|
|
101
|
-
# @return [Boolean] true when submit cancellation settled the
|
|
113
|
+
# @return [Boolean] true when submit cancellation settled the TaskResult
|
|
102
114
|
# @api private
|
|
103
115
|
def cancelled?
|
|
104
116
|
@mutex.synchronize { @cancelled }
|
|
105
117
|
end
|
|
106
118
|
|
|
107
|
-
# @return [Boolean] true when timeout/cancellation settled the
|
|
119
|
+
# @return [Boolean] true when timeout/cancellation settled the TaskResult after
|
|
108
120
|
# worker execution had started. The worker is not forcibly interrupted.
|
|
109
121
|
# @api private
|
|
110
122
|
def abandoned?
|
|
@@ -117,7 +129,7 @@ module Phronomy
|
|
|
117
129
|
@wait_time || 0.0
|
|
118
130
|
end
|
|
119
131
|
|
|
120
|
-
# Settles the caller-facing
|
|
132
|
+
# Settles the caller-facing TaskResult with a submit-time timeout.
|
|
121
133
|
#
|
|
122
134
|
# The worker is not interrupted. If execution has already started, the
|
|
123
135
|
# operation is marked abandoned and the worker's eventual result is discarded.
|
|
@@ -132,7 +144,7 @@ module Phronomy
|
|
|
132
144
|
end
|
|
133
145
|
end
|
|
134
146
|
|
|
135
|
-
# Settles the caller-facing
|
|
147
|
+
# Settles the caller-facing TaskResult because its submit cancellation token was
|
|
136
148
|
# cancelled. Cancellation never injects Thread#raise into the worker.
|
|
137
149
|
#
|
|
138
150
|
# @return [Boolean] true when this call won settlement
|
|
@@ -201,7 +213,7 @@ module Phronomy
|
|
|
201
213
|
complete_with_value!(@block.call)
|
|
202
214
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
203
215
|
# Rescue all Exception subclasses so non-StandardError raises still
|
|
204
|
-
# settle the
|
|
216
|
+
# settle the TaskResult and unblock waiters.
|
|
205
217
|
complete_with_error!(e)
|
|
206
218
|
raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
|
|
207
219
|
end
|
|
@@ -280,6 +292,7 @@ module Phronomy
|
|
|
280
292
|
end
|
|
281
293
|
|
|
282
294
|
def detach_submit_cancellation
|
|
295
|
+
@timer_subscriptions.close
|
|
283
296
|
return unless @cancellation_token && @cancellation_callback
|
|
284
297
|
|
|
285
298
|
@cancellation_token.send(
|
|
@@ -325,20 +338,20 @@ module Phronomy
|
|
|
325
338
|
|
|
326
339
|
# Submits synchronous off-EventLoop work to the pool.
|
|
327
340
|
#
|
|
328
|
-
# Returns a {Phronomy::
|
|
341
|
+
# Returns a {Phronomy::TaskResult} immediately after queue admission; the block
|
|
329
342
|
# runs on a worker thread. Do not submit logical waits (for example waiting
|
|
330
|
-
# for a child Agent
|
|
343
|
+
# for a child Agent TaskResult) merely to make them asynchronous; those belong to
|
|
331
344
|
# FSMSession/EventLoop completion events.
|
|
332
345
|
#
|
|
333
346
|
# A submit-time +timeout+ is an operation-wide deadline measured from the
|
|
334
|
-
# start of this method, including queue wait. The timer settles the
|
|
347
|
+
# start of this method, including queue wait. The timer settles the TaskResult and
|
|
335
348
|
# notifies +on_complete+ without forcibly interrupting a running worker. If
|
|
336
349
|
# the deadline fires before worker execution starts, the block is skipped.
|
|
337
350
|
# If it fires after execution starts, the private Operation is marked
|
|
338
351
|
# abandoned and the eventual worker result is discarded.
|
|
339
352
|
#
|
|
340
353
|
# The submit +cancellation_token+ is also operation-wide. Explicit
|
|
341
|
-
# cancellation settles the
|
|
354
|
+
# cancellation settles the TaskResult immediately. A token with a monotonic
|
|
342
355
|
# deadline is attached to the Runtime timer queue so deadline expiry becomes
|
|
343
356
|
# explicit cancellation without adding a polling Thread. Cancellation before
|
|
344
357
|
# execution skips the block; cancellation after execution starts abandons
|
|
@@ -356,7 +369,7 @@ module Phronomy
|
|
|
356
369
|
# @param on_full [Symbol] +:wait+, +:raise+, or +:timeout+
|
|
357
370
|
# @param full_timeout [Numeric, nil] queue-admission timeout for +on_full: :timeout+
|
|
358
371
|
# @yield block containing synchronous work
|
|
359
|
-
# @return [Phronomy::
|
|
372
|
+
# @return [Phronomy::TaskResult]
|
|
360
373
|
# @raise [Phronomy::ConfigurationError] when a timer is required but no
|
|
361
374
|
# timer queue provider is configured
|
|
362
375
|
# @raise [Phronomy::PoolShutdownError] when the pool has been shut down
|
|
@@ -421,7 +434,7 @@ module Phronomy
|
|
|
421
434
|
|
|
422
435
|
# Arm before queue admission so the deadline includes time spent waiting
|
|
423
436
|
# for a queue slot.
|
|
424
|
-
|
|
437
|
+
operation.schedule(timer_queue, seconds: remaining) { operation.fire_timeout! }
|
|
425
438
|
end
|
|
426
439
|
|
|
427
440
|
if cancellation_remaining
|
|
@@ -431,7 +444,7 @@ module Phronomy
|
|
|
431
444
|
cancellation_token.cancel!
|
|
432
445
|
return task
|
|
433
446
|
end
|
|
434
|
-
|
|
447
|
+
operation.schedule(timer_queue, seconds: remaining) { cancellation_token.cancel! }
|
|
435
448
|
end
|
|
436
449
|
|
|
437
450
|
# Cancellation/timeout can race with timer registration. Do not enqueue
|