phronomy 0.15.1 → 0.17.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/.mutant.yml +8 -9
- data/CHANGELOG.md +159 -28
- data/CONTRIBUTING.md +28 -16
- data/README.md +400 -143
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +7 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +3 -19
- data/benchmark/bench_tool_schema.rb +2 -34
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/lib/phronomy/agent/activation_registry.rb +28 -0
- data/lib/phronomy/agent/agent_execution.rb +97 -0
- data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
- data/lib/phronomy/agent/agent_invocation.rb +44 -46
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +206 -104
- data/lib/phronomy/agent/agent_root.rb +66 -0
- data/lib/phronomy/agent/async_event_api.rb +55 -475
- data/lib/phronomy/agent/base.rb +351 -514
- data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
- data/lib/phronomy/agent/context/capability/base.rb +166 -297
- data/lib/phronomy/agent/context_assembler.rb +357 -0
- data/lib/phronomy/agent/context_candidate.rb +47 -0
- data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
- data/lib/phronomy/agent/context_importer.rb +217 -0
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +118 -0
- data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
- data/lib/phronomy/agent/context_plan.rb +25 -0
- data/lib/phronomy/agent/context_plan_validator.rb +134 -0
- data/lib/phronomy/agent/context_policies/default.rb +53 -0
- data/lib/phronomy/agent/context_policy.rb +15 -0
- data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
- data/lib/phronomy/agent/context_policy_registry.rb +46 -0
- data/lib/phronomy/agent/context_request.rb +35 -0
- data/lib/phronomy/agent/context_selection_unit.rb +38 -0
- data/lib/phronomy/agent/derived_content_spec.rb +34 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1122 -0
- data/lib/phronomy/agent/immutable.rb +31 -0
- data/lib/phronomy/agent/journal_projection.rb +60 -0
- data/lib/phronomy/agent/journal_record.rb +67 -0
- data/lib/phronomy/agent/llm_call_record.rb +51 -0
- data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
- data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
- data/lib/phronomy/agent/llm_input_patch.rb +21 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
- data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +189 -0
- data/lib/phronomy/agent/shared_state.rb +46 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +70 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
- data/lib/phronomy/agent/tool_definition_set.rb +55 -0
- data/lib/phronomy/agent/tool_invocation.rb +108 -314
- data/lib/phronomy/agent.rb +10 -16
- data/lib/phronomy/agent_busy_error.rb +5 -0
- data/lib/phronomy/canonical_json.rb +136 -0
- data/lib/phronomy/configuration.rb +17 -155
- data/lib/phronomy/content_store/base.rb +51 -0
- data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
- data/lib/phronomy/engine/event_loop.rb +3 -0
- data/lib/phronomy/engine/runtime.rb +15 -230
- data/lib/phronomy/engine/task_group.rb +30 -102
- data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/multi_agent/orchestrator.rb +153 -204
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
- data/lib/phronomy/multi_agent/team_coordinator.rb +46 -133
- data/lib/phronomy/persistence/in_memory.rb +247 -0
- data/lib/phronomy/persistence.rb +39 -0
- data/lib/phronomy/tools/agent.rb +14 -36
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +9 -115
- data/scripts/add_to_h_to_token_doubles.rb +33 -0
- data/scripts/add_to_h_unnamed_doubles.rb +27 -0
- data/scripts/api_snapshot.rb +1 -12
- data/scripts/migrate_spec_agent_definition.rb +108 -0
- data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
- data/scripts/migrate_spec_inline_pass3.rb +24 -0
- metadata +54 -13
- data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
- data/lib/phronomy/agent/before_completion_context.rb +0 -47
- data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Phronomy
|
|
7
|
+
module Agent
|
|
8
|
+
class AgentExecution
|
|
9
|
+
ACTIVE_STATUSES = %i[preparing active suspended].freeze
|
|
10
|
+
TERMINAL_STATUSES = %i[completed failed cancelled rejected blocked].freeze
|
|
11
|
+
TRANSITIONS = {
|
|
12
|
+
preparing: %i[preparing active failed cancelled blocked],
|
|
13
|
+
active: %i[active suspended completed failed cancelled rejected blocked],
|
|
14
|
+
suspended: %i[suspended active failed cancelled],
|
|
15
|
+
completed: %i[completed],
|
|
16
|
+
failed: %i[failed],
|
|
17
|
+
cancelled: %i[cancelled],
|
|
18
|
+
rejected: %i[rejected],
|
|
19
|
+
blocked: %i[blocked]
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
ATTRIBUTES = %i[
|
|
23
|
+
execution_id agent_id execution_revision status phase
|
|
24
|
+
base_agent_revision base_context_revision base_journal_position
|
|
25
|
+
working_records llm_calls approval_request result_ref error_ref
|
|
26
|
+
created_at updated_at terminal_reason metadata
|
|
27
|
+
].freeze
|
|
28
|
+
attr_reader(*ATTRIBUTES)
|
|
29
|
+
|
|
30
|
+
def self.start(agent_root:, input_record:, metadata: {})
|
|
31
|
+
now = Time.now.utc.iso8601(6)
|
|
32
|
+
new(
|
|
33
|
+
execution_id: SecureRandom.uuid,
|
|
34
|
+
agent_id: agent_root.agent_id,
|
|
35
|
+
execution_revision: 0,
|
|
36
|
+
status: :preparing,
|
|
37
|
+
phase: :preparing,
|
|
38
|
+
base_agent_revision: agent_root.agent_revision,
|
|
39
|
+
base_context_revision: agent_root.context_revision,
|
|
40
|
+
base_journal_position: agent_root.journal_position,
|
|
41
|
+
working_records: [input_record],
|
|
42
|
+
llm_calls: [],
|
|
43
|
+
approval_request: nil,
|
|
44
|
+
result_ref: nil,
|
|
45
|
+
error_ref: nil,
|
|
46
|
+
created_at: now,
|
|
47
|
+
updated_at: now,
|
|
48
|
+
terminal_reason: nil,
|
|
49
|
+
metadata: metadata
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(**attributes)
|
|
54
|
+
ATTRIBUTES.each do |name|
|
|
55
|
+
value = attributes.fetch(name)
|
|
56
|
+
value = value.to_sym if %i[status phase].include?(name)
|
|
57
|
+
instance_variable_set("@#{name}", Immutable.copy(value))
|
|
58
|
+
end
|
|
59
|
+
raise ArgumentError, "unknown execution status: #{status.inspect}" unless TRANSITIONS.key?(status)
|
|
60
|
+
raise ArgumentError, "execution_revision must be non-negative" if execution_revision.negative?
|
|
61
|
+
Immutable.validate_canonical_json!(metadata, label: "Execution metadata")
|
|
62
|
+
if approval_request
|
|
63
|
+
Immutable.validate_canonical_json!(approval_request, label: "Approval request")
|
|
64
|
+
end
|
|
65
|
+
freeze
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def active?
|
|
69
|
+
ACTIVE_STATUSES.include?(status)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def terminal?
|
|
73
|
+
TERMINAL_STATUSES.include?(status)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def with(**changes)
|
|
77
|
+
next_status = changes.fetch(:status, status).to_sym
|
|
78
|
+
unless TRANSITIONS.fetch(status).include?(next_status)
|
|
79
|
+
raise ArgumentError, "illegal AgentExecution transition: #{status} -> #{next_status}"
|
|
80
|
+
end
|
|
81
|
+
values = ATTRIBUTES.to_h { |name| [name, public_send(name)] }.merge(changes)
|
|
82
|
+
values[:execution_revision] = execution_revision + 1 unless changes.key?(:execution_revision)
|
|
83
|
+
values[:updated_at] = Time.now.utc.iso8601(6) unless changes.key?(:updated_at)
|
|
84
|
+
self.class.new(**values)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_h
|
|
88
|
+
ATTRIBUTES.to_h do |name|
|
|
89
|
+
value = public_send(name)
|
|
90
|
+
value = value.map(&:to_h) if name == :working_records
|
|
91
|
+
value = value.map(&:to_h) if name == :llm_calls
|
|
92
|
+
[name.to_s, value]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Phronomy
|
|
7
|
+
module Agent
|
|
8
|
+
class AgentExecutionActivation
|
|
9
|
+
attr_reader :execution_id, :agent, :application_listener, :coordinator,
|
|
10
|
+
:base_manifest
|
|
11
|
+
attr_accessor :invocation, :session
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
execution:,
|
|
15
|
+
agent:,
|
|
16
|
+
runtime_projection:,
|
|
17
|
+
coordinator:,
|
|
18
|
+
application_listener: nil
|
|
19
|
+
)
|
|
20
|
+
@execution_id = execution.execution_id
|
|
21
|
+
@execution = execution
|
|
22
|
+
@agent = agent
|
|
23
|
+
@runtime_projection = runtime_projection
|
|
24
|
+
@base_manifest = runtime_projection.manifest
|
|
25
|
+
@coordinator = coordinator
|
|
26
|
+
@application_listener = application_listener
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
@active_call = nil
|
|
29
|
+
@llm_results = []
|
|
30
|
+
@runtime_events = []
|
|
31
|
+
@callback_errors = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def execution
|
|
35
|
+
@mutex.synchronize { @execution }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def replace_execution(value)
|
|
39
|
+
@mutex.synchronize { @execution = value }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def runtime_projection
|
|
43
|
+
@mutex.synchronize { @runtime_projection }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def replace_runtime_projection(value)
|
|
47
|
+
@mutex.synchronize { @runtime_projection = value }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Allocates Provider Call identity before transport begins. This identity is
|
|
51
|
+
# provenance only; Context selection must not use it as a semantic boundary.
|
|
52
|
+
def begin_llm_call(projection)
|
|
53
|
+
call_context = {
|
|
54
|
+
llm_call_id: SecureRandom.uuid,
|
|
55
|
+
manifest_ref: projection.manifest_ref,
|
|
56
|
+
started_at: Time.now.utc.iso8601(6)
|
|
57
|
+
}.freeze
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
if @active_call
|
|
60
|
+
raise Phronomy::Error,
|
|
61
|
+
"cannot start a Provider Call while another Provider Call is active"
|
|
62
|
+
end
|
|
63
|
+
@runtime_projection = projection
|
|
64
|
+
@active_call = call_context
|
|
65
|
+
end
|
|
66
|
+
call_context
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def record_llm_result(response:, error:, streaming:)
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
active_call = @active_call
|
|
72
|
+
unless active_call
|
|
73
|
+
raise Phronomy::Error, "LLM result arrived without an active Provider Call"
|
|
74
|
+
end
|
|
75
|
+
@llm_results << {
|
|
76
|
+
llm_call_id: active_call.fetch(:llm_call_id),
|
|
77
|
+
response: response,
|
|
78
|
+
error: error,
|
|
79
|
+
streaming: streaming,
|
|
80
|
+
manifest_ref: active_call.fetch(:manifest_ref),
|
|
81
|
+
started_at: active_call.fetch(:started_at)
|
|
82
|
+
}
|
|
83
|
+
@active_call = nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def runtime_snapshot
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
{
|
|
90
|
+
llm_results: @llm_results.dup,
|
|
91
|
+
runtime_events: @runtime_events.dup,
|
|
92
|
+
active_call: @active_call&.dup
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def acknowledge_runtime_snapshot(snapshot)
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
@llm_results.shift(snapshot.fetch(:llm_results).length)
|
|
100
|
+
@runtime_events.shift(snapshot.fetch(:runtime_events).length)
|
|
101
|
+
if snapshot[:active_call] && @active_call == snapshot[:active_call]
|
|
102
|
+
@active_call = nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
ApplicationCallbackFailure = Data.define(:event_type, :error) do
|
|
108
|
+
def to_stream_callback_error
|
|
109
|
+
wrapped = Phronomy::StreamCallbackError.new(
|
|
110
|
+
event_type: event_type, original_error: error, result: nil
|
|
111
|
+
)
|
|
112
|
+
begin
|
|
113
|
+
raise wrapped, cause: error
|
|
114
|
+
rescue Phronomy::StreamCallbackError => caught
|
|
115
|
+
caught.set_backtrace(error.backtrace)
|
|
116
|
+
caught
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
attr_reader :callback_failure
|
|
122
|
+
|
|
123
|
+
def callback_failed?
|
|
124
|
+
@mutex.synchronize { !@callback_failure.nil? }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Canonical runtime recording is independent of Application callback health.
|
|
128
|
+
# Once an event is observed it is appended even after a listener has failed.
|
|
129
|
+
def record_event(event)
|
|
130
|
+
listener = @mutex.synchronize do
|
|
131
|
+
@runtime_events << event
|
|
132
|
+
@callback_failure ? nil : @application_listener
|
|
133
|
+
end
|
|
134
|
+
return unless listener
|
|
135
|
+
|
|
136
|
+
listener.call(event)
|
|
137
|
+
rescue => callback_error
|
|
138
|
+
failure = ApplicationCallbackFailure.new(
|
|
139
|
+
event_type: event.type, error: callback_error
|
|
140
|
+
)
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
@callback_failure ||= failure
|
|
143
|
+
@application_listener = nil
|
|
144
|
+
end
|
|
145
|
+
notify_callback_failure(failure)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def notify_callback_failure(failure)
|
|
151
|
+
invocation = @mutex.synchronize { @invocation }
|
|
152
|
+
if invocation
|
|
153
|
+
accepted = Phronomy::Runtime.instance.event_loop.post_to_session(
|
|
154
|
+
Phronomy::Event.new(
|
|
155
|
+
type: :application_callback_failed,
|
|
156
|
+
target_id: invocation.id,
|
|
157
|
+
payload: {failure: failure}
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
unless accepted
|
|
161
|
+
Phronomy.configuration.logger&.warn(
|
|
162
|
+
"[Phronomy] Callback failure recorded but could not notify FSM: execution_id=#{@execution_id}"
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
Phronomy.configuration.logger&.warn(
|
|
167
|
+
"[Phronomy] Application event listener failed: #{failure.error.class}: #{failure.error.message}"
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -26,8 +26,9 @@ module Phronomy
|
|
|
26
26
|
llm_failed
|
|
27
27
|
].freeze
|
|
28
28
|
|
|
29
|
+
CALLBACK_FAILED_EVENTS = %i[application_callback_failed].freeze
|
|
30
|
+
|
|
29
31
|
attr_accessor :input,
|
|
30
|
-
:messages,
|
|
31
32
|
:chat,
|
|
32
33
|
:output,
|
|
33
34
|
:usage,
|
|
@@ -50,23 +51,22 @@ module Phronomy
|
|
|
50
51
|
:tool_invocations,
|
|
51
52
|
:session_id,
|
|
52
53
|
:phase,
|
|
53
|
-
:mode
|
|
54
|
+
:mode,
|
|
55
|
+
:current_llm_call_id,
|
|
56
|
+
:tool_batch_llm_call_id
|
|
54
57
|
|
|
55
58
|
def initialize(
|
|
56
59
|
agent:,
|
|
57
60
|
input:,
|
|
58
|
-
messages:,
|
|
59
61
|
config:,
|
|
60
62
|
approval_policy: nil,
|
|
61
63
|
approval_listener: nil,
|
|
62
64
|
event_listener: nil,
|
|
63
|
-
stream_listener: nil,
|
|
64
65
|
mode: nil,
|
|
65
66
|
id: nil
|
|
66
67
|
)
|
|
67
68
|
@agent = agent
|
|
68
69
|
@input = input
|
|
69
|
-
@messages = Array(messages)
|
|
70
70
|
@config = config
|
|
71
71
|
@thread_id = config[:thread_id]
|
|
72
72
|
@id = (id || config[:agent_invocation_id] || SecureRandom.uuid).to_s
|
|
@@ -76,8 +76,8 @@ module Phronomy
|
|
|
76
76
|
end
|
|
77
77
|
@approval_policy = invocation_policy || approval_policy
|
|
78
78
|
@approval_listener = approval_listener
|
|
79
|
-
@event_listener = event_listener
|
|
80
|
-
@mode = (mode ||
|
|
79
|
+
@event_listener = event_listener
|
|
80
|
+
@mode = (mode || :invoke).to_sym
|
|
81
81
|
|
|
82
82
|
@chat = nil
|
|
83
83
|
@output = nil
|
|
@@ -96,15 +96,8 @@ module Phronomy
|
|
|
96
96
|
@error = nil
|
|
97
97
|
@session_id = nil
|
|
98
98
|
@phase = nil
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Compatibility aliases for existing internal callers.
|
|
102
|
-
def stream_listener
|
|
103
|
-
@event_listener
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def stream_listener=(listener)
|
|
107
|
-
@event_listener = listener
|
|
99
|
+
@current_llm_call_id = nil
|
|
100
|
+
@tool_batch_llm_call_id = nil
|
|
108
101
|
end
|
|
109
102
|
|
|
110
103
|
def streaming?
|
|
@@ -116,6 +109,11 @@ module Phronomy
|
|
|
116
109
|
@phase = phase
|
|
117
110
|
end
|
|
118
111
|
|
|
112
|
+
def begin_llm_call!(llm_call_id)
|
|
113
|
+
@current_llm_call_id = llm_call_id.to_s
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
119
117
|
def pending_tool_calls=(calls)
|
|
120
118
|
@pending_tool_calls = Array(calls)
|
|
121
119
|
end
|
|
@@ -128,6 +126,7 @@ module Phronomy
|
|
|
128
126
|
@pending_tool_calls = []
|
|
129
127
|
@tool_invocations = []
|
|
130
128
|
@approval_request = nil
|
|
129
|
+
@tool_batch_llm_call_id = nil
|
|
131
130
|
end
|
|
132
131
|
|
|
133
132
|
def handle_fsm_event(event)
|
|
@@ -146,6 +145,11 @@ module Phronomy
|
|
|
146
145
|
return true
|
|
147
146
|
end
|
|
148
147
|
|
|
148
|
+
if CALLBACK_FAILED_EVENTS.include?(event.type)
|
|
149
|
+
@error ||= event.payload.fetch(:failure).to_stream_callback_error
|
|
150
|
+
return true
|
|
151
|
+
end
|
|
152
|
+
|
|
149
153
|
return false unless TOOL_EVENT_TYPES.include?(event.type)
|
|
150
154
|
|
|
151
155
|
invocation = tool_invocation(
|
|
@@ -164,36 +168,19 @@ module Phronomy
|
|
|
164
168
|
true
|
|
165
169
|
end
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
# this method; asynchronous results enter through explicit events.
|
|
169
|
-
def apply_fsm_action_result(result)
|
|
170
|
-
event_type =
|
|
171
|
-
if result.respond_to?(:error) &&
|
|
172
|
-
result.error &&
|
|
173
|
-
!result.error.is_a?(ToolCallIntercepted)
|
|
174
|
-
:llm_failed
|
|
175
|
-
else
|
|
176
|
-
:llm_completed
|
|
177
|
-
end
|
|
178
|
-
handle_fsm_event(
|
|
179
|
-
Phronomy::Event.new(
|
|
180
|
-
type: event_type,
|
|
181
|
-
target_id: @id,
|
|
182
|
-
payload: result
|
|
183
|
-
)
|
|
184
|
-
)
|
|
185
|
-
self
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def accept_tool_calls!(tool_calls)
|
|
171
|
+
def accept_tool_calls!(tool_calls, llm_call_id: nil)
|
|
189
172
|
@user_message_sent = true
|
|
190
173
|
@pending_tool_calls = Array(tool_calls)
|
|
191
|
-
@
|
|
174
|
+
@tool_batch_llm_call_id = (llm_call_id || @current_llm_call_id)&.to_s
|
|
175
|
+
@current_llm_call_id = nil
|
|
192
176
|
@pending_tool_calls.each do |tool_call|
|
|
193
177
|
deliver_event(
|
|
194
178
|
StreamEvent.new(
|
|
195
179
|
type: :tool_call,
|
|
196
|
-
payload: {
|
|
180
|
+
payload: {
|
|
181
|
+
tool_call: tool_call,
|
|
182
|
+
llm_call_id: @tool_batch_llm_call_id
|
|
183
|
+
}.compact
|
|
197
184
|
)
|
|
198
185
|
)
|
|
199
186
|
end
|
|
@@ -208,8 +195,8 @@ module Phronomy
|
|
|
208
195
|
@user_message_sent = true
|
|
209
196
|
@output = response.content
|
|
210
197
|
@usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
211
|
-
@messages = @chat.messages
|
|
212
198
|
@pending_tool_calls = []
|
|
199
|
+
@current_llm_call_id = nil
|
|
213
200
|
self
|
|
214
201
|
end
|
|
215
202
|
|
|
@@ -255,9 +242,10 @@ module Phronomy
|
|
|
255
242
|
|
|
256
243
|
def record_tool_results!
|
|
257
244
|
@tool_invocations.each do |invocation|
|
|
245
|
+
tool_content = invocation.result.to_s
|
|
258
246
|
@chat.add_message(
|
|
259
247
|
role: :tool,
|
|
260
|
-
content:
|
|
248
|
+
content: tool_content,
|
|
261
249
|
tool_call_id: invocation.tool_call_id
|
|
262
250
|
)
|
|
263
251
|
deliver_event(
|
|
@@ -266,12 +254,17 @@ module Phronomy
|
|
|
266
254
|
payload: {
|
|
267
255
|
tool_call_id: invocation.tool_call_id,
|
|
268
256
|
tool_name: invocation.tool_name,
|
|
269
|
-
tool_result: invocation.result
|
|
270
|
-
|
|
257
|
+
tool_result: invocation.result,
|
|
258
|
+
tool_message: {
|
|
259
|
+
"role" => "tool",
|
|
260
|
+
"content" => tool_content,
|
|
261
|
+
"tool_call_id" => invocation.tool_call_id.to_s
|
|
262
|
+
},
|
|
263
|
+
llm_call_id: @tool_batch_llm_call_id
|
|
264
|
+
}.compact
|
|
271
265
|
)
|
|
272
266
|
)
|
|
273
267
|
end
|
|
274
|
-
@messages = @chat.messages
|
|
275
268
|
clear_tool_batch!
|
|
276
269
|
self
|
|
277
270
|
end
|
|
@@ -361,6 +354,7 @@ module Phronomy
|
|
|
361
354
|
end
|
|
362
355
|
|
|
363
356
|
if event.type == :llm_failed
|
|
357
|
+
@current_llm_call_id = nil
|
|
364
358
|
@error = result.error ||
|
|
365
359
|
Phronomy::Error.new("LLM operation failed without an error")
|
|
366
360
|
return
|
|
@@ -368,8 +362,12 @@ module Phronomy
|
|
|
368
362
|
|
|
369
363
|
if result.error
|
|
370
364
|
if result.error.is_a?(ToolCallIntercepted)
|
|
371
|
-
accept_tool_calls!(
|
|
365
|
+
accept_tool_calls!(
|
|
366
|
+
result.error.tool_calls,
|
|
367
|
+
llm_call_id: result.error.llm_call_id
|
|
368
|
+
)
|
|
372
369
|
else
|
|
370
|
+
@current_llm_call_id = nil
|
|
373
371
|
@error = result.error
|
|
374
372
|
end
|
|
375
373
|
else
|