phronomy 0.19.0 → 0.21.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 +3 -1
- data/CHANGELOG.md +41 -11
- data/CONTRIBUTING.md +74 -6
- data/README.md +15 -6
- data/docs/decisions/010-cooperative-first-concurrency.md +86 -69
- data/docs/decisions/014-unified-persistence-durable-state.md +5 -0
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +184 -0
- data/docs/features.md +31 -2
- data/docs/getting-started.md +10 -1
- data/docs/migrations/0.19.md +12 -6
- data/docs/persistence-backends.md +504 -0
- data/docs/runtime-and-concurrency.md +110 -147
- data/lib/phronomy/agent/agent_execution.rb +29 -0
- data/lib/phronomy/agent/llm_call_record.rb +20 -0
- data/lib/phronomy/agent/tool_executor.rb +7 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +142 -245
- data/lib/phronomy/engine/task.rb +50 -6
- data/lib/phronomy/invocation_context.rb +11 -52
- data/lib/phronomy/llm_adapter/base.rb +29 -32
- data/lib/phronomy/llm_adapter/ruby_llm.rb +13 -12
- data/lib/phronomy/llm_adapter.rb +10 -7
- data/lib/phronomy/output_parser/base.rb +5 -1
- data/lib/phronomy/persistence.rb +101 -7
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +50 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +164 -0
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +215 -0
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +119 -0
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +99 -0
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +202 -0
- data/lib/phronomy/testing/persistence_contract.rb +41 -0
- data/lib/phronomy/tool/base.rb +15 -0
- data/lib/phronomy/tool.rb +11 -0
- data/lib/phronomy/vector_store/async_backend.rb +15 -38
- data/lib/phronomy/vector_store/base.rb +12 -13
- data/lib/phronomy/vector_store/embeddings/base.rb +11 -9
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +6 -0
- data/scripts/run_mutation.sh +2 -1
- data/sig/phronomy/agent.rbs +36 -0
- data/sig/phronomy/extensions.rbs +50 -0
- data/sig/phronomy/llm_adapter.rbs +11 -0
- data/sig/phronomy/persistence.rbs +70 -0
- data/sig/phronomy/runtime.rbs +43 -0
- data/sig/phronomy/tool.rbs +39 -0
- data/sig/phronomy/workflow.rbs +30 -0
- data/sig/phronomy.rbs +49 -1
- metadata +20 -3
- data/scripts/check_private_enforcement.rb +0 -93
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
RSpec.shared_examples "an Execution repository" do
|
|
6
|
+
let(:execution_repository) { persistence.executions }
|
|
7
|
+
let(:execution_agent_root) do
|
|
8
|
+
Phronomy::Agent::AgentRoot.create(
|
|
9
|
+
agent_id: "execution-agent-#{SecureRandom.uuid}",
|
|
10
|
+
agent_definition_id: "contract-agent",
|
|
11
|
+
definition_version: 1
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_contract_execution(root)
|
|
16
|
+
input_record = Phronomy::Agent::JournalRecord.new(
|
|
17
|
+
agent_id: root.agent_id,
|
|
18
|
+
kind: :input_received,
|
|
19
|
+
channel: :external,
|
|
20
|
+
role: :user,
|
|
21
|
+
context_candidate: false
|
|
22
|
+
)
|
|
23
|
+
Phronomy::Agent::AgentExecution.start(
|
|
24
|
+
agent_root: root,
|
|
25
|
+
input_record: input_record
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
persistence.agents.create(execution_agent_root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "creates and loads an active execution" do
|
|
34
|
+
execution = build_contract_execution(execution_agent_root)
|
|
35
|
+
|
|
36
|
+
expect(execution_repository.create_active(execution).to_h).to eq(execution.to_h)
|
|
37
|
+
expect(execution_repository.load(execution.execution_id).to_h).to eq(execution.to_h)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "raises NotFoundError for a missing execution" do
|
|
41
|
+
expect do
|
|
42
|
+
execution_repository.load("missing-#{SecureRandom.uuid}")
|
|
43
|
+
end.to raise_error(Phronomy::Persistence::NotFoundError)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "rejects a duplicate execution_id" do
|
|
47
|
+
execution = build_contract_execution(execution_agent_root)
|
|
48
|
+
execution_repository.create_active(execution)
|
|
49
|
+
|
|
50
|
+
expect do
|
|
51
|
+
execution_repository.create_active(execution)
|
|
52
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "admits at most one active or suspended execution for one Agent" do
|
|
56
|
+
first = build_contract_execution(execution_agent_root)
|
|
57
|
+
second = build_contract_execution(execution_agent_root)
|
|
58
|
+
execution_repository.create_active(first)
|
|
59
|
+
|
|
60
|
+
expect do
|
|
61
|
+
execution_repository.create_active(second)
|
|
62
|
+
end.to raise_error(Phronomy::AgentBusyError)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "allows different Agents to have active executions" do
|
|
66
|
+
other_root = Phronomy::Agent::AgentRoot.create(
|
|
67
|
+
agent_id: "execution-agent-#{SecureRandom.uuid}",
|
|
68
|
+
agent_definition_id: "contract-agent",
|
|
69
|
+
definition_version: 1
|
|
70
|
+
)
|
|
71
|
+
persistence.agents.create(other_root)
|
|
72
|
+
|
|
73
|
+
expect do
|
|
74
|
+
execution_repository.create_active(build_contract_execution(execution_agent_root))
|
|
75
|
+
execution_repository.create_active(build_contract_execution(other_root))
|
|
76
|
+
end.not_to raise_error
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "saves only at the expected execution revision" do
|
|
80
|
+
execution = build_contract_execution(execution_agent_root)
|
|
81
|
+
execution_repository.create_active(execution)
|
|
82
|
+
updated = execution.with(status: :active, phase: :calling_llm)
|
|
83
|
+
|
|
84
|
+
expect(
|
|
85
|
+
execution_repository.save(
|
|
86
|
+
execution.execution_id,
|
|
87
|
+
expected_revision: 0,
|
|
88
|
+
execution: updated
|
|
89
|
+
).to_h
|
|
90
|
+
).to eq(updated.to_h)
|
|
91
|
+
expect(execution_repository.load(execution.execution_id).execution_revision).to eq(1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "rejects a stale execution revision" do
|
|
95
|
+
execution = build_contract_execution(execution_agent_root)
|
|
96
|
+
execution_repository.create_active(execution)
|
|
97
|
+
updated = execution.with(status: :active, phase: :calling_llm)
|
|
98
|
+
execution_repository.save(
|
|
99
|
+
execution.execution_id,
|
|
100
|
+
expected_revision: 0,
|
|
101
|
+
execution: updated
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
expect do
|
|
105
|
+
execution_repository.save(
|
|
106
|
+
execution.execution_id,
|
|
107
|
+
expected_revision: 0,
|
|
108
|
+
execution: updated
|
|
109
|
+
)
|
|
110
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "rejects an execution identity mismatch" do
|
|
114
|
+
execution = build_contract_execution(execution_agent_root)
|
|
115
|
+
execution_repository.create_active(execution)
|
|
116
|
+
other_root = Phronomy::Agent::AgentRoot.create(
|
|
117
|
+
agent_id: "execution-agent-#{SecureRandom.uuid}",
|
|
118
|
+
agent_definition_id: "contract-agent",
|
|
119
|
+
definition_version: 1
|
|
120
|
+
)
|
|
121
|
+
persistence.agents.create(other_root)
|
|
122
|
+
other = build_contract_execution(other_root).with(
|
|
123
|
+
execution_revision: 1,
|
|
124
|
+
status: :active,
|
|
125
|
+
phase: :calling_llm
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect do
|
|
129
|
+
execution_repository.save(
|
|
130
|
+
execution.execution_id,
|
|
131
|
+
expected_revision: 0,
|
|
132
|
+
execution: other
|
|
133
|
+
)
|
|
134
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "requires execution revision to advance exactly once" do
|
|
138
|
+
execution = build_contract_execution(execution_agent_root)
|
|
139
|
+
execution_repository.create_active(execution)
|
|
140
|
+
skipped = execution.with(
|
|
141
|
+
execution_revision: 2,
|
|
142
|
+
status: :active,
|
|
143
|
+
phase: :calling_llm
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
expect do
|
|
147
|
+
execution_repository.save(
|
|
148
|
+
execution.execution_id,
|
|
149
|
+
expected_revision: 0,
|
|
150
|
+
execution: skipped
|
|
151
|
+
)
|
|
152
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "lists active executions for one Agent" do
|
|
156
|
+
execution = build_contract_execution(execution_agent_root)
|
|
157
|
+
execution_repository.create_active(execution)
|
|
158
|
+
|
|
159
|
+
expect(execution_repository.list_active(execution_agent_root.agent_id).map(&:execution_id))
|
|
160
|
+
.to eq([execution.execution_id])
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "asserts idle state and rejects active Agents" do
|
|
164
|
+
expect do
|
|
165
|
+
execution_repository.assert_idle!(execution_agent_root.agent_id)
|
|
166
|
+
end.not_to raise_error
|
|
167
|
+
|
|
168
|
+
execution_repository.create_active(build_contract_execution(execution_agent_root))
|
|
169
|
+
|
|
170
|
+
expect do
|
|
171
|
+
execution_repository.assert_idle!(execution_agent_root.agent_id)
|
|
172
|
+
end.to raise_error(Phronomy::AgentBusyError)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "deletes one execution" do
|
|
176
|
+
execution = build_contract_execution(execution_agent_root)
|
|
177
|
+
execution_repository.create_active(execution)
|
|
178
|
+
execution_repository.delete(execution.execution_id)
|
|
179
|
+
|
|
180
|
+
expect do
|
|
181
|
+
execution_repository.load(execution.execution_id)
|
|
182
|
+
end.to raise_error(Phronomy::Persistence::NotFoundError)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "deletes all executions for one Agent without deleting other Agents' executions" do
|
|
186
|
+
other_root = Phronomy::Agent::AgentRoot.create(
|
|
187
|
+
agent_id: "execution-agent-#{SecureRandom.uuid}",
|
|
188
|
+
agent_definition_id: "contract-agent",
|
|
189
|
+
definition_version: 1
|
|
190
|
+
)
|
|
191
|
+
persistence.agents.create(other_root)
|
|
192
|
+
own = build_contract_execution(execution_agent_root)
|
|
193
|
+
other = build_contract_execution(other_root)
|
|
194
|
+
execution_repository.create_active(own)
|
|
195
|
+
execution_repository.create_active(other)
|
|
196
|
+
|
|
197
|
+
execution_repository.delete_for_agent(execution_agent_root.agent_id)
|
|
198
|
+
|
|
199
|
+
expect(execution_repository.list_active(execution_agent_root.agent_id)).to be_empty
|
|
200
|
+
expect(execution_repository.load(other.execution_id).execution_id).to eq(other.execution_id)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "phronomy"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "rspec/core"
|
|
7
|
+
require "rspec/expectations"
|
|
8
|
+
rescue LoadError => error
|
|
9
|
+
raise LoadError,
|
|
10
|
+
"Phronomy Persistence contract support requires RSpec. " \
|
|
11
|
+
"Add `rspec` to the backend project's development/test dependencies before " \
|
|
12
|
+
"requiring `phronomy/testing/persistence_contract`.",
|
|
13
|
+
error.backtrace
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Phronomy
|
|
17
|
+
module Testing
|
|
18
|
+
# Explicitly loaded RSpec shared examples for Persistence backend authors.
|
|
19
|
+
#
|
|
20
|
+
# This namespace is intentionally excluded from Phronomy's production
|
|
21
|
+
# Zeitwerk eager-load path. Requiring this file is the opt-in boundary that
|
|
22
|
+
# loads RSpec and the backend conformance suite.
|
|
23
|
+
module PersistenceContract
|
|
24
|
+
SHARED_EXAMPLES = [
|
|
25
|
+
"a persistence content store",
|
|
26
|
+
"an Agent repository",
|
|
27
|
+
"a Journal repository",
|
|
28
|
+
"an Execution repository",
|
|
29
|
+
"a workflow state repository",
|
|
30
|
+
"a Persistence backend"
|
|
31
|
+
].freeze
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require_relative "persistence_contract/a_content_store"
|
|
37
|
+
require_relative "persistence_contract/an_agent_repository"
|
|
38
|
+
require_relative "persistence_contract/a_journal_repository"
|
|
39
|
+
require_relative "persistence_contract/an_execution_repository"
|
|
40
|
+
require_relative "persistence_contract/a_workflow_state_repository"
|
|
41
|
+
require_relative "persistence_contract/a_persistence_backend"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Tool
|
|
5
|
+
# Public authoring façade for Phronomy Tools.
|
|
6
|
+
#
|
|
7
|
+
# This constant intentionally refers to the exact same Class object as
|
|
8
|
+
# {Phronomy::Agent::Context::Capability::Base}. The implementation namespace
|
|
9
|
+
# remains canonical internally so the existing Tool DSL state, inheritance,
|
|
10
|
+
# built-in Tools, and compatibility surface are not duplicated.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
Base = Phronomy::Agent::Context::Capability::Base
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -2,43 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module VectorStore
|
|
5
|
-
#
|
|
5
|
+
# Framework-owned async convenience methods for VectorStore backends.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# The backend extension contract is the synchronous interface defined by
|
|
8
|
+
# {VectorStore::Base}: add/search/remove/clear/size. These async convenience
|
|
9
|
+
# methods are inherited by backends and route that synchronous work through
|
|
10
|
+
# Phronomy's bounded {OffloadPool}. This keeps OS-thread creation, queue
|
|
11
|
+
# backpressure, submit timeout/cancellation, and completion semantics owned by
|
|
12
|
+
# the framework rather than by each backend.
|
|
8
13
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
14
|
+
# A future genuine native-async backend may adapt its completion into a
|
|
15
|
+
# {Phronomy::Task} without an OffloadPool worker, but native async override is
|
|
16
|
+
# not part of the current backend SPI.
|
|
11
17
|
#
|
|
12
|
-
#
|
|
13
|
-
# backend has a native async driver, while the remaining methods fall back
|
|
14
|
-
# to the pool.
|
|
15
|
-
#
|
|
16
|
-
# 3. **Implement all natively** — override all async methods to avoid pool
|
|
17
|
-
# allocation entirely.
|
|
18
|
-
#
|
|
19
|
-
# @example Native async search (no pool worker thread allocated)
|
|
20
|
-
# class MyFastStore < Phronomy::VectorStore::Base
|
|
21
|
-
# include Phronomy::VectorStore::AsyncBackend
|
|
22
|
-
#
|
|
23
|
-
# def search_async(query_embedding:, k: 5, cancellation_token: nil, timeout: nil)
|
|
24
|
-
# # Returns a PendingOperation backed by a native async driver.
|
|
25
|
-
# native_async_search(query_embedding, k)
|
|
26
|
-
# end
|
|
27
|
-
# end
|
|
28
|
-
#
|
|
29
|
-
# @api public
|
|
18
|
+
# @api private
|
|
30
19
|
module AsyncBackend
|
|
31
20
|
# Async variant of {VectorStore::Base#add}.
|
|
32
21
|
#
|
|
33
|
-
# Submits the add call to {OffloadPool} by default.
|
|
34
|
-
# Override to use a native async driver.
|
|
35
|
-
#
|
|
36
22
|
# @param id [String]
|
|
37
23
|
# @param embedding [Array<Float>]
|
|
38
24
|
# @param metadata [Hash]
|
|
39
25
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
40
26
|
# @param timeout [Numeric, nil]
|
|
41
|
-
# @return [
|
|
27
|
+
# @return [Phronomy::Task]
|
|
42
28
|
# @api public
|
|
43
29
|
def add_async(id:, embedding:, metadata: {}, cancellation_token: nil, timeout: nil)
|
|
44
30
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -52,14 +38,11 @@ module Phronomy
|
|
|
52
38
|
|
|
53
39
|
# Async variant of {VectorStore::Base#search}.
|
|
54
40
|
#
|
|
55
|
-
# Submits the search call to {OffloadPool} by default.
|
|
56
|
-
# Override to use a native async driver.
|
|
57
|
-
#
|
|
58
41
|
# @param query_embedding [Array<Float>]
|
|
59
42
|
# @param k [Integer]
|
|
60
43
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
61
44
|
# @param timeout [Numeric, nil]
|
|
62
|
-
# @return [
|
|
45
|
+
# @return [Phronomy::Task]
|
|
63
46
|
# @api public
|
|
64
47
|
def search_async(query_embedding:, k: 5, cancellation_token: nil, timeout: nil)
|
|
65
48
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -73,13 +56,10 @@ module Phronomy
|
|
|
73
56
|
|
|
74
57
|
# Async variant of {VectorStore::Base#remove}.
|
|
75
58
|
#
|
|
76
|
-
# Submits the remove call to {OffloadPool} by default.
|
|
77
|
-
# Override to use a native async driver.
|
|
78
|
-
#
|
|
79
59
|
# @param id [String]
|
|
80
60
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
81
61
|
# @param timeout [Numeric, nil]
|
|
82
|
-
# @return [
|
|
62
|
+
# @return [Phronomy::Task]
|
|
83
63
|
# @api public
|
|
84
64
|
def remove_async(id:, cancellation_token: nil, timeout: nil)
|
|
85
65
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -93,12 +73,9 @@ module Phronomy
|
|
|
93
73
|
|
|
94
74
|
# Async variant of {VectorStore::Base#clear}.
|
|
95
75
|
#
|
|
96
|
-
# Submits the clear call to {OffloadPool} by default.
|
|
97
|
-
# Override to use a native async driver.
|
|
98
|
-
#
|
|
99
76
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
100
77
|
# @param timeout [Numeric, nil]
|
|
101
|
-
# @return [
|
|
78
|
+
# @return [Phronomy::Task]
|
|
102
79
|
# @api public
|
|
103
80
|
def clear_async(cancellation_token: nil, timeout: nil)
|
|
104
81
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module VectorStore
|
|
5
|
-
#
|
|
5
|
+
# Public extension SPI for vector stores.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Backends implement the synchronous add/search/remove/clear/size contract.
|
|
8
|
+
# Phronomy supplies async convenience methods through {AsyncBackend}; blocking
|
|
9
|
+
# backend work is offloaded through the framework-owned bounded OffloadPool.
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
# are provided by the {AsyncBackend} mixin which defaults to routing calls
|
|
12
|
-
# through {OffloadPool}. Backends with native async drivers may override
|
|
13
|
-
# individual async methods without touching the pool at all.
|
|
11
|
+
# @api public
|
|
14
12
|
class Base
|
|
15
13
|
include AsyncBackend
|
|
16
14
|
|
|
17
15
|
# Add a document with its vector embedding.
|
|
18
16
|
#
|
|
19
|
-
# @param id [String]
|
|
20
|
-
# @param embedding [Array<Float>]
|
|
21
|
-
# @param metadata [Hash]
|
|
22
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
17
|
+
# @param id [String] unique document identifier
|
|
18
|
+
# @param embedding [Array<Float>] vector embedding
|
|
19
|
+
# @param metadata [Hash] arbitrary metadata
|
|
20
|
+
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
23
21
|
# @api public
|
|
24
22
|
def add(id:, embedding:, metadata: {}, cancellation_token: nil)
|
|
25
23
|
cancellation_token&.raise_if_cancelled!
|
|
@@ -29,8 +27,8 @@ module Phronomy
|
|
|
29
27
|
# Return the k most similar documents to the query embedding.
|
|
30
28
|
#
|
|
31
29
|
# @param query_embedding [Array<Float>]
|
|
32
|
-
# @param k [Integer]
|
|
33
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
30
|
+
# @param k [Integer] number of results
|
|
31
|
+
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
34
32
|
# @return [Array<Hash>] each element: { id:, score:, metadata: }
|
|
35
33
|
# @api public
|
|
36
34
|
def search(query_embedding:, k: 5, cancellation_token: nil)
|
|
@@ -47,6 +45,7 @@ module Phronomy
|
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
# Remove all documents.
|
|
48
|
+
# @api public
|
|
50
49
|
def clear
|
|
51
50
|
raise NotImplementedError, "#{self.class}#clear is not implemented"
|
|
52
51
|
end
|
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module VectorStore
|
|
5
5
|
module Embeddings
|
|
6
|
-
#
|
|
6
|
+
# Public extension SPI for embedding adapters.
|
|
7
7
|
#
|
|
8
|
-
# Concrete implementations
|
|
9
|
-
#
|
|
8
|
+
# Concrete implementations override {#embed}. Phronomy owns the async
|
|
9
|
+
# bridge: {#embed_async} routes the synchronous implementation through the
|
|
10
|
+
# bounded OffloadPool and returns a {Phronomy::Task}.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
10
13
|
class Base
|
|
11
14
|
# Embed the given text and return a vector representation.
|
|
12
15
|
#
|
|
13
|
-
# @param text [String]
|
|
14
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
16
|
+
# @param text [String] the text to embed
|
|
17
|
+
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
15
18
|
# @return [Array<Float>] the embedding vector
|
|
16
19
|
# @api public
|
|
17
20
|
def embed(text, cancellation_token = nil)
|
|
@@ -19,13 +22,12 @@ module Phronomy
|
|
|
19
22
|
raise NotImplementedError, "#{self.class}#embed is not implemented"
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
# Submits an {#embed} call to {OffloadPool}
|
|
23
|
-
# {OffloadPool::PendingOperation}.
|
|
25
|
+
# Submits an {#embed} call to {OffloadPool}.
|
|
24
26
|
#
|
|
25
27
|
# @param text [String]
|
|
26
28
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
27
|
-
# @param timeout [Numeric, nil]
|
|
28
|
-
# @return [
|
|
29
|
+
# @param timeout [Numeric, nil] operation-wide submit timeout
|
|
30
|
+
# @return [Phronomy::Task]
|
|
29
31
|
# @api public
|
|
30
32
|
def embed_async(text, cancellation_token = nil, timeout: nil)
|
|
31
33
|
Phronomy::Runtime.instance.offload.submit(
|
data/lib/phronomy/version.rb
CHANGED
data/lib/phronomy.rb
CHANGED
|
@@ -21,6 +21,12 @@ loader.inflector.inflect("before_llm_input" => "BeforeLLMInput")
|
|
|
21
21
|
loader.collapse("#{__dir__}/phronomy/engine")
|
|
22
22
|
# Loaded via require_relative before loader.setup; ignore to avoid Zeitwerk constant-name mismatch.
|
|
23
23
|
loader.ignore("#{__dir__}/phronomy/ruby_llm_patches.rb")
|
|
24
|
+
# Persistence backend conformance tests are explicit test support. Keep them out
|
|
25
|
+
# of production eager-load so ordinary `require "phronomy"` never requires RSpec.
|
|
26
|
+
loader.ignore(
|
|
27
|
+
"#{__dir__}/phronomy/testing/persistence_contract.rb",
|
|
28
|
+
"#{__dir__}/phronomy/testing/persistence_contract"
|
|
29
|
+
)
|
|
24
30
|
loader.setup
|
|
25
31
|
|
|
26
32
|
require_relative "phronomy/version"
|
data/scripts/run_mutation.sh
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
# Target: mutation score >= 80% for each listed subject.
|
|
14
14
|
# Baseline scores (as of initial run):
|
|
15
15
|
# Phronomy::WorkflowContext 84.85%
|
|
16
|
-
# Phronomy::
|
|
16
|
+
# Phronomy::Agent::Context::Capability::Base 55.74%
|
|
17
|
+
# (public facade alias: Phronomy::Tool::Base; same Class object)
|
|
17
18
|
#
|
|
18
19
|
# Note: mutation testing is slow (~1-5 min per subject). Run locally or via
|
|
19
20
|
# the nightly-mutation GitHub Actions workflow.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Phronomy
|
|
2
|
+
module Agent
|
|
3
|
+
class Base
|
|
4
|
+
def self.model: (?String name) -> String?
|
|
5
|
+
def self.instructions: (?untyped text) { (untyped) -> untyped } -> untyped
|
|
6
|
+
| (?untyped text) -> untyped
|
|
7
|
+
def self.tools: (?Hash[Class, String?] definitions) -> untyped
|
|
8
|
+
def self.tool_aliases: () -> Hash[Class, String]
|
|
9
|
+
def self.provider: (?Symbol name) -> Symbol?
|
|
10
|
+
def self.temperature: (?Float value) -> Float?
|
|
11
|
+
def self.max_iterations: (?Integer value) -> Integer
|
|
12
|
+
def self.cache_instructions: (?bool enabled) -> bool?
|
|
13
|
+
def self.max_output_tokens: (?Integer value) -> Integer?
|
|
14
|
+
def self.context_window: (?Integer value) -> Integer?
|
|
15
|
+
def self.agent_definition: (?id: String?, ?version: Integer?) -> Hash[Symbol, untyped]
|
|
16
|
+
def self.create: (?agent_id: String, ?context: untyped, ?knowledge: Array[untyped], ?persistence: Persistence?, ?metadata: Hash[untyped, untyped]) -> instance
|
|
17
|
+
def self.load: (String agent_id, persistence: Persistence) -> instance
|
|
18
|
+
def self.live_for_execution: (String execution_id) -> instance
|
|
19
|
+
|
|
20
|
+
attr_reader agent_id: String
|
|
21
|
+
attr_reader persistence: Persistence
|
|
22
|
+
|
|
23
|
+
def invoke: (untyped input, **untyped) -> Hash[Symbol, untyped]
|
|
24
|
+
def invoke_async: (untyped input, **untyped) -> Task[Hash[Symbol, untyped]]
|
|
25
|
+
def stream: (untyped input, **untyped) { (untyped event) -> void } -> Hash[Symbol, untyped]
|
|
26
|
+
def stream_async: (untyped input, **untyped) -> Task[Hash[Symbol, untyped]]
|
|
27
|
+
def transcript: () -> Array[untyped]
|
|
28
|
+
def add_knowledge: (untyped content, ?metadata: Hash[untyped, untyped]) -> self
|
|
29
|
+
def clear_transcript!: () -> untyped
|
|
30
|
+
def clear_knowledge!: () -> untyped
|
|
31
|
+
def reset_context!: () -> untyped
|
|
32
|
+
def close!: () -> untyped
|
|
33
|
+
def purge!: () -> bool
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Phronomy
|
|
2
|
+
module Filter
|
|
3
|
+
class Base
|
|
4
|
+
def call: (untyped value, **untyped context) -> untyped
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module OutputParser
|
|
9
|
+
class Base
|
|
10
|
+
def parse: (String text) -> untyped
|
|
11
|
+
def invoke: (untyped input, ?config: Hash[untyped, untyped]) -> untyped
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Tracing
|
|
16
|
+
class Base
|
|
17
|
+
def start_span: (String name, **untyped attributes) -> untyped
|
|
18
|
+
def finish_span: (untyped span, ?output: untyped, ?usage: untyped, ?error: Exception?) -> untyped
|
|
19
|
+
def trace: [A] (String name, ?input: untyped, **untyped meta) { (untyped span) -> [A, untyped] } -> A
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module VectorStore
|
|
24
|
+
class Base
|
|
25
|
+
def add: (id: String, embedding: Array[Float], ?metadata: Hash[untyped, untyped], ?cancellation_token: Concurrency::CancellationToken?) -> untyped
|
|
26
|
+
def search: (query_embedding: Array[Float], ?k: Integer, ?cancellation_token: Concurrency::CancellationToken?) -> Array[Hash[Symbol, untyped]]
|
|
27
|
+
def remove: (id: String) -> untyped
|
|
28
|
+
def clear: () -> untyped
|
|
29
|
+
def size: () -> Integer
|
|
30
|
+
|
|
31
|
+
def add_async: (id: String, embedding: Array[Float], ?metadata: Hash[untyped, untyped], ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[untyped]
|
|
32
|
+
def search_async: (query_embedding: Array[Float], ?k: Integer, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[Array[Hash[Symbol, untyped]]]
|
|
33
|
+
def remove_async: (id: String, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[untyped]
|
|
34
|
+
def clear_async: (?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[untyped]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module Embeddings
|
|
38
|
+
class Base
|
|
39
|
+
def embed: (String text, ?Concurrency::CancellationToken? cancellation_token) -> Array[Float]
|
|
40
|
+
def embed_async: (String text, ?Concurrency::CancellationToken? cancellation_token, ?timeout: Numeric?) -> Task[Array[Float]]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
module Testing
|
|
45
|
+
module PersistenceContract
|
|
46
|
+
SHARED_EXAMPLES: Array[String]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Phronomy
|
|
2
|
+
module LLMAdapter
|
|
3
|
+
class Base
|
|
4
|
+
def complete: (untyped chat, String? message, ?config: Hash[untyped, untyped]) -> untyped
|
|
5
|
+
def stream: (untyped chat, String? message, ?config: Hash[untyped, untyped]) { (untyped chunk) -> void } -> untyped
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class RubyLLM < Base
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Phronomy
|
|
2
|
+
interface _ContentRepository
|
|
3
|
+
def put: (String bytes, canonicalization_version: untyped) -> String
|
|
4
|
+
def fetch: (String content_id) -> String
|
|
5
|
+
def exist?: (String content_id) -> bool
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
interface _AgentRepository
|
|
9
|
+
def create: (untyped root) -> untyped
|
|
10
|
+
def load: (String agent_id) -> untyped
|
|
11
|
+
def save: (String agent_id, expected_revision: Integer, root: untyped) -> untyped
|
|
12
|
+
def delete: (String agent_id) -> untyped
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
interface _JournalRepository
|
|
16
|
+
def append: (String agent_id, expected_position: Integer, records: Array[untyped]) -> Array[untyped]
|
|
17
|
+
def read: (String agent_id, ?after: Integer?, ?limit: Integer?) -> Array[untyped]
|
|
18
|
+
def head: (String agent_id) -> Integer
|
|
19
|
+
def delete: (String agent_id) -> untyped
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
interface _ExecutionRepository
|
|
23
|
+
def create_active: (untyped execution) -> untyped
|
|
24
|
+
def load: (String execution_id) -> untyped
|
|
25
|
+
def save: (String execution_id, expected_revision: Integer, execution: untyped) -> untyped
|
|
26
|
+
def list_active: (String agent_id) -> Array[untyped]
|
|
27
|
+
def delete: (String execution_id) -> untyped
|
|
28
|
+
def delete_for_agent: (String agent_id) -> untyped
|
|
29
|
+
def assert_idle!: (String agent_id) -> untyped
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
interface _WorkflowStateRepository
|
|
33
|
+
def load: (String thread_id) -> Hash[untyped, untyped]?
|
|
34
|
+
def save: (String thread_id, expected_revision: Integer?, snapshot: Hash[untyped, untyped]) -> untyped
|
|
35
|
+
def delete: (String thread_id, expected_revision: Integer) -> untyped
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
interface _PersistenceTransactionView
|
|
39
|
+
def contents: () -> _ContentRepository
|
|
40
|
+
def agents: () -> _AgentRepository
|
|
41
|
+
def journals: () -> _JournalRepository
|
|
42
|
+
def executions: () -> _ExecutionRepository
|
|
43
|
+
def workflow_states: () -> _WorkflowStateRepository
|
|
44
|
+
def assert_agent_watermark!: (agent_id: String, agent_revision: Integer, journal_position: Integer) -> true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Persistence
|
|
48
|
+
class ConflictError < Phronomy::Error
|
|
49
|
+
end
|
|
50
|
+
class NotFoundError < Phronomy::Error
|
|
51
|
+
end
|
|
52
|
+
class UnsupportedBackendError < Phronomy::Error
|
|
53
|
+
end
|
|
54
|
+
class SerializationError < Phronomy::Error
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
REQUIRED_CAPABILITIES: Hash[Symbol, bool]
|
|
58
|
+
|
|
59
|
+
attr_reader contents: _ContentRepository
|
|
60
|
+
attr_reader agents: _AgentRepository
|
|
61
|
+
attr_reader journals: _JournalRepository
|
|
62
|
+
attr_reader executions: _ExecutionRepository
|
|
63
|
+
attr_reader workflow_states: _WorkflowStateRepository
|
|
64
|
+
|
|
65
|
+
def initialize: (contents: _ContentRepository, agents: _AgentRepository, journals: _JournalRepository, executions: _ExecutionRepository, workflow_states: _WorkflowStateRepository) -> void
|
|
66
|
+
def capabilities: () -> Hash[Symbol, bool]
|
|
67
|
+
def transaction: () { (_PersistenceTransactionView) -> untyped } -> untyped
|
|
68
|
+
def assert_agent_watermark!: (agent_id: String, agent_revision: Integer, journal_position: Integer) -> true
|
|
69
|
+
end
|
|
70
|
+
end
|