phronomy 0.16.0 → 0.18.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 +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +6 -7
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +89 -190
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +49 -377
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- 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/agent/fsm_runtime_adapter.rb +0 -210
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -265
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- 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
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -4,27 +4,9 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
module Agent
|
|
7
|
-
# Mutable state for one concrete ToolCall.
|
|
8
|
-
#
|
|
9
|
-
# The object owns Tool-domain state. FSMSession owns transitions, while
|
|
10
|
-
# ToolInvocationSessionBuilder converts Task completion into explicit
|
|
11
|
-
# Tool-internal events.
|
|
12
|
-
#
|
|
13
|
-
# @api private
|
|
14
7
|
class ToolInvocation
|
|
15
|
-
AuthorizationOutcome = Struct.new(
|
|
16
|
-
|
|
17
|
-
:facts,
|
|
18
|
-
:reason,
|
|
19
|
-
:error,
|
|
20
|
-
:cancelled
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
ExecutionOutcome = Struct.new(
|
|
24
|
-
:result,
|
|
25
|
-
:error,
|
|
26
|
-
:cancelled
|
|
27
|
-
)
|
|
8
|
+
AuthorizationOutcome = Struct.new(:decision, :facts, :reason, :error, :cancelled)
|
|
9
|
+
ExecutionOutcome = Struct.new(:result, :error, :cancelled)
|
|
28
10
|
|
|
29
11
|
PREFLIGHT_SETTLED_STATES = %i[
|
|
30
12
|
authorized awaiting_approval rejected failed cancelled completed
|
|
@@ -53,21 +35,14 @@ module Phronomy
|
|
|
53
35
|
:origin,
|
|
54
36
|
:metadata
|
|
55
37
|
|
|
56
|
-
def self.missing(
|
|
57
|
-
parent_agent_invocation_id:,
|
|
58
|
-
agent:,
|
|
59
|
-
tool_call:,
|
|
60
|
-
config: {}
|
|
61
|
-
)
|
|
38
|
+
def self.missing(parent_agent_invocation_id:, agent:, tool_call:, config: {})
|
|
62
39
|
new(
|
|
63
40
|
parent_agent_invocation_id: parent_agent_invocation_id,
|
|
64
41
|
agent: agent,
|
|
65
42
|
tool: nil,
|
|
66
43
|
tool_call: tool_call,
|
|
67
44
|
config: config
|
|
68
|
-
).tap
|
|
69
|
-
invocation.send(:complete_missing_tool!)
|
|
70
|
-
end
|
|
45
|
+
).tap { |invocation| invocation.send(:complete_missing_tool!) }
|
|
71
46
|
end
|
|
72
47
|
|
|
73
48
|
def initialize(
|
|
@@ -81,37 +56,20 @@ module Phronomy
|
|
|
81
56
|
id: SecureRandom.uuid
|
|
82
57
|
)
|
|
83
58
|
@id = id.to_s
|
|
84
|
-
@parent_agent_invocation_id =
|
|
85
|
-
parent_agent_invocation_id.to_s
|
|
59
|
+
@parent_agent_invocation_id = parent_agent_invocation_id.to_s
|
|
86
60
|
@agent = agent
|
|
87
61
|
@tool = tool
|
|
88
62
|
@tool_name = tool_call.name.to_s
|
|
89
|
-
@tool_call_id =
|
|
90
|
-
|
|
91
|
-
raw_arguments =
|
|
92
|
-
if tool_call.respond_to?(:arguments)
|
|
93
|
-
tool_call.arguments || {}
|
|
94
|
-
else
|
|
95
|
-
{}
|
|
96
|
-
end
|
|
63
|
+
@tool_call_id = tool_call.respond_to?(:id) ? tool_call.id : nil
|
|
64
|
+
raw_arguments = tool_call.respond_to?(:arguments) ? (tool_call.arguments || {}) : {}
|
|
97
65
|
@raw_arguments = immutable_copy(raw_arguments)
|
|
98
66
|
@config = config
|
|
99
67
|
@approval_policy = approval_policy
|
|
100
|
-
@approval_context =
|
|
101
|
-
|
|
102
|
-
@
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
else
|
|
106
|
-
:local
|
|
107
|
-
end
|
|
108
|
-
@metadata =
|
|
109
|
-
immutable_copy(
|
|
110
|
-
tool&.respond_to?(:approval_metadata) ?
|
|
111
|
-
tool.approval_metadata :
|
|
112
|
-
{}
|
|
113
|
-
)
|
|
114
|
-
|
|
68
|
+
@approval_context = immutable_copy(approval_context || {})
|
|
69
|
+
@origin = tool&.respond_to?(:tool_origin) ? tool.tool_origin.to_sym : :local
|
|
70
|
+
@metadata = immutable_copy(
|
|
71
|
+
tool&.respond_to?(:approval_metadata) ? tool.approval_metadata : {}
|
|
72
|
+
)
|
|
115
73
|
@arguments = nil
|
|
116
74
|
@facts = {}.freeze
|
|
117
75
|
@final_decision = nil
|
|
@@ -129,17 +87,11 @@ module Phronomy
|
|
|
129
87
|
@phase = phase
|
|
130
88
|
end
|
|
131
89
|
|
|
132
|
-
# Applies Tool-internal asynchronous completion events on the EventLoop
|
|
133
|
-
# thread. The return value tells FSMSession that the event was consumed by
|
|
134
|
-
# the context before its declared transition is evaluated.
|
|
135
90
|
def handle_fsm_event(event)
|
|
136
91
|
case event.type
|
|
137
92
|
when :authorization_completed
|
|
138
93
|
outcome = event.payload
|
|
139
|
-
if outcome.is_a?(Exception)
|
|
140
|
-
outcome =
|
|
141
|
-
AuthorizationOutcome.new(error: outcome)
|
|
142
|
-
end
|
|
94
|
+
outcome = AuthorizationOutcome.new(error: outcome) if outcome.is_a?(Exception)
|
|
143
95
|
apply_authorization_outcome(outcome)
|
|
144
96
|
true
|
|
145
97
|
when :execution_completed
|
|
@@ -157,41 +109,17 @@ module Phronomy
|
|
|
157
109
|
end
|
|
158
110
|
end
|
|
159
111
|
|
|
160
|
-
# Deprecated internal compatibility hook. FSMSession no longer calls
|
|
161
|
-
# this method; asynchronous results enter through explicit events.
|
|
162
|
-
def apply_fsm_action_result(outcome)
|
|
163
|
-
event_type =
|
|
164
|
-
case outcome
|
|
165
|
-
when AuthorizationOutcome
|
|
166
|
-
:authorization_completed
|
|
167
|
-
when ExecutionOutcome
|
|
168
|
-
:execution_completed
|
|
169
|
-
else
|
|
170
|
-
return self
|
|
171
|
-
end
|
|
172
|
-
handle_fsm_event(
|
|
173
|
-
Phronomy::Event.new(
|
|
174
|
-
type: event_type,
|
|
175
|
-
target_id: @id,
|
|
176
|
-
payload: outcome
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
self
|
|
180
|
-
end
|
|
181
|
-
|
|
182
112
|
def validate!
|
|
183
113
|
return self if terminal?
|
|
184
114
|
|
|
185
|
-
validated, schema_error =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
115
|
+
validated, schema_error = if @tool.respond_to?(:validate_and_coerce, true)
|
|
116
|
+
@tool.send(:validate_and_coerce, @raw_arguments)
|
|
117
|
+
else
|
|
118
|
+
[@raw_arguments, nil]
|
|
119
|
+
end
|
|
191
120
|
|
|
192
121
|
if schema_error
|
|
193
|
-
if @tool.class.respond_to?(:on_schema_error) &&
|
|
194
|
-
@tool.class.on_schema_error == :raise
|
|
122
|
+
if @tool.class.respond_to?(:on_schema_error) && @tool.class.on_schema_error == :raise
|
|
195
123
|
@error = Phronomy::ToolError.new(
|
|
196
124
|
"#{@tool.class.name} schema error: #{schema_error}"
|
|
197
125
|
)
|
|
@@ -212,7 +140,11 @@ module Phronomy
|
|
|
212
140
|
self
|
|
213
141
|
end
|
|
214
142
|
|
|
215
|
-
|
|
143
|
+
# Starts authorization and reports exactly one AuthorizationOutcome through
|
|
144
|
+
# the callback. No Task is created.
|
|
145
|
+
def start_authorization(runtime: Phronomy::Runtime.instance, &callback)
|
|
146
|
+
raise ArgumentError, "start_authorization requires a callback" unless callback
|
|
147
|
+
|
|
216
148
|
pool = runtime.pool(
|
|
217
149
|
:authorization,
|
|
218
150
|
size: Phronomy.configuration.authorization_pool_size,
|
|
@@ -222,167 +154,106 @@ module Phronomy
|
|
|
222
154
|
:authorization_timeout,
|
|
223
155
|
Phronomy.configuration.authorization_timeout
|
|
224
156
|
)
|
|
225
|
-
|
|
226
|
-
pending = pool.submit(
|
|
157
|
+
operation = pool.submit(
|
|
227
158
|
timeout: timeout,
|
|
228
|
-
cancellation_token: cancellation_token,
|
|
159
|
+
cancellation_token: @config[:cancellation_token],
|
|
229
160
|
on_full: :raise
|
|
230
|
-
)
|
|
231
|
-
|
|
161
|
+
) { evaluate_authorization }
|
|
162
|
+
operation.on_complete do |outcome, error|
|
|
163
|
+
callback.call(error ? authorization_failure_outcome(error) : outcome)
|
|
232
164
|
end
|
|
233
|
-
|
|
234
|
-
task = Phronomy::Task.deferred(
|
|
235
|
-
name: "tool-authorization:#{@tool_name}"
|
|
236
|
-
)
|
|
237
|
-
pending.on_complete do |outcome, error|
|
|
238
|
-
resolved =
|
|
239
|
-
if error
|
|
240
|
-
authorization_failure_outcome(error)
|
|
241
|
-
else
|
|
242
|
-
outcome
|
|
243
|
-
end
|
|
244
|
-
task.backend.unblock(resolved, nil)
|
|
245
|
-
task.transition!(:completed, value: resolved)
|
|
246
|
-
end
|
|
247
|
-
task
|
|
248
|
-
rescue => error
|
|
249
|
-
task = Phronomy::Task.deferred(
|
|
250
|
-
name: "tool-authorization:#{@tool_name}"
|
|
251
|
-
)
|
|
252
|
-
outcome = authorization_failure_outcome(error)
|
|
253
|
-
task.backend.unblock(outcome, nil)
|
|
254
|
-
task.transition!(:completed, value: outcome)
|
|
255
|
-
task
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def execution_task(runtime: Phronomy::Runtime.instance)
|
|
259
|
-
pending =
|
|
260
|
-
Phronomy::Agent::ToolExecutor.call_invocation_async(
|
|
261
|
-
tool_invocation: self,
|
|
262
|
-
cancellation_token: @config[:cancellation_token],
|
|
263
|
-
config: @config,
|
|
264
|
-
runtime: runtime
|
|
265
|
-
)
|
|
266
|
-
task = Phronomy::Task.deferred(
|
|
267
|
-
name: "tool-execution:#{@tool_name}"
|
|
268
|
-
)
|
|
269
|
-
pending.on_complete do |result, error|
|
|
270
|
-
outcome =
|
|
271
|
-
if error
|
|
272
|
-
ExecutionOutcome.new(
|
|
273
|
-
error: error,
|
|
274
|
-
cancelled: error.is_a?(
|
|
275
|
-
Phronomy::CancellationError
|
|
276
|
-
)
|
|
277
|
-
)
|
|
278
|
-
else
|
|
279
|
-
ExecutionOutcome.new(result: result)
|
|
280
|
-
end
|
|
281
|
-
task.backend.unblock(outcome, nil)
|
|
282
|
-
task.transition!(:completed, value: outcome)
|
|
283
|
-
end
|
|
284
|
-
task
|
|
165
|
+
self
|
|
285
166
|
rescue => error
|
|
286
|
-
|
|
287
|
-
name: "tool-execution:#{@tool_name}"
|
|
288
|
-
)
|
|
289
|
-
outcome = ExecutionOutcome.new(
|
|
290
|
-
error: error,
|
|
291
|
-
cancelled: error.is_a?(Phronomy::CancellationError)
|
|
292
|
-
)
|
|
293
|
-
task.backend.unblock(outcome, nil)
|
|
294
|
-
task.transition!(:completed, value: outcome)
|
|
295
|
-
task
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def mark_awaiting_approval!
|
|
299
|
-
@status = :awaiting_approval
|
|
167
|
+
callback.call(authorization_failure_outcome(error))
|
|
300
168
|
self
|
|
301
169
|
end
|
|
302
170
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
171
|
+
# Starts Tool execution and reports completion through the callback.
|
|
172
|
+
#
|
|
173
|
+
# Both core execution paths use Tool#call_async:
|
|
174
|
+
#
|
|
175
|
+
# - :cooperative returns a Task without consuming an OffloadPool worker.
|
|
176
|
+
# Ordinary cooperative Tools settle that Task inline; Agent-backed Tools
|
|
177
|
+
# may start child EventLoop/FSM work and settle later.
|
|
178
|
+
# - :offloaded returns an OffloadPool PendingOperation for synchronous work
|
|
179
|
+
# that must not occupy the EventLoop.
|
|
180
|
+
#
|
|
181
|
+
# In either case ToolInvocation remains in :running and resumes only from
|
|
182
|
+
# the explicit :execution_completed FSM event posted by the session builder.
|
|
183
|
+
def start_execution(runtime: Phronomy::Runtime.instance, &callback)
|
|
184
|
+
raise ArgumentError, "start_execution requires a callback" unless callback
|
|
185
|
+
unless dispatchable?
|
|
186
|
+
callback.call(ExecutionOutcome.new(error: Phronomy::ToolError.new(
|
|
187
|
+
"ToolInvocation #{@id} is not authorized for dispatch"
|
|
188
|
+
)))
|
|
189
|
+
return self
|
|
190
|
+
end
|
|
308
191
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
192
|
+
case @tool.class.execution_mode
|
|
193
|
+
when :cooperative, :offloaded
|
|
194
|
+
operation = start_async_tool_operation(runtime)
|
|
195
|
+
unless operation.respond_to?(:on_complete)
|
|
196
|
+
raise Phronomy::ToolError,
|
|
197
|
+
"Tool #{@tool.class.name}#call_async must return a completion handle"
|
|
198
|
+
end
|
|
313
199
|
|
|
314
|
-
|
|
315
|
-
|
|
200
|
+
operation.on_complete do |result, error|
|
|
201
|
+
callback.call(execution_outcome(result, error))
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
callback.call(ExecutionOutcome.new(error: Phronomy::ConfigurationError.new(
|
|
205
|
+
"unknown Tool execution_mode: #{@tool.class.execution_mode.inspect}"
|
|
206
|
+
)))
|
|
207
|
+
end
|
|
316
208
|
self
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def mark_rejected!
|
|
320
|
-
@final_decision = :reject
|
|
321
|
-
@status = :rejected
|
|
209
|
+
rescue => error
|
|
210
|
+
callback.call(execution_outcome(nil, error))
|
|
322
211
|
self
|
|
323
212
|
end
|
|
324
213
|
|
|
325
|
-
def
|
|
326
|
-
|
|
327
|
-
self
|
|
328
|
-
end
|
|
214
|
+
def mark_awaiting_approval! = (@status = :awaiting_approval
|
|
215
|
+
self)
|
|
329
216
|
|
|
330
|
-
def
|
|
331
|
-
@
|
|
332
|
-
@status = :
|
|
217
|
+
def mark_authorized!
|
|
218
|
+
@approval_consumed = true if @status == :awaiting_approval
|
|
219
|
+
@status = :authorized
|
|
333
220
|
self
|
|
334
221
|
end
|
|
335
222
|
|
|
336
|
-
def
|
|
337
|
-
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def validation_completed?
|
|
341
|
-
@status == :completed
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def failed?
|
|
345
|
-
@status == :failed
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def cancelled?
|
|
349
|
-
@status == :cancelled
|
|
350
|
-
end
|
|
223
|
+
def mark_queued! = (@status = :queued
|
|
224
|
+
self)
|
|
351
225
|
|
|
352
|
-
def
|
|
353
|
-
|
|
354
|
-
end
|
|
226
|
+
def mark_running! = (@status = :running
|
|
227
|
+
self)
|
|
355
228
|
|
|
356
|
-
def
|
|
357
|
-
|
|
358
|
-
|
|
229
|
+
def mark_rejected! = (@final_decision = :reject
|
|
230
|
+
@status = :rejected
|
|
231
|
+
self)
|
|
359
232
|
|
|
360
|
-
def
|
|
361
|
-
|
|
362
|
-
end
|
|
233
|
+
def mark_cancelled! = (@status = :cancelled
|
|
234
|
+
self)
|
|
363
235
|
|
|
364
|
-
def
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def preflight_settled?
|
|
369
|
-
PREFLIGHT_SETTLED_STATES.include?(@status)
|
|
370
|
-
end
|
|
236
|
+
def mark_framework_failed!(error) = (@error = error
|
|
237
|
+
@status = :failed
|
|
238
|
+
self)
|
|
371
239
|
|
|
372
|
-
def
|
|
373
|
-
|
|
374
|
-
|
|
240
|
+
def validation_passed? = @status == :valid
|
|
241
|
+
def validation_completed? = @status == :completed
|
|
242
|
+
def failed? = @status == :failed
|
|
243
|
+
def cancelled? = @status == :cancelled
|
|
244
|
+
def rejected? = @status == :rejected
|
|
245
|
+
def awaiting_approval? = @status == :awaiting_approval
|
|
246
|
+
def authorized? = @status == :authorized
|
|
247
|
+
def execution_completed? = @status == :completed
|
|
248
|
+
def preflight_settled? = PREFLIGHT_SETTLED_STATES.include?(@status)
|
|
249
|
+
def terminal? = TERMINAL_STATES.include?(@status)
|
|
375
250
|
|
|
376
251
|
def dispatchable?
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
@final_decision == :allow || @approval_consumed
|
|
252
|
+
@status == :queued && (@final_decision == :allow || @approval_consumed)
|
|
380
253
|
end
|
|
381
254
|
|
|
382
255
|
def tool_schema
|
|
383
|
-
@tool&.respond_to?(:params_schema) ?
|
|
384
|
-
@tool.params_schema :
|
|
385
|
-
{}
|
|
256
|
+
@tool&.respond_to?(:params_schema) ? @tool.params_schema : {}
|
|
386
257
|
end
|
|
387
258
|
|
|
388
259
|
def display_arguments
|
|
@@ -390,94 +261,95 @@ module Phronomy
|
|
|
390
261
|
end
|
|
391
262
|
|
|
392
263
|
def display_facts
|
|
393
|
-
|
|
394
|
-
redact_value(@facts, sensitive_values)
|
|
264
|
+
redact_value(@facts, sensitive_argument_values)
|
|
395
265
|
end
|
|
396
266
|
|
|
397
267
|
private
|
|
398
268
|
|
|
269
|
+
# Runtime is framework execution infrastructure, not part of the public
|
|
270
|
+
# Tool#call_async protocol.
|
|
271
|
+
#
|
|
272
|
+
# Tools using Capability::Base's default async implementation are routed
|
|
273
|
+
# directly through ToolExecutor so this ToolInvocation can supply its
|
|
274
|
+
# owning Runtime internally. Tools that override #call_async (for example
|
|
275
|
+
# Agent-backed Tools) receive only the public Tool async keywords.
|
|
276
|
+
def start_async_tool_operation(runtime)
|
|
277
|
+
if uses_default_call_async?
|
|
278
|
+
Phronomy::Agent::ToolExecutor.call_async(
|
|
279
|
+
tool: @tool,
|
|
280
|
+
args: @arguments,
|
|
281
|
+
cancellation_token: @config[:cancellation_token],
|
|
282
|
+
config: @config,
|
|
283
|
+
runtime: runtime,
|
|
284
|
+
on_full: :raise
|
|
285
|
+
)
|
|
286
|
+
else
|
|
287
|
+
@tool.call_async(
|
|
288
|
+
@arguments,
|
|
289
|
+
cancellation_token: @config[:cancellation_token],
|
|
290
|
+
config: @config
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def uses_default_call_async?
|
|
296
|
+
@tool.method(:call_async).owner ==
|
|
297
|
+
Phronomy::Agent::Context::Capability::Base
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def execution_outcome(result, error)
|
|
301
|
+
if error
|
|
302
|
+
ExecutionOutcome.new(
|
|
303
|
+
error: error,
|
|
304
|
+
cancelled: error.is_a?(Phronomy::CancellationError)
|
|
305
|
+
)
|
|
306
|
+
else
|
|
307
|
+
ExecutionOutcome.new(result: result)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
399
311
|
def evaluate_authorization
|
|
400
|
-
request = build_request(
|
|
401
|
-
facts: {},
|
|
402
|
-
default_decision: nil
|
|
403
|
-
)
|
|
312
|
+
request = build_request(facts: {}, default_decision: nil)
|
|
404
313
|
facts = evaluate_facts
|
|
405
314
|
request = request.with(facts: facts)
|
|
406
315
|
default_decision = evaluate_default_decision(request)
|
|
407
|
-
request = request.with(
|
|
408
|
-
|
|
409
|
-
)
|
|
410
|
-
decision =
|
|
411
|
-
if @approval_policy
|
|
412
|
-
@approval_policy.call(request)
|
|
413
|
-
else
|
|
414
|
-
default_decision
|
|
415
|
-
end
|
|
316
|
+
request = request.with(default_decision: default_decision)
|
|
317
|
+
decision = @approval_policy ? @approval_policy.call(request) : default_decision
|
|
416
318
|
decision = decision.to_sym if decision.respond_to?(:to_sym)
|
|
417
319
|
|
|
418
|
-
unless ApprovalEvaluationRequest::VALID_DECISIONS
|
|
419
|
-
.include?(decision)
|
|
320
|
+
unless ApprovalEvaluationRequest::VALID_DECISIONS.include?(decision)
|
|
420
321
|
raise Phronomy::ConfigurationError,
|
|
421
|
-
"tool_approval_policy must return :allow, " \
|
|
422
|
-
":require_approval, or :reject " \
|
|
322
|
+
"tool_approval_policy must return :allow, :require_approval, or :reject " \
|
|
423
323
|
"(got #{decision.inspect})"
|
|
424
324
|
end
|
|
425
325
|
|
|
426
|
-
reason =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
else
|
|
431
|
-
"Tool execution requires approval"
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
AuthorizationOutcome.new(
|
|
436
|
-
decision: decision,
|
|
437
|
-
facts: facts,
|
|
438
|
-
reason: reason
|
|
439
|
-
)
|
|
326
|
+
reason = if decision == :require_approval
|
|
327
|
+
(@origin == :mcp) ? "MCP Tool execution requires approval" : "Tool execution requires approval"
|
|
328
|
+
end
|
|
329
|
+
AuthorizationOutcome.new(decision: decision, facts: facts, reason: reason)
|
|
440
330
|
end
|
|
441
331
|
|
|
442
332
|
def evaluate_facts
|
|
443
|
-
callable =
|
|
444
|
-
if @tool.class.respond_to?(:approval_facts)
|
|
445
|
-
@tool.class.approval_facts
|
|
446
|
-
end
|
|
333
|
+
callable = @tool.class.approval_facts if @tool.class.respond_to?(:approval_facts)
|
|
447
334
|
return {} unless callable
|
|
448
335
|
|
|
449
|
-
value = callable.call(
|
|
450
|
-
@arguments,
|
|
451
|
-
@approval_context
|
|
452
|
-
)
|
|
336
|
+
value = callable.call(@arguments, @approval_context)
|
|
453
337
|
unless value.nil? || value.is_a?(Hash)
|
|
454
338
|
raise Phronomy::ConfigurationError,
|
|
455
|
-
"approval_facts must return a Hash or nil "
|
|
456
|
-
"(got #{value.class})"
|
|
339
|
+
"approval_facts must return a Hash or nil (got #{value.class})"
|
|
457
340
|
end
|
|
458
341
|
immutable_copy(value || {})
|
|
459
342
|
end
|
|
460
343
|
|
|
461
344
|
def evaluate_default_decision(request)
|
|
462
|
-
requirement =
|
|
463
|
-
|
|
464
|
-
@tool.requires_approval
|
|
465
|
-
else
|
|
466
|
-
false
|
|
467
|
-
end
|
|
468
|
-
if requirement.respond_to?(:call)
|
|
469
|
-
requirement = requirement.call(request)
|
|
470
|
-
end
|
|
471
|
-
|
|
345
|
+
requirement = @tool.respond_to?(:requires_approval) ? @tool.requires_approval : false
|
|
346
|
+
requirement = requirement.call(request) if requirement.respond_to?(:call)
|
|
472
347
|
case requirement
|
|
473
|
-
when true
|
|
474
|
-
|
|
475
|
-
when false, nil
|
|
476
|
-
:allow
|
|
348
|
+
when true then :require_approval
|
|
349
|
+
when false, nil then :allow
|
|
477
350
|
else
|
|
478
351
|
raise Phronomy::ConfigurationError,
|
|
479
|
-
"requires_approval callable must return true or false "
|
|
480
|
-
"(got #{requirement.inspect})"
|
|
352
|
+
"requires_approval callable must return true or false (got #{requirement.inspect})"
|
|
481
353
|
end
|
|
482
354
|
end
|
|
483
355
|
|
|
@@ -506,15 +378,10 @@ module Phronomy
|
|
|
506
378
|
AuthorizationOutcome.new(
|
|
507
379
|
decision: :require_approval,
|
|
508
380
|
facts: {},
|
|
509
|
-
reason:
|
|
510
|
-
"Authorization could not be completed safely: " \
|
|
511
|
-
"#{error.message}"
|
|
381
|
+
reason: "Authorization could not be completed safely: #{error.message}"
|
|
512
382
|
)
|
|
513
383
|
elsif error.is_a?(Phronomy::CancellationError)
|
|
514
|
-
AuthorizationOutcome.new(
|
|
515
|
-
error: error,
|
|
516
|
-
cancelled: true
|
|
517
|
-
)
|
|
384
|
+
AuthorizationOutcome.new(error: error, cancelled: true)
|
|
518
385
|
else
|
|
519
386
|
AuthorizationOutcome.new(error: error)
|
|
520
387
|
end
|
|
@@ -522,48 +389,38 @@ module Phronomy
|
|
|
522
389
|
|
|
523
390
|
def apply_authorization_outcome(outcome)
|
|
524
391
|
unless outcome.is_a?(AuthorizationOutcome)
|
|
525
|
-
raise Phronomy::Error,
|
|
526
|
-
"Expected AuthorizationOutcome, got #{outcome.class}"
|
|
392
|
+
raise Phronomy::Error, "Expected AuthorizationOutcome, got #{outcome.class}"
|
|
527
393
|
end
|
|
528
|
-
|
|
529
394
|
@facts = immutable_copy(outcome.facts || {})
|
|
530
395
|
@authorization_reason = outcome.reason
|
|
531
396
|
@error = outcome.error
|
|
532
|
-
|
|
533
397
|
if outcome.cancelled
|
|
534
398
|
@status = :cancelled
|
|
535
399
|
elsif outcome.error
|
|
536
400
|
@status = :failed
|
|
537
401
|
else
|
|
538
402
|
@final_decision = outcome.decision
|
|
539
|
-
@status =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
:awaiting_approval
|
|
545
|
-
when :reject
|
|
546
|
-
:rejected
|
|
547
|
-
end
|
|
403
|
+
@status = case outcome.decision
|
|
404
|
+
when :allow then :authorized
|
|
405
|
+
when :require_approval then :awaiting_approval
|
|
406
|
+
when :reject then :rejected
|
|
407
|
+
end
|
|
548
408
|
end
|
|
549
409
|
end
|
|
550
410
|
|
|
551
411
|
def apply_execution_outcome(outcome)
|
|
552
412
|
unless outcome.is_a?(ExecutionOutcome)
|
|
553
|
-
raise Phronomy::Error,
|
|
554
|
-
"Expected ExecutionOutcome, got #{outcome.class}"
|
|
413
|
+
raise Phronomy::Error, "Expected ExecutionOutcome, got #{outcome.class}"
|
|
555
414
|
end
|
|
556
|
-
|
|
557
415
|
@result = outcome.result
|
|
558
416
|
@error = outcome.error
|
|
559
|
-
@status =
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
end
|
|
417
|
+
@status = if outcome.cancelled
|
|
418
|
+
:cancelled
|
|
419
|
+
elsif outcome.error
|
|
420
|
+
:failed
|
|
421
|
+
else
|
|
422
|
+
:completed
|
|
423
|
+
end
|
|
567
424
|
end
|
|
568
425
|
|
|
569
426
|
def complete_missing_tool!
|
|
@@ -575,8 +432,7 @@ module Phronomy
|
|
|
575
432
|
case value
|
|
576
433
|
when Hash
|
|
577
434
|
value.each_with_object({}) do |(key, item), result|
|
|
578
|
-
result[immutable_copy(key)] =
|
|
579
|
-
immutable_copy(item)
|
|
435
|
+
result[immutable_copy(key)] = immutable_copy(item)
|
|
580
436
|
end.freeze
|
|
581
437
|
when Array
|
|
582
438
|
value.map { |item| immutable_copy(item) }.freeze
|
|
@@ -589,9 +445,7 @@ module Phronomy
|
|
|
589
445
|
|
|
590
446
|
def redact_for_display(value)
|
|
591
447
|
if @tool&.respond_to?(:redacted_args, true)
|
|
592
|
-
immutable_copy(
|
|
593
|
-
@tool.send(:redacted_args, value || {})
|
|
594
|
-
)
|
|
448
|
+
immutable_copy(@tool.send(:redacted_args, value || {}))
|
|
595
449
|
else
|
|
596
450
|
immutable_copy(value || {})
|
|
597
451
|
end
|
|
@@ -599,30 +453,20 @@ module Phronomy
|
|
|
599
453
|
|
|
600
454
|
def sensitive_argument_values
|
|
601
455
|
return [] unless @tool&.class&.respond_to?(:redact_params)
|
|
602
|
-
|
|
603
|
-
normalized
|
|
604
|
-
(@arguments || @raw_arguments || {})
|
|
605
|
-
.transform_keys(&:to_sym)
|
|
606
|
-
@tool.class.redact_params.filter_map do |name|
|
|
607
|
-
normalized[name]
|
|
608
|
-
end
|
|
456
|
+
normalized = (@arguments || @raw_arguments || {}).transform_keys(&:to_sym)
|
|
457
|
+
@tool.class.redact_params.filter_map { |name| normalized[name] }
|
|
609
458
|
end
|
|
610
459
|
|
|
611
460
|
def redact_value(value, sensitive_values)
|
|
612
|
-
if sensitive_values.any? { |sensitive| sensitive == value }
|
|
613
|
-
return "[REDACTED]"
|
|
614
|
-
end
|
|
461
|
+
return "[REDACTED]" if sensitive_values.any? { |sensitive| sensitive == value }
|
|
615
462
|
|
|
616
463
|
case value
|
|
617
464
|
when Hash
|
|
618
465
|
value.each_with_object({}) do |(key, item), result|
|
|
619
|
-
result[key] =
|
|
620
|
-
redact_value(item, sensitive_values)
|
|
466
|
+
result[key] = redact_value(item, sensitive_values)
|
|
621
467
|
end.freeze
|
|
622
468
|
when Array
|
|
623
|
-
value.map
|
|
624
|
-
redact_value(item, sensitive_values)
|
|
625
|
-
end.freeze
|
|
469
|
+
value.map { |item| redact_value(item, sensitive_values) }.freeze
|
|
626
470
|
when String
|
|
627
471
|
"[REDACTED]"
|
|
628
472
|
else
|