phronomy 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.mutant.yml +8 -9
- data/CHANGELOG.md +54 -0
- data/CONTRIBUTING.md +28 -16
- data/README.md +124 -92
- 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 +1 -1
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +156 -93
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/base.rb +135 -314
- data/lib/phronomy/agent/context/capability/base.rb +166 -297
- 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 +0 -1
- 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 +46 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_invocation.rb +108 -314
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +15 -158
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
- data/lib/phronomy/engine/runtime.rb +15 -230
- data/lib/phronomy/engine/task_group.rb +30 -102
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/multi_agent/orchestrator.rb +152 -204
- data/lib/phronomy/multi_agent/team_coordinator.rb +42 -133
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +3 -120
- data/scripts/api_snapshot.rb +1 -12
- metadata +3 -9
- 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/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
|
@@ -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,37 +109,14 @@ 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
122
|
if @tool.class.respond_to?(:on_schema_error) &&
|
|
@@ -218,37 +147,22 @@ module Phronomy
|
|
|
218
147
|
size: Phronomy.configuration.authorization_pool_size,
|
|
219
148
|
queue_size: Phronomy.configuration.authorization_queue_size
|
|
220
149
|
)
|
|
221
|
-
timeout = @config.fetch(
|
|
222
|
-
:authorization_timeout,
|
|
223
|
-
Phronomy.configuration.authorization_timeout
|
|
224
|
-
)
|
|
225
|
-
cancellation_token = @config[:cancellation_token]
|
|
150
|
+
timeout = @config.fetch(:authorization_timeout, Phronomy.configuration.authorization_timeout)
|
|
226
151
|
pending = pool.submit(
|
|
227
152
|
timeout: timeout,
|
|
228
|
-
cancellation_token: cancellation_token,
|
|
153
|
+
cancellation_token: @config[:cancellation_token],
|
|
229
154
|
on_full: :raise
|
|
230
|
-
)
|
|
231
|
-
evaluate_authorization
|
|
232
|
-
end
|
|
155
|
+
) { evaluate_authorization }
|
|
233
156
|
|
|
234
|
-
task = Phronomy::Task.deferred(
|
|
235
|
-
name: "tool-authorization:#{@tool_name}"
|
|
236
|
-
)
|
|
157
|
+
task = Phronomy::Task.deferred(name: "tool-authorization:#{@tool_name}")
|
|
237
158
|
pending.on_complete do |outcome, error|
|
|
238
|
-
resolved =
|
|
239
|
-
if error
|
|
240
|
-
authorization_failure_outcome(error)
|
|
241
|
-
else
|
|
242
|
-
outcome
|
|
243
|
-
end
|
|
159
|
+
resolved = error ? authorization_failure_outcome(error) : outcome
|
|
244
160
|
task.backend.unblock(resolved, nil)
|
|
245
161
|
task.transition!(:completed, value: resolved)
|
|
246
162
|
end
|
|
247
163
|
task
|
|
248
164
|
rescue => error
|
|
249
|
-
task = Phronomy::Task.deferred(
|
|
250
|
-
name: "tool-authorization:#{@tool_name}"
|
|
251
|
-
)
|
|
165
|
+
task = Phronomy::Task.deferred(name: "tool-authorization:#{@tool_name}")
|
|
252
166
|
outcome = authorization_failure_outcome(error)
|
|
253
167
|
task.backend.unblock(outcome, nil)
|
|
254
168
|
task.transition!(:completed, value: outcome)
|
|
@@ -256,36 +170,28 @@ module Phronomy
|
|
|
256
170
|
end
|
|
257
171
|
|
|
258
172
|
def execution_task(runtime: Phronomy::Runtime.instance)
|
|
259
|
-
pending =
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
runtime: runtime
|
|
265
|
-
)
|
|
266
|
-
task = Phronomy::Task.deferred(
|
|
267
|
-
name: "tool-execution:#{@tool_name}"
|
|
173
|
+
pending = Phronomy::Agent::ToolExecutor.call_invocation_async(
|
|
174
|
+
tool_invocation: self,
|
|
175
|
+
cancellation_token: @config[:cancellation_token],
|
|
176
|
+
config: @config,
|
|
177
|
+
runtime: runtime
|
|
268
178
|
)
|
|
179
|
+
task = Phronomy::Task.deferred(name: "tool-execution:#{@tool_name}")
|
|
269
180
|
pending.on_complete do |result, error|
|
|
270
|
-
outcome =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
else
|
|
279
|
-
ExecutionOutcome.new(result: result)
|
|
280
|
-
end
|
|
181
|
+
outcome = if error
|
|
182
|
+
ExecutionOutcome.new(
|
|
183
|
+
error: error,
|
|
184
|
+
cancelled: error.is_a?(Phronomy::CancellationError)
|
|
185
|
+
)
|
|
186
|
+
else
|
|
187
|
+
ExecutionOutcome.new(result: result)
|
|
188
|
+
end
|
|
281
189
|
task.backend.unblock(outcome, nil)
|
|
282
190
|
task.transition!(:completed, value: outcome)
|
|
283
191
|
end
|
|
284
192
|
task
|
|
285
193
|
rescue => error
|
|
286
|
-
task = Phronomy::Task.deferred(
|
|
287
|
-
name: "tool-execution:#{@tool_name}"
|
|
288
|
-
)
|
|
194
|
+
task = Phronomy::Task.deferred(name: "tool-execution:#{@tool_name}")
|
|
289
195
|
outcome = ExecutionOutcome.new(
|
|
290
196
|
error: error,
|
|
291
197
|
cancelled: error.is_a?(Phronomy::CancellationError)
|
|
@@ -295,10 +201,8 @@ module Phronomy
|
|
|
295
201
|
task
|
|
296
202
|
end
|
|
297
203
|
|
|
298
|
-
def mark_awaiting_approval!
|
|
299
|
-
|
|
300
|
-
self
|
|
301
|
-
end
|
|
204
|
+
def mark_awaiting_approval! = (@status = :awaiting_approval
|
|
205
|
+
self)
|
|
302
206
|
|
|
303
207
|
def mark_authorized!
|
|
304
208
|
@approval_consumed = true if @status == :awaiting_approval
|
|
@@ -306,83 +210,40 @@ module Phronomy
|
|
|
306
210
|
self
|
|
307
211
|
end
|
|
308
212
|
|
|
309
|
-
def mark_queued!
|
|
310
|
-
|
|
311
|
-
self
|
|
312
|
-
end
|
|
213
|
+
def mark_queued! = (@status = :queued
|
|
214
|
+
self)
|
|
313
215
|
|
|
314
|
-
def mark_running!
|
|
315
|
-
|
|
316
|
-
self
|
|
317
|
-
end
|
|
216
|
+
def mark_running! = (@status = :running
|
|
217
|
+
self)
|
|
318
218
|
|
|
319
|
-
def mark_rejected!
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
self
|
|
323
|
-
end
|
|
219
|
+
def mark_rejected! = (@final_decision = :reject
|
|
220
|
+
@status = :rejected
|
|
221
|
+
self)
|
|
324
222
|
|
|
325
|
-
def mark_cancelled!
|
|
326
|
-
|
|
327
|
-
self
|
|
328
|
-
end
|
|
223
|
+
def mark_cancelled! = (@status = :cancelled
|
|
224
|
+
self)
|
|
329
225
|
|
|
330
|
-
def mark_framework_failed!(error)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
self
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
def validation_passed?
|
|
337
|
-
@status == :valid
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def validation_completed?
|
|
341
|
-
@status == :completed
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def failed?
|
|
345
|
-
@status == :failed
|
|
346
|
-
end
|
|
226
|
+
def mark_framework_failed!(error) = (@error = error
|
|
227
|
+
@status = :failed
|
|
228
|
+
self)
|
|
347
229
|
|
|
348
|
-
def
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def rejected?
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def
|
|
357
|
-
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
def authorized?
|
|
361
|
-
@status == :authorized
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def execution_completed?
|
|
365
|
-
@status == :completed
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def preflight_settled?
|
|
369
|
-
PREFLIGHT_SETTLED_STATES.include?(@status)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
def terminal?
|
|
373
|
-
TERMINAL_STATES.include?(@status)
|
|
374
|
-
end
|
|
230
|
+
def validation_passed? = @status == :valid
|
|
231
|
+
def validation_completed? = @status == :completed
|
|
232
|
+
def failed? = @status == :failed
|
|
233
|
+
def cancelled? = @status == :cancelled
|
|
234
|
+
def rejected? = @status == :rejected
|
|
235
|
+
def awaiting_approval? = @status == :awaiting_approval
|
|
236
|
+
def authorized? = @status == :authorized
|
|
237
|
+
def execution_completed? = @status == :completed
|
|
238
|
+
def preflight_settled? = PREFLIGHT_SETTLED_STATES.include?(@status)
|
|
239
|
+
def terminal? = TERMINAL_STATES.include?(@status)
|
|
375
240
|
|
|
376
241
|
def dispatchable?
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
@final_decision == :allow || @approval_consumed
|
|
242
|
+
@status == :queued && (@final_decision == :allow || @approval_consumed)
|
|
380
243
|
end
|
|
381
244
|
|
|
382
245
|
def tool_schema
|
|
383
|
-
@tool&.respond_to?(:params_schema) ?
|
|
384
|
-
@tool.params_schema :
|
|
385
|
-
{}
|
|
246
|
+
@tool&.respond_to?(:params_schema) ? @tool.params_schema : {}
|
|
386
247
|
end
|
|
387
248
|
|
|
388
249
|
def display_arguments
|
|
@@ -390,94 +251,55 @@ module Phronomy
|
|
|
390
251
|
end
|
|
391
252
|
|
|
392
253
|
def display_facts
|
|
393
|
-
|
|
394
|
-
redact_value(@facts, sensitive_values)
|
|
254
|
+
redact_value(@facts, sensitive_argument_values)
|
|
395
255
|
end
|
|
396
256
|
|
|
397
257
|
private
|
|
398
258
|
|
|
399
259
|
def evaluate_authorization
|
|
400
|
-
request = build_request(
|
|
401
|
-
facts: {},
|
|
402
|
-
default_decision: nil
|
|
403
|
-
)
|
|
260
|
+
request = build_request(facts: {}, default_decision: nil)
|
|
404
261
|
facts = evaluate_facts
|
|
405
262
|
request = request.with(facts: facts)
|
|
406
263
|
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
|
|
264
|
+
request = request.with(default_decision: default_decision)
|
|
265
|
+
decision = @approval_policy ? @approval_policy.call(request) : default_decision
|
|
416
266
|
decision = decision.to_sym if decision.respond_to?(:to_sym)
|
|
417
267
|
|
|
418
|
-
unless ApprovalEvaluationRequest::VALID_DECISIONS
|
|
419
|
-
.include?(decision)
|
|
268
|
+
unless ApprovalEvaluationRequest::VALID_DECISIONS.include?(decision)
|
|
420
269
|
raise Phronomy::ConfigurationError,
|
|
421
|
-
"tool_approval_policy must return :allow, " \
|
|
422
|
-
":require_approval, or :reject " \
|
|
270
|
+
"tool_approval_policy must return :allow, :require_approval, or :reject " \
|
|
423
271
|
"(got #{decision.inspect})"
|
|
424
272
|
end
|
|
425
273
|
|
|
426
|
-
reason =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
AuthorizationOutcome.new(
|
|
436
|
-
decision: decision,
|
|
437
|
-
facts: facts,
|
|
438
|
-
reason: reason
|
|
439
|
-
)
|
|
274
|
+
reason = if decision == :require_approval
|
|
275
|
+
(@origin == :mcp) ?
|
|
276
|
+
"MCP Tool execution requires approval" :
|
|
277
|
+
"Tool execution requires approval"
|
|
278
|
+
end
|
|
279
|
+
AuthorizationOutcome.new(decision: decision, facts: facts, reason: reason)
|
|
440
280
|
end
|
|
441
281
|
|
|
442
282
|
def evaluate_facts
|
|
443
|
-
callable =
|
|
444
|
-
if @tool.class.respond_to?(:approval_facts)
|
|
445
|
-
@tool.class.approval_facts
|
|
446
|
-
end
|
|
283
|
+
callable = @tool.class.approval_facts if @tool.class.respond_to?(:approval_facts)
|
|
447
284
|
return {} unless callable
|
|
448
285
|
|
|
449
|
-
value = callable.call(
|
|
450
|
-
@arguments,
|
|
451
|
-
@approval_context
|
|
452
|
-
)
|
|
286
|
+
value = callable.call(@arguments, @approval_context)
|
|
453
287
|
unless value.nil? || value.is_a?(Hash)
|
|
454
288
|
raise Phronomy::ConfigurationError,
|
|
455
|
-
"approval_facts must return a Hash or nil "
|
|
456
|
-
"(got #{value.class})"
|
|
289
|
+
"approval_facts must return a Hash or nil (got #{value.class})"
|
|
457
290
|
end
|
|
458
291
|
immutable_copy(value || {})
|
|
459
292
|
end
|
|
460
293
|
|
|
461
294
|
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
|
-
|
|
295
|
+
requirement = @tool.respond_to?(:requires_approval) ? @tool.requires_approval : false
|
|
296
|
+
requirement = requirement.call(request) if requirement.respond_to?(:call)
|
|
472
297
|
case requirement
|
|
473
|
-
when true
|
|
474
|
-
|
|
475
|
-
when false, nil
|
|
476
|
-
:allow
|
|
298
|
+
when true then :require_approval
|
|
299
|
+
when false, nil then :allow
|
|
477
300
|
else
|
|
478
301
|
raise Phronomy::ConfigurationError,
|
|
479
|
-
"requires_approval callable must return true or false "
|
|
480
|
-
"(got #{requirement.inspect})"
|
|
302
|
+
"requires_approval callable must return true or false (got #{requirement.inspect})"
|
|
481
303
|
end
|
|
482
304
|
end
|
|
483
305
|
|
|
@@ -506,15 +328,10 @@ module Phronomy
|
|
|
506
328
|
AuthorizationOutcome.new(
|
|
507
329
|
decision: :require_approval,
|
|
508
330
|
facts: {},
|
|
509
|
-
reason:
|
|
510
|
-
"Authorization could not be completed safely: " \
|
|
511
|
-
"#{error.message}"
|
|
331
|
+
reason: "Authorization could not be completed safely: #{error.message}"
|
|
512
332
|
)
|
|
513
333
|
elsif error.is_a?(Phronomy::CancellationError)
|
|
514
|
-
AuthorizationOutcome.new(
|
|
515
|
-
error: error,
|
|
516
|
-
cancelled: true
|
|
517
|
-
)
|
|
334
|
+
AuthorizationOutcome.new(error: error, cancelled: true)
|
|
518
335
|
else
|
|
519
336
|
AuthorizationOutcome.new(error: error)
|
|
520
337
|
end
|
|
@@ -522,48 +339,38 @@ module Phronomy
|
|
|
522
339
|
|
|
523
340
|
def apply_authorization_outcome(outcome)
|
|
524
341
|
unless outcome.is_a?(AuthorizationOutcome)
|
|
525
|
-
raise Phronomy::Error,
|
|
526
|
-
"Expected AuthorizationOutcome, got #{outcome.class}"
|
|
342
|
+
raise Phronomy::Error, "Expected AuthorizationOutcome, got #{outcome.class}"
|
|
527
343
|
end
|
|
528
|
-
|
|
529
344
|
@facts = immutable_copy(outcome.facts || {})
|
|
530
345
|
@authorization_reason = outcome.reason
|
|
531
346
|
@error = outcome.error
|
|
532
|
-
|
|
533
347
|
if outcome.cancelled
|
|
534
348
|
@status = :cancelled
|
|
535
349
|
elsif outcome.error
|
|
536
350
|
@status = :failed
|
|
537
351
|
else
|
|
538
352
|
@final_decision = outcome.decision
|
|
539
|
-
@status =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
:awaiting_approval
|
|
545
|
-
when :reject
|
|
546
|
-
:rejected
|
|
547
|
-
end
|
|
353
|
+
@status = case outcome.decision
|
|
354
|
+
when :allow then :authorized
|
|
355
|
+
when :require_approval then :awaiting_approval
|
|
356
|
+
when :reject then :rejected
|
|
357
|
+
end
|
|
548
358
|
end
|
|
549
359
|
end
|
|
550
360
|
|
|
551
361
|
def apply_execution_outcome(outcome)
|
|
552
362
|
unless outcome.is_a?(ExecutionOutcome)
|
|
553
|
-
raise Phronomy::Error,
|
|
554
|
-
"Expected ExecutionOutcome, got #{outcome.class}"
|
|
363
|
+
raise Phronomy::Error, "Expected ExecutionOutcome, got #{outcome.class}"
|
|
555
364
|
end
|
|
556
|
-
|
|
557
365
|
@result = outcome.result
|
|
558
366
|
@error = outcome.error
|
|
559
|
-
@status =
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
end
|
|
367
|
+
@status = if outcome.cancelled
|
|
368
|
+
:cancelled
|
|
369
|
+
elsif outcome.error
|
|
370
|
+
:failed
|
|
371
|
+
else
|
|
372
|
+
:completed
|
|
373
|
+
end
|
|
567
374
|
end
|
|
568
375
|
|
|
569
376
|
def complete_missing_tool!
|
|
@@ -575,8 +382,7 @@ module Phronomy
|
|
|
575
382
|
case value
|
|
576
383
|
when Hash
|
|
577
384
|
value.each_with_object({}) do |(key, item), result|
|
|
578
|
-
result[immutable_copy(key)] =
|
|
579
|
-
immutable_copy(item)
|
|
385
|
+
result[immutable_copy(key)] = immutable_copy(item)
|
|
580
386
|
end.freeze
|
|
581
387
|
when Array
|
|
582
388
|
value.map { |item| immutable_copy(item) }.freeze
|
|
@@ -589,9 +395,7 @@ module Phronomy
|
|
|
589
395
|
|
|
590
396
|
def redact_for_display(value)
|
|
591
397
|
if @tool&.respond_to?(:redacted_args, true)
|
|
592
|
-
immutable_copy(
|
|
593
|
-
@tool.send(:redacted_args, value || {})
|
|
594
|
-
)
|
|
398
|
+
immutable_copy(@tool.send(:redacted_args, value || {}))
|
|
595
399
|
else
|
|
596
400
|
immutable_copy(value || {})
|
|
597
401
|
end
|
|
@@ -599,30 +403,20 @@ module Phronomy
|
|
|
599
403
|
|
|
600
404
|
def sensitive_argument_values
|
|
601
405
|
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
|
|
406
|
+
normalized = (@arguments || @raw_arguments || {}).transform_keys(&:to_sym)
|
|
407
|
+
@tool.class.redact_params.filter_map { |name| normalized[name] }
|
|
609
408
|
end
|
|
610
409
|
|
|
611
410
|
def redact_value(value, sensitive_values)
|
|
612
|
-
if sensitive_values.any? { |sensitive| sensitive == value }
|
|
613
|
-
return "[REDACTED]"
|
|
614
|
-
end
|
|
411
|
+
return "[REDACTED]" if sensitive_values.any? { |sensitive| sensitive == value }
|
|
615
412
|
|
|
616
413
|
case value
|
|
617
414
|
when Hash
|
|
618
415
|
value.each_with_object({}) do |(key, item), result|
|
|
619
|
-
result[key] =
|
|
620
|
-
redact_value(item, sensitive_values)
|
|
416
|
+
result[key] = redact_value(item, sensitive_values)
|
|
621
417
|
end.freeze
|
|
622
418
|
when Array
|
|
623
|
-
value.map
|
|
624
|
-
redact_value(item, sensitive_values)
|
|
625
|
-
end.freeze
|
|
419
|
+
value.map { |item| redact_value(item, sensitive_values) }.freeze
|
|
626
420
|
when String
|
|
627
421
|
"[REDACTED]"
|
|
628
422
|
else
|
data/lib/phronomy/agent.rb
CHANGED
|
@@ -4,24 +4,20 @@ module Phronomy
|
|
|
4
4
|
module Agent
|
|
5
5
|
StreamEvent = Data.define(:type, :payload)
|
|
6
6
|
|
|
7
|
-
def self.run_once(definition:, input:, context: nil, **invoke_options)
|
|
7
|
+
def self.run_once(definition:, input:, context: nil, knowledge: [], **invoke_options)
|
|
8
8
|
persistence = Phronomy::Persistence::InMemory.new
|
|
9
|
-
agent = definition.create(
|
|
9
|
+
agent = definition.create(
|
|
10
|
+
context: context,
|
|
11
|
+
knowledge: knowledge,
|
|
12
|
+
persistence: persistence
|
|
13
|
+
)
|
|
10
14
|
agent.invoke(input, **invoke_options)
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
require_relative "agent/fsm_runtime_adapter"
|
|
16
19
|
require_relative "agent/async_event_api"
|
|
17
20
|
|
|
18
|
-
unless Phronomy::Agent::AgentInvocationSessionBuilder.singleton_class <
|
|
19
|
-
Phronomy::Agent::FsmRuntimeAdapter
|
|
20
|
-
Phronomy::Agent::AgentInvocationSessionBuilder.singleton_class.prepend(
|
|
21
|
-
Phronomy::Agent::FsmRuntimeAdapter
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
21
|
unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
|
|
26
22
|
Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
|
|
27
23
|
end
|