phronomy 0.15.1 → 0.16.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 +105 -28
- data/README.md +300 -75
- data/benchmark/bench_agent_invoke.rb +3 -0
- data/benchmark/bench_regression.rb +2 -18
- data/benchmark/bench_tool_schema.rb +1 -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/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 +42 -10
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +50 -11
- data/lib/phronomy/agent/agent_root.rb +67 -0
- data/lib/phronomy/agent/async_event_api.rb +55 -475
- data/lib/phronomy/agent/base.rb +301 -285
- data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
- data/lib/phronomy/agent/context_assembler.rb +321 -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 +188 -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 +167 -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 +1123 -0
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +210 -0
- data/lib/phronomy/agent/immutable.rb +31 -0
- data/lib/phronomy/agent/journal_projection.rb +34 -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 +298 -0
- data/lib/phronomy/agent/token_budget_resolver.rb +69 -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.rb +14 -16
- data/lib/phronomy/agent_busy_error.rb +5 -0
- data/lib/phronomy/canonical_json.rb +136 -0
- data/lib/phronomy/configuration.rb +9 -4
- data/lib/phronomy/content_store/base.rb +51 -0
- data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
- data/lib/phronomy/engine/event_loop.rb +3 -0
- 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/assembler.rb +8 -8
- data/lib/phronomy/multi_agent/orchestrator.rb +1 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
- data/lib/phronomy/multi_agent/team_coordinator.rb +6 -2
- 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/version.rb +1 -1
- data/lib/phronomy.rb +11 -0
- data/scripts/add_to_h_to_token_doubles.rb +33 -0
- data/scripts/add_to_h_unnamed_doubles.rb +27 -0
- 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 -7
- 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
|
@@ -2,55 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# invoke_async and stream_async share lifecycle/tool events. stream_async
|
|
8
|
-
# additionally emits :token events. The returned Task remains a normal Task
|
|
9
|
-
# and is settled after the terminal event listener returns.
|
|
10
|
-
#
|
|
11
|
-
# @api private
|
|
5
|
+
# Public Agent execution API. Agent instances are always stateful and all
|
|
6
|
+
# operations are coordinated through Phronomy::Persistence.
|
|
12
7
|
module AsyncEventApi
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
# Provider errors are translated after the configured LLM adapter returns
|
|
16
|
-
# its final result. Phronomy does not replay the Agent invocation.
|
|
17
|
-
#
|
|
18
|
-
# @param input [String, Hash] user input for this invocation
|
|
19
|
-
# @param messages [Array<RubyLLM::Message>] conversation history
|
|
20
|
-
# @param thread_id [String, nil] conversation thread identifier
|
|
21
|
-
# @param config [Hash] additional runtime options
|
|
22
|
-
# @param invocation_context [Phronomy::InvocationContext, nil]
|
|
23
|
-
# first-class invocation context
|
|
24
|
-
# @param on_event [Proc, nil] listener for lifecycle and Tool events
|
|
25
|
-
# @return [Hash] terminal invocation result
|
|
26
|
-
# @raise [Phronomy::SchedulerReentrancyError] when called from the
|
|
27
|
-
# EventLoop thread or a guarded scheduler context
|
|
28
|
-
# @api public
|
|
29
|
-
def invoke(
|
|
30
|
-
input,
|
|
31
|
-
messages: [],
|
|
32
|
-
thread_id: nil,
|
|
33
|
-
config: {},
|
|
34
|
-
invocation_context: nil,
|
|
35
|
-
on_event: nil
|
|
36
|
-
)
|
|
8
|
+
def invoke(input, thread_id: nil, config: {}, invocation_context: nil, on_event: nil)
|
|
37
9
|
if invocation_context
|
|
38
|
-
thread_id, config = _apply_invocation_context(
|
|
39
|
-
thread_id,
|
|
40
|
-
config,
|
|
41
|
-
invocation_context
|
|
42
|
-
)
|
|
10
|
+
thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
|
|
43
11
|
end
|
|
44
12
|
_check_scheduler_reentrancy(:invoke, :invoke_async)
|
|
45
|
-
|
|
46
|
-
trace(
|
|
47
|
-
"agent.invoke",
|
|
48
|
-
input: input,
|
|
49
|
-
**_build_caller_meta(config)
|
|
50
|
-
) do |_span|
|
|
13
|
+
trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
|
|
51
14
|
result = invoke_async(
|
|
52
15
|
input,
|
|
53
|
-
messages: messages,
|
|
54
16
|
thread_id: thread_id,
|
|
55
17
|
config: config,
|
|
56
18
|
on_event: on_event
|
|
@@ -59,26 +21,8 @@ module Phronomy
|
|
|
59
21
|
end
|
|
60
22
|
end
|
|
61
23
|
|
|
62
|
-
# Invokes the agent asynchronously.
|
|
63
|
-
#
|
|
64
|
-
# The returned Task settles after the terminal event listener returns.
|
|
65
|
-
# Lifecycle and Tool events are delivered from the Runtime-owned
|
|
66
|
-
# EventLoop thread.
|
|
67
|
-
#
|
|
68
|
-
# @param input [String, Hash] user input for this invocation
|
|
69
|
-
# @param messages [Array<RubyLLM::Message>] conversation history
|
|
70
|
-
# @param thread_id [String, nil] conversation thread identifier
|
|
71
|
-
# @param config [Hash] additional runtime options
|
|
72
|
-
# @param invocation_context [Phronomy::InvocationContext, nil]
|
|
73
|
-
# first-class invocation context
|
|
74
|
-
# @param on_tool_approval_required [Proc, nil] per-invocation approval
|
|
75
|
-
# notification listener
|
|
76
|
-
# @param on_event [Proc, nil] listener for lifecycle and Tool events
|
|
77
|
-
# @return [Phronomy::Task] Task resolving to the terminal result
|
|
78
|
-
# @api public
|
|
79
24
|
def invoke_async(
|
|
80
25
|
input,
|
|
81
|
-
messages: [],
|
|
82
26
|
thread_id: nil,
|
|
83
27
|
config: {},
|
|
84
28
|
invocation_context: nil,
|
|
@@ -86,123 +30,22 @@ module Phronomy
|
|
|
86
30
|
on_event: nil
|
|
87
31
|
)
|
|
88
32
|
if invocation_context
|
|
89
|
-
thread_id, config = _apply_invocation_context(
|
|
90
|
-
thread_id,
|
|
91
|
-
config,
|
|
92
|
-
invocation_context
|
|
93
|
-
)
|
|
33
|
+
thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
|
|
94
34
|
end
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
name:
|
|
98
|
-
"agent-#{(self.class.name || "anonymous").downcase}-async"
|
|
99
|
-
)
|
|
100
|
-
approval_snapshot = _approval_configuration_snapshot(
|
|
101
|
-
on_tool_approval_required
|
|
102
|
-
)
|
|
103
|
-
_start_invocation(
|
|
104
|
-
result_task,
|
|
35
|
+
approval = _approval_configuration_snapshot(on_tool_approval_required)
|
|
36
|
+
execution_coordinator.start(
|
|
105
37
|
input,
|
|
106
|
-
messages: messages,
|
|
107
38
|
thread_id: thread_id,
|
|
108
39
|
config: config,
|
|
109
|
-
approval_snapshot: approval_snapshot,
|
|
110
40
|
mode: :invoke,
|
|
41
|
+
approval_policy: approval[:policy],
|
|
42
|
+
approval_listener: approval[:listener],
|
|
111
43
|
on_event: on_event
|
|
112
44
|
)
|
|
113
|
-
result_task
|
|
114
45
|
end
|
|
115
46
|
|
|
116
|
-
# Invokes the agent asynchronously and emits streaming events.
|
|
117
|
-
#
|
|
118
|
-
# Accepts either +on_event:+ or a block. Supplying both is rejected.
|
|
119
|
-
# Token events are emitted only by streaming invocations; lifecycle and
|
|
120
|
-
# Tool events use the same contract as {#invoke_async}.
|
|
121
|
-
#
|
|
122
|
-
# @param input [String, Hash] user input for this invocation
|
|
123
|
-
# @param messages [Array<RubyLLM::Message>] conversation history
|
|
124
|
-
# @param thread_id [String, nil] conversation thread identifier
|
|
125
|
-
# @param config [Hash] additional runtime options
|
|
126
|
-
# @param invocation_context [Phronomy::InvocationContext, nil]
|
|
127
|
-
# first-class invocation context
|
|
128
|
-
# @param on_tool_approval_required [Proc, nil] per-invocation approval
|
|
129
|
-
# notification listener
|
|
130
|
-
# @param on_event [Proc, nil] event listener
|
|
131
|
-
# @yieldparam event [Phronomy::Agent::StreamEvent]
|
|
132
|
-
# @return [Phronomy::Task] Task resolving to the terminal result
|
|
133
|
-
# @raise [ArgumentError] when no listener is supplied or both listener
|
|
134
|
-
# forms are supplied
|
|
135
|
-
# @api public
|
|
136
|
-
def stream_async(
|
|
137
|
-
input,
|
|
138
|
-
messages: [],
|
|
139
|
-
thread_id: nil,
|
|
140
|
-
config: {},
|
|
141
|
-
invocation_context: nil,
|
|
142
|
-
on_tool_approval_required: nil,
|
|
143
|
-
on_event: nil,
|
|
144
|
-
&block
|
|
145
|
-
)
|
|
146
|
-
listener = resolve_event_listener(on_event, block)
|
|
147
|
-
unless listener
|
|
148
|
-
raise ArgumentError,
|
|
149
|
-
"stream_async requires on_event: or a block"
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
if invocation_context
|
|
153
|
-
thread_id, config = _apply_invocation_context(
|
|
154
|
-
thread_id,
|
|
155
|
-
config,
|
|
156
|
-
invocation_context
|
|
157
|
-
)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
result_task = Phronomy::Task.deferred(
|
|
161
|
-
name:
|
|
162
|
-
"agent-#{(self.class.name || "anonymous").downcase}" \
|
|
163
|
-
"-stream-async"
|
|
164
|
-
)
|
|
165
|
-
approval_snapshot = _approval_configuration_snapshot(
|
|
166
|
-
on_tool_approval_required
|
|
167
|
-
)
|
|
168
|
-
_start_invocation(
|
|
169
|
-
result_task,
|
|
170
|
-
input,
|
|
171
|
-
messages: messages,
|
|
172
|
-
thread_id: thread_id,
|
|
173
|
-
config: config,
|
|
174
|
-
approval_snapshot: approval_snapshot,
|
|
175
|
-
mode: :stream,
|
|
176
|
-
on_event: listener
|
|
177
|
-
)
|
|
178
|
-
result_task
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Invokes the agent synchronously and emits streaming events.
|
|
182
|
-
#
|
|
183
|
-
# Accepts either +on_event:+ or a block. Event callbacks run on the
|
|
184
|
-
# Runtime-owned EventLoop thread; the calling thread waits for the final
|
|
185
|
-
# Task result.
|
|
186
|
-
#
|
|
187
|
-
# @param input [String, Hash] user input for this invocation
|
|
188
|
-
# @param messages [Array<RubyLLM::Message>] conversation history
|
|
189
|
-
# @param thread_id [String, nil] conversation thread identifier
|
|
190
|
-
# @param config [Hash] additional runtime options
|
|
191
|
-
# @param invocation_context [Phronomy::InvocationContext, nil]
|
|
192
|
-
# first-class invocation context
|
|
193
|
-
# @param on_tool_approval_required [Proc, nil] per-invocation approval
|
|
194
|
-
# notification listener
|
|
195
|
-
# @param on_event [Proc, nil] event listener
|
|
196
|
-
# @yieldparam event [Phronomy::Agent::StreamEvent]
|
|
197
|
-
# @return [Hash] terminal invocation result
|
|
198
|
-
# @raise [ArgumentError] when no listener is supplied or both listener
|
|
199
|
-
# forms are supplied
|
|
200
|
-
# @raise [Phronomy::SchedulerReentrancyError] when called from the
|
|
201
|
-
# EventLoop thread or a guarded scheduler context
|
|
202
|
-
# @api public
|
|
203
47
|
def stream(
|
|
204
48
|
input,
|
|
205
|
-
messages: [],
|
|
206
49
|
thread_id: nil,
|
|
207
50
|
config: {},
|
|
208
51
|
invocation_context: nil,
|
|
@@ -211,342 +54,79 @@ module Phronomy
|
|
|
211
54
|
&block
|
|
212
55
|
)
|
|
213
56
|
listener = resolve_event_listener(on_event, block)
|
|
214
|
-
unless listener
|
|
215
|
-
raise ArgumentError,
|
|
216
|
-
"stream requires on_event: or a block"
|
|
217
|
-
end
|
|
218
|
-
|
|
57
|
+
raise ArgumentError, "stream requires on_event: or a block" unless listener
|
|
219
58
|
if invocation_context
|
|
220
|
-
thread_id, config = _apply_invocation_context(
|
|
221
|
-
thread_id,
|
|
222
|
-
config,
|
|
223
|
-
invocation_context
|
|
224
|
-
)
|
|
59
|
+
thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
|
|
225
60
|
end
|
|
226
61
|
_check_scheduler_reentrancy(:stream, :stream_async)
|
|
227
|
-
|
|
228
|
-
trace(
|
|
229
|
-
"agent.stream",
|
|
230
|
-
input: input,
|
|
231
|
-
**_build_caller_meta(config)
|
|
232
|
-
) do |_span|
|
|
62
|
+
trace("agent.stream", input: input, **_build_caller_meta(config)) do |_span|
|
|
233
63
|
result = stream_async(
|
|
234
64
|
input,
|
|
235
|
-
messages: messages,
|
|
236
65
|
thread_id: thread_id,
|
|
237
66
|
config: config,
|
|
238
|
-
on_tool_approval_required:
|
|
239
|
-
on_tool_approval_required,
|
|
67
|
+
on_tool_approval_required: on_tool_approval_required,
|
|
240
68
|
on_event: listener
|
|
241
69
|
).wait_result
|
|
242
70
|
[result, result[:usage]]
|
|
243
71
|
end
|
|
244
72
|
end
|
|
245
73
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def resolve_event_listener(keyword_listener, block_listener)
|
|
249
|
-
if keyword_listener && block_listener
|
|
250
|
-
raise ArgumentError,
|
|
251
|
-
"Provide either on_event: or a block, not both"
|
|
252
|
-
end
|
|
253
|
-
keyword_listener || block_listener
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Installs the Application listener before EventLoop admission.
|
|
257
|
-
# Cancellation is checked by the first Agent entry action on the EventLoop
|
|
258
|
-
# thread so even pre-cancelled invocations produce a terminal event before
|
|
259
|
-
# the returned Task settles.
|
|
260
|
-
# @api private
|
|
261
|
-
def _start_invocation(
|
|
262
|
-
result_task,
|
|
74
|
+
def stream_async(
|
|
263
75
|
input,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
)
|
|
271
|
-
effective_config =
|
|
272
|
-
thread_id ? config.merge(thread_id: thread_id) : config
|
|
273
|
-
runtime = Phronomy::Runtime.instance
|
|
274
|
-
event_loop = runtime.event_loop
|
|
275
|
-
session = Agent::AgentInvocationSessionBuilder.build(
|
|
276
|
-
agent: self,
|
|
277
|
-
input: input,
|
|
278
|
-
messages: messages,
|
|
279
|
-
config: effective_config,
|
|
280
|
-
approval_policy: approval_snapshot[:policy],
|
|
281
|
-
approval_listener: approval_snapshot[:listener],
|
|
282
|
-
mode: mode,
|
|
283
|
-
on_event: on_event,
|
|
284
|
-
runtime: runtime
|
|
285
|
-
)
|
|
286
|
-
callback_error_policy =
|
|
287
|
-
Phronomy.configuration.stream_callback_error_policy
|
|
288
|
-
source_task = Phronomy::Task.deferred(
|
|
289
|
-
name: "#{result_task.name}-source"
|
|
290
|
-
)
|
|
291
|
-
source_task.on_complete do |invocation, error|
|
|
292
|
-
completed_invocation = invocation || session.context
|
|
293
|
-
_handle_agent_completion(
|
|
294
|
-
result_task: result_task,
|
|
295
|
-
invocation: completed_invocation,
|
|
296
|
-
error: error,
|
|
297
|
-
mode: mode,
|
|
298
|
-
listener: on_event,
|
|
299
|
-
event_loop: event_loop,
|
|
300
|
-
callback_error_policy: callback_error_policy
|
|
301
|
-
)
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
event_loop.register(session, completion: source_task)
|
|
305
|
-
rescue => error
|
|
306
|
-
_fail_result_task(result_task, error)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def _handle_agent_completion(
|
|
310
|
-
result_task:,
|
|
311
|
-
invocation:,
|
|
312
|
-
error:,
|
|
313
|
-
mode:,
|
|
314
|
-
listener:,
|
|
315
|
-
event_loop:,
|
|
316
|
-
callback_error_policy:
|
|
76
|
+
thread_id: nil,
|
|
77
|
+
config: {},
|
|
78
|
+
invocation_context: nil,
|
|
79
|
+
on_tool_approval_required: nil,
|
|
80
|
+
on_event: nil,
|
|
81
|
+
&block
|
|
317
82
|
)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)
|
|
322
|
-
_fail_result_task(
|
|
323
|
-
result_task,
|
|
324
|
-
_translated_error(completion_error)
|
|
325
|
-
)
|
|
326
|
-
return
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
result = nil
|
|
330
|
-
execution_error = nil
|
|
331
|
-
begin
|
|
332
|
-
raise error if error
|
|
333
|
-
|
|
334
|
-
result = _extract_invoke_result(invocation)
|
|
335
|
-
rescue => caught
|
|
336
|
-
execution_error = _translated_error(caught)
|
|
337
|
-
end
|
|
338
|
-
if execution_error
|
|
339
|
-
execution_error = normalize_terminal_error(
|
|
340
|
-
execution_error,
|
|
341
|
-
invocation
|
|
342
|
-
)
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
if execution_error
|
|
346
|
-
terminal_event = StreamEvent.new(
|
|
347
|
-
type: terminal_error_event_type(execution_error),
|
|
348
|
-
payload: {error: execution_error}
|
|
349
|
-
)
|
|
350
|
-
callback_error = _deliver_stream_event(
|
|
351
|
-
listener,
|
|
352
|
-
terminal_event
|
|
353
|
-
)
|
|
354
|
-
if callback_error
|
|
355
|
-
report_agent_event_callback_error(
|
|
356
|
-
callback_error,
|
|
357
|
-
event: terminal_event,
|
|
358
|
-
invocation_id: invocation&.id,
|
|
359
|
-
callback_error_policy: callback_error_policy
|
|
360
|
-
)
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
# Listener failure never replaces an Agent execution failure.
|
|
364
|
-
_fail_result_task(result_task, execution_error)
|
|
365
|
-
return
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
terminal_event = _build_stream_terminal_event(result)
|
|
369
|
-
callback_error = _deliver_stream_event(
|
|
370
|
-
listener,
|
|
371
|
-
terminal_event
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
unless callback_error
|
|
375
|
-
_complete_result_task(result_task, result)
|
|
376
|
-
return
|
|
83
|
+
listener = resolve_event_listener(on_event, block)
|
|
84
|
+
raise ArgumentError, "stream_async requires on_event: or a block" unless listener
|
|
85
|
+
if invocation_context
|
|
86
|
+
thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
|
|
377
87
|
end
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
88
|
+
approval = _approval_configuration_snapshot(on_tool_approval_required)
|
|
89
|
+
execution_coordinator.start(
|
|
90
|
+
input,
|
|
91
|
+
thread_id: thread_id,
|
|
92
|
+
config: config,
|
|
93
|
+
mode: :stream,
|
|
94
|
+
approval_policy: approval[:policy],
|
|
95
|
+
approval_listener: approval[:listener],
|
|
96
|
+
on_event: listener
|
|
384
97
|
)
|
|
385
|
-
|
|
386
|
-
if callback_error_policy == :fail_task
|
|
387
|
-
wrapped = _build_stream_callback_error(
|
|
388
|
-
event_type: terminal_event.type,
|
|
389
|
-
callback_error: callback_error,
|
|
390
|
-
result: result
|
|
391
|
-
)
|
|
392
|
-
_fail_result_task(result_task, wrapped)
|
|
393
|
-
else
|
|
394
|
-
_complete_result_task(result_task, result)
|
|
395
|
-
end
|
|
396
98
|
end
|
|
397
99
|
|
|
398
|
-
def
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
end
|
|
100
|
+
def approve(execution_id, approval_request_id:, approved: true, config: {})
|
|
101
|
+
_check_scheduler_reentrancy(:approve, :approve_async)
|
|
102
|
+
approve_async(
|
|
103
|
+
execution_id,
|
|
104
|
+
approval_request_id: approval_request_id,
|
|
105
|
+
approved: approved,
|
|
106
|
+
config: config
|
|
107
|
+
).wait_result
|
|
407
108
|
end
|
|
408
109
|
|
|
409
|
-
def
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
timeout_error
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def invocation_timeout_expired?(invocation)
|
|
419
|
-
config = invocation&.config || {}
|
|
420
|
-
invocation_context = config[:invocation_context]
|
|
421
|
-
deadline = invocation_context&.deadline
|
|
422
|
-
return true if deadline&.expired?
|
|
423
|
-
|
|
424
|
-
token = config[:cancellation_token]
|
|
425
|
-
return false unless token
|
|
426
|
-
|
|
427
|
-
remaining =
|
|
428
|
-
if token.respond_to?(:remaining_monotonic_seconds)
|
|
429
|
-
token.remaining_monotonic_seconds
|
|
430
|
-
end
|
|
431
|
-
return true if remaining == 0.0
|
|
432
|
-
|
|
433
|
-
wall_deadline = token.deadline if token.respond_to?(:deadline)
|
|
434
|
-
wall_deadline && Time.now >= wall_deadline
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
def report_agent_event_callback_error(
|
|
438
|
-
callback_error,
|
|
439
|
-
event:,
|
|
440
|
-
invocation_id:,
|
|
441
|
-
callback_error_policy:
|
|
442
|
-
)
|
|
443
|
-
_report_stream_callback_error(
|
|
444
|
-
callback_error,
|
|
445
|
-
event: event,
|
|
446
|
-
invocation_id: invocation_id,
|
|
447
|
-
callback_error_policy: callback_error_policy
|
|
110
|
+
def approve_async(execution_id, approval_request_id:, approved: true, config: {})
|
|
111
|
+
execution_coordinator.resume(
|
|
112
|
+
execution_id,
|
|
113
|
+
approval_request_id: approval_request_id,
|
|
114
|
+
approved: approved,
|
|
115
|
+
config: config
|
|
448
116
|
)
|
|
449
117
|
end
|
|
450
118
|
|
|
451
|
-
|
|
452
|
-
event_loop,
|
|
453
|
-
runtime,
|
|
454
|
-
child,
|
|
455
|
-
session
|
|
456
|
-
)
|
|
457
|
-
completion = Phronomy::Task.deferred(
|
|
458
|
-
name: "tool-session:#{child.id}"
|
|
459
|
-
)
|
|
460
|
-
completion.on_complete do |_result, error|
|
|
461
|
-
next unless error
|
|
119
|
+
private
|
|
462
120
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
Phronomy::Event.new(
|
|
466
|
-
type: :tool_failed,
|
|
467
|
-
target_id: child.parent_agent_invocation_id,
|
|
468
|
-
payload: {tool_invocation_id: child.id}
|
|
469
|
-
)
|
|
470
|
-
)
|
|
471
|
-
end
|
|
472
|
-
event_loop.register(session, completion: completion)
|
|
121
|
+
def execution_coordinator
|
|
122
|
+
@execution_coordinator ||= Agent::ExecutionCoordinator.new(self)
|
|
473
123
|
end
|
|
474
124
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
# and the parent session is registered before child sessions so immediate
|
|
479
|
-
# child events cannot be lost.
|
|
480
|
-
# @api private
|
|
481
|
-
def _start_approval_resume(
|
|
482
|
-
result_task,
|
|
483
|
-
invocation,
|
|
484
|
-
approved:,
|
|
485
|
-
config:
|
|
486
|
-
)
|
|
487
|
-
invocation.merge_config!(config)
|
|
488
|
-
invocation.begin_approval_resume!(approved: approved)
|
|
489
|
-
runtime = Phronomy::Runtime.instance
|
|
490
|
-
event_loop = runtime.event_loop
|
|
491
|
-
source_task = Phronomy::Task.deferred(
|
|
492
|
-
name: "#{result_task.name}-source"
|
|
493
|
-
)
|
|
494
|
-
parent_session =
|
|
495
|
-
Agent::AgentInvocationSessionBuilder.build_for_resume(
|
|
496
|
-
agent_invocation: invocation,
|
|
497
|
-
resume_event: :resume,
|
|
498
|
-
resume_phase: :suspended,
|
|
499
|
-
runtime: runtime
|
|
500
|
-
)
|
|
501
|
-
listener = invocation.event_listener
|
|
502
|
-
mode = invocation.mode
|
|
503
|
-
callback_error_policy =
|
|
504
|
-
Phronomy.configuration.stream_callback_error_policy
|
|
505
|
-
|
|
506
|
-
source_task.on_complete do |completed_invocation, error|
|
|
507
|
-
invocation_result = completed_invocation || parent_session.context
|
|
508
|
-
_handle_agent_completion(
|
|
509
|
-
result_task: result_task,
|
|
510
|
-
invocation: invocation_result,
|
|
511
|
-
error: error,
|
|
512
|
-
mode: mode,
|
|
513
|
-
listener: listener,
|
|
514
|
-
event_loop: event_loop,
|
|
515
|
-
callback_error_policy: callback_error_policy
|
|
516
|
-
)
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
event_loop.register(
|
|
520
|
-
parent_session,
|
|
521
|
-
completion: source_task
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
invocation.tool_invocations.each do |child|
|
|
525
|
-
child_session =
|
|
526
|
-
if child.awaiting_approval?
|
|
527
|
-
Agent::ToolInvocationSessionBuilder.build_for_resume(
|
|
528
|
-
tool_invocation: child,
|
|
529
|
-
resume_event: approved ? :approve : :reject,
|
|
530
|
-
resume_phase: :awaiting_approval,
|
|
531
|
-
runtime: runtime
|
|
532
|
-
)
|
|
533
|
-
elsif !approved && child.authorized?
|
|
534
|
-
Agent::ToolInvocationSessionBuilder.build_for_resume(
|
|
535
|
-
tool_invocation: child,
|
|
536
|
-
resume_event: :cancel,
|
|
537
|
-
resume_phase: :authorized,
|
|
538
|
-
runtime: runtime
|
|
539
|
-
)
|
|
540
|
-
end
|
|
541
|
-
if child_session
|
|
542
|
-
_register_tool_invocation_session(
|
|
543
|
-
event_loop,
|
|
544
|
-
runtime,
|
|
545
|
-
child,
|
|
546
|
-
child_session
|
|
547
|
-
)
|
|
548
|
-
end
|
|
125
|
+
def resolve_event_listener(keyword_listener, block_listener)
|
|
126
|
+
if keyword_listener && block_listener
|
|
127
|
+
raise ArgumentError, "Provide either on_event: or a block, not both"
|
|
549
128
|
end
|
|
129
|
+
keyword_listener || block_listener
|
|
550
130
|
end
|
|
551
131
|
end
|
|
552
132
|
end
|