phronomy 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +4 -0
- data/VERIFY.sh +27 -27
- data/benchmark/bench_agent_invoke.rb +26 -22
- data/benchmark/bench_context_assembler.rb +4 -5
- data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
- data/docs/architecture/agent-context.md +4 -2
- data/docs/architecture/agent-transition-ownership.md +89 -0
- data/docs/architecture/before-llm-input.md +6 -0
- data/docs/architecture/context-management.md +30 -0
- data/docs/architecture/context-preparation-steps.md +90 -0
- data/docs/architecture/entry-action-and-team-wording.md +72 -0
- data/docs/architecture/execution-metadata-and-values.md +80 -0
- data/docs/architecture/generator-verifier-ownership.md +111 -0
- data/docs/architecture/multi-agent-handoff.md +8 -2
- data/docs/architecture/persistence-refactoring-plan.md +50 -0
- data/docs/architecture/persistence.md +27 -4
- data/docs/architecture/refactoring-closure.md +334 -0
- data/docs/architecture/remaining-refactoring-plan.md +374 -0
- data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
- data/docs/architecture/tool-schema-recording-gap.md +50 -0
- data/docs/architecture/tracing.md +1 -1
- data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
- data/docs/architecture.md +209 -0
- data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
- data/docs/decisions/032-storage-backend-composition.md +80 -0
- data/docs/decisions/033-domain-persistence-ownership.md +79 -0
- data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
- data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
- data/docs/decisions/036-context-contract-ownership.md +86 -0
- data/docs/decisions/037-common-definition-ownership.md +62 -0
- data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
- data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
- data/docs/decisions/040-configuration-default-composition.md +94 -0
- data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
- data/docs/decisions/042-feature-owned-execution-state.md +106 -0
- data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
- data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
- data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
- data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
- data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
- data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
- data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
- data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
- data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
- data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
- data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
- data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
- data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
- data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
- data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
- data/docs/decisions/058-neutral-storage-primitives.md +78 -0
- data/docs/decisions/README.md +35 -6
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
- data/docs/features.md +9 -3
- data/docs/getting-started.md +4 -4
- data/docs/migrations/0.15.md +5 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +6 -0
- data/docs/migrations/handoff-runner-multi-agent.md +35 -0
- data/docs/migrations/neutral-storage-spi.md +43 -0
- data/docs/migrations/parallel-tool-chat-removal.md +49 -0
- data/docs/migrations/shared-state-multi-agent.md +44 -0
- data/docs/migrations/storage-backend-composition.md +117 -0
- data/docs/migrations/storage-transaction-boundaries.md +74 -0
- data/docs/persistence-backends.md +162 -204
- data/docs/runtime-and-concurrency.md +117 -32
- data/lib/phronomy/agent/api/agent.rb +17 -0
- data/lib/phronomy/agent/async_event_api.rb +2 -2
- data/lib/phronomy/agent/base.rb +51 -241
- data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
- data/lib/phronomy/agent/context/capability/base.rb +67 -26
- data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
- data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
- data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
- data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
- data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
- data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
- data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
- data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
- data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
- data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
- data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
- data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
- data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
- data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
- data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
- data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +19 -95
- data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
- data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +9 -10
- data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
- data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
- data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
- data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
- data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
- data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
- data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
- data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
- data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +20 -71
- data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
- data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
- data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
- data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
- data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
- data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
- data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
- data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
- data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
- data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
- data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
- data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
- data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
- data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
- data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
- data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
- data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
- data/lib/phronomy/agent/persistence/codec.rb +358 -0
- data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
- data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
- data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
- data/lib/phronomy/agent/persistence/queries.rb +61 -0
- data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
- data/lib/phronomy/agent/persistence/watermark.rb +27 -0
- data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +32 -37
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
- data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +6 -10
- data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
- data/lib/phronomy/agent/selection/candidate.rb +1 -1
- data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
- data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
- data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
- data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
- data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
- data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
- data/lib/phronomy/common/configuration_error.rb +7 -0
- data/lib/phronomy/common/error.rb +5 -0
- data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
- data/lib/phronomy/common/values/serializable.rb +32 -0
- data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
- data/lib/phronomy/configuration/global_configuration.rb +26 -0
- data/lib/phronomy/content_store/storage_schema.rb +11 -0
- data/lib/phronomy/content_store/stored_contents.rb +43 -0
- data/lib/phronomy/engine/backpressure_error.rb +7 -0
- data/lib/phronomy/{blocking.rb → engine/blocking.rb} +1 -1
- data/lib/phronomy/engine/cancellation_error.rb +7 -0
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +4 -0
- data/lib/phronomy/engine/concurrency/operation_binding.rb +1 -1
- data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
- data/lib/phronomy/engine/event_loop.rb +158 -624
- data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
- data/lib/phronomy/{execution.rb → engine/execution.rb} +0 -7
- data/lib/phronomy/{execution_cancellation_error.rb → engine/execution_cancellation_error.rb} +2 -0
- data/lib/phronomy/engine/execution_receiver.rb +65 -0
- data/lib/phronomy/{execution_timeout_error.rb → engine/execution_timeout_error.rb} +2 -0
- data/lib/phronomy/engine/fsm_protocol.rb +14 -0
- data/lib/phronomy/engine/fsm_session.rb +35 -28
- data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +2 -0
- data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
- data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
- data/lib/phronomy/engine/runtime.rb +80 -84
- data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
- data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
- data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
- data/lib/phronomy/engine/task_result.rb +4 -0
- data/lib/phronomy/engine/timeout_error.rb +7 -0
- data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
- data/lib/phronomy/generation/generator_verifier.rb +118 -0
- data/lib/phronomy/generation/low_confidence_error.rb +14 -0
- data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
- data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
- data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
- data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
- data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
- data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
- data/lib/phronomy/llm_contract/transport_error.rb +7 -0
- data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +5 -5
- data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
- data/lib/phronomy/multi_agent/orchestrator.rb +2 -2
- data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
- data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
- data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
- data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
- data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
- data/lib/phronomy/multi_agent/team_execution.rb +1 -1
- data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
- data/lib/phronomy/multi_agent/team_root.rb +1 -1
- data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
- data/lib/phronomy/persistence/api/persistence.rb +140 -0
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
- data/lib/phronomy/persistence_composition/repositories.rb +77 -0
- data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
- data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
- data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
- data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
- data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
- data/lib/phronomy/storage/backend.rb +101 -0
- data/lib/phronomy/storage/backends/in_memory.rb +157 -0
- data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
- data/lib/phronomy/storage/blobs.rb +31 -0
- data/lib/phronomy/storage/condition.rb +25 -0
- data/lib/phronomy/storage/condition_failed_error.rb +16 -0
- data/lib/phronomy/storage/conflict_error.rb +9 -0
- data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
- data/lib/phronomy/storage/entry.rb +33 -0
- data/lib/phronomy/storage/guard_ref.rb +13 -0
- data/lib/phronomy/storage/not_found_error.rb +9 -0
- data/lib/phronomy/storage/record_codec.rb +177 -0
- data/lib/phronomy/storage/records.rb +61 -0
- data/lib/phronomy/storage/resource.rb +126 -0
- data/lib/phronomy/storage/scope.rb +25 -0
- data/lib/phronomy/storage/serialization_error.rb +9 -0
- data/lib/phronomy/storage/streams.rb +44 -0
- data/lib/phronomy/storage/transaction_error.rb +10 -0
- data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
- data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
- data/lib/phronomy/storage/validation.rb +53 -0
- data/lib/phronomy/storage/view.rb +131 -0
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
- data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
- data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
- data/lib/phronomy/testing/persistence_contract.rb +4 -0
- data/lib/phronomy/tool/contract/tool_error.rb +7 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +8 -6
- data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
- data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
- data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +122 -71
- data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
- data/lib/phronomy/workflow/persistence/codec.rb +153 -0
- data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
- data/lib/phronomy/workflow/phase_machine_builder.rb +2 -2
- data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
- data/lib/phronomy.rb +52 -94
- data/scripts/api_snapshot.rb +1 -1
- data/scripts/storage_spi_snapshot.rb +36 -0
- data/sig/phronomy/agent.rbs +0 -1
- data/sig/phronomy/execution_receiver.rbs +34 -0
- data/sig/phronomy/handoff.rbs +4 -2
- data/sig/phronomy/multi_agent.rbs +18 -0
- data/sig/phronomy/persistence.rbs +7 -91
- data/sig/phronomy/storage.rbs +174 -0
- data/sig/phronomy/tool.rbs +10 -1
- metadata +230 -99
- data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
- data/lib/phronomy/agent/recovery_support.rb +0 -504
- data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
- data/lib/phronomy/agent/tool_executor.rb +0 -55
- data/lib/phronomy/generator_verifier.rb +0 -369
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
- data/lib/phronomy/persistence/durable_codec.rb +0 -706
- data/lib/phronomy/persistence/in_memory.rb +0 -690
- data/lib/phronomy/persistence/repository_facades.rb +0 -535
- data/lib/phronomy/persistence.rb +0 -276
- data/lib/phronomy/ruby_llm_patches.rb +0 -24
- data/lib/phronomy/workflow_recovery.rb +0 -123
- /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
- /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
- /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
- /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
- /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
- /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
- /data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +0 -0
- /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
- /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
- /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
- /data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +0 -0
- /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
- /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Agent-owned admission, live execution state and physical quiescence.
|
|
6
|
+
# The EventLoop remains the sole writer during execution.
|
|
7
|
+
# @api private
|
|
8
|
+
class ExecutionRegistry < Phronomy::ExecutionReceiver
|
|
9
|
+
# Immutable Agent-owned value. The map containing these records is the
|
|
10
|
+
# mutable authority; records are replaced rather than mutated in place.
|
|
11
|
+
AgentExecutionState = Data.define(
|
|
12
|
+
:execution_id,
|
|
13
|
+
:agent,
|
|
14
|
+
:coordinator,
|
|
15
|
+
:execution,
|
|
16
|
+
:runtime_projection,
|
|
17
|
+
:base_manifest,
|
|
18
|
+
:invocation,
|
|
19
|
+
:fsm_session_id
|
|
20
|
+
)
|
|
21
|
+
private_constant :AgentExecutionState
|
|
22
|
+
|
|
23
|
+
# Read-only process-local lookup view used by approval/live-owner APIs.
|
|
24
|
+
# It intentionally exposes no mutable execution, invocation, or projection.
|
|
25
|
+
AgentExecutionOwner = Data.define(:execution_id, :agent, :coordinator, :status)
|
|
26
|
+
private_constant :AgentExecutionOwner
|
|
27
|
+
|
|
28
|
+
# Agent-owned process-local top-level execution admission. This is
|
|
29
|
+
# separate from AgentExecutionState because admission begins before the
|
|
30
|
+
# durable AgentExecution exists. owner_token is coordination-only and is
|
|
31
|
+
# never a semantic result-authority identifier.
|
|
32
|
+
AgentAdmission = Data.define(:agent_id, :owner_token, :execution_id, :state)
|
|
33
|
+
private_constant :AgentAdmission
|
|
34
|
+
|
|
35
|
+
UNSET = Object.new.freeze
|
|
36
|
+
PhysicalCompletion = Data.define(:execution_id, :token)
|
|
37
|
+
private_constant :UNSET, :PhysicalCompletion
|
|
38
|
+
|
|
39
|
+
def initialize(event_loop:)
|
|
40
|
+
super
|
|
41
|
+
@agent_admissions = {}
|
|
42
|
+
@agent_executions = {}
|
|
43
|
+
@agent_completion_waiters = Hash.new { |hash, key| hash[key] = [] }
|
|
44
|
+
@agent_inflight_work = Hash.new { |hash, key| hash[key] = {} }
|
|
45
|
+
@agent_deferred_terminals = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def post(command, admission: false, completion: nil)
|
|
49
|
+
post_message(command, admission: admission, completion: completion)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def deliver(command)
|
|
53
|
+
assert_event_loop_thread!
|
|
54
|
+
if command.is_a?(PhysicalCompletion)
|
|
55
|
+
complete_agent_physical_work(command)
|
|
56
|
+
else
|
|
57
|
+
command.coordinator.deliver_on_event_loop(command)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Called under the EventLoop lifecycle lock, including during shutdown.
|
|
62
|
+
def idle?
|
|
63
|
+
@agent_inflight_work.values.all?(&:empty?) &&
|
|
64
|
+
@agent_deferred_terminals.empty? &&
|
|
65
|
+
!agent_admission_transition_in_progress_locked?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A dispatcher failure runs this on EventLoop; normal teardown runs it only
|
|
69
|
+
# after that thread has joined. Task callbacks run outside the lifecycle lock.
|
|
70
|
+
def shutdown(error:)
|
|
71
|
+
pending = synchronize do
|
|
72
|
+
waiters = @agent_completion_waiters.values.flatten
|
|
73
|
+
@agent_completion_waiters.clear
|
|
74
|
+
@agent_inflight_work.clear
|
|
75
|
+
@agent_deferred_terminals.clear
|
|
76
|
+
@agent_admissions.clear
|
|
77
|
+
@agent_executions.clear
|
|
78
|
+
waiters
|
|
79
|
+
end
|
|
80
|
+
return if pending.empty?
|
|
81
|
+
|
|
82
|
+
error ||= Phronomy::ExecutionRehydrationRequiredError.new(
|
|
83
|
+
"Runtime terminated while Agent execution remained nonterminal; " \
|
|
84
|
+
"process-local TaskResult handles are not rehydrated"
|
|
85
|
+
)
|
|
86
|
+
pending.each { |waiter| waiter.fail(error) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Process-local read-only admission check used by destructive Agent lifecycle
|
|
90
|
+
# operations. The mutable admission map itself remains Agent-owned.
|
|
91
|
+
def agent_execution_admitted?(agent_id)
|
|
92
|
+
synchronize { @agent_admissions.key?(agent_id.to_s) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Reserves the one top-level logical execution slot for agent_id before any
|
|
96
|
+
# Persistence execution admission is attempted.
|
|
97
|
+
# @api private
|
|
98
|
+
def admit_agent_execution(agent_id, owner_token:)
|
|
99
|
+
assert_event_loop_thread!
|
|
100
|
+
key = agent_id.to_s
|
|
101
|
+
raise ArgumentError, "agent_id must not be empty" if key.empty?
|
|
102
|
+
raise ArgumentError, "owner_token is required" unless owner_token
|
|
103
|
+
|
|
104
|
+
admit do
|
|
105
|
+
if @agent_admissions.key?(key)
|
|
106
|
+
raise Phronomy::AgentBusyError,
|
|
107
|
+
"Agent #{key.inspect} already has a nonterminal top-level execution"
|
|
108
|
+
end
|
|
109
|
+
@agent_admissions[key] = AgentAdmission.new(
|
|
110
|
+
agent_id: key.freeze,
|
|
111
|
+
owner_token: owner_token,
|
|
112
|
+
execution_id: nil,
|
|
113
|
+
state: :admitting
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Binds a successful durable AgentExecution identity to the earlier
|
|
120
|
+
# process-local admission.
|
|
121
|
+
# @api private
|
|
122
|
+
def bind_agent_execution_admission(agent_id, owner_token:, execution_id:)
|
|
123
|
+
assert_event_loop_thread!
|
|
124
|
+
key = agent_id.to_s
|
|
125
|
+
execution_key = execution_id.to_s
|
|
126
|
+
synchronize do
|
|
127
|
+
current = @agent_admissions.fetch(key) do
|
|
128
|
+
raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
|
|
129
|
+
end
|
|
130
|
+
unless current.owner_token.equal?(owner_token) && current.execution_id.nil?
|
|
131
|
+
raise Phronomy::Error, "stale Agent admission bind for #{key.inspect}"
|
|
132
|
+
end
|
|
133
|
+
@agent_admissions[key] = AgentAdmission.new(
|
|
134
|
+
agent_id: current.agent_id,
|
|
135
|
+
owner_token: current.owner_token,
|
|
136
|
+
execution_id: execution_key.freeze,
|
|
137
|
+
state: :executing
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @api private
|
|
144
|
+
def mark_agent_execution_admission(agent_id, execution_id:, state:)
|
|
145
|
+
assert_event_loop_thread!
|
|
146
|
+
key = agent_id.to_s
|
|
147
|
+
execution_key = execution_id.to_s
|
|
148
|
+
next_state = state.to_sym
|
|
149
|
+
unless %i[executing suspended resuming cancelling terminalizing recovery_required].include?(next_state)
|
|
150
|
+
raise ArgumentError, "unsupported Agent admission state: #{next_state.inspect}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
synchronize do
|
|
154
|
+
current = @agent_admissions.fetch(key) do
|
|
155
|
+
raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
|
|
156
|
+
end
|
|
157
|
+
unless current.execution_id.to_s == execution_key
|
|
158
|
+
raise Phronomy::Error, "stale Agent admission state update for #{key.inspect}"
|
|
159
|
+
end
|
|
160
|
+
@agent_admissions[key] = AgentAdmission.new(
|
|
161
|
+
agent_id: current.agent_id,
|
|
162
|
+
owner_token: current.owner_token,
|
|
163
|
+
execution_id: current.execution_id,
|
|
164
|
+
state: next_state
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @api private
|
|
171
|
+
def mark_agent_admission_recovery_required(agent_id, owner_token:)
|
|
172
|
+
assert_event_loop_thread!
|
|
173
|
+
key = agent_id.to_s
|
|
174
|
+
synchronize do
|
|
175
|
+
current = @agent_admissions.fetch(key) do
|
|
176
|
+
raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
|
|
177
|
+
end
|
|
178
|
+
unless current.owner_token.equal?(owner_token)
|
|
179
|
+
raise Phronomy::Error, "stale Agent admission recovery update for #{key.inspect}"
|
|
180
|
+
end
|
|
181
|
+
@agent_admissions[key] = AgentAdmission.new(
|
|
182
|
+
agent_id: current.agent_id,
|
|
183
|
+
owner_token: current.owner_token,
|
|
184
|
+
execution_id: current.execution_id,
|
|
185
|
+
state: :recovery_required
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Owner-aware release. Pre-durable failures release by owner_token; durable
|
|
192
|
+
# terminal outcomes release by execution_id.
|
|
193
|
+
# @api private
|
|
194
|
+
def release_agent_execution_admission(agent_id, owner_token: nil, execution_id: nil)
|
|
195
|
+
assert_event_loop_thread!
|
|
196
|
+
key = agent_id.to_s
|
|
197
|
+
synchronize do
|
|
198
|
+
current = @agent_admissions[key]
|
|
199
|
+
next false unless current
|
|
200
|
+
|
|
201
|
+
authoritative = if execution_id
|
|
202
|
+
current.execution_id.to_s == execution_id.to_s
|
|
203
|
+
elsif owner_token
|
|
204
|
+
current.owner_token.equal?(owner_token)
|
|
205
|
+
else
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
next false unless authoritative
|
|
209
|
+
|
|
210
|
+
@agent_admissions.delete(key)
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Process-local read-only owner lookup. Mutable Agent execution state never
|
|
216
|
+
# crosses this boundary; external callers receive only routing/ownership data.
|
|
217
|
+
def agent_execution_owner(execution_id)
|
|
218
|
+
key = execution_id.to_s
|
|
219
|
+
synchronize do
|
|
220
|
+
state = @agent_executions[key]
|
|
221
|
+
next nil unless state
|
|
222
|
+
|
|
223
|
+
AgentExecutionOwner.new(
|
|
224
|
+
execution_id: key.freeze,
|
|
225
|
+
agent: state.agent,
|
|
226
|
+
coordinator: state.coordinator,
|
|
227
|
+
status: state.execution.status
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# EventLoop-only accessors below form the live Agent execution authority.
|
|
233
|
+
# Offload workers receive operation-specific immutable snapshots instead.
|
|
234
|
+
# @api private
|
|
235
|
+
def agent_execution_state(execution_id)
|
|
236
|
+
assert_event_loop_thread!
|
|
237
|
+
@agent_executions[execution_id.to_s]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# @api private
|
|
241
|
+
def install_agent_execution(
|
|
242
|
+
execution_id:,
|
|
243
|
+
agent:,
|
|
244
|
+
coordinator:,
|
|
245
|
+
execution:,
|
|
246
|
+
runtime_projection:,
|
|
247
|
+
base_manifest:,
|
|
248
|
+
invocation:,
|
|
249
|
+
fsm_session_id:
|
|
250
|
+
)
|
|
251
|
+
assert_event_loop_thread!
|
|
252
|
+
key = execution_id.to_s
|
|
253
|
+
state = AgentExecutionState.new(
|
|
254
|
+
execution_id: key.freeze,
|
|
255
|
+
agent: agent,
|
|
256
|
+
coordinator: coordinator,
|
|
257
|
+
execution: execution,
|
|
258
|
+
runtime_projection: runtime_projection,
|
|
259
|
+
base_manifest: base_manifest,
|
|
260
|
+
invocation: invocation,
|
|
261
|
+
fsm_session_id: fsm_session_id&.to_s&.freeze
|
|
262
|
+
)
|
|
263
|
+
synchronize do
|
|
264
|
+
if @agent_executions.key?(key)
|
|
265
|
+
raise Phronomy::Error, "Agent execution #{key.inspect} is already live"
|
|
266
|
+
end
|
|
267
|
+
@agent_executions[key] = state
|
|
268
|
+
end
|
|
269
|
+
state
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# @api private
|
|
273
|
+
def replace_agent_execution(
|
|
274
|
+
execution_id,
|
|
275
|
+
execution: UNSET,
|
|
276
|
+
runtime_projection: UNSET,
|
|
277
|
+
invocation: UNSET,
|
|
278
|
+
fsm_session_id: UNSET
|
|
279
|
+
)
|
|
280
|
+
assert_event_loop_thread!
|
|
281
|
+
key = execution_id.to_s
|
|
282
|
+
synchronize do
|
|
283
|
+
current = @agent_executions.fetch(key) do
|
|
284
|
+
raise Phronomy::Error, "Agent execution #{key.inspect} is not live"
|
|
285
|
+
end
|
|
286
|
+
updated = AgentExecutionState.new(
|
|
287
|
+
execution_id: current.execution_id,
|
|
288
|
+
agent: current.agent,
|
|
289
|
+
coordinator: current.coordinator,
|
|
290
|
+
execution: execution.equal?(UNSET) ? current.execution : execution,
|
|
291
|
+
runtime_projection: runtime_projection.equal?(UNSET) ?
|
|
292
|
+
current.runtime_projection : runtime_projection,
|
|
293
|
+
base_manifest: current.base_manifest,
|
|
294
|
+
invocation: invocation.equal?(UNSET) ? current.invocation : invocation,
|
|
295
|
+
fsm_session_id: fsm_session_id.equal?(UNSET) ?
|
|
296
|
+
current.fsm_session_id : fsm_session_id&.to_s&.freeze
|
|
297
|
+
)
|
|
298
|
+
@agent_executions[key] = updated
|
|
299
|
+
updated
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Registers a caller-facing TaskResult that observes the authoritative terminal
|
|
304
|
+
# outcome of one logical Agent execution. Waiters are Runtime-only and are
|
|
305
|
+
# never persisted or rehydrated.
|
|
306
|
+
# @api private
|
|
307
|
+
def register_agent_completion_waiter(execution_id, task)
|
|
308
|
+
assert_event_loop_thread!
|
|
309
|
+
unless task.is_a?(Phronomy::TaskResult)
|
|
310
|
+
raise ArgumentError, "Agent completion waiter must be a Phronomy::TaskResult"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
key = execution_id.to_s
|
|
314
|
+
synchronize do
|
|
315
|
+
waiters = @agent_completion_waiters[key]
|
|
316
|
+
waiters << task unless waiters.include?(task)
|
|
317
|
+
end
|
|
318
|
+
task
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Atomically detaches all process-local completion waiters at authoritative
|
|
322
|
+
# terminal delivery. A fallback TaskResult is included for pre-install terminal
|
|
323
|
+
# paths that never acquired a live execution directory entry.
|
|
324
|
+
# @api private
|
|
325
|
+
def take_agent_completion_waiters(execution_id, fallback: nil)
|
|
326
|
+
assert_event_loop_thread!
|
|
327
|
+
key = execution_id.to_s
|
|
328
|
+
synchronize do
|
|
329
|
+
waiters = @agent_completion_waiters.delete(key) || []
|
|
330
|
+
waiters << fallback if fallback && !waiters.include?(fallback)
|
|
331
|
+
waiters
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Registers one execution-owned asynchronous operation for physical
|
|
336
|
+
# quiescence supervision. OffloadPool tasks expose a private physical
|
|
337
|
+
# completion signal; custom asynchronous handles are required to make their
|
|
338
|
+
# ordinary completion mean that no residual execution-affecting work remains.
|
|
339
|
+
# @api private
|
|
340
|
+
def supervise_agent_operation(execution_id, operation)
|
|
341
|
+
assert_event_loop_thread!
|
|
342
|
+
key = execution_id.to_s
|
|
343
|
+
unless operation.respond_to?(:on_complete)
|
|
344
|
+
raise ArgumentError, "supervised operation must expose on_complete"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
physically_done = if operation.respond_to?(:physical_complete?)
|
|
348
|
+
operation.physical_complete?
|
|
349
|
+
elsif operation.respond_to?(:done?)
|
|
350
|
+
operation.done?
|
|
351
|
+
else
|
|
352
|
+
false
|
|
353
|
+
end
|
|
354
|
+
return operation if physically_done
|
|
355
|
+
|
|
356
|
+
token = Object.new.freeze
|
|
357
|
+
synchronize do
|
|
358
|
+
unless @agent_executions.key?(key)
|
|
359
|
+
raise Phronomy::Error, "Agent execution #{key.inspect} is not live"
|
|
360
|
+
end
|
|
361
|
+
@agent_inflight_work[key][token] = true
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
callback = lambda do
|
|
365
|
+
accepted = post_message(PhysicalCompletion.new(execution_id: key, token: token))
|
|
366
|
+
unless accepted
|
|
367
|
+
Phronomy.configuration.logger&.warn(
|
|
368
|
+
"[Phronomy] EventLoop rejected physical-completion delivery for #{key}"
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
if operation.respond_to?(:on_physical_complete)
|
|
374
|
+
operation.on_physical_complete(&callback)
|
|
375
|
+
else
|
|
376
|
+
operation.on_complete { |_value, _error| callback.call }
|
|
377
|
+
end
|
|
378
|
+
operation
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# @api private
|
|
382
|
+
def agent_execution_quiescent?(execution_id)
|
|
383
|
+
assert_event_loop_thread!
|
|
384
|
+
key = execution_id.to_s
|
|
385
|
+
synchronize do
|
|
386
|
+
work = @agent_inflight_work.fetch(key, nil)
|
|
387
|
+
work.nil? || work.empty?
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Holds exactly one terminal continuation while cancellation/deadline has
|
|
392
|
+
# revoked result authority but execution-owned physical work is still live.
|
|
393
|
+
# @api private
|
|
394
|
+
def defer_agent_terminal_until_quiescent(execution_id, command)
|
|
395
|
+
assert_event_loop_thread!
|
|
396
|
+
key = execution_id.to_s
|
|
397
|
+
synchronize do
|
|
398
|
+
if @agent_deferred_terminals.key?(key)
|
|
399
|
+
raise Phronomy::Error, "Agent execution #{key.inspect} already has a deferred terminal"
|
|
400
|
+
end
|
|
401
|
+
@agent_deferred_terminals[key] = command
|
|
402
|
+
end
|
|
403
|
+
true
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# @api private
|
|
407
|
+
def agent_inflight_work_count(execution_id)
|
|
408
|
+
key = execution_id.to_s
|
|
409
|
+
synchronize do
|
|
410
|
+
(@agent_inflight_work.fetch(key, nil) || {}).size
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# @api private
|
|
415
|
+
def release_agent_execution(execution_id)
|
|
416
|
+
assert_event_loop_thread!
|
|
417
|
+
key = execution_id.to_s
|
|
418
|
+
synchronize do
|
|
419
|
+
work = @agent_inflight_work.fetch(key, nil)
|
|
420
|
+
unless work.nil? || work.empty?
|
|
421
|
+
raise Phronomy::Error,
|
|
422
|
+
"cannot release non-quiescent Agent execution #{key.inspect}"
|
|
423
|
+
end
|
|
424
|
+
if @agent_deferred_terminals.key?(key)
|
|
425
|
+
raise Phronomy::Error,
|
|
426
|
+
"cannot release Agent execution #{key.inspect} with deferred terminal work"
|
|
427
|
+
end
|
|
428
|
+
@agent_inflight_work.delete(key)
|
|
429
|
+
@agent_executions.delete(key)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
private
|
|
434
|
+
|
|
435
|
+
def complete_agent_physical_work(payload)
|
|
436
|
+
key = payload.execution_id.to_s
|
|
437
|
+
token = payload.token
|
|
438
|
+
deferred = synchronize do
|
|
439
|
+
work = @agent_inflight_work.fetch(key, nil)
|
|
440
|
+
next nil unless work&.delete(token)
|
|
441
|
+
|
|
442
|
+
if work.empty?
|
|
443
|
+
@agent_inflight_work.delete(key)
|
|
444
|
+
command = @agent_deferred_terminals.delete(key)
|
|
445
|
+
command
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
deferred&.coordinator&.deliver_on_event_loop(deferred)
|
|
449
|
+
true
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def agent_admission_transition_in_progress_locked?
|
|
453
|
+
@agent_admissions.values.any? do |admission|
|
|
454
|
+
%i[admitting executing resuming cancelling terminalizing].include?(admission.state)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../engine/concurrency/worker_input_restricted"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Agent
|
|
7
|
+
# Registers ordinary and resumed Agent/Tool sessions on EventLoop.
|
|
8
|
+
# ExecutionCoordinator owns admission and terminal persistence; this object
|
|
9
|
+
# only wires the live sessions and reports their completion to that owner.
|
|
10
|
+
# @api private
|
|
11
|
+
class ExecutionSessionRunner
|
|
12
|
+
include Phronomy::Concurrency::WorkerInputRestricted
|
|
13
|
+
|
|
14
|
+
def initialize(runtime:, on_complete:)
|
|
15
|
+
@on_complete = on_complete
|
|
16
|
+
@runtime = runtime
|
|
17
|
+
@event_loop = runtime.event_loop
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def register(session, result_task, source_name: "#{result_task.name}-source")
|
|
21
|
+
assert_event_loop!
|
|
22
|
+
source = Phronomy::TaskResult.deferred(name: source_name)
|
|
23
|
+
source.on_complete do |invocation, error|
|
|
24
|
+
@on_complete.call(
|
|
25
|
+
execution_id: session.context.execution_id,
|
|
26
|
+
result_task: result_task,
|
|
27
|
+
invocation: invocation || session.context,
|
|
28
|
+
error: error,
|
|
29
|
+
fsm_session_id: session.id
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
@event_loop.register(session, completion: source)
|
|
33
|
+
session
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resume(invocation, result_task, resume_event:, resume_phase:,
|
|
37
|
+
source_name: "#{result_task.name}-source")
|
|
38
|
+
assert_event_loop!
|
|
39
|
+
session = AgentInvocationSessionBuilder.build_for_resume(
|
|
40
|
+
agent_invocation: invocation,
|
|
41
|
+
resume_event: resume_event,
|
|
42
|
+
resume_phase: resume_phase,
|
|
43
|
+
runtime: @runtime
|
|
44
|
+
)
|
|
45
|
+
ExecutionRegistry.for(@event_loop).replace_agent_execution(
|
|
46
|
+
invocation.execution_id,
|
|
47
|
+
invocation: invocation,
|
|
48
|
+
fsm_session_id: session.id
|
|
49
|
+
)
|
|
50
|
+
register(session, result_task, source_name: source_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resume_approval(invocation, result_task, approved:, config:)
|
|
54
|
+
assert_event_loop!
|
|
55
|
+
invocation.merge_config!(config)
|
|
56
|
+
invocation.begin_approval_resume!(approved: approved)
|
|
57
|
+
parent = resume(invocation, result_task,
|
|
58
|
+
resume_event: :resume, resume_phase: :suspended)
|
|
59
|
+
invocation.tool_invocations.each do |child|
|
|
60
|
+
session = if child.awaiting_approval?
|
|
61
|
+
ToolInvocationSessionBuilder.build_for_resume(
|
|
62
|
+
tool_invocation: child,
|
|
63
|
+
parent_event_sink: parent.event_sink,
|
|
64
|
+
resume_event: approved ? :approve : :reject,
|
|
65
|
+
resume_phase: :awaiting_approval,
|
|
66
|
+
runtime: @runtime
|
|
67
|
+
)
|
|
68
|
+
elsif !approved && child.authorized?
|
|
69
|
+
ToolInvocationSessionBuilder.build_for_resume(
|
|
70
|
+
tool_invocation: child,
|
|
71
|
+
parent_event_sink: parent.event_sink,
|
|
72
|
+
resume_event: :cancel,
|
|
73
|
+
resume_phase: :authorized,
|
|
74
|
+
runtime: @runtime
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
register_child(child, session, parent.event_sink) if session
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def resume_framework_tools(invocation, result_task)
|
|
82
|
+
assert_event_loop!
|
|
83
|
+
parent = resume(invocation, result_task,
|
|
84
|
+
resume_event: :resume, resume_phase: :suspended,
|
|
85
|
+
source_name: "framework-tool-recovery-source")
|
|
86
|
+
invocation.tool_invocations.select(&:authorized?).each do |child|
|
|
87
|
+
session = ToolInvocationSessionBuilder.build_for_resume(
|
|
88
|
+
tool_invocation: child,
|
|
89
|
+
parent_event_sink: parent.event_sink,
|
|
90
|
+
resume_event: :dispatch,
|
|
91
|
+
resume_phase: :authorized,
|
|
92
|
+
runtime: @runtime
|
|
93
|
+
)
|
|
94
|
+
register_child(child, session, parent.event_sink)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def register_child(child, session, parent_event_sink)
|
|
101
|
+
completion = Phronomy::TaskResult.deferred(name: "tool-session:#{child.id}")
|
|
102
|
+
completion.on_complete do |_result, error|
|
|
103
|
+
next unless error
|
|
104
|
+
|
|
105
|
+
child.mark_framework_failed!(error)
|
|
106
|
+
parent_event_sink.post(:tool_failed, {tool_invocation_id: child.id})
|
|
107
|
+
end
|
|
108
|
+
@event_loop.register(session, completion: completion)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def assert_event_loop!
|
|
112
|
+
return if @event_loop.current?
|
|
113
|
+
|
|
114
|
+
raise Phronomy::Error, "Agent session registration must run on EventLoop"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|