phronomy 0.8.0 → 0.9.1

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -4
  3. data/README.md +32 -41
  4. data/benchmark/baseline.json +1 -1
  5. data/benchmark/bench_agent_invoke.rb +1 -1
  6. data/benchmark/bench_context_assembler.rb +9 -1
  7. data/benchmark/bench_regression.rb +8 -8
  8. data/benchmark/bench_tool_schema.rb +2 -2
  9. data/benchmark/bench_vector_store.rb +1 -1
  10. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +224 -0
  11. data/lib/phronomy/agent/base.rb +328 -366
  12. data/lib/phronomy/agent/checkpoint.rb +30 -1
  13. data/lib/phronomy/agent/checkpoint_store.rb +97 -0
  14. data/lib/phronomy/agent/concerns/retryable.rb +1 -1
  15. data/lib/phronomy/agent/concerns/suspendable.rb +63 -8
  16. data/lib/phronomy/agent/context/capability/base.rb +689 -0
  17. data/lib/phronomy/agent/context/capability/scope_policy.rb +54 -0
  18. data/lib/phronomy/agent/context/knowledge/base.rb +58 -0
  19. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +102 -0
  20. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +58 -0
  21. data/lib/phronomy/agent/shared_state.rb +2 -2
  22. data/lib/phronomy/agent/tool_executor.rb +1 -1
  23. data/lib/phronomy/concurrency/gate_registry.rb +0 -1
  24. data/lib/phronomy/configuration.rb +13 -6
  25. data/lib/phronomy/event_loop.rb +1 -18
  26. data/lib/phronomy/llm_context_window/assembler.rb +77 -44
  27. data/lib/phronomy/multi_agent/handoff.rb +4 -4
  28. data/lib/phronomy/multi_agent/orchestrator.rb +1 -1
  29. data/lib/phronomy/multi_agent/team_coordinator.rb +2 -2
  30. data/lib/phronomy/runtime/runtime_metrics.rb +0 -1
  31. data/lib/phronomy/runtime.rb +1 -2
  32. data/lib/phronomy/tool.rb +3 -4
  33. data/lib/phronomy/{tool/agent_tool.rb → tools/agent.rb} +8 -9
  34. data/lib/phronomy/{tool/mcp_tool.rb → tools/mcp.rb} +9 -9
  35. data/lib/phronomy/tools/vector_search.rb +70 -0
  36. data/lib/phronomy/vector_store/async_backend.rb +110 -0
  37. data/lib/phronomy/vector_store/base.rb +89 -0
  38. data/lib/phronomy/vector_store/embeddings/base.rb +41 -0
  39. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +47 -0
  40. data/lib/phronomy/vector_store/in_memory.rb +103 -0
  41. data/lib/phronomy/vector_store/loader/base.rb +27 -0
  42. data/lib/phronomy/vector_store/loader/csv_loader.rb +58 -0
  43. data/lib/phronomy/vector_store/loader/markdown_loader.rb +78 -0
  44. data/lib/phronomy/vector_store/loader/plain_text_loader.rb +24 -0
  45. data/lib/phronomy/vector_store/pgvector.rb +127 -0
  46. data/lib/phronomy/vector_store/redis_search.rb +192 -0
  47. data/lib/phronomy/vector_store/splitter/base.rb +49 -0
  48. data/lib/phronomy/vector_store/splitter/fixed_size_splitter.rb +53 -0
  49. data/lib/phronomy/vector_store/splitter/recursive_splitter.rb +107 -0
  50. data/lib/phronomy/vector_store.rb +16 -4
  51. data/lib/phronomy/version.rb +1 -1
  52. data/lib/phronomy/workflow/fsm_session.rb +249 -0
  53. data/lib/phronomy/workflow/phase_machine_builder.rb +247 -0
  54. data/lib/phronomy/workflow_runner.rb +2 -2
  55. data/lib/phronomy.rb +10 -3
  56. data/scripts/api_snapshot.rb +11 -10
  57. metadata +31 -37
  58. data/lib/phronomy/agent/context/conversation/compaction_context.rb +0 -117
  59. data/lib/phronomy/agent/context/conversation/trigger_context.rb +0 -43
  60. data/lib/phronomy/agent/context/conversation/trim_context.rb +0 -82
  61. data/lib/phronomy/agent/context/knowledge/embeddings/base.rb +0 -45
  62. data/lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb +0 -51
  63. data/lib/phronomy/agent/context/knowledge/loader/base.rb +0 -31
  64. data/lib/phronomy/agent/context/knowledge/loader/csv_loader.rb +0 -62
  65. data/lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb +0 -82
  66. data/lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb +0 -28
  67. data/lib/phronomy/agent/context/knowledge/source/base.rb +0 -60
  68. data/lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb +0 -102
  69. data/lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb +0 -63
  70. data/lib/phronomy/agent/context/knowledge/source/static_knowledge.rb +0 -58
  71. data/lib/phronomy/agent/context/knowledge/splitter/base.rb +0 -53
  72. data/lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb +0 -57
  73. data/lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb +0 -111
  74. data/lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb +0 -116
  75. data/lib/phronomy/agent/context/knowledge/vector_store/base.rb +0 -95
  76. data/lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb +0 -109
  77. data/lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb +0 -133
  78. data/lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb +0 -198
  79. data/lib/phronomy/agent/fsm.rb +0 -157
  80. data/lib/phronomy/agent/invocation_pipeline.rb +0 -99
  81. data/lib/phronomy/agent/lifecycle/fsm_session.rb +0 -251
  82. data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +0 -249
  83. data/lib/phronomy/agent/react_agent.rb +0 -204
  84. data/lib/phronomy/embeddings.rb +0 -11
  85. data/lib/phronomy/loader.rb +0 -13
  86. data/lib/phronomy/splitter.rb +0 -12
  87. data/lib/phronomy/tool/base.rb +0 -685
  88. data/lib/phronomy/tool/scope_policy.rb +0 -50
@@ -1,204 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- # ReAct pattern (Reasoning + Acting) agent.
6
- # Repeats the LLM <-> Tool loop until no more tool calls are made.
7
- class ReactAgent < Base
8
- private
9
-
10
- # Performs a single (non-retried) ReAct invocation.
11
- # Overrides Base#invoke_once so that Base#invoke's retry loop is inherited.
12
- def invoke_once(input, messages: [], thread_id: nil, config: {})
13
- caller_meta = {}
14
- caller_meta[:user_id] = config[:user_id] if config[:user_id]
15
- caller_meta[:session_id] = config[:session_id] if config[:session_id]
16
- if (ic = config[:invocation_context])
17
- caller_meta[:task_id] = ic.task_id if ic.task_id
18
- caller_meta[:parent_task_id] = ic.parent_task_id if ic.parent_task_id
19
- end
20
-
21
- trace("agent.invoke", input: input, **caller_meta) do |_span|
22
- # Run input guardrails before any LLM interaction.
23
- run_input_guardrails!(input)
24
-
25
- max_iter = self.class.max_iterations
26
-
27
- # Seed with app-managed conversation history when provided.
28
- messages = Array(messages).dup
29
- user_asked = false
30
- total_usage = Phronomy::TokenUsage.zero
31
- iterations_exhausted = true
32
-
33
- max_iter.times do
34
- response = step(messages, input, user_asked: user_asked, thread_id: thread_id, config: config)
35
- user_asked = true
36
- messages = response[:messages]
37
- total_usage += response[:usage]
38
- if response[:done]
39
- iterations_exhausted = false
40
- break
41
- end
42
- end
43
-
44
- # Select the last assistant-produced content as the output, skipping
45
- # raw tool result messages (role: :tool) to avoid returning tool JSON
46
- # or status strings as the agent's answer when iterations are exhausted.
47
- output = messages.reverse.find { |m| m.content && !m.content.empty? && m.role != :tool }&.content
48
-
49
- # Run output guardrails before returning to the caller.
50
- run_output_guardrails!(output)
51
-
52
- result = {output: output, messages: messages, usage: total_usage, iterations_exhausted: iterations_exhausted}
53
- [result, total_usage]
54
- end
55
- end
56
-
57
- public
58
-
59
- # Streaming version of #invoke for the ReAct loop.
60
- # Yields {Phronomy::Agent::StreamEvent} events while the LLM-tool loop runs.
61
- #
62
- # @param input [String, Hash]
63
- # @param messages [Array<RubyLLM::Message>] same as #invoke
64
- # @param thread_id [String, nil] same as #invoke
65
- # @param config [Hash]
66
- # @yield [Phronomy::Agent::StreamEvent]
67
- # @return [Hash] { output:, messages:, usage: }
68
- # @api public
69
- def stream(input, messages: [], thread_id: nil, config: {}, &block)
70
- return invoke(input, messages: messages, thread_id: thread_id, config: config) unless block
71
-
72
- caller_meta = {}
73
- caller_meta[:user_id] = config[:user_id] if config[:user_id]
74
- caller_meta[:session_id] = config[:session_id] if config[:session_id]
75
- if (ic = config[:invocation_context])
76
- caller_meta[:task_id] = ic.task_id if ic.task_id
77
- caller_meta[:parent_task_id] = ic.parent_task_id if ic.parent_task_id
78
- end
79
-
80
- trace("agent.invoke", input: input, **caller_meta) do |_span|
81
- run_input_guardrails!(input)
82
-
83
- max_iter = self.class.max_iterations
84
-
85
- messages = Array(messages).dup
86
- user_asked = false
87
- total_usage = Phronomy::TokenUsage.zero
88
- iterations_exhausted = true
89
-
90
- max_iter.times do
91
- response = stream_step(messages, input, user_asked: user_asked, thread_id: thread_id, config: config, &block)
92
- user_asked = true
93
- messages = response[:messages]
94
- total_usage += response[:usage]
95
- if response[:done]
96
- iterations_exhausted = false
97
- break
98
- end
99
- end
100
-
101
- # Select the last assistant-produced content as the output, skipping
102
- # raw tool result messages (role: :tool) — same as the non-streaming path.
103
- output = messages.reverse.find { |m| m.content && !m.content.empty? && m.role != :tool }&.content
104
- run_output_guardrails!(output)
105
-
106
- result = {output: output, messages: messages, usage: total_usage, iterations_exhausted: iterations_exhausted}
107
- block.call(StreamEvent.new(type: :done, payload: result))
108
- [result, total_usage]
109
- end
110
- rescue => e
111
- block&.call(StreamEvent.new(type: :error, payload: {error: e}))
112
- raise
113
- end
114
-
115
- private
116
-
117
- def step(messages, initial_input, user_asked: false, thread_id: nil, config: {})
118
- chat = build_chat
119
-
120
- if user_asked
121
- # Subsequent loop iteration — messages already contains the full conversation
122
- # (including the user's original input from the first step); apply system
123
- # instructions and replay the accumulated history, then let the LLM continue.
124
- system_text = build_cached_system_text(initial_input)
125
- apply_instructions(chat, system_text) if system_text
126
- messages.each { |m| chat.add_message(m) }
127
- else
128
- # First iteration — assemble context (system + history) via build_context so
129
- # that trimming, compaction, and knowledge sources are applied consistently.
130
- context = build_context(initial_input, messages: messages, thread_id: thread_id, config: config)
131
- apply_instructions(chat, context[:system]) if context[:system]
132
- context[:messages].each { |m| chat.messages << m }
133
- end
134
-
135
- # Run before_completion hooks before each LLM call in the ReAct loop.
136
- run_before_completion_hooks!(chat, config)
137
-
138
- # Route the LLM call through the configured LLMAdapter so that the
139
- # blocking HTTP request runs inside BlockingAdapterPool and the
140
- # adapter can be swapped without changing agent code.
141
- # Passing nil as message signals the adapter to call chat.complete
142
- # (no new user turn) for continuation iterations.
143
- adapter = Phronomy.configuration.llm_adapter
144
- message = user_asked ? nil : extract_message(initial_input)
145
- response = adapter.complete_async(chat, message, config: config).await
146
-
147
- usage = Phronomy::TokenUsage.from_tokens(response&.tokens)
148
- tool_calls = chat.messages.last&.tool_calls
149
- done = tool_calls.nil? || tool_calls.empty?
150
- {messages: chat.messages, done: done, usage: usage}
151
- end
152
-
153
- # Streaming variant of #step. Yields :token / :tool_call / :tool_result events
154
- # via the block while the LLM call is in progress.
155
- def stream_step(messages, initial_input, user_asked: false, thread_id: nil, config: {}, &block)
156
- chat = build_chat
157
-
158
- if user_asked
159
- system_text = build_cached_system_text(initial_input)
160
- apply_instructions(chat, system_text) if system_text
161
- messages.each { |m| chat.add_message(m) }
162
- else
163
- context = build_context(initial_input, messages: messages, thread_id: thread_id, config: config)
164
- apply_instructions(chat, context[:system]) if context[:system]
165
- context[:messages].each { |m| chat.messages << m }
166
- end
167
-
168
- current_tool_call = nil
169
- chat.on_tool_call do |tc|
170
- current_tool_call = tc
171
- block.call(StreamEvent.new(type: :tool_call, payload: {tool_call: tc}))
172
- end
173
- chat.on_tool_result do |tr|
174
- block.call(StreamEvent.new(type: :tool_result, payload: {
175
- tool_call_id: current_tool_call&.id,
176
- tool_name: current_tool_call&.name,
177
- tool_result: tr
178
- }))
179
- end
180
-
181
- # Run before_completion hooks before each LLM call in the streaming loop.
182
- run_before_completion_hooks!(chat, config)
183
-
184
- # Route the streaming LLM call through the configured LLMAdapter so that
185
- # the blocking HTTP request runs inside BlockingAdapterPool.
186
- # Passing nil as message signals the adapter to call chat.complete
187
- # (no new user turn) for continuation iterations.
188
- # Streaming chunks and tool event callbacks are delivered directly via the
189
- # block on the pool worker thread; pending.await yields cooperatively until
190
- # streaming is complete.
191
- adapter = Phronomy.configuration.llm_adapter
192
- message = user_asked ? nil : extract_message(initial_input)
193
- streaming_block = proc { |chunk| block.call(StreamEvent.new(type: :token, payload: {content: chunk.content})) }
194
- pending = adapter.stream_async(chat, message, config: config, &streaming_block)
195
- response = pending.await
196
-
197
- usage = Phronomy::TokenUsage.from_tokens(response&.tokens)
198
- tool_calls = chat.messages.last&.tool_calls
199
- done = tool_calls.nil? || tool_calls.empty?
200
- {messages: chat.messages, done: done, usage: usage}
201
- end
202
- end
203
- end
204
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- # Embeddings adapters for converting text into vector representations.
5
- #
6
- # Sub-classes are auto-loaded by Zeitwerk:
7
- # Phronomy::Agent::Context::Knowledge::Embeddings::Base
8
- # Phronomy::Agent::Context::Knowledge::Embeddings::RubyLLMEmbeddings
9
- module Embeddings
10
- end
11
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- # Document loader implementations for ingesting files into a RAG pipeline.
5
- #
6
- # Sub-classes are auto-loaded by Zeitwerk:
7
- # Phronomy::Agent::Context::Knowledge::Loader::Base
8
- # Phronomy::Agent::Context::Knowledge::Loader::PlainTextLoader
9
- # Phronomy::Agent::Context::Knowledge::Loader::MarkdownLoader
10
- # Phronomy::Agent::Context::Knowledge::Loader::CsvLoader
11
- module Loader
12
- end
13
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- # Text splitter implementations for chunking documents before embedding.
5
- #
6
- # Sub-classes are auto-loaded by Zeitwerk:
7
- # Phronomy::Agent::Context::Knowledge::Splitter::Base
8
- # Phronomy::Agent::Context::Knowledge::Splitter::FixedSizeSplitter
9
- # Phronomy::Agent::Context::Knowledge::Splitter::RecursiveSplitter
10
- module Splitter
11
- end
12
- end