phronomy 0.23.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 +2 -2
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +2 -2
- data/README.md +1 -1
- data/docs/architecture/multi-agent-handoff.md +35 -40
- data/docs/architecture/persistence.md +19 -8
- data/docs/architecture.md +8 -1
- data/docs/decisions/016-semantic-multi-agent-handoff.md +3 -1
- 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 +5 -1
- 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 +35 -1
- data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +42 -3
- data/lib/phronomy/agent/agent_execution.rb +2 -2
- data/lib/phronomy/agent/agent_invocation.rb +1 -1
- data/lib/phronomy/agent/async_event_api.rb +18 -1
- data/lib/phronomy/agent/base.rb +28 -0
- data/lib/phronomy/agent/context_assembler.rb +1 -1
- 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 +484 -27
- data/lib/phronomy/{multi_agent → agent}/handoff.rb +4 -4
- data/lib/phronomy/{multi_agent → agent}/handoff_capability_factory.rb +3 -45
- data/lib/phronomy/{multi_agent → agent}/handoff_context.rb +26 -1
- data/lib/phronomy/{multi_agent/execution_coordinator.rb → agent/handoff_execution_coordinator.rb} +32 -5
- data/lib/phronomy/{multi_agent → agent}/handoff_policy.rb +7 -1
- data/lib/phronomy/{multi_agent → agent}/handoff_projection.rb +18 -2
- data/lib/phronomy/{multi_agent → agent}/handoff_request.rb +2 -2
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +114 -211
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +84 -130
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +76 -200
- data/lib/phronomy/agent/recovery_coordinator.rb +11 -5
- data/lib/phronomy/agent/recovery_support.rb +15 -23
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -8
- data/lib/phronomy/agent/tool_invocation.rb +4 -2
- data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +16 -1
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +59 -11
- 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 +60 -0
- data/lib/phronomy/persistence/in_memory.rb +264 -2
- data/lib/phronomy/persistence/repository_facades.rb +221 -2
- data/lib/phronomy/persistence.rb +95 -1
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -0
- 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/version.rb +1 -1
- data/scripts/api_snapshot.rb +3 -3
- data/sig/phronomy/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +28 -32
- data/sig/phronomy/persistence.rbs +64 -3
- metadata +30 -12
- data/lib/phronomy/multi_agent/coordination_state.rb +0 -18
- data/lib/phronomy/multi_agent/coordinator.rb +0 -154
- data/lib/phronomy/multi_agent/runner.rb +0 -98
|
@@ -9,7 +9,7 @@ module Phronomy
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
def begin_resolve_on_event_loop(request)
|
|
12
|
-
event_loop =
|
|
12
|
+
event_loop = @runtime.event_loop
|
|
13
13
|
state = event_loop.agent_execution_state(
|
|
14
14
|
request.execution_id
|
|
15
15
|
)
|
|
@@ -36,7 +36,7 @@ module Phronomy
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
descriptor =
|
|
39
|
-
|
|
39
|
+
coordination_recovery_descriptor(current)
|
|
40
40
|
unless descriptor &&
|
|
41
41
|
Phronomy::Recovery.subject_equal?(
|
|
42
42
|
descriptor.fetch(:subject),
|
|
@@ -74,10 +74,10 @@ module Phronomy
|
|
|
74
74
|
execution_id: current.execution_id,
|
|
75
75
|
state: :recovery_required
|
|
76
76
|
)
|
|
77
|
-
task =
|
|
77
|
+
task = @runtime.offload.submit(
|
|
78
78
|
on_full: :raise
|
|
79
79
|
) do
|
|
80
|
-
|
|
80
|
+
prepare_resolution(operation)
|
|
81
81
|
end
|
|
82
82
|
task.on_complete do |result, error|
|
|
83
83
|
ready = ResolveReady.new(
|
|
@@ -146,13 +146,7 @@ module Phronomy
|
|
|
146
146
|
RecoverySupport::RECOVERY_METADATA_KEY => recovery
|
|
147
147
|
)
|
|
148
148
|
)
|
|
149
|
-
intended = ResolutionResult.new(
|
|
150
|
-
execution: updated,
|
|
151
|
-
root: operation.root,
|
|
152
|
-
continuation: :failed_terminal,
|
|
153
|
-
failure: failure,
|
|
154
|
-
appended_records: [].freeze
|
|
155
|
-
)
|
|
149
|
+
intended = ResolutionResult.new(execution: updated)
|
|
156
150
|
save_resolution_result(current, intended)
|
|
157
151
|
when :failed
|
|
158
152
|
recovery = {
|
|
@@ -166,13 +160,7 @@ module Phronomy
|
|
|
166
160
|
recovery
|
|
167
161
|
)
|
|
168
162
|
)
|
|
169
|
-
intended = ResolutionResult.new(
|
|
170
|
-
execution: updated,
|
|
171
|
-
root: operation.root,
|
|
172
|
-
continuation: :failed_terminal,
|
|
173
|
-
failure: operation.failure,
|
|
174
|
-
appended_records: [].freeze
|
|
175
|
-
)
|
|
163
|
+
intended = ResolutionResult.new(execution: updated)
|
|
176
164
|
save_resolution_result(current, intended)
|
|
177
165
|
when :succeeded
|
|
178
166
|
# simplecov:disable
|
|
@@ -182,7 +170,7 @@ module Phronomy
|
|
|
182
170
|
)
|
|
183
171
|
updated = nil
|
|
184
172
|
with_resolution_f1_capture do |tx|
|
|
185
|
-
main = agent.send(:
|
|
173
|
+
main = agent.send(:execution_coordinator_for, agent.__coordination_config)
|
|
186
174
|
snapshot = {
|
|
187
175
|
llm_results: [{
|
|
188
176
|
llm_call_id: llm_call_id,
|
|
@@ -217,7 +205,8 @@ module Phronomy
|
|
|
217
205
|
llm_call_id,
|
|
218
206
|
outcome
|
|
219
207
|
)
|
|
220
|
-
|
|
208
|
+
framework_subjects, external_subjects = subjects.partition { |entry| agent.__framework_call?(entry.fetch("tool_name")) }
|
|
209
|
+
next_phase = external_subjects.empty? ?
|
|
221
210
|
:recovery_provider_completed : :recovery_tools
|
|
222
211
|
metadata = current.metadata.dup
|
|
223
212
|
metadata.delete(
|
|
@@ -226,7 +215,21 @@ module Phronomy
|
|
|
226
215
|
metadata.delete(
|
|
227
216
|
RecoverySupport::PENDING_LLM_STARTED_AT_KEY
|
|
228
217
|
)
|
|
229
|
-
|
|
218
|
+
metadata.delete("framework_calls_pending")
|
|
219
|
+
metadata["framework_calls_pending"] = true unless framework_subjects.empty?
|
|
220
|
+
if subjects.any? && Array(metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY]).empty?
|
|
221
|
+
metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY] = subjects.map do |entry|
|
|
222
|
+
{
|
|
223
|
+
"tool_invocation_id" => entry.fetch("tool_invocation_id"),
|
|
224
|
+
"llm_call_id" => entry.fetch("llm_call_id"),
|
|
225
|
+
"tool_call_id" => entry.fetch("tool_call_id"),
|
|
226
|
+
"tool_name" => entry.fetch("tool_name"),
|
|
227
|
+
"arguments" => RecoverySupport.canonical_copy(entry.fetch("arguments", {})),
|
|
228
|
+
"status" => "authorized"
|
|
229
|
+
}.freeze
|
|
230
|
+
end.freeze
|
|
231
|
+
end
|
|
232
|
+
if external_subjects.empty?
|
|
230
233
|
metadata.delete(
|
|
231
234
|
RecoverySupport::RECOVERY_METADATA_KEY
|
|
232
235
|
)
|
|
@@ -234,7 +237,7 @@ module Phronomy
|
|
|
234
237
|
metadata[
|
|
235
238
|
RecoverySupport::RECOVERY_METADATA_KEY
|
|
236
239
|
] = RecoverySupport.build_recovery_hash(
|
|
237
|
-
|
|
240
|
+
external_subjects
|
|
238
241
|
)
|
|
239
242
|
end
|
|
240
243
|
updated = current.with(
|
|
@@ -249,18 +252,7 @@ module Phronomy
|
|
|
249
252
|
expected_revision: current.execution_revision,
|
|
250
253
|
execution: updated
|
|
251
254
|
)
|
|
252
|
-
ResolutionResult.new(
|
|
253
|
-
execution: updated,
|
|
254
|
-
root: operation.root,
|
|
255
|
-
continuation: (
|
|
256
|
-
(updated.phase.to_sym ==
|
|
257
|
-
:recovery_provider_completed) ?
|
|
258
|
-
:provider_completed :
|
|
259
|
-
:resolution_required
|
|
260
|
-
),
|
|
261
|
-
failure: nil,
|
|
262
|
-
appended_records: [].freeze
|
|
263
|
-
)
|
|
255
|
+
ResolutionResult.new(execution: updated)
|
|
264
256
|
end
|
|
265
257
|
# simplecov:enable
|
|
266
258
|
end
|
|
@@ -321,13 +313,7 @@ module Phronomy
|
|
|
321
313
|
resolved_recovery
|
|
322
314
|
)
|
|
323
315
|
)
|
|
324
|
-
intended = ResolutionResult.new(
|
|
325
|
-
execution: updated,
|
|
326
|
-
root: operation.root,
|
|
327
|
-
continuation: :failed_terminal,
|
|
328
|
-
failure: failure,
|
|
329
|
-
appended_records: [].freeze
|
|
330
|
-
)
|
|
316
|
+
intended = ResolutionResult.new(execution: updated)
|
|
331
317
|
save_resolution_result(current, intended)
|
|
332
318
|
when :failed
|
|
333
319
|
recovery_with_failure =
|
|
@@ -347,18 +333,12 @@ module Phronomy
|
|
|
347
333
|
recovery_with_failure
|
|
348
334
|
)
|
|
349
335
|
)
|
|
350
|
-
intended = ResolutionResult.new(
|
|
351
|
-
execution: updated,
|
|
352
|
-
root: operation.root,
|
|
353
|
-
continuation: :failed_terminal,
|
|
354
|
-
failure: operation.failure,
|
|
355
|
-
appended_records: [].freeze
|
|
356
|
-
)
|
|
336
|
+
intended = ResolutionResult.new(execution: updated)
|
|
357
337
|
save_resolution_result(current, intended)
|
|
358
338
|
when :succeeded
|
|
359
339
|
updated = nil
|
|
360
340
|
with_resolution_f1_capture do |tx|
|
|
361
|
-
main = agent.send(:
|
|
341
|
+
main = agent.send(:execution_coordinator_for, agent.__coordination_config)
|
|
362
342
|
result_ref = main.send(
|
|
363
343
|
:put_runtime_content,
|
|
364
344
|
tx,
|
|
@@ -408,6 +388,13 @@ module Phronomy
|
|
|
408
388
|
RecoverySupport.unresolved_subjects(
|
|
409
389
|
next_recovery
|
|
410
390
|
)
|
|
391
|
+
tool_batch = Array(current.metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY]).map do |entry|
|
|
392
|
+
if entry.fetch("tool_invocation_id") == subject_entry.fetch("tool_invocation_id")
|
|
393
|
+
entry.merge("status" => "completed", "result" => operation.result)
|
|
394
|
+
else
|
|
395
|
+
entry
|
|
396
|
+
end
|
|
397
|
+
end
|
|
411
398
|
next_phase = unresolved.empty? ?
|
|
412
399
|
:recovery_tools_completed : :recovery_tools
|
|
413
400
|
updated = current.with(
|
|
@@ -415,8 +402,8 @@ module Phronomy
|
|
|
415
402
|
working_records:
|
|
416
403
|
current.working_records + records,
|
|
417
404
|
metadata: current.metadata.merge(
|
|
418
|
-
RecoverySupport::RECOVERY_METADATA_KEY =>
|
|
419
|
-
|
|
405
|
+
RecoverySupport::RECOVERY_METADATA_KEY => next_recovery,
|
|
406
|
+
RecoverySupport::TOOL_BATCH_METADATA_KEY => tool_batch
|
|
420
407
|
)
|
|
421
408
|
)
|
|
422
409
|
tx.executions.save(
|
|
@@ -424,18 +411,7 @@ module Phronomy
|
|
|
424
411
|
expected_revision: current.execution_revision,
|
|
425
412
|
execution: updated
|
|
426
413
|
)
|
|
427
|
-
ResolutionResult.new(
|
|
428
|
-
execution: updated,
|
|
429
|
-
root: operation.root,
|
|
430
|
-
continuation: (
|
|
431
|
-
(updated.phase.to_sym ==
|
|
432
|
-
:recovery_tools_completed) ?
|
|
433
|
-
:tools_completed :
|
|
434
|
-
:resolution_required
|
|
435
|
-
),
|
|
436
|
-
failure: nil,
|
|
437
|
-
appended_records: [].freeze
|
|
438
|
-
)
|
|
414
|
+
ResolutionResult.new(execution: updated)
|
|
439
415
|
end
|
|
440
416
|
end
|
|
441
417
|
end
|
|
@@ -480,154 +456,54 @@ module Phronomy
|
|
|
480
456
|
end
|
|
481
457
|
end
|
|
482
458
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
)
|
|
494
|
-
)
|
|
495
|
-
return
|
|
496
|
-
end
|
|
497
|
-
|
|
498
|
-
if ready.error
|
|
499
|
-
reconcile_resolution_f1_on_event_loop(
|
|
500
|
-
ready,
|
|
501
|
-
state
|
|
502
|
-
)
|
|
503
|
-
return
|
|
504
|
-
end
|
|
459
|
+
# Commit/readback and materialization share one worker operation. A read
|
|
460
|
+
# failure after a confirmed commit must retain that committed execution;
|
|
461
|
+
# it must not be mistaken for another uncertain resolution write.
|
|
462
|
+
def prepare_resolution(operation)
|
|
463
|
+
result = resolve_with_readback(operation)
|
|
464
|
+
material = prepare_recovery_material(result.execution)
|
|
465
|
+
ResolutionPreparation.new(execution: result.execution, material: material, error: nil)
|
|
466
|
+
rescue => error
|
|
467
|
+
ResolutionPreparation.new(execution: result&.execution, material: nil, error: error)
|
|
468
|
+
end
|
|
505
469
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
)
|
|
470
|
+
def resolve_with_readback(operation)
|
|
471
|
+
perform_resolution(operation)
|
|
472
|
+
rescue => error
|
|
473
|
+
intended = error.intended_result.execution if error.is_a?(ResolutionOutcomeUnknownError)
|
|
474
|
+
current = agent.persistence.executions.load(operation.execution.execution_id)
|
|
475
|
+
return ResolutionResult.new(execution: current) if intended && current.to_h == intended.to_h
|
|
511
476
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
event_loop.mark_agent_execution_admission(
|
|
515
|
-
agent.agent_id,
|
|
516
|
-
execution_id: request.execution_id,
|
|
517
|
-
state: :recovery_required
|
|
518
|
-
)
|
|
519
|
-
descriptor =
|
|
520
|
-
RecoverySupport.recovery_descriptor(
|
|
521
|
-
result.execution
|
|
522
|
-
)
|
|
523
|
-
deliver_resolution_required(
|
|
524
|
-
result.execution,
|
|
525
|
-
descriptor
|
|
526
|
-
)
|
|
527
|
-
request.completion.complete(
|
|
528
|
-
{
|
|
529
|
-
execution_id: request.execution_id,
|
|
530
|
-
execution_revision:
|
|
531
|
-
result.execution.execution_revision,
|
|
532
|
-
recovery: :resolution_required
|
|
533
|
-
}.freeze
|
|
534
|
-
)
|
|
535
|
-
when :provider_completed
|
|
536
|
-
observe_recovery_execution(request.completion, result.execution)
|
|
537
|
-
continue_provider_completed_after_resolution(
|
|
538
|
-
event_loop,
|
|
539
|
-
state,
|
|
540
|
-
result.execution,
|
|
541
|
-
request.completion
|
|
542
|
-
)
|
|
543
|
-
when :tools_completed
|
|
544
|
-
observe_recovery_execution(request.completion, result.execution)
|
|
545
|
-
continue_tools_completed_after_resolution(
|
|
546
|
-
event_loop,
|
|
547
|
-
state,
|
|
548
|
-
result.execution,
|
|
549
|
-
request.completion
|
|
550
|
-
)
|
|
551
|
-
when :failed_terminal
|
|
552
|
-
observe_recovery_execution(request.completion, result.execution)
|
|
553
|
-
continue_failed_after_resolution(
|
|
554
|
-
event_loop,
|
|
555
|
-
state,
|
|
556
|
-
result.execution,
|
|
557
|
-
request.completion,
|
|
558
|
-
result.failure
|
|
559
|
-
)
|
|
560
|
-
else
|
|
561
|
-
request.completion.fail(
|
|
562
|
-
Phronomy::Error.new(
|
|
563
|
-
"unknown Recovery continuation: #{result.continuation.inspect}"
|
|
564
|
-
)
|
|
565
|
-
)
|
|
477
|
+
if current.to_h == operation.execution.to_h
|
|
478
|
+
raise(error.is_a?(ResolutionOutcomeUnknownError) ? error.original_error : error)
|
|
566
479
|
end
|
|
567
|
-
|
|
568
|
-
|
|
480
|
+
raise Phronomy::Persistence::ConflictError,
|
|
481
|
+
"Recovery resolution durable outcome conflicts with both expected pre-state and intended post-state"
|
|
569
482
|
end
|
|
570
483
|
|
|
571
|
-
def
|
|
484
|
+
def apply_resolve_on_event_loop(ready)
|
|
572
485
|
request = ready.request
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
486
|
+
event_loop = @runtime.event_loop
|
|
487
|
+
state = event_loop.agent_execution_state(request.execution_id)
|
|
488
|
+
unless state && state.agent.equal?(agent)
|
|
489
|
+
raise Phronomy::ExecutionRehydrationRequiredError,
|
|
490
|
+
"recovered execution disappeared before resolution apply"
|
|
577
491
|
end
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
)
|
|
582
|
-
|
|
583
|
-
if intended &&
|
|
584
|
-
current.execution_revision ==
|
|
585
|
-
intended.execution_revision &&
|
|
586
|
-
current.to_h == intended.to_h
|
|
587
|
-
Phronomy::Runtime.instance.event_loop.replace_agent_execution(
|
|
588
|
-
request.execution_id,
|
|
589
|
-
execution: current
|
|
590
|
-
)
|
|
591
|
-
synthetic = ResolveReady.new(
|
|
592
|
-
coordinator: self,
|
|
593
|
-
request: request,
|
|
594
|
-
operation: ready.operation,
|
|
595
|
-
result: intended_result.class.new(
|
|
596
|
-
execution: current,
|
|
597
|
-
root: intended_result.root,
|
|
598
|
-
continuation: intended_result.continuation,
|
|
599
|
-
failure: intended_result.failure,
|
|
600
|
-
appended_records:
|
|
601
|
-
intended_result.appended_records
|
|
602
|
-
),
|
|
603
|
-
error: nil
|
|
604
|
-
)
|
|
605
|
-
apply_resolve_on_event_loop(synthetic)
|
|
606
|
-
return
|
|
492
|
+
unless state.execution.equal?(ready.operation.execution) && state.fsm_session_id.nil?
|
|
493
|
+
raise Phronomy::Persistence::ConflictError,
|
|
494
|
+
"Recovery execution changed before resolution apply"
|
|
607
495
|
end
|
|
496
|
+
raise ready.error if ready.error
|
|
608
497
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
ready.operation.execution.to_h
|
|
613
|
-
failure = if ready.error.is_a?(
|
|
614
|
-
ResolutionOutcomeUnknownError
|
|
615
|
-
)
|
|
616
|
-
ready.error.original_error
|
|
617
|
-
else
|
|
618
|
-
ready.error
|
|
619
|
-
end
|
|
620
|
-
request.completion.fail(failure)
|
|
621
|
-
return
|
|
498
|
+
result = ready.result
|
|
499
|
+
if result.execution
|
|
500
|
+
event_loop.replace_agent_execution(request.execution_id, execution: result.execution)
|
|
622
501
|
end
|
|
502
|
+
raise result.error if result.error
|
|
623
503
|
|
|
624
|
-
request.completion.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
)
|
|
628
|
-
)
|
|
629
|
-
rescue => reconciliation_error
|
|
630
|
-
request.completion.fail(reconciliation_error)
|
|
504
|
+
continue_recovery_on_event_loop(result.execution, request.completion, material: result.material)
|
|
505
|
+
rescue => caught
|
|
506
|
+
request.completion.fail(caught)
|
|
631
507
|
end
|
|
632
508
|
end
|
|
633
509
|
end
|
|
@@ -37,18 +37,19 @@ module Phronomy
|
|
|
37
37
|
ResolutionOperation = Data.define(
|
|
38
38
|
:execution, :root, :subject, :outcome, :result, :failure
|
|
39
39
|
)
|
|
40
|
-
ResolutionResult = Data.define(
|
|
41
|
-
|
|
42
|
-
)
|
|
40
|
+
ResolutionResult = Data.define(:execution)
|
|
41
|
+
ResolutionPreparation = Data.define(:execution, :material, :error)
|
|
42
|
+
RecoveryMaterial = Data.define(:projection, :messages, :assistant_message, :output, :usage)
|
|
43
43
|
RecoveryPlan = Data.define(
|
|
44
44
|
:execution, :root, :manifest, :base_manifest,
|
|
45
|
-
:projection, :classification
|
|
45
|
+
:projection, :classification, :material
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
attr_reader :agent
|
|
49
49
|
|
|
50
50
|
def initialize(agent)
|
|
51
51
|
@agent = agent
|
|
52
|
+
@runtime = Phronomy::Runtime.instance
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def recover_on_load!
|
|
@@ -67,6 +68,11 @@ module Phronomy
|
|
|
67
68
|
"active AgentExecution belongs to another Agent: #{execution.agent_id}"
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
coordination = execution.metadata["coordination"]
|
|
72
|
+
if coordination && agent.__coordination_config.empty?
|
|
73
|
+
return agent
|
|
74
|
+
end
|
|
75
|
+
|
|
70
76
|
plan = prepare_plan(execution)
|
|
71
77
|
classification = plan.classification
|
|
72
78
|
if classification.disposition ==
|
|
@@ -181,7 +187,7 @@ module Phronomy
|
|
|
181
187
|
private
|
|
182
188
|
|
|
183
189
|
def post_control(command)
|
|
184
|
-
|
|
190
|
+
@runtime.event_loop.post(
|
|
185
191
|
Phronomy::Event.new(
|
|
186
192
|
type: :agent_control,
|
|
187
193
|
target_id:
|
|
@@ -305,8 +305,8 @@ module Phronomy
|
|
|
305
305
|
end
|
|
306
306
|
end
|
|
307
307
|
|
|
308
|
-
def build_invocation_for_suspended(agent, execution, projection, main_coordinator, listener)
|
|
309
|
-
request = Phronomy::Agent::ToolApprovalRequest.from_h(
|
|
308
|
+
def build_invocation_for_suspended(agent, execution, projection, main_coordinator, listener, assistant_message:)
|
|
309
|
+
request = execution.approval_request && Phronomy::Agent::ToolApprovalRequest.from_h(
|
|
310
310
|
execution.approval_request
|
|
311
311
|
)
|
|
312
312
|
assistant_record = latest_assistant_record(execution)
|
|
@@ -315,19 +315,15 @@ module Phronomy
|
|
|
315
315
|
"suspended execution #{execution.execution_id} has no durable assistant Tool Call message"
|
|
316
316
|
end
|
|
317
317
|
|
|
318
|
-
materializer = Phronomy::Agent::RubyLLMMaterializer.new(
|
|
319
|
-
agent: agent,
|
|
320
|
-
persistence: agent.persistence
|
|
321
|
-
)
|
|
322
|
-
assistant_message = materializer.materialize_journal_record(
|
|
323
|
-
assistant_record
|
|
324
|
-
)
|
|
325
|
-
|
|
326
318
|
config = {
|
|
327
319
|
execution_id: execution.execution_id,
|
|
328
320
|
phronomy_execution_coordinator: main_coordinator,
|
|
329
321
|
phronomy_runtime_projection: projection
|
|
330
|
-
}
|
|
322
|
+
}.merge(agent.__coordination_config)
|
|
323
|
+
config = agent.__invocation_config(config)
|
|
324
|
+
if execution.metadata["coordination_cancel_requested"]
|
|
325
|
+
config = config.merge(cancellation_token: Phronomy::Concurrency::CancellationToken.new.cancel!)
|
|
326
|
+
end
|
|
331
327
|
invocation = Phronomy::Agent::AgentInvocation.new(
|
|
332
328
|
agent: agent,
|
|
333
329
|
input: projection.ask_message,
|
|
@@ -359,7 +355,7 @@ module Phronomy
|
|
|
359
355
|
assistant_record.llm_call_id&.to_s
|
|
360
356
|
)
|
|
361
357
|
|
|
362
|
-
by_id = request.items.to_h do |item|
|
|
358
|
+
by_id = (request ? request.items : []).to_h do |item|
|
|
363
359
|
[item.tool_invocation_id.to_s, item]
|
|
364
360
|
end
|
|
365
361
|
snapshots = Array(execution.metadata[TOOL_BATCH_METADATA_KEY])
|
|
@@ -445,12 +441,16 @@ module Phronomy
|
|
|
445
441
|
child
|
|
446
442
|
end
|
|
447
443
|
|
|
448
|
-
def build_chat_for_recovery(agent, execution, projection, main_coordinator, listener)
|
|
444
|
+
def build_chat_for_recovery(agent, execution, projection, main_coordinator, listener, messages:)
|
|
449
445
|
config = {
|
|
450
446
|
execution_id: execution.execution_id,
|
|
451
447
|
phronomy_execution_coordinator: main_coordinator,
|
|
452
448
|
phronomy_runtime_projection: projection
|
|
453
|
-
}
|
|
449
|
+
}.merge(agent.__coordination_config)
|
|
450
|
+
config = agent.__invocation_config(config)
|
|
451
|
+
if execution.metadata["coordination_cancel_requested"]
|
|
452
|
+
config = config.merge(cancellation_token: Phronomy::Concurrency::CancellationToken.new.cancel!)
|
|
453
|
+
end
|
|
454
454
|
invocation = Phronomy::Agent::AgentInvocation.new(
|
|
455
455
|
agent: agent,
|
|
456
456
|
input: projection.ask_message,
|
|
@@ -473,15 +473,7 @@ module Phronomy
|
|
|
473
473
|
)
|
|
474
474
|
end
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
agent: agent,
|
|
478
|
-
persistence: agent.persistence
|
|
479
|
-
)
|
|
480
|
-
Array(execution.working_records).each do |record|
|
|
481
|
-
next unless %i[assistant_message tool_message].include?(record.kind.to_sym)
|
|
482
|
-
|
|
483
|
-
chat.messages << materializer.materialize_journal_record(record)
|
|
484
|
-
end
|
|
476
|
+
messages.each { |message| chat.messages << message }
|
|
485
477
|
|
|
486
478
|
invocation.chat = chat
|
|
487
479
|
invocation.user_message_sent = true
|
|
@@ -105,14 +105,8 @@ module Phronomy
|
|
|
105
105
|
|
|
106
106
|
expected_definitions = fetch_json(manifest.tool_definitions_ref)
|
|
107
107
|
additional = @additional_tools
|
|
108
|
-
if additional.empty?
|
|
109
|
-
|
|
110
|
-
.map { |definition| definition.fetch("name") }
|
|
111
|
-
additional = Array(expected_definitions).filter_map do |definition|
|
|
112
|
-
name = definition.fetch("name")
|
|
113
|
-
next if ordinary_names.include?(name)
|
|
114
|
-
Phronomy::MultiAgent::HandoffCapabilityFactory.lookup(name)&.tool_class
|
|
115
|
-
end
|
|
108
|
+
if additional.empty?
|
|
109
|
+
additional = Array(@agent.__coordination_config[:phronomy_handoff_bindings]).map(&:tool_class)
|
|
116
110
|
end
|
|
117
111
|
|
|
118
112
|
ToolDefinitionSet.build(@agent, additional_tools: additional)
|
|
@@ -113,7 +113,7 @@ module Phronomy
|
|
|
113
113
|
@tool_call_id = tool_call.respond_to?(:id) ? tool_call.id : nil
|
|
114
114
|
raw_arguments = tool_call.respond_to?(:arguments) ? (tool_call.arguments || {}) : {}
|
|
115
115
|
@raw_arguments = immutable_copy(raw_arguments)
|
|
116
|
-
@config = config
|
|
116
|
+
@config = config.dup.freeze
|
|
117
117
|
@approval_policy = approval_policy
|
|
118
118
|
@approval_context = immutable_copy(approval_context || {})
|
|
119
119
|
@origin = tool&.respond_to?(:tool_origin) ? tool.tool_origin.to_sym : :local
|
|
@@ -560,10 +560,12 @@ module Phronomy
|
|
|
560
560
|
on_full: :raise
|
|
561
561
|
)
|
|
562
562
|
else
|
|
563
|
+
tool_config = @tool.class.respond_to?(:__framework_owned_operation?) ?
|
|
564
|
+
@config.merge(phronomy_tool_invocation_id: @id, execution_id: @execution_id).freeze : @config
|
|
563
565
|
@tool.call_async(
|
|
564
566
|
@arguments,
|
|
565
567
|
cancellation_token: @config[:cancellation_token],
|
|
566
|
-
config:
|
|
568
|
+
config: tool_config
|
|
567
569
|
)
|
|
568
570
|
end
|
|
569
571
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
class Runtime
|
|
5
|
+
# Runtime-local Team identity and construction exclusion; no execution state.
|
|
6
|
+
# @api private
|
|
7
|
+
class TeamOwnershipRegistry
|
|
8
|
+
def initialize
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@condition = ConditionVariable.new
|
|
11
|
+
@owners = {}
|
|
12
|
+
@constructing = {}
|
|
13
|
+
@draining = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch(id, klass:, create:, persistence:)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
loop do
|
|
19
|
+
raise Phronomy::RuntimeShutdownError, "Runtime is draining" if @draining
|
|
20
|
+
if (owner = @owners[id])
|
|
21
|
+
raise Phronomy::Persistence::ConflictError, "Team #{id} already exists" if create
|
|
22
|
+
unless owner.is_a?(klass) && owner.persistence.equal?(persistence)
|
|
23
|
+
raise Phronomy::ConfigurationError, "Team #{id} owner or Persistence mismatch"
|
|
24
|
+
end
|
|
25
|
+
return owner
|
|
26
|
+
end
|
|
27
|
+
break unless @constructing[id]
|
|
28
|
+
@condition.wait(@mutex)
|
|
29
|
+
end
|
|
30
|
+
@constructing[id] = true
|
|
31
|
+
end
|
|
32
|
+
begin
|
|
33
|
+
owner = yield
|
|
34
|
+
@mutex.synchronize { @owners[id] = owner }
|
|
35
|
+
owner
|
|
36
|
+
ensure
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
@constructing.delete(id)
|
|
39
|
+
@condition.broadcast
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get(id, klass:)
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
owner = @owners[id]
|
|
47
|
+
if owner && !owner.is_a?(klass)
|
|
48
|
+
raise Phronomy::ConfigurationError, "Team #{id} definition mismatch"
|
|
49
|
+
end
|
|
50
|
+
owner
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def begin_draining
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
@draining = true
|
|
57
|
+
@condition.broadcast
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def wait_until_stable(deadline)
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
until @constructing.empty?
|
|
64
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
65
|
+
return false if remaining <= 0
|
|
66
|
+
@condition.wait(@mutex, remaining)
|
|
67
|
+
end
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def shutdown!
|
|
73
|
+
@mutex.synchronize { @owners.clear }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|