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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -2,104 +2,81 @@
2
2
 
3
3
  module Phronomy
4
4
  class Runtime
5
- # A thread-safe timer queue backed by a single background thread.
6
- #
7
- # Replaces the pattern of spawning one +Thread.new { sleep(t); callback }+
8
- # per deadline. Any number of timers share a single background thread that
9
- # sleeps until the earliest pending deadline.
10
- #
11
- # Use {#schedule} to register a one-shot callback; call {#shutdown} when the
12
- # queue is no longer needed (e.g. on process exit) to stop the background
13
- # thread cleanly.
5
+ # Threadless monotonic timer heap driven by EventLoop.
14
6
  class TimerQueue
15
- # @param clock [#call] zero-argument callable that returns the current
16
- # monotonic time in seconds (defaults to +Process::CLOCK_MONOTONIC+).
17
- # Override in tests to inject a fake clock.
18
- # @api private
19
7
  def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
20
8
  @clock = clock
21
- @heap = [] # [[fire_at, callback], ...]
9
+ @heap = []
22
10
  @mutex = Mutex.new
23
- @cond = ConditionVariable.new
24
11
  @stopped = false
25
- @thread = Thread.new { run_loop }
26
- @thread.name = "phronomy-timer-queue"
12
+ @wake = nil
13
+ end
14
+
15
+ # Installs a lightweight wake callback used when a newly scheduled timer
16
+ # may change EventLoop's current wait deadline.
17
+ def wake_with(&block)
18
+ @mutex.synchronize { @wake = block }
19
+ self
27
20
  end
28
21
 
29
- # Schedule a one-shot callback to fire after +seconds+ from now.
30
- #
31
- # @param seconds [Numeric] delay before the callback fires
32
- # @yield called (in the timer thread) when the deadline is reached
33
- # @return [self]
34
- # @api private
35
22
  def schedule(seconds:, &callback)
23
+ raise ArgumentError, "schedule requires a block" unless callback
24
+
36
25
  fire_at = @clock.call + seconds.to_f
26
+ wake = nil
37
27
  @mutex.synchronize do
38
28
  raise Phronomy::PoolShutdownError, "TimerQueue has been shut down" if @stopped
39
- insert_sorted(fire_at, callback)
40
- @cond.signal
29
+
30
+ previous_first = @heap.first&.first
31
+ @heap << [fire_at, callback]
32
+ @heap.sort_by!(&:first)
33
+ wake = @wake if previous_first.nil? || fire_at < previous_first
41
34
  end
35
+ wake&.call
42
36
  self
43
37
  end
44
38
 
45
- # Stop the background thread. Pending (un-fired) callbacks are discarded.
46
- #
47
- # @return [self]
48
- # @api private
49
- def shutdown
39
+ # Seconds until the next timer is due, nil when no timers are pending.
40
+ def seconds_until_next
50
41
  @mutex.synchronize do
51
- @stopped = true
52
- @cond.signal
42
+ return nil if @stopped || @heap.empty?
43
+ [@heap.first.first - @clock.call, 0.0].max
53
44
  end
54
- @thread.join
55
- self
56
- end
57
-
58
- # Number of pending (not yet fired) callbacks. Primarily for testing.
59
- # @return [Integer]
60
- # @api private
61
- def pending_count
62
- @mutex.synchronize { @heap.size }
63
45
  end
64
46
 
65
- private
47
+ # Executes all callbacks whose deadline is due. Must be called by EventLoop.
48
+ def fire_due
49
+ callbacks = @mutex.synchronize do
50
+ return 0 if @stopped
66
51
 
67
- def insert_sorted(fire_at, callback)
68
- @heap << [fire_at, callback]
69
- @heap.sort_by! { |(t, _)| t }
70
- end
52
+ now = @clock.call
53
+ due_count = @heap.bsearch_index { |(fire_at, _)| fire_at > now } || @heap.length
54
+ @heap.shift(due_count).map(&:last)
55
+ end
71
56
 
72
- def run_loop
73
- loop do
74
- callback = next_callback
75
- break if callback == :stopped
76
- begin
77
- callback&.call
78
- rescue => e
79
- Phronomy.configuration.logger&.error { "[TimerQueue] callback raised #{e.class}: #{e.message}" }
57
+ callbacks.each do |callback|
58
+ callback.call
59
+ rescue => error
60
+ Phronomy.configuration.logger&.error do
61
+ "[TimerQueue] callback raised #{error.class}: #{error.message}"
80
62
  end
81
63
  end
64
+ callbacks.length
82
65
  end
83
66
 
84
- def next_callback
85
- @mutex.synchronize do
86
- loop do
87
- return :stopped if @stopped
67
+ def pending_count
68
+ @mutex.synchronize { @heap.size }
69
+ end
88
70
 
89
- if @heap.empty?
90
- @cond.wait(@mutex)
91
- else
92
- now = @clock.call
93
- fire_at, = @heap.first
94
- if fire_at <= now
95
- return @heap.shift[1]
96
- else
97
- remaining = fire_at - now
98
- @cond.wait(@mutex, remaining)
99
- end
100
- end
101
- end
71
+ def shutdown
72
+ wake = @mutex.synchronize do
73
+ return self if @stopped
74
+ @stopped = true
75
+ @heap.clear
76
+ @wake
102
77
  end
78
+ wake&.call
79
+ self
103
80
  end
104
81
  end
105
82
  end
@@ -2,38 +2,30 @@
2
2
 
3
3
  module Phronomy
4
4
  class Runtime
5
- # Lazy-initialised timer service for a {Runtime} instance.
6
- #
7
- # Returns a {SchedulerTimerAdapter} when the backing scheduler is a
8
- # {DeterministicScheduler} (enabling virtual-time integration for the
9
- # `:fiber` backend), or a standard {TimerQueue} (OS-thread backed) for all
10
- # other schedulers.
11
- # @api private
5
+ # Lazy owner of the Runtime's threadless timer queue.
12
6
  class TimerService
13
- # @param scheduler [Scheduler]
14
- # @api private
15
- def initialize(scheduler)
16
- @scheduler = scheduler
7
+ def initialize
17
8
  @mutex = Mutex.new
18
9
  @timer = nil
10
+ @waker = nil
19
11
  end
20
12
 
21
- # Returns (or lazily creates) the timer queue for this runtime.
22
- # @return [TimerQueue, SchedulerTimerAdapter]
23
- # @api private
24
13
  def timer_queue
25
14
  @mutex.synchronize do
26
- @timer ||= if @scheduler.is_a?(DeterministicScheduler)
27
- SchedulerTimerAdapter.new(@scheduler)
28
- else
29
- TimerQueue.new
15
+ @timer ||= TimerQueue.new.tap do |timer|
16
+ timer.wake_with(&@waker) if @waker
30
17
  end
31
18
  end
32
19
  end
33
20
 
34
- # Shuts down the timer queue if it was started.
35
- # @return [void]
36
- # @api private
21
+ def wake_with(&block)
22
+ @mutex.synchronize do
23
+ @waker = block
24
+ @timer&.wake_with(&block)
25
+ end
26
+ self
27
+ end
28
+
37
29
  def shutdown
38
30
  @mutex.synchronize { @timer&.shutdown }
39
31
  end