phronomy 0.28.0 → 0.30.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 +96 -1
- data/README.md +32 -6
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +8 -0
- data/docs/architecture/agent-iteration-budget.md +81 -0
- data/docs/architecture/dx05-agent-scope.md +126 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/architecture/generator-verifier-lifecycle-result.md +126 -0
- data/docs/architecture/generator-verifier-ownership.md +18 -109
- data/docs/architecture/llm-continuation-metadata.md +88 -0
- data/docs/architecture/lm-studio-output-failures.md +111 -0
- data/docs/architecture/r8-unit15.md +3 -0
- data/docs/architecture/r8-unit7.md +2 -1
- data/docs/architecture/structured-output-and-scope.md +183 -0
- data/docs/architecture/tracing.md +22 -4
- data/docs/architecture/transport-error-boundaries.md +103 -0
- data/docs/architecture/trust-acceptance-followup.md +87 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +14 -2
- data/docs/getting-started.md +44 -43
- data/docs/migrations/r8-unit15.md +3 -0
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +10 -0
- data/lib/phronomy/agent/admission.rb +1 -1
- data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
- data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
- data/lib/phronomy/agent/api/agent.rb +13 -0
- data/lib/phronomy/agent/async_event_api.rb +121 -88
- data/lib/phronomy/agent/base.rb +82 -44
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/context_importer.rb +4 -0
- data/lib/phronomy/agent/execution/agent_invocation.rb +6 -2
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +6 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +20 -9
- data/lib/phronomy/agent/execution/execution_cancellation.rb +1 -1
- data/lib/phronomy/agent/execution/execution_coordinator.rb +79 -18
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +24 -4
- data/lib/phronomy/agent/execution/execution_registry.rb +5 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +11 -5
- data/lib/phronomy/agent/execution/invocation_actions.rb +1 -1
- data/lib/phronomy/agent/execution/invocation_transitions.rb +4 -4
- data/lib/phronomy/agent/execution/iteration_budget.rb +35 -0
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +4 -4
- data/lib/phronomy/agent/execution_observation.rb +5 -5
- data/lib/phronomy/agent/filterable.rb +21 -15
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
- data/lib/phronomy/agent/lifecycle/scope.rb +208 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +21 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/output_contract.rb +64 -0
- data/lib/phronomy/agent/output_error.rb +53 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +6 -6
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +8 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +6 -10
- data/lib/phronomy/agent/recovery/recovery_support.rb +6 -8
- data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -1
- data/lib/phronomy/agent/runtime_input.rb +15 -4
- data/lib/phronomy/agent/saved_context_reader.rb +2 -1
- data/lib/phronomy/agent/store.rb +10 -2
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/common/values/immutable.rb +33 -0
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +18 -22
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +8 -2
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- data/lib/phronomy/engine/fsm_session.rb +16 -1
- data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
- data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
- data/lib/phronomy/execution/task_result.rb +7 -2
- data/lib/phronomy/filter/filter_block_error.rb +2 -2
- data/lib/phronomy/generation/generator_verifier/agent_operation.rb +28 -5
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +14 -27
- data/lib/phronomy/generation/generator_verifier/approval.rb +30 -0
- data/lib/phronomy/generation/generator_verifier/cleanup_error.rb +15 -0
- data/lib/phronomy/generation/generator_verifier/invocation.rb +147 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +16 -0
- data/lib/phronomy/generation/generator_verifier/protocol.rb +55 -0
- data/lib/phronomy/generation/generator_verifier/result.rb +121 -0
- data/lib/phronomy/generation/generator_verifier/unaccepted_result_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +13 -11
- data/lib/phronomy/generation/generator_verifier.rb +170 -85
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/llm_adapter/backends/lm_studio.rb +139 -0
- data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +134 -12
- data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +41 -0
- data/lib/phronomy/llm_adapter/message.rb +4 -0
- data/lib/phronomy/llm_adapter/request.rb +17 -2
- data/lib/phronomy/llm_adapter/response.rb +11 -4
- data/lib/phronomy/llm_adapter/structured_output_unsupported_error.rb +14 -0
- data/lib/phronomy/llm_adapter/transport_error.rb +4 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- data/lib/phronomy/multi_agent/shared_state.rb +1 -0
- data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
- data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
- data/lib/phronomy/output_parser/json_schema_parser.rb +113 -0
- data/lib/phronomy/output_parser/parse_error.rb +10 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +4 -0
- data/lib/phronomy/storage/condition_failed_error.rb +2 -2
- data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
- data/lib/phronomy/tool/base.rb +25 -23
- data/lib/phronomy/tracing/automatic.rb +14 -8
- data/lib/phronomy/tracing/base.rb +37 -7
- data/lib/phronomy/tracing/langfuse_tracer.rb +3 -3
- data/lib/phronomy/tracing/observation.rb +2 -1
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +7 -32
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +30 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
- data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
- data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
- data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
- data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
- data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
- data/lib/phronomy.rb +7 -2
- data/scripts/api_snapshot.rb +15 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- data/scripts/migrate_generator_verifier_result.rb +18 -0
- data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
- data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
- data/sig/phronomy/agent/execution_contract.rbs +4 -3
- data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +36 -5
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +8 -2
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/generator_verifier.rbs +70 -0
- data/sig/phronomy/integrations.rbs +14 -0
- data/sig/phronomy/llm_adapter/backends/lm_studio.rbs +8 -0
- data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +1 -0
- data/sig/phronomy/llm_adapter/values.rbs +8 -2
- data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
- data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
- data/sig/phronomy/storage/contracts.rbs +2 -2
- data/sig/phronomy/tracing/observation.rbs +1 -1
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +6 -0
- metadata +103 -5
- data/lib/phronomy/generation/generator_verifier/default_parser.rb +0 -27
- data/lib/phronomy/generation/low_confidence_error.rb +0 -14
- data/lib/phronomy/runtime_composition/generation_defaults.rb +0 -6
|
@@ -9,7 +9,13 @@ module Phronomy
|
|
|
9
9
|
@runtime = runtime
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def registry
|
|
12
|
+
def registry
|
|
13
|
+
receiver = WorkflowExecutionRegistry.for(@runtime.event_loop)
|
|
14
|
+
@runtime.__shutdown_participant(key: WorkflowExecutionRegistry) ||
|
|
15
|
+
@runtime.__register_shutdown_participant(key: WorkflowExecutionRegistry, participant: receiver)
|
|
16
|
+
receiver
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def existing_registry = WorkflowExecutionRegistry.existing_for(@runtime)
|
|
14
20
|
def executing? = @runtime.event_loop.current?
|
|
15
21
|
|
|
@@ -21,18 +27,35 @@ module Phronomy
|
|
|
21
27
|
WorkflowPhaseMachineBuilder.new(**definition).build
|
|
22
28
|
end
|
|
23
29
|
|
|
24
|
-
def build_session(entry_actions:, persist: nil, **options)
|
|
30
|
+
def build_session(entry_actions:, persist: nil, task_execution: nil, **options)
|
|
25
31
|
# Preserve nil/scalar results: initial entry must not re-adopt metadata
|
|
26
32
|
# unless the application actually returns a context.
|
|
27
33
|
initial_actions = entry_actions.to_h do |state_name, callbacks|
|
|
28
34
|
[state_name, callbacks.map { |callback| ->(context) { WorkflowActionRules.entry(callback, context, state_name) } }]
|
|
29
35
|
end
|
|
30
|
-
Phronomy::
|
|
36
|
+
klass = task_execution ? Phronomy::WorkflowTaskSession : Phronomy::FSMSession
|
|
37
|
+
options = options.merge(task_execution: task_execution) if task_execution
|
|
38
|
+
klass.new(**options, entry_actions: initial_actions, event_loop: @runtime.event_loop,
|
|
31
39
|
terminal_policy: persist && WorkflowTerminalPolicy.new(persist: persist))
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
def register_session(session, completion:)
|
|
35
43
|
@runtime.event_loop.register(session, completion: completion, receiver: registry)
|
|
36
44
|
end
|
|
45
|
+
|
|
46
|
+
def subscribe_cancellation(token, &callback)
|
|
47
|
+
subscriptions = Phronomy::Concurrency::ResultSubscriptions.new
|
|
48
|
+
subscriptions.cancellation(token, &callback)
|
|
49
|
+
remaining = token.remaining_monotonic_seconds
|
|
50
|
+
if remaining && !token.cancelled?
|
|
51
|
+
timer = @runtime.timer_queue
|
|
52
|
+
timer.schedule(seconds: remaining, &callback)
|
|
53
|
+
subscriptions.add { timer.cancel(callback) }
|
|
54
|
+
end
|
|
55
|
+
subscriptions
|
|
56
|
+
rescue
|
|
57
|
+
subscriptions&.close
|
|
58
|
+
raise
|
|
59
|
+
end
|
|
37
60
|
end
|
|
38
61
|
end
|
|
@@ -47,7 +47,8 @@ module Phronomy
|
|
|
47
47
|
:context,
|
|
48
48
|
:current_event,
|
|
49
49
|
:selected_transition_action,
|
|
50
|
-
:selected_transition_metadata
|
|
50
|
+
:selected_transition_metadata,
|
|
51
|
+
:workflow_task_execution
|
|
51
52
|
)
|
|
52
53
|
|
|
53
54
|
state_machine :phase, initial: entry do
|
|
@@ -85,6 +86,7 @@ module Phronomy
|
|
|
85
86
|
# Both source exit callbacks and the transition action are
|
|
86
87
|
# before_transition callbacks. Register exits first to preserve the
|
|
87
88
|
# required exit -> transition action -> entry ordering.
|
|
89
|
+
before_transition { |machine| machine.workflow_task_execution&.leave }
|
|
88
90
|
exit_actions.each do |state_name, callables|
|
|
89
91
|
callables.each do |callable|
|
|
90
92
|
before_transition(
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# I07 translates C02 task-state decisions to generic FSM progression. Engine
|
|
5
|
+
# does not learn TaskResult ownership, cancellation or stale-result policy.
|
|
6
|
+
# @api private
|
|
7
|
+
class WorkflowTaskSession < FSMSession
|
|
8
|
+
def initialize(task_execution:, **options)
|
|
9
|
+
super(**options)
|
|
10
|
+
@task_execution = task_execution
|
|
11
|
+
@task_execution.bind(self)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start
|
|
15
|
+
@task_execution.activate
|
|
16
|
+
super unless @done
|
|
17
|
+
rescue => error
|
|
18
|
+
finish_with_error(error)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def apply_task_context(context)
|
|
22
|
+
return unless @terminal_lifecycle_state == :running
|
|
23
|
+
|
|
24
|
+
@ctx = @context = context
|
|
25
|
+
apply_context_metadata!
|
|
26
|
+
@tracker.context = context
|
|
27
|
+
advance_or_halt
|
|
28
|
+
rescue => error
|
|
29
|
+
finish_with_error(error)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fail_task_execution(error)
|
|
33
|
+
finish_with_error(error) if @terminal_lifecycle_state == :running
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle(event)
|
|
37
|
+
return if event.type == :state_completed && !@task_execution.permits_completion?(event.payload)
|
|
38
|
+
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def post_session_event(type, payload = nil)
|
|
45
|
+
payload = @task_execution.completion_key if type == :state_completed
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def advance_or_halt
|
|
50
|
+
if @task_execution.enter(@current_state, @ctx)
|
|
51
|
+
notify_stable_state!
|
|
52
|
+
else
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_tracker(from_state)
|
|
58
|
+
super.tap { |tracker| tracker.workflow_task_execution = @task_execution }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def request_terminal!(...)
|
|
62
|
+
@task_execution.close
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def finish_with_error(error)
|
|
67
|
+
@task_execution.close
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def retire_without_result!(error)
|
|
72
|
+
@task_execution.retire(error)
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/phronomy.rb
CHANGED
|
@@ -12,6 +12,7 @@ loader.inflector.inflect("fsm_protocol" => "FSMProtocol")
|
|
|
12
12
|
loader.inflector.inflect("llm_adapter" => "LLMAdapter")
|
|
13
13
|
loader.inflector.inflect("llm_operation_result" => "LLMOperationResult")
|
|
14
14
|
loader.inflector.inflect("ruby_llm" => "RubyLLM")
|
|
15
|
+
loader.inflector.inflect("lm_studio" => "LMStudio")
|
|
15
16
|
loader.inflector.inflect("canonical_json" => "CanonicalJSON")
|
|
16
17
|
loader.inflector.inflect("llm_call_record" => "LLMCallRecord")
|
|
17
18
|
loader.inflector.inflect("llm_input_manifest" => "LLMInputManifest")
|
|
@@ -60,7 +61,6 @@ loader.ignore(
|
|
|
60
61
|
"#{__dir__}/phronomy/runtime_composition/global_configuration.rb",
|
|
61
62
|
"#{__dir__}/phronomy/agent/composition",
|
|
62
63
|
"#{__dir__}/phronomy/runtime_composition/agent_defaults.rb",
|
|
63
|
-
"#{__dir__}/phronomy/runtime_composition/generation_defaults.rb",
|
|
64
64
|
"#{__dir__}/phronomy/runtime_composition/evaluation_defaults.rb",
|
|
65
65
|
"#{__dir__}/phronomy/runtime_composition/workflow_defaults.rb",
|
|
66
66
|
"#{__dir__}/phronomy/runtime_composition/multi_agent_defaults.rb",
|
|
@@ -68,6 +68,8 @@ loader.ignore(
|
|
|
68
68
|
"#{__dir__}/phronomy/runtime_composition/global_runtime.rb",
|
|
69
69
|
"#{__dir__}/phronomy/runtime_composition/execution_defaults.rb"
|
|
70
70
|
)
|
|
71
|
+
# Optional application integrations are loaded explicitly by their consumers.
|
|
72
|
+
loader.ignore("#{__dir__}/phronomy/integrations")
|
|
71
73
|
# Persistence conformance support must never make production loading require RSpec.
|
|
72
74
|
loader.ignore(
|
|
73
75
|
"#{__dir__}/phronomy/testing/persistence_contract.rb",
|
|
@@ -85,6 +87,8 @@ loader.ignore("#{__dir__}/phronomy/agent/agent_purged_error.rb")
|
|
|
85
87
|
loader.ignore("#{__dir__}/phronomy/agent/stream_callback_error.rb")
|
|
86
88
|
loader.ignore("#{__dir__}/phronomy/agent/handoff_error.rb")
|
|
87
89
|
loader.ignore("#{__dir__}/phronomy/agent/agent_already_exists_error.rb")
|
|
90
|
+
loader.ignore("#{__dir__}/phronomy/agent/agent_not_found_error.rb")
|
|
91
|
+
loader.ignore("#{__dir__}/phronomy/agent/agent_unloaded_error.rb")
|
|
88
92
|
loader.setup
|
|
89
93
|
require_relative "phronomy/persistence/transaction"
|
|
90
94
|
require_relative "phronomy/persistence_composition/stores"
|
|
@@ -96,6 +100,8 @@ require_relative "phronomy/agent/agent_purged_error"
|
|
|
96
100
|
require_relative "phronomy/agent/stream_callback_error"
|
|
97
101
|
require_relative "phronomy/agent/handoff_error"
|
|
98
102
|
require_relative "phronomy/agent/agent_already_exists_error"
|
|
103
|
+
require_relative "phronomy/agent/agent_not_found_error"
|
|
104
|
+
require_relative "phronomy/agent/agent_unloaded_error"
|
|
99
105
|
|
|
100
106
|
require_relative "phronomy/runtime_composition/configuration_defaults"
|
|
101
107
|
require_relative "phronomy/llm_adapter/token_usage"
|
|
@@ -110,7 +116,6 @@ require_relative "phronomy/agent/api/agent"
|
|
|
110
116
|
require_relative "phronomy/runtime_composition/agent_defaults"
|
|
111
117
|
require_relative "phronomy/runtime_composition/multi_agent_defaults"
|
|
112
118
|
require_relative "phronomy/runtime_composition/workflow_defaults"
|
|
113
|
-
require_relative "phronomy/runtime_composition/generation_defaults"
|
|
114
119
|
require_relative "phronomy/runtime_composition/evaluation_defaults"
|
|
115
120
|
require_relative "phronomy/agent/composition/run_once"
|
|
116
121
|
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -18,6 +18,17 @@ require_relative "../lib/phronomy"
|
|
|
18
18
|
|
|
19
19
|
PUBLIC_API_ENTRIES = [
|
|
20
20
|
# Stable
|
|
21
|
+
Phronomy::Error,
|
|
22
|
+
Phronomy::AgentNotFoundError,
|
|
23
|
+
Phronomy::AgentUnloadedError,
|
|
24
|
+
Phronomy::Agent,
|
|
25
|
+
Phronomy::Agent::Scope,
|
|
26
|
+
Phronomy::Agent::OutputError,
|
|
27
|
+
Phronomy::Agent::ScopeCleanupError,
|
|
28
|
+
Phronomy::MultiAgent::TeamNotFoundError,
|
|
29
|
+
Phronomy::MultiAgent::TeamBusyError,
|
|
30
|
+
Phronomy::Embeddings::AuthenticationError,
|
|
31
|
+
Phronomy::Embeddings::RateLimitError,
|
|
21
32
|
Phronomy::Agent::Base,
|
|
22
33
|
Phronomy::Agent::ReservedExecution,
|
|
23
34
|
Phronomy::Agent::ExecutionObservation,
|
|
@@ -36,6 +47,8 @@ PUBLIC_API_ENTRIES = [
|
|
|
36
47
|
Phronomy::InvocationContext,
|
|
37
48
|
Phronomy::Tool::Base,
|
|
38
49
|
Phronomy::Workflow,
|
|
50
|
+
Phronomy::Workflow::Builder,
|
|
51
|
+
Phronomy::WorkflowTaskResumeError,
|
|
39
52
|
Phronomy::WorkflowContext,
|
|
40
53
|
Phronomy::Runnable,
|
|
41
54
|
Phronomy::Context::PromptTemplate,
|
|
@@ -45,6 +58,8 @@ PUBLIC_API_ENTRIES = [
|
|
|
45
58
|
Phronomy::Tool::Operation,
|
|
46
59
|
Phronomy::LLMAdapter::Base,
|
|
47
60
|
Phronomy::LLMAdapter::Request,
|
|
61
|
+
Phronomy::LLMAdapter::StructuredOutputUnsupportedError,
|
|
62
|
+
Phronomy::OutputParser::JsonSchemaParser,
|
|
48
63
|
Phronomy::LLMAdapter::Message,
|
|
49
64
|
Phronomy::LLMAdapter::Response,
|
|
50
65
|
Phronomy::LLMAdapter::StreamChunk,
|
|
@@ -1,38 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
# Exits non-zero if any block has a syntax error.
|
|
5
|
-
|
|
3
|
+
# Check all Ruby fences, including executable examples, in the entry documents.
|
|
6
4
|
require "tempfile"
|
|
7
5
|
require "open3"
|
|
8
|
-
|
|
9
|
-
readme_path = File.expand_path("../README.md", __dir__)
|
|
10
|
-
readme = File.read(readme_path)
|
|
11
|
-
blocks = readme.scan(/^```ruby\n(.*?)^```/m).map.with_index(1) { |(code), i| [i, code] }
|
|
12
|
-
|
|
13
|
-
puts "Checking #{blocks.size} Ruby code blocks in README.md..."
|
|
6
|
+
require_relative "check_readme_runnable"
|
|
14
7
|
|
|
15
8
|
failures = []
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
count = 0
|
|
10
|
+
paths = ARGV.empty? ? RunnableDocumentation::PATHS : ARGV
|
|
11
|
+
paths.each do |relative|
|
|
12
|
+
text = File.read(File.expand_path(relative, RunnableDocumentation::ROOT))
|
|
13
|
+
text.to_enum(:scan, /^```ruby(?: runnable)?\n(.*?)^```/m).each do
|
|
14
|
+
match = Regexp.last_match
|
|
15
|
+
line = text[0...match.begin(1)].count("\n") + 1
|
|
16
|
+
label = "#{relative}:#{line}"
|
|
17
|
+
count += 1
|
|
18
|
+
Tempfile.create(["documentation_block", ".rb"]) do |file|
|
|
19
|
+
file.write(match[1])
|
|
20
|
+
file.flush
|
|
21
|
+
out, status = Open3.capture2e(RbConfig.ruby, "-c", file.path)
|
|
22
|
+
if status.success?
|
|
23
|
+
puts "OK #{label}"
|
|
24
|
+
else
|
|
25
|
+
failures << label
|
|
26
|
+
puts "FAIL #{label}"
|
|
27
|
+
puts out.gsub(file.path, label)
|
|
28
|
+
end
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
puts "All #{blocks.size} Ruby code blocks passed syntax check."
|
|
34
|
-
exit 0
|
|
35
|
-
else
|
|
36
|
-
puts "\n#{failures.size} block(s) failed syntax check: #{failures.join(", ")}"
|
|
37
|
-
exit 1
|
|
38
|
-
end
|
|
32
|
+
puts "#{count} Ruby blocks; #{failures.size} syntax failures"
|
|
33
|
+
exit(failures.empty? ? 0 : 1)
|
|
@@ -1,101 +1,112 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# Any block that raises NoMethodError / ArgumentError / NameError causes a
|
|
9
|
-
# non-zero exit, failing the CI step.
|
|
10
|
-
#
|
|
11
|
-
# Usage (from the phronomy/ root):
|
|
12
|
-
# bundle exec ruby scripts/check_readme_runnable.rb
|
|
13
|
-
|
|
3
|
+
# Runs annotated, self-contained examples through the real public API.
|
|
4
|
+
# Only the external LLM adapter is replaced. Each block has its own process,
|
|
5
|
+
# configuration and Runtime; an accidental network request fails immediately.
|
|
6
|
+
# Usage: bundle exec ruby scripts/check_readme_runnable.rb [markdown paths...]
|
|
14
7
|
require "tempfile"
|
|
15
8
|
require "open3"
|
|
16
9
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
module RunnableDocumentation
|
|
11
|
+
ROOT = File.expand_path("..", __dir__)
|
|
12
|
+
PATHS = %w[README.md docs/getting-started.md docs/application-recipes.md docs/workflow-completion.md].freeze
|
|
13
|
+
|
|
14
|
+
PREAMBLE = <<~RUBY
|
|
15
|
+
require "bundler/setup"
|
|
16
|
+
require "phronomy"
|
|
17
|
+
require "webmock"
|
|
18
|
+
require "timeout"
|
|
19
|
+
WebMock.enable!
|
|
20
|
+
WebMock.disable_net_connect!
|
|
21
|
+
|
|
22
|
+
class DocumentationLLM < Phronomy::LLMAdapter::Base
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def perform_complete(request, cancellation_token:)
|
|
26
|
+
previous = request.messages.last
|
|
27
|
+
if previous&.role == :tool
|
|
28
|
+
return Phronomy::LLMAdapter::Response.new(content: previous.content)
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
module Agent
|
|
32
|
-
# Prepend overrides AsyncEventApi (which is also prepended) so invoke
|
|
33
|
-
# returns a canned response without triggering the EventLoop.
|
|
34
|
-
module CiInvokeStub
|
|
35
|
-
def invoke(input = nil, **)
|
|
36
|
-
{output: "ci-stub-output", messages: []}
|
|
31
|
+
search = request.tools.find do |tool|
|
|
32
|
+
tool.fetch("parameters_schema").fetch("properties", {}).key?("query")
|
|
37
33
|
end
|
|
34
|
+
if search
|
|
35
|
+
return Phronomy::LLMAdapter::Response.new(tool_calls: [
|
|
36
|
+
Phronomy::Tool::CallRequest.new(
|
|
37
|
+
id: SecureRandom.uuid, name: search.fetch("name"),
|
|
38
|
+
arguments: {"query" => "Ruby AI frameworks"}
|
|
39
|
+
)
|
|
40
|
+
])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Phronomy::LLMAdapter::Response.new(content: "ci-stub-output")
|
|
38
44
|
end
|
|
39
|
-
Base.prepend(CiInvokeStub)
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
def perform_stream(request, cancellation_token:)
|
|
47
|
+
response = perform_complete(request, cancellation_token: cancellation_token)
|
|
48
|
+
yield Phronomy::LLMAdapter::StreamChunk.new(content: response.content) if response.content
|
|
49
|
+
response
|
|
45
50
|
end
|
|
46
51
|
end
|
|
52
|
+
RUBY
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
54
|
+
def self.blocks(path)
|
|
55
|
+
text = File.read(path)
|
|
56
|
+
text.to_enum(:scan, /^```ruby runnable\n(.*?)^```/m).map do
|
|
57
|
+
match = Regexp.last_match
|
|
58
|
+
[text[0...match.begin(1)].count("\n") + 1, match[1]]
|
|
54
59
|
end
|
|
55
60
|
end
|
|
56
|
-
# --- end CI preamble ---
|
|
57
|
-
|
|
58
|
-
RUBY
|
|
59
|
-
|
|
60
|
-
readme = File.read(README_PATH)
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
puts "
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
62
|
+
def self.check(paths = PATHS, output: $stdout)
|
|
63
|
+
failures = []
|
|
64
|
+
count = 0
|
|
65
|
+
paths.each do |relative|
|
|
66
|
+
path = File.expand_path(relative, ROOT)
|
|
67
|
+
examples = blocks(path)
|
|
68
|
+
if examples.empty?
|
|
69
|
+
failures << relative
|
|
70
|
+
output.puts "FAIL #{relative}: no ruby runnable blocks"
|
|
71
|
+
end
|
|
72
|
+
examples.each do |line, code|
|
|
73
|
+
count += 1
|
|
74
|
+
label = "#{relative}:#{line}"
|
|
75
|
+
Tempfile.create(["phronomy_documentation", ".rb"]) do |file|
|
|
76
|
+
file.write(PREAMBLE)
|
|
77
|
+
file.write(<<~RUBY)
|
|
78
|
+
Phronomy.with_configuration do |configuration|
|
|
79
|
+
configuration.llm_adapter = DocumentationLLM.new
|
|
80
|
+
begin
|
|
81
|
+
Timeout.timeout(20) do
|
|
82
|
+
eval(#{code.dump}, TOPLEVEL_BINDING, #{path.dump}, #{line})
|
|
83
|
+
end
|
|
84
|
+
ensure
|
|
85
|
+
Phronomy::Runtime.reset_default!(timeout: 5)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
RUBY
|
|
89
|
+
file.flush
|
|
90
|
+
out, err, status = Open3.capture3(
|
|
91
|
+
{"BUNDLE_GEMFILE" => File.join(ROOT, "Gemfile")},
|
|
92
|
+
RbConfig.ruby, file.path, chdir: ROOT
|
|
93
|
+
)
|
|
94
|
+
if status.success?
|
|
95
|
+
output.puts "OK #{label}"
|
|
96
|
+
else
|
|
97
|
+
failures << label
|
|
98
|
+
output.puts "FAIL #{label}"
|
|
99
|
+
output.puts (out + err).gsub(file.path, label).lines.first(15).join
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
90
103
|
end
|
|
104
|
+
output.puts "#{count} runnable examples; #{failures.size} failures"
|
|
105
|
+
failures.empty?
|
|
91
106
|
end
|
|
92
107
|
end
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
exit 0
|
|
98
|
-
else
|
|
99
|
-
puts "#{failures.size} block(s) failed: #{failures.join(", ")}"
|
|
100
|
-
exit 1
|
|
109
|
+
if $PROGRAM_NAME == __FILE__
|
|
110
|
+
paths = ARGV.empty? ? RunnableDocumentation::PATHS : ARGV
|
|
111
|
+
exit(RunnableDocumentation.check(paths) ? 0 : 1)
|
|
101
112
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Explicit one-time conversion of a pre-structured-output Result JSON object.
|
|
5
|
+
# Never infer old formats in the runtime reader; never overwrite the source.
|
|
6
|
+
require_relative "../lib/phronomy"
|
|
7
|
+
|
|
8
|
+
abort "Usage: ruby scripts/migrate_generator_verifier_result.rb OLD.json NEW.json" unless ARGV.length == 2
|
|
9
|
+
source, destination = ARGV
|
|
10
|
+
value = JSON.parse(File.read(source), allow_duplicate_key: false, allow_nan: false,
|
|
11
|
+
allow_comments: false, allow_invalid_escape: false, allow_control_characters: false)
|
|
12
|
+
abort "Expected an old Result object without failure" unless value.is_a?(Hash) && !value.key?("failure")
|
|
13
|
+
value["failure"] = nil
|
|
14
|
+
result = Phronomy::GeneratorVerifier::Result.from_h(value)
|
|
15
|
+
File.open(destination, File::WRONLY | File::CREAT | File::EXCL) do |file|
|
|
16
|
+
file.write(JSON.pretty_generate(result.to_h))
|
|
17
|
+
file.write("\n")
|
|
18
|
+
end
|
|
@@ -2,7 +2,7 @@ module Phronomy
|
|
|
2
2
|
|
|
3
3
|
module Agent
|
|
4
4
|
type execution_wiring = { ?phronomy_execution_participant: _ExecutionParticipant, ?phronomy_control_bindings: Array[_ControlCapability], ?phronomy_transfer_context: TransferContext, ?phronomy_admission: _AdmissionOwner, ?phronomy_reservation: Hash[String, json_value] }
|
|
5
|
-
type execution_result = { agent_id: String, execution_id: String, status: Symbol, result:
|
|
5
|
+
type execution_result = { agent_id: String, execution_id: String, status: Symbol, result: json_value, output: json_value, ?raw_output: String?, error: Hash[String, json_value]? }
|
|
6
6
|
|
|
7
7
|
class ReservedExecution < Data
|
|
8
8
|
attr_reader agent_id: String
|
|
@@ -15,12 +15,13 @@ module Phronomy
|
|
|
15
15
|
attr_reader agent_id: String
|
|
16
16
|
attr_reader execution_id: String
|
|
17
17
|
attr_reader status: Symbol
|
|
18
|
-
attr_reader result:
|
|
18
|
+
attr_reader result: json_value
|
|
19
|
+
attr_reader raw_output: String?
|
|
19
20
|
attr_reader error: Hash[String, json_value]?
|
|
20
21
|
attr_reader reservation: ReservedExecution?
|
|
21
22
|
attr_reader transfer_receipt: Hash[String, json_value]?
|
|
22
23
|
attr_reader cancellation_requested: bool
|
|
23
|
-
def self.new: (agent_id: String, execution_id: String, status: Symbol, result: String?, error: Hash[String, json_value]?, reservation: ReservedExecution?, transfer_receipt: Hash[String, json_value]?, cancellation_requested: bool) -> instance
|
|
24
|
+
def self.new: (agent_id: String, execution_id: String, status: Symbol, result: json_value, ?raw_output: String?, error: Hash[String, json_value]?, reservation: ReservedExecution?, transfer_receipt: Hash[String, json_value]?, cancellation_requested: bool) -> instance
|
|
24
25
|
def self.from_result: (execution_result) -> instance
|
|
25
26
|
def absent?: () -> bool
|
|
26
27
|
def terminal?: () -> bool
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module Phronomy
|
|
2
|
+
class StreamCallbackError < Error
|
|
3
|
+
attr_reader event_type: Symbol
|
|
4
|
+
attr_reader original_error: Exception
|
|
5
|
+
attr_reader result: untyped
|
|
6
|
+
def initialize: (event_type: Symbol, original_error: Exception, ?result: untyped, ?code: String?) -> void
|
|
7
|
+
end
|
|
8
|
+
end
|
data/sig/phronomy/agent.rbs
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
module Phronomy
|
|
2
2
|
module Agent
|
|
3
|
+
# The block's TaskResult is awaited and unwrapped; Scope itself is process-local.
|
|
4
|
+
def self.open_scope: [R] () { (Scope) -> (R | Phronomy::TaskResult[R]) } -> R
|
|
5
|
+
|
|
6
|
+
class Scope
|
|
7
|
+
def release!: () -> bool
|
|
8
|
+
def create: (singleton(Base) agent_class, **untyped options) ?{ (untyped event) -> void } -> Base
|
|
9
|
+
def create_async: (singleton(Base) agent_class, **untyped options) ?{ (untyped event) -> void } -> Phronomy::TaskResult[Base]
|
|
10
|
+
def load: (singleton(Base) agent_class, String agent_id, persistence: Store, **untyped options) ?{ (untyped event) -> void } -> Base
|
|
11
|
+
def load_async: (singleton(Base) agent_class, String agent_id, persistence: Store, **untyped options) ?{ (untyped event) -> void } -> Phronomy::TaskResult[Base]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class ScopeCleanupError < Phronomy::Error
|
|
15
|
+
attr_reader failures: Array[{ agent_id: String?, error: Exception }]
|
|
16
|
+
attr_reader result: untyped
|
|
17
|
+
attr_reader scope: Scope
|
|
18
|
+
attr_reader primary_error: Exception?
|
|
19
|
+
def initialize: (scope: Scope, failures: Array[{ agent_id: String?, error: Exception }], ?result: untyped, ?primary_error: Exception?) -> void
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class OutputError < Phronomy::Error
|
|
23
|
+
attr_reader execution_id: String
|
|
24
|
+
attr_reader schema_digest: String
|
|
25
|
+
attr_reader reason: String
|
|
26
|
+
attr_reader violations: Array[Hash[String, untyped]]
|
|
27
|
+
attr_reader raw_output: untyped
|
|
28
|
+
def initialize: (execution_id: String, schema_digest: String, reason: String | Symbol,
|
|
29
|
+
?violations: Array[Hash[String, untyped]], ?raw_output: untyped) -> void
|
|
30
|
+
end
|
|
31
|
+
|
|
3
32
|
interface _BeforeLLMInputHook
|
|
4
33
|
def call: (Phronomy::Context::LLMInputBuildContext context) -> Phronomy::Context::LLMInputPatch?
|
|
5
34
|
end
|
|
@@ -79,7 +108,7 @@ module Phronomy
|
|
|
79
108
|
attr_reader persistence: Store
|
|
80
109
|
def knowledge_snapshot: () -> Array[KnowledgeItem]
|
|
81
110
|
def event_listener: () -> (^(Object) -> void)?
|
|
82
|
-
def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
|
|
111
|
+
def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[execution_result]
|
|
83
112
|
def resume_async: (String execution_id, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
|
|
84
113
|
def resume: (String execution_id, ?config: Hash[Symbol, Object]) -> execution_result
|
|
85
114
|
def cancel_async: (String execution_id) -> TaskResult[ExecutionObservation]
|
|
@@ -87,10 +116,10 @@ module Phronomy
|
|
|
87
116
|
|
|
88
117
|
# Public operation methods no longer accept per-invocation event listeners
|
|
89
118
|
# or event-listener blocks.
|
|
90
|
-
def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> Hash[Symbol, untyped]
|
|
91
|
-
def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[Hash[Symbol, untyped]]
|
|
92
|
-
def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> Hash[Symbol, untyped]
|
|
93
|
-
def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[Hash[Symbol, untyped]]
|
|
119
|
+
def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
120
|
+
def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[Hash[Symbol, untyped]]
|
|
121
|
+
def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
122
|
+
def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[Hash[Symbol, untyped]]
|
|
94
123
|
|
|
95
124
|
def approve: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> untyped
|
|
96
125
|
def approve_async: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> TaskResult[untyped]
|
|
@@ -108,6 +137,8 @@ module Phronomy
|
|
|
108
137
|
def reset_context!: () -> untyped
|
|
109
138
|
def close!: () -> untyped
|
|
110
139
|
def purge!: () -> bool
|
|
140
|
+
def unload_async: () -> Phronomy::TaskResult[bool]
|
|
141
|
+
def unload!: (?timeout: Numeric?) -> bool
|
|
111
142
|
end
|
|
112
143
|
end
|
|
113
144
|
end
|
|
@@ -6,6 +6,12 @@ module Phronomy
|
|
|
6
6
|
class TransportError < Phronomy::Error
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
class AuthenticationError < TransportError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class RateLimitError < TransportError
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
class Base
|
|
10
16
|
def embed: (String text, ?_CancellationSignal? cancellation_token) -> Array[Float]
|
|
11
17
|
|
|
@@ -30,12 +30,12 @@ module Phronomy
|
|
|
30
30
|
|
|
31
31
|
class ExecutionTimeoutError < TimeoutError
|
|
32
32
|
attr_reader outcomes: Array[TaskResult::Outcome[untyped]]
|
|
33
|
-
def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]]) -> void
|
|
33
|
+
def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]], ?code: String?) -> void
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
class ExecutionCancellationError < CancellationError
|
|
37
37
|
attr_reader outcomes: Array[TaskResult::Outcome[untyped]]
|
|
38
|
-
def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]]) -> void
|
|
38
|
+
def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]], ?code: String?) -> void
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
end
|