phronomy 0.6.0 → 0.7.1
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/.mutant.yml +22 -0
- data/CHANGELOG.md +488 -0
- data/CONTRIBUTING.md +102 -0
- data/README.md +374 -36
- data/RELEASE_CHECKLIST.md +86 -0
- data/Rakefile +33 -0
- data/SECURITY.md +80 -0
- data/benchmark/baseline.json +9 -0
- data/benchmark/bench_agent_invoke.rb +105 -0
- data/benchmark/bench_context_assembler.rb +46 -0
- data/benchmark/bench_regression.rb +172 -0
- data/benchmark/bench_token_estimator.rb +44 -0
- data/benchmark/bench_tool_schema.rb +69 -0
- data/benchmark/bench_vector_store.rb +39 -0
- data/benchmark/bench_workflow.rb +55 -0
- data/benchmark/run_all.rb +118 -0
- data/docs/decisions/001-rubyllm-as-provider-layer.md +42 -0
- data/docs/decisions/002-workflow-context-immutability.md +42 -0
- data/docs/decisions/003-event-loop-singleton.md +48 -0
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +75 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +45 -0
- data/docs/decisions/006-no-built-in-guardrails.md +66 -0
- data/docs/decisions/007-mcp-is-beta-stability.md +51 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +52 -0
- data/docs/decisions/009-state-store-abstraction.md +141 -0
- data/docs/decisions/010-cooperative-first-concurrency.md +248 -0
- data/lib/phronomy/agent/base.rb +416 -49
- data/lib/phronomy/agent/before_completion_context.rb +1 -0
- data/lib/phronomy/agent/checkpoint.rb +1 -0
- data/lib/phronomy/agent/concerns/before_completion.rb +6 -0
- data/lib/phronomy/agent/concerns/error_translation.rb +45 -0
- data/lib/phronomy/agent/concerns/guardrailable.rb +3 -0
- data/lib/phronomy/agent/concerns/retryable.rb +12 -1
- data/lib/phronomy/agent/concerns/suspendable.rb +19 -0
- data/lib/phronomy/agent/fsm.rb +44 -52
- data/lib/phronomy/agent/handoff.rb +3 -0
- data/lib/phronomy/agent/orchestrator.rb +191 -54
- data/lib/phronomy/agent/parallel_tool_chat.rb +87 -13
- data/lib/phronomy/agent/react_agent.rb +16 -6
- data/lib/phronomy/agent/runner.rb +2 -0
- data/lib/phronomy/agent/shared_state.rb +11 -0
- data/lib/phronomy/agent/suspend_signal.rb +2 -0
- data/lib/phronomy/agent/team_coordinator.rb +17 -5
- data/lib/phronomy/async_queue.rb +155 -0
- data/lib/phronomy/blocking_adapter_pool.rb +435 -0
- data/lib/phronomy/cancellation_scope.rb +123 -0
- data/lib/phronomy/cancellation_token.rb +133 -0
- data/lib/phronomy/concurrency_gate.rb +155 -0
- data/lib/phronomy/configuration.rb +168 -2
- data/lib/phronomy/context/assembler.rb +6 -0
- data/lib/phronomy/context/compaction_context.rb +2 -0
- data/lib/phronomy/context/context_version_cache.rb +2 -0
- data/lib/phronomy/context/token_budget.rb +3 -0
- data/lib/phronomy/context/token_estimator.rb +9 -2
- data/lib/phronomy/context/trigger_context.rb +1 -0
- data/lib/phronomy/context/trim_context.rb +4 -0
- data/lib/phronomy/deadline.rb +63 -0
- data/lib/phronomy/diagnostics.rb +62 -0
- data/lib/phronomy/embeddings/base.rb +22 -2
- data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +6 -2
- data/lib/phronomy/eval/comparison.rb +2 -0
- data/lib/phronomy/eval/dataset.rb +4 -0
- data/lib/phronomy/eval/metrics.rb +6 -0
- data/lib/phronomy/eval/runner.rb +11 -9
- data/lib/phronomy/eval/scorer/base.rb +1 -0
- data/lib/phronomy/eval/scorer/exact_match.rb +2 -0
- data/lib/phronomy/eval/scorer/includes_scorer.rb +2 -0
- data/lib/phronomy/eval/scorer/llm_judge.rb +2 -0
- data/lib/phronomy/event_loop.rb +275 -30
- data/lib/phronomy/fsm_session.rb +57 -4
- data/lib/phronomy/generator_verifier.rb +2 -0
- data/lib/phronomy/guardrail/base.rb +3 -0
- data/lib/phronomy/guardrail/prompt_injection_guardrail.rb +58 -0
- data/lib/phronomy/invocation_context.rb +152 -0
- data/lib/phronomy/knowledge_source/base.rb +24 -2
- data/lib/phronomy/knowledge_source/entity_knowledge.rb +7 -2
- data/lib/phronomy/knowledge_source/rag_knowledge.rb +8 -4
- data/lib/phronomy/knowledge_source/static_knowledge.rb +7 -2
- data/lib/phronomy/llm_adapter/base.rb +104 -0
- data/lib/phronomy/llm_adapter/ruby_llm.rb +41 -0
- data/lib/phronomy/llm_adapter.rb +20 -0
- data/lib/phronomy/loader/base.rb +1 -0
- data/lib/phronomy/loader/csv_loader.rb +2 -0
- data/lib/phronomy/loader/markdown_loader.rb +2 -0
- data/lib/phronomy/loader/plain_text_loader.rb +1 -0
- data/lib/phronomy/metrics.rb +38 -0
- data/lib/phronomy/output_parser/base.rb +1 -0
- data/lib/phronomy/output_parser/json_parser.rb +22 -3
- data/lib/phronomy/output_parser/structured_parser.rb +2 -0
- data/lib/phronomy/prompt_template.rb +5 -0
- data/lib/phronomy/runnable.rb +20 -3
- data/lib/phronomy/runtime/deterministic_scheduler.rb +412 -0
- data/lib/phronomy/runtime/fake_scheduler.rb +165 -0
- data/lib/phronomy/runtime/gate_registry.rb +52 -0
- data/lib/phronomy/runtime/pool_registry.rb +57 -0
- data/lib/phronomy/runtime/runtime_metrics.rb +117 -0
- data/lib/phronomy/runtime/scheduler.rb +98 -0
- data/lib/phronomy/runtime/scheduler_timer_adapter.rb +79 -0
- data/lib/phronomy/runtime/task_registry.rb +48 -0
- data/lib/phronomy/runtime/thread_scheduler.rb +30 -0
- data/lib/phronomy/runtime/timer_queue.rb +106 -0
- data/lib/phronomy/runtime/timer_service.rb +42 -0
- data/lib/phronomy/runtime.rb +374 -0
- data/lib/phronomy/splitter/base.rb +2 -0
- data/lib/phronomy/splitter/fixed_size_splitter.rb +2 -0
- data/lib/phronomy/splitter/recursive_splitter.rb +2 -0
- data/lib/phronomy/state_store/base.rb +48 -0
- data/lib/phronomy/state_store/in_memory.rb +62 -0
- data/lib/phronomy/task/backend.rb +80 -0
- data/lib/phronomy/task/fiber_backend.rb +157 -0
- data/lib/phronomy/task/immediate_backend.rb +89 -0
- data/lib/phronomy/task/thread_backend.rb +84 -0
- data/lib/phronomy/task.rb +275 -0
- data/lib/phronomy/task_group.rb +265 -0
- data/lib/phronomy/testing/fake_clock.rb +109 -0
- data/lib/phronomy/testing/fake_scheduler.rb +104 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +59 -0
- data/lib/phronomy/testing.rb +12 -0
- data/lib/phronomy/tool/agent_tool.rb +1 -0
- data/lib/phronomy/tool/base.rb +298 -28
- data/lib/phronomy/tool/mcp_tool.rb +103 -17
- data/lib/phronomy/tool/scope_policy.rb +50 -0
- data/lib/phronomy/tool_executor.rb +106 -0
- data/lib/phronomy/tracing/base.rb +3 -0
- data/lib/phronomy/tracing/langfuse_tracer.rb +2 -0
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +36 -0
- data/lib/phronomy/vector_store/async_backend.rb +110 -0
- data/lib/phronomy/vector_store/base.rb +40 -7
- data/lib/phronomy/vector_store/in_memory.rb +16 -7
- data/lib/phronomy/vector_store/pgvector.rb +40 -9
- data/lib/phronomy/vector_store/redis_search.rb +29 -8
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +147 -11
- data/lib/phronomy/workflow_context.rb +83 -6
- data/lib/phronomy/workflow_runner.rb +106 -7
- data/lib/phronomy.rb +112 -1
- data/scripts/api_snapshot.rb +91 -0
- data/scripts/check_api_annotations.rb +68 -0
- data/scripts/check_private_enforcement.rb +93 -0
- data/scripts/check_readme_runnable.rb +98 -0
- data/scripts/run_mutation.sh +46 -0
- metadata +83 -2
|
@@ -35,6 +35,7 @@ module Phronomy
|
|
|
35
35
|
# @param messages [Array]
|
|
36
36
|
# @param config [Hash]
|
|
37
37
|
# @param params [Hash] initial params (model, temperature already set on chat)
|
|
38
|
+
# @api public
|
|
38
39
|
def initialize(agent:, messages:, config:, params: {})
|
|
39
40
|
@agent = agent
|
|
40
41
|
@messages = messages.dup.freeze
|
|
@@ -47,6 +47,7 @@ module Phronomy
|
|
|
47
47
|
# @param pending_tool_name [String]
|
|
48
48
|
# @param pending_tool_args [Hash]
|
|
49
49
|
# @param pending_tool_call_id [String]
|
|
50
|
+
# @api public
|
|
50
51
|
def initialize(thread_id:, original_input:, messages:, pending_tool_name:, pending_tool_args:, pending_tool_call_id:)
|
|
51
52
|
@thread_id = thread_id
|
|
52
53
|
@original_input = original_input
|
|
@@ -8,6 +8,7 @@ module Phronomy
|
|
|
8
8
|
# Included in {Phronomy::Agent::Base}. Hooks are executed just before every
|
|
9
9
|
# LLM call (global → class → instance order) and may inject or override
|
|
10
10
|
# LLM parameters such as temperature or model.
|
|
11
|
+
# @api private
|
|
11
12
|
module BeforeCompletion
|
|
12
13
|
def self.included(base)
|
|
13
14
|
base.extend(ClassMethods)
|
|
@@ -26,6 +27,7 @@ module Phronomy
|
|
|
26
27
|
# class MyAgent < Phronomy::Agent::Base
|
|
27
28
|
# before_completion ->(ctx) { { temperature: 0.2 } }
|
|
28
29
|
# end
|
|
30
|
+
# @api private
|
|
29
31
|
def before_completion(callable = nil)
|
|
30
32
|
if callable.nil? && !block_given?
|
|
31
33
|
@before_completion
|
|
@@ -35,6 +37,7 @@ module Phronomy
|
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# @return [#call, nil]
|
|
40
|
+
# @api private
|
|
38
41
|
def _before_completion
|
|
39
42
|
@before_completion
|
|
40
43
|
end
|
|
@@ -53,6 +56,7 @@ module Phronomy
|
|
|
53
56
|
# @param chat [RubyLLM::Chat] the assembled chat object
|
|
54
57
|
# @param config [Hash] the invocation config hash
|
|
55
58
|
# @return [Hash] the merged params applied to the chat
|
|
59
|
+
# @api private
|
|
56
60
|
def run_before_completion_hooks!(chat, config)
|
|
57
61
|
hooks = [
|
|
58
62
|
Phronomy.configuration.before_completion,
|
|
@@ -72,6 +76,7 @@ module Phronomy
|
|
|
72
76
|
merged = {}
|
|
73
77
|
hooks.each do |hook|
|
|
74
78
|
result = hook.call(ctx)
|
|
79
|
+
check_cancellation!(config, "invocation cancelled during before_completion hook")
|
|
75
80
|
merged.merge!(result) if result.is_a?(Hash)
|
|
76
81
|
end
|
|
77
82
|
|
|
@@ -86,6 +91,7 @@ module Phronomy
|
|
|
86
91
|
#
|
|
87
92
|
# @param chat [RubyLLM::Chat]
|
|
88
93
|
# @param params [Hash]
|
|
94
|
+
# @api private
|
|
89
95
|
def apply_before_completion_params!(chat, params)
|
|
90
96
|
params.each do |key, value|
|
|
91
97
|
case key
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module Concerns
|
|
6
|
+
# Translates RubyLLM transport errors into the corresponding Phronomy error
|
|
7
|
+
# classes so that callers can rescue Phronomy-namespaced exceptions rather
|
|
8
|
+
# than coupling themselves to the underlying provider library.
|
|
9
|
+
#
|
|
10
|
+
# Included in {Phronomy::Agent::Base}.
|
|
11
|
+
module ErrorTranslation
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Re-raises +error+ as the most specific Phronomy error class that
|
|
15
|
+
# corresponds to it. Non-RubyLLM errors are re-raised unchanged.
|
|
16
|
+
# The original exception is available as +#cause+ on the translated error.
|
|
17
|
+
#
|
|
18
|
+
# Must be called from within an active +rescue+ block so that Ruby
|
|
19
|
+
# automatically sets +#cause+ on the new exception.
|
|
20
|
+
#
|
|
21
|
+
# @param error [Exception]
|
|
22
|
+
# @raise [Phronomy::RateLimitError] for provider HTTP 429
|
|
23
|
+
# @raise [Phronomy::AuthenticationError] for provider HTTP 401 / 403
|
|
24
|
+
# @raise [Phronomy::ContextLengthError] for context window overflow
|
|
25
|
+
# @raise [Phronomy::TransportError] for all other +RubyLLM::Error+ subclasses
|
|
26
|
+
# @raise re-raises +error+ unchanged for non-RubyLLM exceptions
|
|
27
|
+
# @api private
|
|
28
|
+
def translate_and_reraise!(error)
|
|
29
|
+
case error
|
|
30
|
+
when RubyLLM::RateLimitError
|
|
31
|
+
raise Phronomy::RateLimitError, error.message
|
|
32
|
+
when RubyLLM::UnauthorizedError, RubyLLM::ForbiddenError
|
|
33
|
+
raise Phronomy::AuthenticationError, error.message
|
|
34
|
+
when RubyLLM::ContextLengthExceededError
|
|
35
|
+
raise Phronomy::ContextLengthError, error.message
|
|
36
|
+
when RubyLLM::Error
|
|
37
|
+
raise Phronomy::TransportError, error.message
|
|
38
|
+
else
|
|
39
|
+
raise # bare re-raise preserves $! and its backtrace unchanged
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -8,10 +8,12 @@ module Phronomy
|
|
|
8
8
|
# Included in {Phronomy::Agent::Base}. Guardrails are run on the raw
|
|
9
9
|
# input string before the LLM is called, and on the raw output string
|
|
10
10
|
# before the result is returned to the caller.
|
|
11
|
+
# @api private
|
|
11
12
|
module Guardrailable
|
|
12
13
|
# Attach a guardrail that validates input before every #invoke call.
|
|
13
14
|
# @param guardrail [Phronomy::Guardrail::InputGuardrail]
|
|
14
15
|
# @return [self]
|
|
16
|
+
# @api private
|
|
15
17
|
def add_input_guardrail(guardrail)
|
|
16
18
|
@input_guardrails ||= []
|
|
17
19
|
@input_guardrails << guardrail
|
|
@@ -21,6 +23,7 @@ module Phronomy
|
|
|
21
23
|
# Attach a guardrail that validates output before it is returned.
|
|
22
24
|
# @param guardrail [Phronomy::Guardrail::OutputGuardrail]
|
|
23
25
|
# @return [self]
|
|
26
|
+
# @api private
|
|
24
27
|
def add_output_guardrail(guardrail)
|
|
25
28
|
@output_guardrails ||= []
|
|
26
29
|
@output_guardrails << guardrail
|
|
@@ -7,6 +7,7 @@ module Phronomy
|
|
|
7
7
|
#
|
|
8
8
|
# Included in {Phronomy::Agent::Base}. The retry loop wraps the full
|
|
9
9
|
# #invoke_once call; {Phronomy::GuardrailError} is never retried.
|
|
10
|
+
# @api private
|
|
10
11
|
module Retryable
|
|
11
12
|
def self.included(base)
|
|
12
13
|
base.extend(ClassMethods)
|
|
@@ -25,6 +26,7 @@ module Phronomy
|
|
|
25
26
|
# class MyAgent < Phronomy::Agent::Base
|
|
26
27
|
# retry_policy times: 2, wait: :exponential, base: 1.0
|
|
27
28
|
# end
|
|
29
|
+
# @api private
|
|
28
30
|
def retry_policy(times: 0, wait: 0, base: 1.0)
|
|
29
31
|
@_retry_policy = {times: times, wait: wait, base: base}
|
|
30
32
|
end
|
|
@@ -35,6 +37,7 @@ module Phronomy
|
|
|
35
37
|
|
|
36
38
|
# Injectable sleep callable for testing (shared with Tool::Base pattern).
|
|
37
39
|
# @return [#call]
|
|
40
|
+
# @api private
|
|
38
41
|
def _sleep_proc
|
|
39
42
|
@_sleep_proc || method(:sleep)
|
|
40
43
|
end
|
|
@@ -48,12 +51,19 @@ module Phronomy
|
|
|
48
51
|
|
|
49
52
|
# Retry loop for #invoke. Separated so that ReactAgent can override #invoke_once.
|
|
50
53
|
def _invoke_impl(input, messages: [], thread_id: nil, config: {})
|
|
54
|
+
# Fail fast when the token is already cancelled before any LLM call.
|
|
55
|
+
if (token = config[:cancellation_token]) && token.cancelled?
|
|
56
|
+
raise Phronomy::CancellationError, "invocation cancelled"
|
|
57
|
+
end
|
|
58
|
+
|
|
51
59
|
policy = self.class._retry_policy
|
|
52
60
|
attempt = 0
|
|
53
61
|
begin
|
|
54
62
|
invoke_once(input, messages: messages, thread_id: thread_id, config: config)
|
|
55
63
|
rescue Phronomy::GuardrailError
|
|
56
64
|
raise
|
|
65
|
+
rescue Phronomy::CancellationError
|
|
66
|
+
raise # Never retry after cancellation.
|
|
57
67
|
rescue
|
|
58
68
|
if policy && attempt < policy[:times]
|
|
59
69
|
wait = compute_agent_retry_wait(policy[:wait], policy[:base], attempt)
|
|
@@ -61,7 +71,7 @@ module Phronomy
|
|
|
61
71
|
attempt += 1
|
|
62
72
|
retry
|
|
63
73
|
end
|
|
64
|
-
|
|
74
|
+
translate_and_reraise!($!)
|
|
65
75
|
end
|
|
66
76
|
end
|
|
67
77
|
|
|
@@ -70,6 +80,7 @@ module Phronomy
|
|
|
70
80
|
# @param base [Float]
|
|
71
81
|
# @param attempt [Integer]
|
|
72
82
|
# @return [Float]
|
|
83
|
+
# @api private
|
|
73
84
|
def compute_agent_retry_wait(strategy, base, attempt)
|
|
74
85
|
case strategy
|
|
75
86
|
when :exponential
|
|
@@ -8,6 +8,7 @@ module Phronomy
|
|
|
8
8
|
# Included in {Phronomy::Agent::Base}. When a tool decorated with
|
|
9
9
|
# +requires_approval true+ is called and no synchronous approval handler
|
|
10
10
|
# has been registered, the invocation is suspended and a
|
|
11
|
+
# @api private
|
|
11
12
|
# {Phronomy::Agent::Checkpoint} is returned so the caller can resume later.
|
|
12
13
|
module Suspendable
|
|
13
14
|
# Registers a callback that is invoked before executing any tool that has
|
|
@@ -25,11 +26,27 @@ module Phronomy
|
|
|
25
26
|
# agent = MyAgent.new
|
|
26
27
|
# agent.on_approval_required { |tool_name, args| prompt_user(tool_name, args) }
|
|
27
28
|
# @return [self]
|
|
29
|
+
# @api private
|
|
28
30
|
def on_approval_required(&block)
|
|
29
31
|
@approval_handler = block
|
|
30
32
|
self
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# Registers a scope policy callable for this agent instance.
|
|
36
|
+
#
|
|
37
|
+
# The callable receives +(tool_class, scope, agent)+ and must return
|
|
38
|
+
# +:allow+, +:reject+, or +:approve+.
|
|
39
|
+
#
|
|
40
|
+
# @example Reject all write-scoped tools
|
|
41
|
+
# agent.scope_policy = ->(_tc, scope, _agent) { scope == :write ? :reject : :allow }
|
|
42
|
+
#
|
|
43
|
+
# @param policy [#call]
|
|
44
|
+
# @return [void]
|
|
45
|
+
# @api public
|
|
46
|
+
def scope_policy=(policy)
|
|
47
|
+
@scope_policy = policy
|
|
48
|
+
end
|
|
49
|
+
|
|
33
50
|
# Resumes a previously suspended invocation from a {Phronomy::Agent::Checkpoint}.
|
|
34
51
|
#
|
|
35
52
|
# This method reconstructs the conversation state captured at suspension
|
|
@@ -43,6 +60,7 @@ module Phronomy
|
|
|
43
60
|
# @param config [Hash] same runtime options as #invoke
|
|
44
61
|
# @return [Hash] +{ output: String, suspended: false, messages: Array, usage: Phronomy::TokenUsage }+
|
|
45
62
|
# @raise [Phronomy::GuardrailError] when an output guardrail rejects the value
|
|
63
|
+
# @api private
|
|
46
64
|
def resume(checkpoint, approved:, config: {})
|
|
47
65
|
# Build a fresh chat with all tools registered.
|
|
48
66
|
chat = build_chat
|
|
@@ -95,6 +113,7 @@ module Phronomy
|
|
|
95
113
|
# - none of the agent's tools have requires_approval set.
|
|
96
114
|
#
|
|
97
115
|
# @param chat [RubyLLM::Chat]
|
|
116
|
+
# @api private
|
|
98
117
|
def _register_suspension_hook!(chat)
|
|
99
118
|
return if @approval_handler
|
|
100
119
|
return if self.class.tools.none? { |tc| tc.requires_approval }
|
data/lib/phronomy/agent/fsm.rb
CHANGED
|
@@ -15,14 +15,14 @@ module Phronomy
|
|
|
15
15
|
# == Execution model
|
|
16
16
|
#
|
|
17
17
|
# {#start} is called by the EventLoop on the +:start+ event. It immediately
|
|
18
|
-
# returns after spawning a
|
|
18
|
+
# returns after spawning a {Phronomy::Task} that runs the agent's full
|
|
19
19
|
# invocation pipeline (via +_invoke_impl+). The EventLoop thread is never
|
|
20
20
|
# blocked by agent execution.
|
|
21
21
|
#
|
|
22
|
-
# Inside the
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
22
|
+
# Inside the task, {Agent::Base#build_chat} returns a
|
|
23
|
+
# {ParallelToolChat} instance when EventLoop mode is enabled, allowing
|
|
24
|
+
# concurrent tool dispatch when the LLM returns multiple tool calls in one
|
|
25
|
+
# response.
|
|
26
26
|
#
|
|
27
27
|
# == Completion events
|
|
28
28
|
#
|
|
@@ -57,6 +57,7 @@ module Phronomy
|
|
|
57
57
|
# {Agent::Base#run_as_child} creates an +AgentFSM+ with +parent_id+ set to
|
|
58
58
|
# +ctx.thread_id+, registers it with the EventLoop, and returns immediately.
|
|
59
59
|
# The parent {FSMSession} waits for the +:child_completed+ event.
|
|
60
|
+
# @api private
|
|
60
61
|
class FSM
|
|
61
62
|
# @return [String] unique identifier used as the EventLoop target_id
|
|
62
63
|
attr_reader :id
|
|
@@ -71,39 +72,30 @@ module Phronomy
|
|
|
71
72
|
# auto-generated when nil
|
|
72
73
|
# @param config [Hash] invocation config forwarded to
|
|
73
74
|
# +_invoke_impl+
|
|
74
|
-
# @param parent_id
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# +:child_completed+ is posted.
|
|
81
|
-
# Use this to write the agent output
|
|
82
|
-
# back into the parent WorkflowContext.
|
|
83
|
-
# Thread::Queue provides the
|
|
84
|
-
# happens-before guarantee.
|
|
75
|
+
# @param parent_id [String, nil] EventLoop id of the parent FSMSession;
|
|
76
|
+
# when set, a +:child_completed+ event
|
|
77
|
+
# is posted on completion. The result
|
|
78
|
+
# is delivered exclusively as the event
|
|
79
|
+
# payload — no cross-thread writes to the
|
|
80
|
+
# parent WorkflowContext are performed.
|
|
85
81
|
#
|
|
86
|
-
# @
|
|
87
|
-
|
|
88
|
-
# MyAgent.new.run_as_child(ctx.query, ctx: ctx) { |r| ctx.answer = r[:output] }
|
|
89
|
-
# }
|
|
90
|
-
def initialize(agent:, input:, messages: [], thread_id: nil, config: {}, parent_id: nil, result_writer: nil)
|
|
82
|
+
# @api private
|
|
83
|
+
def initialize(agent:, input:, messages: [], thread_id: nil, config: {}, parent_id: nil)
|
|
91
84
|
@agent = agent
|
|
92
85
|
@input = input
|
|
93
86
|
@messages = Array(messages).dup
|
|
94
87
|
@thread_id = thread_id || SecureRandom.uuid
|
|
95
88
|
@config = config
|
|
96
89
|
@parent_id = parent_id
|
|
97
|
-
@result_writer = result_writer
|
|
98
90
|
@id = @thread_id
|
|
99
91
|
@current_phase = :idle
|
|
100
92
|
end
|
|
101
93
|
|
|
102
94
|
# Called by {EventLoop} on the +:start+ event.
|
|
103
|
-
# Transitions to +:running+ and spawns the agent
|
|
95
|
+
# Transitions to +:running+ and spawns the agent task.
|
|
104
96
|
def start
|
|
105
97
|
@current_phase = :running
|
|
106
|
-
|
|
98
|
+
spawn_agent_task
|
|
107
99
|
end
|
|
108
100
|
|
|
109
101
|
# Called by {EventLoop} for external events dispatched to this id.
|
|
@@ -115,10 +107,10 @@ module Phronomy
|
|
|
115
107
|
|
|
116
108
|
private
|
|
117
109
|
|
|
118
|
-
# Spawns
|
|
119
|
-
# Captures all instance variables by value so the
|
|
110
|
+
# Spawns a {Phronomy::Task} that runs the agent invocation pipeline.
|
|
111
|
+
# Captures all instance variables by value so the task closure is
|
|
120
112
|
# safe even if the FSM object is modified (though it is not in practice).
|
|
121
|
-
def
|
|
113
|
+
def spawn_agent_task
|
|
122
114
|
agent = @agent
|
|
123
115
|
input = @input
|
|
124
116
|
messages = @messages
|
|
@@ -126,38 +118,38 @@ module Phronomy
|
|
|
126
118
|
config = @config
|
|
127
119
|
fsm_id = @id
|
|
128
120
|
parent_id = @parent_id
|
|
129
|
-
result_writer = @result_writer
|
|
130
121
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
input,
|
|
138
|
-
messages: messages,
|
|
139
|
-
thread_id: thread_id,
|
|
140
|
-
config: config)
|
|
141
|
-
|
|
142
|
-
if parent_id
|
|
143
|
-
# Let the caller write the result into the context BEFORE the
|
|
144
|
-
# parent FSMSession advances. Thread::Queue provides the
|
|
145
|
-
# happens-before guarantee — no Mutex needed.
|
|
146
|
-
result_writer&.call(result)
|
|
147
|
-
|
|
148
|
-
Phronomy::EventLoop.instance.post(
|
|
149
|
-
Phronomy::Event.new(type: :child_completed, target_id: parent_id, payload: result)
|
|
150
|
-
)
|
|
151
|
-
end
|
|
122
|
+
Phronomy::Runtime.instance.spawn(name: "agent-fsm:#{fsm_id}") do
|
|
123
|
+
result = agent.send(:_invoke_impl,
|
|
124
|
+
input,
|
|
125
|
+
messages: messages,
|
|
126
|
+
thread_id: thread_id,
|
|
127
|
+
config: config)
|
|
152
128
|
|
|
129
|
+
if parent_id
|
|
130
|
+
# Result is delivered exclusively as the :child_completed payload.
|
|
131
|
+
# The parent Workflow task is the sole owner of WorkflowContext
|
|
132
|
+
# and applies the result after receiving the event.
|
|
153
133
|
Phronomy::EventLoop.instance.post(
|
|
154
|
-
Phronomy::Event.new(type: :
|
|
134
|
+
Phronomy::Event.new(type: :child_completed, target_id: parent_id, payload: result)
|
|
155
135
|
)
|
|
156
|
-
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Phronomy::EventLoop.instance.post(
|
|
139
|
+
Phronomy::Event.new(type: :finished, target_id: fsm_id, payload: result)
|
|
140
|
+
)
|
|
141
|
+
rescue => e
|
|
142
|
+
if parent_id
|
|
157
143
|
Phronomy::EventLoop.instance.post(
|
|
158
|
-
Phronomy::Event.new(type: :
|
|
144
|
+
Phronomy::Event.new(type: :child_failed, target_id: parent_id, payload: e)
|
|
159
145
|
)
|
|
160
146
|
end
|
|
147
|
+
|
|
148
|
+
Phronomy::EventLoop.instance.post(
|
|
149
|
+
Phronomy::Event.new(type: :error, target_id: fsm_id, payload: e)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Context caches are instance variables; no thread-local cleanup needed.
|
|
161
153
|
end
|
|
162
154
|
end
|
|
163
155
|
end
|
|
@@ -22,6 +22,7 @@ module Phronomy
|
|
|
22
22
|
|
|
23
23
|
# @param target_agent [Phronomy::Agent::Base] the agent to hand off to
|
|
24
24
|
# @param description [String, nil] overrides the auto-generated tool description
|
|
25
|
+
# @api public
|
|
25
26
|
def initialize(target_agent:, description: nil)
|
|
26
27
|
@target_agent = target_agent
|
|
27
28
|
klass_name = target_agent.class.name&.split("::")&.last || "Agent"
|
|
@@ -33,6 +34,7 @@ module Phronomy
|
|
|
33
34
|
|
|
34
35
|
# Builds an anonymous Phronomy::Tool::Base subclass for this handoff.
|
|
35
36
|
# @return [Class<Phronomy::Tool::Base>]
|
|
37
|
+
# @api public
|
|
36
38
|
def to_tool_class
|
|
37
39
|
sentinel_value = sentinel
|
|
38
40
|
tn = tool_name
|
|
@@ -46,6 +48,7 @@ module Phronomy
|
|
|
46
48
|
|
|
47
49
|
# The sentinel string embedded in the tool result.
|
|
48
50
|
# @return [String]
|
|
51
|
+
# @api public
|
|
49
52
|
def sentinel
|
|
50
53
|
"#{SENTINEL_PREFIX}:#{target_agent.class.name}:#{@uuid}"
|
|
51
54
|
end
|