phronomy 0.22.0 → 0.24.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 +3 -4
- data/CHANGELOG.md +215 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +147 -0
- data/docs/architecture/persistence.md +186 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +89 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
- data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
- data/docs/decisions/README.md +176 -0
- data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
- data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
- data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
- data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
- data/docs/features.md +65 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +129 -40
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +21 -17
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +162 -72
- data/lib/phronomy/agent/base.rb +416 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/exact_execution.rb +153 -0
- data/lib/phronomy/agent/execution_cancellation.rb +25 -0
- data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
- data/lib/phronomy/agent/handoff.rb +43 -0
- data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
- data/lib/phronomy/agent/handoff_context.rb +120 -0
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
- data/lib/phronomy/agent/handoff_policy.rb +143 -0
- data/lib/phronomy/agent/handoff_projection.rb +207 -0
- data/lib/phronomy/agent/handoff_request.rb +45 -0
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
- data/lib/phronomy/agent/recovery_support.rb +504 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +339 -103
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +92 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
- data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
- data/lib/phronomy/multi_agent/team_execution.rb +44 -0
- data/lib/phronomy/multi_agent/team_root.rb +41 -0
- data/lib/phronomy/persistence/durable_codec.rb +706 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +471 -133
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +535 -0
- data/lib/phronomy/persistence.rb +174 -40
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
- data/lib/phronomy/testing/persistence_contract.rb +5 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +35 -0
- data/sig/phronomy/persistence.rbs +123 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +84 -18
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
- data/lib/phronomy/multi_agent/handoff.rb +0 -64
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
# Builds FSMSession instances for AgentInvocation objects.
|
|
6
|
-
#
|
|
7
|
-
# Offloaded/provider work returns through explicit Agent-internal events.
|
|
8
|
-
# Entry actions start operations and return synchronously.
|
|
9
|
-
# Every LLM Call is prepared from a canonical Manifest and RuntimeProjection.
|
|
10
|
-
#
|
|
11
|
-
# @api private
|
|
12
5
|
class AgentInvocationSessionBuilder
|
|
13
6
|
AUTO_STATE_SET = {
|
|
14
7
|
idle: true,
|
|
@@ -16,7 +9,6 @@ module Phronomy
|
|
|
16
9
|
building_context: true,
|
|
17
10
|
starting_tools: true,
|
|
18
11
|
evaluating_tools: true,
|
|
19
|
-
dispatching_tools: true,
|
|
20
12
|
recording_tool_results: true,
|
|
21
13
|
output_filtering: true
|
|
22
14
|
}.freeze
|
|
@@ -24,7 +16,7 @@ module Phronomy
|
|
|
24
16
|
DECLARED_STATES = %i[
|
|
25
17
|
idle filtering_input building_context calling_llm starting_tools
|
|
26
18
|
evaluating_tools waiting_for_tools dispatching_tools
|
|
27
|
-
recording_tool_results suspended output_filtering completed blocked failed
|
|
19
|
+
recording_tool_results suspended output_filtering handed_off completed blocked failed
|
|
28
20
|
].freeze
|
|
29
21
|
|
|
30
22
|
WAIT_STATES = %i[suspended].freeze
|
|
@@ -55,7 +47,8 @@ module Phronomy
|
|
|
55
47
|
approval_policy: approval_policy,
|
|
56
48
|
approval_listener: approval_listener,
|
|
57
49
|
event_listener: on_event,
|
|
58
|
-
mode: mode
|
|
50
|
+
mode: mode,
|
|
51
|
+
execution_id: config.fetch(:execution_id)
|
|
59
52
|
)
|
|
60
53
|
build_session(
|
|
61
54
|
agent_invocation: invocation,
|
|
@@ -87,15 +80,19 @@ module Phronomy
|
|
|
87
80
|
resume_phase: nil
|
|
88
81
|
)
|
|
89
82
|
agent = agent_invocation.agent
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
83
|
+
event_sink = Phronomy::FSMSession::EventSink.new(
|
|
84
|
+
event_loop: runtime.event_loop
|
|
85
|
+
)
|
|
86
|
+
agent_invocation.bind_event_sink!(event_sink)
|
|
87
|
+
actions = build_entry_actions(
|
|
88
|
+
agent, runtime, mode: mode, event_sink: event_sink
|
|
89
|
+
)
|
|
90
|
+
phase_machine = Agent::PhaseMachineBuilder.new(entry_actions: actions).build
|
|
94
91
|
iterations = agent.class.max_iterations || 10
|
|
95
92
|
|
|
96
93
|
Phronomy::FSMSession.new(
|
|
97
|
-
id: agent_invocation.id,
|
|
98
94
|
context: agent_invocation,
|
|
95
|
+
event_sink: event_sink,
|
|
99
96
|
entry_point: :idle,
|
|
100
97
|
phase_machine_class: phase_machine,
|
|
101
98
|
entry_actions: {},
|
|
@@ -113,22 +110,29 @@ module Phronomy
|
|
|
113
110
|
|
|
114
111
|
def self.external_events
|
|
115
112
|
tool_transitions = TOOL_EVENTS.to_h do |event_name|
|
|
116
|
-
[
|
|
117
|
-
event_name,
|
|
118
|
-
[{from: :waiting_for_tools, to: :evaluating_tools, guard: nil}]
|
|
119
|
-
]
|
|
113
|
+
[event_name, [{from: :waiting_for_tools, to: :evaluating_tools, guard: nil}]]
|
|
120
114
|
end
|
|
121
115
|
|
|
122
116
|
tool_transitions.merge(
|
|
123
117
|
llm_completed: [
|
|
124
|
-
{from: :calling_llm, to: :
|
|
125
|
-
|
|
126
|
-
}},
|
|
118
|
+
{from: :calling_llm, to: :failed, guard: ->(ctx) { ctx.callback_failed? }},
|
|
119
|
+
{from: :calling_llm, to: :failed, guard: ->(ctx) { ctx.handoff_failed? }},
|
|
120
|
+
{from: :calling_llm, to: :handed_off, guard: ->(ctx) { ctx.handoff_requested? }},
|
|
121
|
+
{from: :calling_llm, to: :starting_tools, guard: ->(ctx) { ctx.tool_call_pending? }},
|
|
127
122
|
{from: :calling_llm, to: :output_filtering, guard: nil}
|
|
128
123
|
],
|
|
129
124
|
llm_failed: [
|
|
130
125
|
{from: :calling_llm, to: :failed, guard: nil}
|
|
131
126
|
],
|
|
127
|
+
llm_setup_failed: [
|
|
128
|
+
{from: :calling_llm, to: :failed, guard: nil}
|
|
129
|
+
],
|
|
130
|
+
tool_setup_failed: [
|
|
131
|
+
{from: :dispatching_tools, to: :failed, guard: nil}
|
|
132
|
+
],
|
|
133
|
+
tool_dispatch_prepared: [
|
|
134
|
+
{from: :dispatching_tools, to: :evaluating_tools, guard: nil}
|
|
135
|
+
],
|
|
132
136
|
resume: [
|
|
133
137
|
{from: :suspended, to: :waiting_for_tools, guard: nil}
|
|
134
138
|
],
|
|
@@ -147,34 +151,22 @@ module Phronomy
|
|
|
147
151
|
end
|
|
148
152
|
private_class_method :external_events
|
|
149
153
|
|
|
150
|
-
def self.build_entry_actions(agent, runtime, mode:)
|
|
154
|
+
def self.build_entry_actions(agent, runtime, mode:, event_sink:)
|
|
151
155
|
calling_action = if mode.to_sym == :stream
|
|
152
|
-
method(:calling_llm_stream_action).curry.call(agent, runtime)
|
|
156
|
+
method(:calling_llm_stream_action).curry.call(agent, runtime, event_sink)
|
|
153
157
|
else
|
|
154
|
-
method(:calling_llm_action).curry.call(agent, runtime)
|
|
158
|
+
method(:calling_llm_action).curry.call(agent, runtime, event_sink)
|
|
155
159
|
end
|
|
156
160
|
|
|
157
161
|
{
|
|
158
|
-
filtering_input: [
|
|
159
|
-
|
|
160
|
-
],
|
|
161
|
-
building_context: [
|
|
162
|
-
method(:building_context_action).curry.call(agent)
|
|
163
|
-
],
|
|
162
|
+
filtering_input: [method(:filtering_input_action).curry.call(agent)],
|
|
163
|
+
building_context: [method(:building_context_action).curry.call(agent)],
|
|
164
164
|
calling_llm: [calling_action],
|
|
165
|
-
starting_tools: [
|
|
166
|
-
|
|
167
|
-
],
|
|
168
|
-
dispatching_tools: [
|
|
169
|
-
method(:dispatching_tools_action).curry.call(runtime)
|
|
170
|
-
],
|
|
171
|
-
recording_tool_results: [
|
|
172
|
-
method(:recording_tool_results_action)
|
|
173
|
-
],
|
|
165
|
+
starting_tools: [method(:starting_tools_action).curry.call(runtime, event_sink)],
|
|
166
|
+
dispatching_tools: [method(:dispatching_tools_action).curry.call(runtime, event_sink)],
|
|
167
|
+
recording_tool_results: [method(:recording_tool_results_action)],
|
|
174
168
|
suspended: [method(:suspended_action)],
|
|
175
|
-
output_filtering: [
|
|
176
|
-
method(:output_filtering_action).curry.call(agent)
|
|
177
|
-
],
|
|
169
|
+
output_filtering: [method(:output_filtering_action).curry.call(agent)],
|
|
178
170
|
failed: [method(:failed_action)]
|
|
179
171
|
}
|
|
180
172
|
end
|
|
@@ -195,15 +187,11 @@ module Phronomy
|
|
|
195
187
|
projection,
|
|
196
188
|
invocation: invocation
|
|
197
189
|
)
|
|
198
|
-
install_tool_interceptors(invocation.chat, invocation)
|
|
199
190
|
invocation
|
|
200
191
|
end
|
|
201
192
|
private_class_method :building_context_action
|
|
202
193
|
|
|
203
|
-
|
|
204
|
-
# before before_tool_call is fired. Capture that Message at the control
|
|
205
|
-
# boundary instead of trying to recover it after the exception unwinds.
|
|
206
|
-
def self.install_tool_interceptors(chat, invocation = nil)
|
|
194
|
+
def self.install_tool_interceptors(chat, llm_call_id:)
|
|
207
195
|
unless chat.respond_to?(:before_tool_call)
|
|
208
196
|
raise Phronomy::ConfigurationError,
|
|
209
197
|
"Agent-owned Tool execution requires RubyLLM >= 1.15 (before_tool_call callback)"
|
|
@@ -211,17 +199,17 @@ module Phronomy
|
|
|
211
199
|
|
|
212
200
|
if chat.respond_to?(:on_tool_call_batch)
|
|
213
201
|
chat.on_tool_call_batch do |tool_calls|
|
|
214
|
-
raise build_tool_interception(chat, tool_calls,
|
|
202
|
+
raise build_tool_interception(chat, tool_calls, llm_call_id)
|
|
215
203
|
end
|
|
216
204
|
end
|
|
217
205
|
|
|
218
206
|
chat.before_tool_call do |tool_call|
|
|
219
|
-
raise build_tool_interception(chat, [tool_call],
|
|
207
|
+
raise build_tool_interception(chat, [tool_call], llm_call_id)
|
|
220
208
|
end
|
|
221
209
|
end
|
|
222
210
|
private_class_method :install_tool_interceptors
|
|
223
211
|
|
|
224
|
-
def self.build_tool_interception(chat, fallback_tool_calls,
|
|
212
|
+
def self.build_tool_interception(chat, fallback_tool_calls, llm_call_id)
|
|
225
213
|
assistant_message = chat.messages.last
|
|
226
214
|
unless assistant_message&.respond_to?(:role) &&
|
|
227
215
|
assistant_message.role.to_sym == :assistant &&
|
|
@@ -231,94 +219,88 @@ module Phronomy
|
|
|
231
219
|
end
|
|
232
220
|
|
|
233
221
|
message_tool_calls = assistant_message.tool_calls
|
|
234
|
-
tool_calls =
|
|
235
|
-
message_tool_calls.values
|
|
236
|
-
else
|
|
237
|
-
Array(message_tool_calls)
|
|
238
|
-
end
|
|
222
|
+
tool_calls = message_tool_calls.respond_to?(:values) ? message_tool_calls.values : Array(message_tool_calls)
|
|
239
223
|
tool_calls = Array(fallback_tool_calls) if tool_calls.empty?
|
|
240
224
|
|
|
241
225
|
ToolCallIntercepted.new(
|
|
242
226
|
tool_calls,
|
|
243
227
|
assistant_message: assistant_message,
|
|
244
228
|
assistant_outcome: ProviderCallOutcome.capture(assistant_message),
|
|
245
|
-
llm_call_id:
|
|
229
|
+
llm_call_id: llm_call_id
|
|
246
230
|
)
|
|
247
231
|
end
|
|
248
232
|
private_class_method :build_tool_interception
|
|
249
233
|
|
|
250
|
-
def self.calling_llm_action(agent, runtime, invocation)
|
|
251
|
-
prepare_and_start_llm_call(
|
|
252
|
-
agent,
|
|
253
|
-
runtime,
|
|
254
|
-
invocation,
|
|
255
|
-
streaming: false
|
|
256
|
-
)
|
|
234
|
+
def self.calling_llm_action(agent, runtime, event_sink, invocation)
|
|
235
|
+
prepare_and_start_llm_call(agent, runtime, event_sink, invocation, streaming: false)
|
|
257
236
|
invocation
|
|
258
237
|
end
|
|
259
238
|
private_class_method :calling_llm_action
|
|
260
239
|
|
|
261
|
-
def self.calling_llm_stream_action(agent, runtime, invocation)
|
|
262
|
-
prepare_and_start_llm_call(
|
|
263
|
-
agent,
|
|
264
|
-
runtime,
|
|
265
|
-
invocation,
|
|
266
|
-
streaming: true
|
|
267
|
-
)
|
|
240
|
+
def self.calling_llm_stream_action(agent, runtime, event_sink, invocation)
|
|
241
|
+
prepare_and_start_llm_call(agent, runtime, event_sink, invocation, streaming: true)
|
|
268
242
|
invocation
|
|
269
243
|
end
|
|
270
244
|
private_class_method :calling_llm_stream_action
|
|
271
245
|
|
|
272
|
-
def self.prepare_and_start_llm_call(agent, runtime, invocation, streaming:)
|
|
273
|
-
|
|
246
|
+
def self.prepare_and_start_llm_call(agent, runtime, event_sink, invocation, streaming:)
|
|
247
|
+
# Callback failure is recorded synchronously on EventLoop before its
|
|
248
|
+
# explicit failure event is queued. Do not start a competing durable
|
|
249
|
+
# follow-up operation from the same execution revision while that failure
|
|
250
|
+
# event is waiting to terminalize the FSMSession.
|
|
251
|
+
return if invocation.callback_failed?
|
|
252
|
+
|
|
274
253
|
if invocation.user_message_sent
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
post_preparation_failure(runtime, invocation, error, streaming: streaming)
|
|
281
|
-
else
|
|
282
|
-
start_provider_call(
|
|
283
|
-
agent,
|
|
284
|
-
runtime,
|
|
285
|
-
invocation,
|
|
286
|
-
activation,
|
|
287
|
-
projection,
|
|
288
|
-
streaming: streaming,
|
|
289
|
-
replace_messages: true
|
|
290
|
-
)
|
|
291
|
-
end
|
|
292
|
-
end
|
|
254
|
+
invocation.config.fetch(:phronomy_execution_coordinator).prepare_provider_dispatch(
|
|
255
|
+
invocation,
|
|
256
|
+
event_sink: event_sink,
|
|
257
|
+
streaming: streaming
|
|
258
|
+
)
|
|
293
259
|
else
|
|
294
260
|
start_provider_call(
|
|
295
261
|
agent,
|
|
296
262
|
runtime,
|
|
263
|
+
event_sink,
|
|
297
264
|
invocation,
|
|
298
|
-
|
|
299
|
-
activation.runtime_projection,
|
|
265
|
+
invocation.config.fetch(:phronomy_runtime_projection),
|
|
300
266
|
streaming: streaming,
|
|
301
267
|
replace_messages: false
|
|
302
268
|
)
|
|
303
269
|
end
|
|
270
|
+
rescue => error
|
|
271
|
+
post_setup_failure(event_sink, error)
|
|
304
272
|
end
|
|
305
273
|
private_class_method :prepare_and_start_llm_call
|
|
306
274
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
275
|
+
# Continues a follow-up Provider Call after its durable preparation result
|
|
276
|
+
# has been validated/applied by ExecutionCoordinator on EventLoop.
|
|
277
|
+
# @api private
|
|
278
|
+
def self.start_prepared_provider_call(
|
|
279
|
+
agent:,
|
|
280
|
+
runtime:,
|
|
281
|
+
event_sink:,
|
|
282
|
+
invocation:,
|
|
283
|
+
projection:,
|
|
284
|
+
streaming:
|
|
315
285
|
)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
286
|
+
start_provider_call(
|
|
287
|
+
agent,
|
|
288
|
+
runtime,
|
|
289
|
+
event_sink,
|
|
290
|
+
invocation,
|
|
291
|
+
projection,
|
|
292
|
+
streaming: streaming,
|
|
293
|
+
replace_messages: true
|
|
321
294
|
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.start_provider_call(
|
|
298
|
+
agent, runtime, event_sink, invocation, projection,
|
|
299
|
+
streaming:, replace_messages:
|
|
300
|
+
)
|
|
301
|
+
call_context = trace_handle = nil
|
|
302
|
+
config = invocation.config
|
|
303
|
+
agent.send(:check_cancellation!, config, "invocation cancelled before LLM call")
|
|
322
304
|
if replace_messages
|
|
323
305
|
invocation.chat = agent.send(:build_chat, model_config: projection.model_config)
|
|
324
306
|
agent.send(
|
|
@@ -327,69 +309,83 @@ module Phronomy
|
|
|
327
309
|
projection,
|
|
328
310
|
invocation: invocation
|
|
329
311
|
)
|
|
330
|
-
install_tool_interceptors(invocation.chat, invocation)
|
|
331
312
|
invocation.config[:phronomy_runtime_projection] = projection
|
|
332
313
|
end
|
|
333
314
|
|
|
334
|
-
call_context =
|
|
335
|
-
|
|
336
|
-
|
|
315
|
+
call_context = invocation.begin_llm_call!(projection)
|
|
316
|
+
install_tool_interceptors(
|
|
317
|
+
invocation.chat,
|
|
318
|
+
llm_call_id: call_context.fetch(:llm_call_id)
|
|
319
|
+
)
|
|
337
320
|
message = projection.ask_message
|
|
321
|
+
chat = invocation.chat
|
|
322
|
+
trace_handle = Phronomy::Tracing::Automatic.start(
|
|
323
|
+
"llm.call",
|
|
324
|
+
input: message,
|
|
325
|
+
agent_id: agent.agent_id,
|
|
326
|
+
execution_id: invocation.execution_id,
|
|
327
|
+
llm_call_id: call_context.fetch(:llm_call_id),
|
|
328
|
+
mode: invocation.mode,
|
|
329
|
+
streaming: streaming,
|
|
330
|
+
**agent.send(:_build_caller_meta, config)
|
|
331
|
+
)
|
|
338
332
|
|
|
339
333
|
operation = if streaming
|
|
340
334
|
Phronomy.configuration.llm_adapter.stream_async(
|
|
341
|
-
|
|
342
|
-
message,
|
|
343
|
-
config: invocation.config
|
|
335
|
+
chat, message, config: config
|
|
344
336
|
) do |chunk|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
runtime,
|
|
352
|
-
invocation.id,
|
|
353
|
-
:llm_stream_chunk,
|
|
354
|
-
{content: chunk.content}
|
|
337
|
+
token = config[:cancellation_token]
|
|
338
|
+
token&.raise_if_cancelled!("invocation cancelled during streaming")
|
|
339
|
+
post_stream_chunk(
|
|
340
|
+
event_sink,
|
|
341
|
+
call_context.fetch(:llm_call_id),
|
|
342
|
+
chunk.content
|
|
355
343
|
)
|
|
356
344
|
end
|
|
357
345
|
else
|
|
358
346
|
Phronomy.configuration.llm_adapter.complete_async(
|
|
359
|
-
|
|
360
|
-
message,
|
|
361
|
-
config: invocation.config
|
|
347
|
+
chat, message, config: config
|
|
362
348
|
)
|
|
363
349
|
end
|
|
350
|
+
runtime.event_loop.supervise_agent_operation(
|
|
351
|
+
invocation.execution_id,
|
|
352
|
+
operation
|
|
353
|
+
)
|
|
364
354
|
observe_manifest_call(
|
|
365
355
|
operation,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
356
|
+
event_sink,
|
|
357
|
+
call_context,
|
|
358
|
+
trace_handle,
|
|
369
359
|
streaming: streaming
|
|
370
360
|
)
|
|
371
361
|
rescue => error
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
362
|
+
finish_provider_trace(trace_handle, nil, error)
|
|
363
|
+
if call_context
|
|
364
|
+
post_llm_result(
|
|
365
|
+
event_sink,
|
|
366
|
+
call_context,
|
|
367
|
+
nil,
|
|
368
|
+
error,
|
|
376
369
|
streaming: streaming
|
|
377
370
|
)
|
|
371
|
+
else
|
|
372
|
+
post_setup_failure(event_sink, error)
|
|
378
373
|
end
|
|
379
|
-
post_llm_result(runtime, invocation, nil, error, streaming: streaming)
|
|
380
374
|
end
|
|
381
375
|
private_class_method :start_provider_call
|
|
382
376
|
|
|
383
|
-
def self.observe_manifest_call(
|
|
377
|
+
def self.observe_manifest_call(
|
|
378
|
+
operation,
|
|
379
|
+
event_sink,
|
|
380
|
+
call_context,
|
|
381
|
+
trace_handle,
|
|
382
|
+
streaming:
|
|
383
|
+
)
|
|
384
384
|
operation.on_complete do |response, error|
|
|
385
|
-
|
|
386
|
-
response: canonical_response_for(response, error),
|
|
387
|
-
error: error,
|
|
388
|
-
streaming: streaming
|
|
389
|
-
)
|
|
385
|
+
finish_provider_trace(trace_handle, response, error)
|
|
390
386
|
post_llm_result(
|
|
391
|
-
|
|
392
|
-
|
|
387
|
+
event_sink,
|
|
388
|
+
call_context,
|
|
393
389
|
response,
|
|
394
390
|
error,
|
|
395
391
|
streaming: streaming
|
|
@@ -398,22 +394,27 @@ module Phronomy
|
|
|
398
394
|
end
|
|
399
395
|
private_class_method :observe_manifest_call
|
|
400
396
|
|
|
401
|
-
def self.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
397
|
+
def self.finish_provider_trace(trace_handle, response, error)
|
|
398
|
+
control_transfer = error.is_a?(ToolCallIntercepted)
|
|
399
|
+
trace_output = control_transfer ?
|
|
400
|
+
(error.assistant_outcome || error.assistant_message) : response
|
|
401
|
+
trace_error = control_transfer ? nil : error
|
|
402
|
+
usage_source = control_transfer ? error.assistant_message : response
|
|
403
|
+
usage = if usage_source&.respond_to?(:tokens)
|
|
404
|
+
Phronomy::TokenUsage.from_tokens(usage_source.tokens)
|
|
406
405
|
end
|
|
406
|
+
Phronomy::Tracing::Automatic.finish(
|
|
407
|
+
trace_handle,
|
|
408
|
+
output: trace_output,
|
|
409
|
+
usage: usage,
|
|
410
|
+
error: trace_error
|
|
411
|
+
)
|
|
407
412
|
end
|
|
408
|
-
private_class_method :
|
|
409
|
-
|
|
410
|
-
def self.post_preparation_failure(runtime, invocation, error, streaming:)
|
|
411
|
-
post_llm_result(runtime, invocation, nil, error, streaming: streaming)
|
|
412
|
-
end
|
|
413
|
-
private_class_method :post_preparation_failure
|
|
413
|
+
private_class_method :finish_provider_trace
|
|
414
414
|
|
|
415
|
-
def self.post_llm_result(
|
|
415
|
+
def self.post_llm_result(event_sink, call_context, response, error, streaming:)
|
|
416
416
|
result = LLMOperationResult.new(
|
|
417
|
+
llm_call_id: call_context.fetch(:llm_call_id),
|
|
417
418
|
response: response,
|
|
418
419
|
error: error,
|
|
419
420
|
streaming: streaming
|
|
@@ -423,46 +424,65 @@ module Phronomy
|
|
|
423
424
|
else
|
|
424
425
|
:llm_completed
|
|
425
426
|
end
|
|
426
|
-
|
|
427
|
+
post_session_event!(event_sink, event_type, result)
|
|
427
428
|
end
|
|
428
429
|
private_class_method :post_llm_result
|
|
429
430
|
|
|
430
|
-
def self.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
payload: payload
|
|
436
|
-
)
|
|
431
|
+
def self.post_stream_chunk(event_sink, llm_call_id, content)
|
|
432
|
+
post_session_event!(
|
|
433
|
+
event_sink,
|
|
434
|
+
:llm_stream_chunk,
|
|
435
|
+
{llm_call_id: llm_call_id.to_s.freeze, content: content}.freeze
|
|
437
436
|
)
|
|
438
|
-
|
|
437
|
+
end
|
|
438
|
+
private_class_method :post_stream_chunk
|
|
439
|
+
|
|
440
|
+
def self.post_setup_failure(event_sink, error)
|
|
441
|
+
post_session_event!(event_sink, :llm_setup_failed, error)
|
|
442
|
+
end
|
|
443
|
+
private_class_method :post_setup_failure
|
|
444
|
+
|
|
445
|
+
def self.post_session_event!(event_sink, event_type, payload)
|
|
446
|
+
return if event_sink.post(event_type, payload)
|
|
439
447
|
|
|
440
448
|
Phronomy.configuration.logger&.warn(
|
|
441
449
|
"[Phronomy] Dropped late #{event_type.inspect} for " \
|
|
442
|
-
"
|
|
450
|
+
"FSMSession #{event_sink.fsm_session_id}"
|
|
443
451
|
)
|
|
444
452
|
end
|
|
445
|
-
private_class_method :
|
|
453
|
+
private_class_method :post_session_event!
|
|
446
454
|
|
|
447
|
-
def self.starting_tools_action(runtime, invocation)
|
|
455
|
+
def self.starting_tools_action(runtime, parent_event_sink, invocation)
|
|
448
456
|
children = invocation.pending_tool_calls.map do |tool_call|
|
|
449
457
|
tool = invocation.chat.tools[tool_call.name.to_sym]
|
|
458
|
+
tool_invocation_id =
|
|
459
|
+
RecoverySupport.semantic_tool_id(
|
|
460
|
+
execution_id: invocation.execution_id,
|
|
461
|
+
llm_call_id: invocation.tool_batch_llm_call_id,
|
|
462
|
+
tool_call_id: (
|
|
463
|
+
tool_call.respond_to?(:id) ? tool_call.id : nil
|
|
464
|
+
),
|
|
465
|
+
tool_name: tool_call.name
|
|
466
|
+
)
|
|
467
|
+
|
|
450
468
|
if tool
|
|
451
469
|
ToolInvocation.new(
|
|
452
|
-
|
|
470
|
+
execution_id: invocation.execution_id,
|
|
453
471
|
agent: invocation.agent,
|
|
454
472
|
tool: tool,
|
|
455
473
|
tool_call: tool_call,
|
|
456
474
|
config: invocation.config,
|
|
457
475
|
approval_policy: invocation.approval_policy,
|
|
458
|
-
approval_context: invocation.approval_context
|
|
476
|
+
approval_context: invocation.approval_context,
|
|
477
|
+
id: tool_invocation_id
|
|
459
478
|
)
|
|
460
479
|
else
|
|
461
480
|
ToolInvocation.missing(
|
|
462
|
-
|
|
481
|
+
execution_id: invocation.execution_id,
|
|
463
482
|
agent: invocation.agent,
|
|
464
483
|
tool_call: tool_call,
|
|
465
|
-
config: invocation.config
|
|
484
|
+
config: invocation.config,
|
|
485
|
+
id: tool_invocation_id
|
|
466
486
|
)
|
|
467
487
|
end
|
|
468
488
|
end
|
|
@@ -471,45 +491,65 @@ module Phronomy
|
|
|
471
491
|
children.reject(&:terminal?).each do |child|
|
|
472
492
|
session = ToolInvocationSessionBuilder.build(
|
|
473
493
|
tool_invocation: child,
|
|
494
|
+
parent_event_sink: parent_event_sink,
|
|
474
495
|
runtime: runtime
|
|
475
496
|
)
|
|
476
|
-
register_child_session(
|
|
497
|
+
register_child_session(
|
|
498
|
+
runtime,
|
|
499
|
+
child,
|
|
500
|
+
session,
|
|
501
|
+
parent_event_sink
|
|
502
|
+
)
|
|
477
503
|
end
|
|
478
504
|
invocation
|
|
479
505
|
end
|
|
480
506
|
private_class_method :starting_tools_action
|
|
481
507
|
|
|
482
|
-
def self.dispatching_tools_action(
|
|
483
|
-
invocation.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
tool_invocation: child,
|
|
488
|
-
resume_event: :dispatch,
|
|
489
|
-
resume_phase: :authorized,
|
|
490
|
-
runtime: runtime
|
|
491
|
-
)
|
|
492
|
-
register_child_session(runtime, child, session)
|
|
493
|
-
end
|
|
508
|
+
def self.dispatching_tools_action(_runtime, parent_event_sink, invocation)
|
|
509
|
+
invocation.config.fetch(:phronomy_execution_coordinator).prepare_tool_dispatch(
|
|
510
|
+
invocation,
|
|
511
|
+
event_sink: parent_event_sink
|
|
512
|
+
)
|
|
494
513
|
invocation
|
|
495
514
|
end
|
|
496
515
|
private_class_method :dispatching_tools_action
|
|
497
516
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
517
|
+
# Continues Tool physical dispatch after the operation-specific durable
|
|
518
|
+
# preparation has been confirmed and applied by ExecutionCoordinator.
|
|
519
|
+
# @api private
|
|
520
|
+
def self.start_prepared_tool_dispatch(
|
|
521
|
+
runtime:,
|
|
522
|
+
event_sink:,
|
|
523
|
+
invocation:
|
|
524
|
+
)
|
|
525
|
+
invocation.tool_invocations.select(&:authorized?).each do |child|
|
|
526
|
+
session = ToolInvocationSessionBuilder.build_for_resume(
|
|
527
|
+
tool_invocation: child,
|
|
528
|
+
parent_event_sink: event_sink,
|
|
529
|
+
resume_event: :dispatch,
|
|
530
|
+
resume_phase: :authorized,
|
|
531
|
+
runtime: runtime
|
|
532
|
+
)
|
|
533
|
+
begin
|
|
534
|
+
register_child_session(runtime, child, session, event_sink)
|
|
535
|
+
rescue => error
|
|
536
|
+
child.mark_framework_failed!(error)
|
|
537
|
+
event_sink.post(:tool_failed, {tool_invocation_id: child.id})
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
post_session_event!(event_sink, :tool_dispatch_prepared, nil)
|
|
541
|
+
invocation
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def self.register_child_session(runtime, child, session, parent_event_sink)
|
|
545
|
+
completion = Phronomy::Task.deferred(name: "tool-session:#{child.id}")
|
|
502
546
|
completion.on_complete do |_result, error|
|
|
503
547
|
next unless error
|
|
504
548
|
|
|
549
|
+
# Tool FSMSession completion is settled by EventLoop, so this callback
|
|
550
|
+
# also executes on EventLoop and remains inside the single-writer domain.
|
|
505
551
|
child.mark_framework_failed!(error)
|
|
506
|
-
|
|
507
|
-
Phronomy::Event.new(
|
|
508
|
-
type: :tool_failed,
|
|
509
|
-
target_id: child.parent_agent_invocation_id,
|
|
510
|
-
payload: {tool_invocation_id: child.id}
|
|
511
|
-
)
|
|
512
|
-
)
|
|
552
|
+
parent_event_sink.post(:tool_failed, {tool_invocation_id: child.id})
|
|
513
553
|
end
|
|
514
554
|
runtime.event_loop.register(session, completion: completion)
|
|
515
555
|
end
|
|
@@ -526,18 +566,12 @@ module Phronomy
|
|
|
526
566
|
private_class_method :suspended_action
|
|
527
567
|
|
|
528
568
|
def self.failed_action(invocation)
|
|
529
|
-
raise(
|
|
530
|
-
invocation.error ||
|
|
531
|
-
Phronomy::ToolError.new("Agent invocation failed")
|
|
532
|
-
)
|
|
569
|
+
raise(invocation.error || Phronomy::ToolError.new("Agent invocation failed"))
|
|
533
570
|
end
|
|
534
571
|
private_class_method :failed_action
|
|
535
572
|
|
|
536
573
|
def self.output_filtering_action(agent, invocation)
|
|
537
|
-
invocation.output = agent.send(
|
|
538
|
-
:run_output_filters!,
|
|
539
|
-
invocation.output
|
|
540
|
-
)
|
|
574
|
+
invocation.output = agent.send(:run_output_filters!, invocation.output)
|
|
541
575
|
invocation
|
|
542
576
|
rescue Phronomy::FilterBlockError => error
|
|
543
577
|
invocation.output_blocked = true
|