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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -4
- data/README.md +32 -41
- data/benchmark/baseline.json +1 -1
- data/benchmark/bench_agent_invoke.rb +1 -1
- data/benchmark/bench_context_assembler.rb +9 -1
- data/benchmark/bench_regression.rb +8 -8
- data/benchmark/bench_tool_schema.rb +2 -2
- data/benchmark/bench_vector_store.rb +1 -1
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +224 -0
- data/lib/phronomy/agent/base.rb +328 -366
- data/lib/phronomy/agent/checkpoint.rb +30 -1
- data/lib/phronomy/agent/checkpoint_store.rb +97 -0
- data/lib/phronomy/agent/concerns/retryable.rb +1 -1
- data/lib/phronomy/agent/concerns/suspendable.rb +63 -8
- data/lib/phronomy/agent/context/capability/base.rb +689 -0
- data/lib/phronomy/agent/context/capability/scope_policy.rb +54 -0
- data/lib/phronomy/agent/context/knowledge/base.rb +58 -0
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +102 -0
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +58 -0
- data/lib/phronomy/agent/shared_state.rb +2 -2
- data/lib/phronomy/agent/tool_executor.rb +1 -1
- data/lib/phronomy/concurrency/gate_registry.rb +0 -1
- data/lib/phronomy/configuration.rb +13 -6
- data/lib/phronomy/event_loop.rb +1 -18
- data/lib/phronomy/llm_context_window/assembler.rb +77 -44
- data/lib/phronomy/multi_agent/handoff.rb +4 -4
- data/lib/phronomy/multi_agent/orchestrator.rb +1 -1
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -2
- data/lib/phronomy/runtime/runtime_metrics.rb +0 -1
- data/lib/phronomy/runtime.rb +1 -2
- data/lib/phronomy/tool.rb +3 -4
- data/lib/phronomy/{tool/agent_tool.rb → tools/agent.rb} +8 -9
- data/lib/phronomy/{tool/mcp_tool.rb → tools/mcp.rb} +9 -9
- data/lib/phronomy/tools/vector_search.rb +70 -0
- data/lib/phronomy/vector_store/async_backend.rb +110 -0
- data/lib/phronomy/vector_store/base.rb +89 -0
- data/lib/phronomy/vector_store/embeddings/base.rb +41 -0
- data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +47 -0
- data/lib/phronomy/vector_store/in_memory.rb +103 -0
- data/lib/phronomy/vector_store/loader/base.rb +27 -0
- data/lib/phronomy/vector_store/loader/csv_loader.rb +58 -0
- data/lib/phronomy/vector_store/loader/markdown_loader.rb +78 -0
- data/lib/phronomy/vector_store/loader/plain_text_loader.rb +24 -0
- data/lib/phronomy/vector_store/pgvector.rb +127 -0
- data/lib/phronomy/vector_store/redis_search.rb +192 -0
- data/lib/phronomy/vector_store/splitter/base.rb +49 -0
- data/lib/phronomy/vector_store/splitter/fixed_size_splitter.rb +53 -0
- data/lib/phronomy/vector_store/splitter/recursive_splitter.rb +107 -0
- data/lib/phronomy/vector_store.rb +16 -4
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/fsm_session.rb +249 -0
- data/lib/phronomy/workflow/phase_machine_builder.rb +247 -0
- data/lib/phronomy/workflow_runner.rb +2 -2
- data/lib/phronomy.rb +10 -3
- data/scripts/api_snapshot.rb +11 -10
- metadata +31 -37
- data/lib/phronomy/agent/context/conversation/compaction_context.rb +0 -117
- data/lib/phronomy/agent/context/conversation/trigger_context.rb +0 -43
- data/lib/phronomy/agent/context/conversation/trim_context.rb +0 -82
- data/lib/phronomy/agent/context/knowledge/embeddings/base.rb +0 -45
- data/lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb +0 -51
- data/lib/phronomy/agent/context/knowledge/loader/base.rb +0 -31
- data/lib/phronomy/agent/context/knowledge/loader/csv_loader.rb +0 -62
- data/lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb +0 -82
- data/lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb +0 -28
- data/lib/phronomy/agent/context/knowledge/source/base.rb +0 -60
- data/lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb +0 -63
- data/lib/phronomy/agent/context/knowledge/source/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/splitter/base.rb +0 -53
- data/lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb +0 -57
- data/lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb +0 -111
- data/lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb +0 -116
- data/lib/phronomy/agent/context/knowledge/vector_store/base.rb +0 -95
- data/lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb +0 -109
- data/lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb +0 -133
- data/lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb +0 -198
- data/lib/phronomy/agent/fsm.rb +0 -157
- data/lib/phronomy/agent/invocation_pipeline.rb +0 -99
- data/lib/phronomy/agent/lifecycle/fsm_session.rb +0 -251
- data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +0 -249
- data/lib/phronomy/agent/react_agent.rb +0 -204
- data/lib/phronomy/embeddings.rb +0 -11
- data/lib/phronomy/loader.rb +0 -13
- data/lib/phronomy/splitter.rb +0 -12
- data/lib/phronomy/tool/base.rb +0 -685
- 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
|
data/lib/phronomy/embeddings.rb
DELETED
|
@@ -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
|
data/lib/phronomy/loader.rb
DELETED
|
@@ -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
|
data/lib/phronomy/splitter.rb
DELETED
|
@@ -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
|