phronomy 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -18
  3. data/README.md +300 -75
  4. data/benchmark/bench_agent_invoke.rb +3 -0
  5. data/benchmark/bench_regression.rb +2 -18
  6. data/benchmark/bench_tool_schema.rb +1 -0
  7. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
  8. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
  9. data/lib/phronomy/agent/activation_registry.rb +28 -0
  10. data/lib/phronomy/agent/agent_execution.rb +97 -0
  11. data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
  12. data/lib/phronomy/agent/agent_invocation.rb +42 -10
  13. data/lib/phronomy/agent/agent_invocation_session_builder.rb +50 -11
  14. data/lib/phronomy/agent/agent_root.rb +67 -0
  15. data/lib/phronomy/agent/async_event_api.rb +55 -393
  16. data/lib/phronomy/agent/base.rb +301 -641
  17. data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
  18. data/lib/phronomy/agent/context_assembler.rb +321 -0
  19. data/lib/phronomy/agent/context_candidate.rb +47 -0
  20. data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
  21. data/lib/phronomy/agent/context_importer.rb +217 -0
  22. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
  23. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
  24. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
  25. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +188 -0
  26. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
  27. data/lib/phronomy/agent/context_plan.rb +25 -0
  28. data/lib/phronomy/agent/context_plan_validator.rb +167 -0
  29. data/lib/phronomy/agent/context_policies/default.rb +53 -0
  30. data/lib/phronomy/agent/context_policy.rb +15 -0
  31. data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
  32. data/lib/phronomy/agent/context_policy_registry.rb +46 -0
  33. data/lib/phronomy/agent/context_request.rb +35 -0
  34. data/lib/phronomy/agent/context_selection_unit.rb +38 -0
  35. data/lib/phronomy/agent/derived_content_spec.rb +34 -0
  36. data/lib/phronomy/agent/execution_coordinator.rb +1123 -0
  37. data/lib/phronomy/agent/fsm_runtime_adapter.rb +210 -0
  38. data/lib/phronomy/agent/immutable.rb +31 -0
  39. data/lib/phronomy/agent/journal_projection.rb +34 -0
  40. data/lib/phronomy/agent/journal_record.rb +67 -0
  41. data/lib/phronomy/agent/llm_call_record.rb +51 -0
  42. data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
  43. data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
  44. data/lib/phronomy/agent/llm_input_patch.rb +21 -0
  45. data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
  46. data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
  47. data/lib/phronomy/agent/ruby_llm_materializer.rb +298 -0
  48. data/lib/phronomy/agent/token_budget_resolver.rb +69 -0
  49. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
  50. data/lib/phronomy/agent/tool_definition_set.rb +55 -0
  51. data/lib/phronomy/agent.rb +14 -16
  52. data/lib/phronomy/agent_busy_error.rb +5 -0
  53. data/lib/phronomy/canonical_json.rb +136 -0
  54. data/lib/phronomy/configuration.rb +9 -4
  55. data/lib/phronomy/content_store/base.rb +51 -0
  56. data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
  57. data/lib/phronomy/engine/event_loop.rb +3 -0
  58. data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
  59. data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
  60. data/lib/phronomy/llm_context_window/assembler.rb +8 -8
  61. data/lib/phronomy/multi_agent/orchestrator.rb +1 -0
  62. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +6 -2
  64. data/lib/phronomy/persistence/in_memory.rb +247 -0
  65. data/lib/phronomy/persistence.rb +39 -0
  66. data/lib/phronomy/tools/agent.rb +14 -36
  67. data/lib/phronomy/version.rb +1 -1
  68. data/lib/phronomy.rb +11 -0
  69. data/scripts/add_to_h_to_token_doubles.rb +33 -0
  70. data/scripts/add_to_h_unnamed_doubles.rb +27 -0
  71. data/scripts/migrate_spec_agent_definition.rb +108 -0
  72. data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
  73. data/scripts/migrate_spec_inline_pass3.rb +24 -0
  74. metadata +54 -47
  75. data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
  76. data/lib/phronomy/agent/before_completion_context.rb +0 -47
  77. data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Migrates spec files to add agent_definition to Agent::Base subclasses.
5
+ # - Named classes: inserts after the class declaration line
6
+ # - Anonymous blocks (do...end): inserts after the opening `do`
7
+ # - Inline anonymous ({ ... }): expands to multi-line with agent_definition
8
+ # Safe: skips classes that already have agent_definition
9
+
10
+ require "fileutils"
11
+
12
+ SPEC_DIR = File.expand_path("../spec", __dir__)
13
+ AGENT_BASE_PATTERN = /Phronomy::Agent::Base/
14
+
15
+ def snake_case(class_name)
16
+ # Remove trailing Agent/Tool suffixes, snake_case the name
17
+ name = class_name.split("::").last
18
+ name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
19
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
20
+ .downcase
21
+ end
22
+
23
+ counter = 0
24
+ files_modified = 0
25
+
26
+ Dir.glob("#{SPEC_DIR}/**/*.rb").sort.each do |file|
27
+ lines = File.readlines(file, chomp: false)
28
+ new_lines = []
29
+ modified = false
30
+ i = 0
31
+
32
+ while i < lines.length
33
+ line = lines[i]
34
+
35
+ # ── Named class: class FooAgent < Phronomy::Agent::Base ──────────────────
36
+ if line =~ /^(\s*)class\s+(\w+)\s*<\s*(?:::)?Phronomy::Agent::Base\s*$/
37
+ indent = $1
38
+ class_name = $2
39
+ id = snake_case(class_name).sub(/_agent$/, "-agent").tr("_", "-")
40
+ id = "#{id}-agent" unless id.end_with?("-agent")
41
+ new_lines << line
42
+ i += 1
43
+ # Check if agent_definition already present in the class body
44
+ look_ahead = lines[i..]&.take(10) || []
45
+ unless look_ahead.any? { |l| l =~ /agent_definition/ }
46
+ new_lines << "#{indent} agent_definition id: #{id.inspect}, version: 1\n"
47
+ counter += 1
48
+ modified = true
49
+ end
50
+ next
51
+ end
52
+
53
+ # ── Anonymous block (do...end): Class.new(Phronomy::Agent::Base) do ─────
54
+ if line =~ /^(\s*)([\w.]*\s*=\s*)?Class\.new\((?:::)?Phronomy::Agent::Base\)\s+do\s*(\|\w+\|)?\s*$/
55
+ indent = $1
56
+ # Check if agent_definition already present
57
+ look_ahead = lines[i + 1..]&.take(10) || []
58
+ new_lines << line
59
+ i += 1
60
+ unless look_ahead.any? { |l| l =~ /agent_definition/ }
61
+ # Derive indentation from the `do` block body (first non-blank line)
62
+ inner = lines[i..]&.take(5)&.find { |l| l.strip.length > 0 } || ""
63
+ inner_indent = inner.match(/^(\s*)/)[1]
64
+ inner_indent = "#{indent} " if inner_indent.strip.length > 0 && inner_indent.length < indent.length + 2
65
+ new_lines << "#{inner_indent}agent_definition id: \"test-agent-#{counter += 1}\", version: 1\n"
66
+ modified = true
67
+ end
68
+ next
69
+ end
70
+
71
+ # ── Inline anonymous: Class.new(Phronomy::Agent::Base) { ... } ───────────
72
+ if line =~ /^(\s*)((?:[\w.]*\s*=\s*)?)Class\.new\((?:::)?Phronomy::Agent::Base\)\s*\{(.+)\}\s*$/
73
+ indent = $1
74
+ assignment = $2
75
+ body = $3
76
+ # Skip if agent_definition already present
77
+ if /agent_definition/.match?(body)
78
+ new_lines << line
79
+ else
80
+ id = counter += 1
81
+ definition_line = "agent_definition id: \"test-agent-#{id}\", version: 1;"
82
+ new_lines << "#{indent}#{assignment}Class.new(Phronomy::Agent::Base) do\n"
83
+ new_lines << "#{indent} #{definition_line}\n"
84
+ # Rewrite the original body lines, stripping leading spaces
85
+ body.strip.split(";").each do |stmt|
86
+ stmt = stmt.strip
87
+ next if stmt.empty?
88
+ new_lines << "#{indent} #{stmt}\n"
89
+ end
90
+ new_lines << "#{indent}end\n"
91
+ modified = true
92
+ end
93
+ i += 1
94
+ next
95
+ end
96
+
97
+ new_lines << line
98
+ i += 1
99
+ end
100
+
101
+ if modified
102
+ File.write(file, new_lines.join)
103
+ files_modified += 1
104
+ puts " patched: #{file.sub("#{SPEC_DIR}/", "spec/")}"
105
+ end
106
+ end
107
+
108
+ puts "\nDone: #{counter} agent_definition insertions in #{files_modified} files."
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Second pass: handles patterns missed by the first script
5
+ # - Class.new(Phronomy::Agent::Base) { ... }.new (inline + .new suffix)
6
+ # - Class.new(Phronomy::Agent::Base).new (no block + .new)
7
+ # - { Class.new(Phronomy::Agent::Base) } (no block, inside outer { })
8
+
9
+ require "fileutils"
10
+
11
+ SPEC_DIR = File.expand_path("../spec", __dir__)
12
+ counter = 200
13
+
14
+ files_modified = 0
15
+
16
+ Dir.glob("#{SPEC_DIR}/**/*.rb").sort.each do |file|
17
+ content = File.read(file)
18
+ new_content = content.dup
19
+
20
+ # Pattern A: Class.new(Phronomy::Agent::Base) { body }.new
21
+ new_content = new_content.gsub(
22
+ /Class\.new\(Phronomy::Agent::Base\) \{([^}]*)\}\.new/
23
+ ) do
24
+ body = $1
25
+ next $& if body.include?("agent_definition")
26
+ counter += 1
27
+ "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1;#{body}}.new"
28
+ end
29
+
30
+ # Pattern B: Class.new(Phronomy::Agent::Base).new (no block, chained .new)
31
+ new_content = new_content.gsub(
32
+ /Class\.new\(Phronomy::Agent::Base\)\.new\b/
33
+ ) do
34
+ counter += 1
35
+ "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1 }.new"
36
+ end
37
+
38
+ # Pattern C: { Class.new(Phronomy::Agent::Base) } (no block, inside outer block)
39
+ new_content = new_content.gsub(
40
+ "{ Class.new(Phronomy::Agent::Base) }"
41
+ ) do
42
+ counter += 1
43
+ "{ Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1 } }"
44
+ end
45
+
46
+ if new_content != content
47
+ File.write(file, new_content)
48
+ files_modified += 1
49
+ puts " patched: #{file.sub("#{SPEC_DIR}/", "spec/")}"
50
+ end
51
+ end
52
+
53
+ puts "\nSecond pass done: #{files_modified} files modified."
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Adds agent_definition to single-line inline anonymous blocks like:
5
+ # Class.new(Phronomy::Agent::Base) { model "test-model" }
6
+
7
+ counter = 300
8
+ files = Dir.glob("spec/**/*.rb").sort
9
+
10
+ files.each do |f|
11
+ content = File.read(f)
12
+ modified = content.gsub(
13
+ /Class\.new\(Phronomy::Agent::Base\) \{ ([^{}]+) \}/
14
+ ) do
15
+ body = $1
16
+ next $& if body.include?("agent_definition")
17
+ counter += 1
18
+ "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1; #{body} }"
19
+ end
20
+ if modified != content
21
+ File.write(f, modified)
22
+ puts " patched: #{f}"
23
+ end
24
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-02 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.3'
19
+ version: '1.15'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '2'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '1.3'
29
+ version: '1.15'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2'
@@ -78,46 +78,6 @@ dependencies:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
80
  version: '1.0'
81
- - !ruby/object:Gem::Dependency
82
- name: faraday
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - ">="
86
- - !ruby/object:Gem::Version
87
- version: '2'
88
- - - "<"
89
- - !ruby/object:Gem::Version
90
- version: '3'
91
- type: :runtime
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - ">="
96
- - !ruby/object:Gem::Version
97
- version: '2'
98
- - - "<"
99
- - !ruby/object:Gem::Version
100
- version: '3'
101
- - !ruby/object:Gem::Dependency
102
- name: event_stream_parser
103
- requirement: !ruby/object:Gem::Requirement
104
- requirements:
105
- - - ">="
106
- - !ruby/object:Gem::Version
107
- version: '1'
108
- - - "<"
109
- - !ruby/object:Gem::Version
110
- version: '2'
111
- type: :runtime
112
- prerelease: false
113
- version_requirements: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '1'
118
- - - "<"
119
- - !ruby/object:Gem::Version
120
- version: '2'
121
81
  description: Phronomy is a Ruby AI agent framework that provides composable building
122
82
  blocks — Agents, Workflows, Tools, Filters, and Tracing — for building AI agents
123
83
  in Ruby. Powered by RubyLLM for LLM abstraction.
@@ -156,20 +116,23 @@ files:
156
116
  - docs/decisions/010-cooperative-first-concurrency.md
157
117
  - docs/decisions/011-build-context-as-single-llm-input-authority.md
158
118
  - docs/decisions/011-delegate-transport-policy-to-adapters.md
119
+ - docs/decisions/012-canonical-execution-log-and-context-policy.md
159
120
  - docs/mcp-client.md
160
121
  - examples/workflows/agent_event_mapping.rb
161
122
  - examples/workflows/generic_task_event_mapping.rb
162
123
  - gemfiles/mcp_1_0.gemfile
163
124
  - lib/phronomy.rb
164
125
  - lib/phronomy/agent.rb
126
+ - lib/phronomy/agent/activation_registry.rb
127
+ - lib/phronomy/agent/agent_execution.rb
128
+ - lib/phronomy/agent/agent_execution_activation.rb
165
129
  - lib/phronomy/agent/agent_invocation.rb
166
- - lib/phronomy/agent/agent_invocation_registry.rb
167
130
  - lib/phronomy/agent/agent_invocation_session_builder.rb
131
+ - lib/phronomy/agent/agent_root.rb
168
132
  - lib/phronomy/agent/approval_evaluation_request.rb
169
133
  - lib/phronomy/agent/async_event_api.rb
170
134
  - lib/phronomy/agent/base.rb
171
- - lib/phronomy/agent/before_completion_context.rb
172
- - lib/phronomy/agent/concerns/before_completion.rb
135
+ - lib/phronomy/agent/concerns/before_llm_input.rb
173
136
  - lib/phronomy/agent/concerns/error_translation.rb
174
137
  - lib/phronomy/agent/concerns/filterable.rb
175
138
  - lib/phronomy/agent/context/capability/base.rb
@@ -177,16 +140,51 @@ files:
177
140
  - lib/phronomy/agent/context/knowledge/base.rb
178
141
  - lib/phronomy/agent/context/knowledge/entity_knowledge.rb
179
142
  - lib/phronomy/agent/context/knowledge/static_knowledge.rb
143
+ - lib/phronomy/agent/context_assembler.rb
144
+ - lib/phronomy/agent/context_candidate.rb
145
+ - lib/phronomy/agent/context_candidate_resolver.rb
146
+ - lib/phronomy/agent/context_importer.rb
147
+ - lib/phronomy/agent/context_parts/budget/token_budget_packer.rb
148
+ - lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb
149
+ - lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb
150
+ - lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb
151
+ - lib/phronomy/agent/context_parts/validators/final_budget_validator.rb
152
+ - lib/phronomy/agent/context_plan.rb
153
+ - lib/phronomy/agent/context_plan_validator.rb
154
+ - lib/phronomy/agent/context_policies/default.rb
155
+ - lib/phronomy/agent/context_policy.rb
156
+ - lib/phronomy/agent/context_policy_descriptor.rb
157
+ - lib/phronomy/agent/context_policy_registry.rb
158
+ - lib/phronomy/agent/context_request.rb
159
+ - lib/phronomy/agent/context_selection_unit.rb
160
+ - lib/phronomy/agent/derived_content_spec.rb
161
+ - lib/phronomy/agent/execution_coordinator.rb
162
+ - lib/phronomy/agent/fsm_runtime_adapter.rb
163
+ - lib/phronomy/agent/immutable.rb
164
+ - lib/phronomy/agent/journal_projection.rb
165
+ - lib/phronomy/agent/journal_record.rb
166
+ - lib/phronomy/agent/llm_call_record.rb
167
+ - lib/phronomy/agent/llm_input_build_context.rb
168
+ - lib/phronomy/agent/llm_input_manifest.rb
169
+ - lib/phronomy/agent/llm_input_patch.rb
180
170
  - lib/phronomy/agent/llm_operation_result.rb
181
171
  - lib/phronomy/agent/phase_machine_builder.rb
172
+ - lib/phronomy/agent/provider_call_outcome.rb
173
+ - lib/phronomy/agent/ruby_llm_materializer.rb
182
174
  - lib/phronomy/agent/runner.rb
183
175
  - lib/phronomy/agent/shared_state.rb
176
+ - lib/phronomy/agent/token_budget_resolver.rb
184
177
  - lib/phronomy/agent/tool_approval_request.rb
185
178
  - lib/phronomy/agent/tool_call_intercepted.rb
179
+ - lib/phronomy/agent/tool_definition_set.rb
186
180
  - lib/phronomy/agent/tool_executor.rb
187
181
  - lib/phronomy/agent/tool_invocation.rb
188
182
  - lib/phronomy/agent/tool_invocation_session_builder.rb
183
+ - lib/phronomy/agent_busy_error.rb
184
+ - lib/phronomy/canonical_json.rb
189
185
  - lib/phronomy/configuration.rb
186
+ - lib/phronomy/content_store/base.rb
187
+ - lib/phronomy/context_budget_exceeded_error.rb
190
188
  - lib/phronomy/diagnostics.rb
191
189
  - lib/phronomy/engine/concurrency/async_queue.rb
192
190
  - lib/phronomy/engine/concurrency/blocking_adapter_pool.rb
@@ -228,6 +226,7 @@ files:
228
226
  - lib/phronomy/eval/scorer/includes_scorer.rb
229
227
  - lib/phronomy/eval/scorer/llm_judge.rb
230
228
  - lib/phronomy/event.rb
229
+ - lib/phronomy/execution_rehydration_required_error.rb
231
230
  - lib/phronomy/filter.rb
232
231
  - lib/phronomy/filter/base.rb
233
232
  - lib/phronomy/filter/prompt_injection_filter.rb
@@ -235,6 +234,7 @@ files:
235
234
  - lib/phronomy/invalid_async_entry_action_error.rb
236
235
  - lib/phronomy/invalid_async_transition_action_error.rb
237
236
  - lib/phronomy/invalid_async_workflow_action_error.rb
237
+ - lib/phronomy/invalid_context_budget_configuration_error.rb
238
238
  - lib/phronomy/invocation_context.rb
239
239
  - lib/phronomy/knowledge_source.rb
240
240
  - lib/phronomy/llm_adapter.rb
@@ -253,6 +253,8 @@ files:
253
253
  - lib/phronomy/output_parser/base.rb
254
254
  - lib/phronomy/output_parser/json_parser.rb
255
255
  - lib/phronomy/output_parser/structured_parser.rb
256
+ - lib/phronomy/persistence.rb
257
+ - lib/phronomy/persistence/in_memory.rb
256
258
  - lib/phronomy/ruby_llm_patches.rb
257
259
  - lib/phronomy/runnable.rb
258
260
  - lib/phronomy/state_store/base.rb
@@ -291,11 +293,16 @@ files:
291
293
  - lib/phronomy/workflow/phase_machine_builder.rb
292
294
  - lib/phronomy/workflow_context.rb
293
295
  - lib/phronomy/workflow_runner.rb
296
+ - scripts/add_to_h_to_token_doubles.rb
297
+ - scripts/add_to_h_unnamed_doubles.rb
294
298
  - scripts/api_snapshot.rb
295
299
  - scripts/check_api_annotations.rb
296
300
  - scripts/check_private_enforcement.rb
297
301
  - scripts/check_readme_ruby.rb
298
302
  - scripts/check_readme_runnable.rb
303
+ - scripts/migrate_spec_agent_definition.rb
304
+ - scripts/migrate_spec_agent_definition_pass2.rb
305
+ - scripts/migrate_spec_inline_pass3.rb
299
306
  - scripts/run_mutation.sh
300
307
  - sig/phronomy.rbs
301
308
  homepage: https://github.com/Raizo-TCS/phronomy
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- # In-process registry for suspended AgentInvocation aggregates.
6
- #
7
- # The registry is the single in-process source for pending Human approval.
8
- # Cross-process persistence remains outside the scope of this implementation.
9
- #
10
- # @api private
11
- module AgentInvocationRegistry
12
- Entry = Struct.new(:invocation, :approval_request)
13
-
14
- @entries = {}
15
- @approval_index = {}
16
- @mutex = Mutex.new
17
-
18
- def self.store_suspended(invocation, approval_request)
19
- @mutex.synchronize do
20
- invocation_id = invocation.id
21
- request_id = approval_request.id
22
- if @entries.key?(invocation_id) || @approval_index.key?(request_id)
23
- raise Phronomy::Error,
24
- "Suspended AgentInvocation is already registered: #{invocation_id}"
25
- end
26
-
27
- @entries[invocation_id] = Entry.new(
28
- invocation: invocation,
29
- approval_request: approval_request
30
- )
31
- @approval_index[request_id] = invocation_id
32
- end
33
- approval_request
34
- end
35
-
36
- # Atomically removes and returns one pending approval aggregate.
37
- # Duplicate approval commands therefore cannot execute the Tool twice.
38
- def self.consume_approval(agent_invocation_id, approval_request_id)
39
- @mutex.synchronize do
40
- indexed_invocation_id = @approval_index[approval_request_id.to_s]
41
- return nil unless indexed_invocation_id == agent_invocation_id.to_s
42
-
43
- entry = @entries.delete(indexed_invocation_id)
44
- return nil unless entry
45
-
46
- @approval_index.delete(entry.approval_request.id)
47
- entry
48
- end
49
- end
50
-
51
- def self.lookup(agent_invocation_id)
52
- @mutex.synchronize { @entries[agent_invocation_id.to_s] }
53
- end
54
-
55
- def self.exists?(agent_invocation_id)
56
- @mutex.synchronize { @entries.key?(agent_invocation_id.to_s) }
57
- end
58
-
59
- def self.remove_terminal(agent_invocation_id)
60
- @mutex.synchronize do
61
- entry = @entries.delete(agent_invocation_id.to_s)
62
- @approval_index.delete(entry.approval_request.id) if entry
63
- entry
64
- end
65
- end
66
-
67
- def self.clear!
68
- @mutex.synchronize do
69
- @entries.clear
70
- @approval_index.clear
71
- end
72
- end
73
- end
74
- end
75
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- # Passed to every before_completion hook callable.
6
- # Provides read access to the agent, assembled messages, and invocation config.
7
- # Hooks may inspect these and return a Hash of params to merge into the LLM request.
8
- #
9
- # @example Reading context inside a hook
10
- # Phronomy.configure do |cfg|
11
- # cfg.before_completion = lambda do |ctx|
12
- # Rails.logger.info "LLM request: model=#{ctx.params[:model]}"
13
- # { temperature: 0.2 }
14
- # end
15
- # end
16
- class BeforeCompletionContext
17
- # The agent instance making the LLM call.
18
- # @return [Phronomy::Agent::Base]
19
- attr_reader :agent
20
-
21
- # Messages currently assembled for this invocation (read-only snapshot).
22
- # @return [Array]
23
- attr_reader :messages
24
-
25
- # Runtime config hash passed to invoke/stream (e.g. thread_id, memory).
26
- # @return [Hash]
27
- attr_reader :config
28
-
29
- # Current LLM params being built (model, temperature, etc.).
30
- # Read-only; return a Hash from the hook to merge overrides.
31
- # @return [Hash]
32
- attr_reader :params
33
-
34
- # @param agent [Phronomy::Agent::Base]
35
- # @param messages [Array]
36
- # @param config [Hash]
37
- # @param params [Hash] initial params (model, temperature already set on chat)
38
- # @api public
39
- def initialize(agent:, messages:, config:, params: {})
40
- @agent = agent
41
- @messages = messages.dup.freeze
42
- @config = config
43
- @params = params.dup.freeze
44
- end
45
- end
46
- end
47
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Concerns
6
- # Adds before_completion hook support to an agent.
7
- #
8
- # Included in {Phronomy::Agent::Base}. Hooks are executed just before every
9
- # LLM call (global → class → instance order) and may inject or override
10
- # LLM parameters such as temperature or model.
11
- # @api private
12
- module BeforeCompletion
13
- def self.included(base)
14
- base.extend(ClassMethods)
15
- end
16
-
17
- # Class-level DSL methods mixed into the including agent class.
18
- module ClassMethods
19
- # Sets or reads the class-level before_completion hook.
20
- # The hook is called before every LLM request for instances of this class.
21
- # Receives a {Phronomy::Agent::BeforeCompletionContext}; must return a Hash
22
- # of params to merge into the LLM call, or nil to pass through unchanged.
23
- #
24
- # @param callable [#call, nil] lambda/proc to register, or nil to clear
25
- # @return [#call, nil]
26
- # @example
27
- # class MyAgent < Phronomy::Agent::Base
28
- # before_completion ->(ctx) { { temperature: 0.2 } }
29
- # end
30
- # @api private
31
- def before_completion(callable = nil)
32
- if callable.nil? && !block_given?
33
- @before_completion
34
- else
35
- @before_completion = callable
36
- end
37
- end
38
-
39
- # @return [#call, nil]
40
- # @api private
41
- def _before_completion
42
- @before_completion
43
- end
44
- end
45
-
46
- # Instance-level before_completion hook. When set, takes precedence over
47
- # the class-level hook for this specific agent instance only.
48
- # @return [#call, nil]
49
- attr_accessor :before_completion
50
-
51
- private
52
-
53
- # Collects and runs all registered before_completion hooks in order
54
- # (global → class → instance) and applies the merged params to the chat.
55
- #
56
- # @param chat [RubyLLM::Chat] the assembled chat object
57
- # @param config [Hash] the invocation config hash
58
- # @return [Hash] the merged params applied to the chat
59
- # @api private
60
- def run_before_completion_hooks!(chat, config)
61
- hooks = [
62
- Phronomy.configuration.before_completion,
63
- self.class._before_completion,
64
- @before_completion
65
- ].compact
66
-
67
- return {} if hooks.empty?
68
-
69
- ctx = BeforeCompletionContext.new(
70
- agent: self,
71
- messages: chat.messages,
72
- config: config,
73
- params: {}
74
- )
75
-
76
- merged = {}
77
- hooks.each do |hook|
78
- result = hook.call(ctx)
79
- check_cancellation!(config, "invocation cancelled during before_completion hook")
80
- merged.merge!(result) if result.is_a?(Hash)
81
- end
82
-
83
- apply_before_completion_params!(chat, merged)
84
- merged
85
- end
86
-
87
- # Applies a merged param hash returned by before_completion hooks to
88
- # the chat object using the appropriate RubyLLM::Chat API methods.
89
- # When overriding the model, reuses the agent's configured provider and
90
- # assume_exists setting so that local/namespaced models continue to work.
91
- #
92
- # @param chat [RubyLLM::Chat]
93
- # @param params [Hash]
94
- # @api private
95
- def apply_before_completion_params!(chat, params)
96
- params.each do |key, value|
97
- case key
98
- when :model
99
- prov = self.class.provider
100
- chat.with_model(value, provider: prov, assume_exists: !prov.nil?)
101
- when :temperature
102
- chat.with_temperature(value)
103
- else
104
- chat.with_params(key => value)
105
- end
106
- end
107
- end
108
- end
109
- end
110
- end
111
- end