phronomy 0.19.0 → 0.20.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/CONTRIBUTING.md +30 -0
- data/docs/decisions/014-unified-persistence-durable-state.md +5 -0
- data/docs/features.md +21 -1
- data/docs/migrations/0.19.md +12 -6
- data/docs/persistence-backends.md +504 -0
- data/lib/phronomy/agent/agent_execution.rb +29 -0
- data/lib/phronomy/agent/llm_call_record.rb +20 -0
- 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/version.rb +1 -1
- data/lib/phronomy.rb +6 -0
- metadata +10 -3
- data/scripts/check_private_enforcement.rb +0 -93
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
RSpec.shared_examples "a workflow state repository" do
|
|
6
|
+
let(:repository) { persistence.workflow_states }
|
|
7
|
+
|
|
8
|
+
def workflow_contract_value(hash, key)
|
|
9
|
+
hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def workflow_contract_snapshot(record)
|
|
13
|
+
workflow_contract_value(record, :snapshot)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def workflow_contract_revision(record)
|
|
17
|
+
workflow_contract_value(record, :revision)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "returns nil for an unknown thread_id" do
|
|
21
|
+
expect(repository.load("missing-#{SecureRandom.uuid}")).to be_nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "uses optimistic revisions for save" do
|
|
25
|
+
thread_id = "t1-#{SecureRandom.uuid}"
|
|
26
|
+
expect(
|
|
27
|
+
repository.save(
|
|
28
|
+
thread_id,
|
|
29
|
+
expected_revision: nil,
|
|
30
|
+
snapshot: {fields: {value: 1}, phase: "pause"}
|
|
31
|
+
)
|
|
32
|
+
).to eq(1)
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
repository.save(
|
|
36
|
+
thread_id,
|
|
37
|
+
expected_revision: 1,
|
|
38
|
+
snapshot: {fields: {value: 2}, phase: "__end__"}
|
|
39
|
+
)
|
|
40
|
+
).to eq(2)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "rejects a stale expected_revision" do
|
|
44
|
+
thread_id = "t1-#{SecureRandom.uuid}"
|
|
45
|
+
repository.save(
|
|
46
|
+
thread_id,
|
|
47
|
+
expected_revision: nil,
|
|
48
|
+
snapshot: {fields: {value: 1}, phase: "pause"}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect do
|
|
52
|
+
repository.save(
|
|
53
|
+
thread_id,
|
|
54
|
+
expected_revision: nil,
|
|
55
|
+
snapshot: {fields: {value: 2}, phase: "__end__"}
|
|
56
|
+
)
|
|
57
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "returns a snapshot representation accepted by WorkflowRunner" do
|
|
61
|
+
thread_id = "t1-#{SecureRandom.uuid}"
|
|
62
|
+
repository.save(
|
|
63
|
+
thread_id,
|
|
64
|
+
expected_revision: nil,
|
|
65
|
+
snapshot: {fields: {value: 1}, phase: "pause"}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
record = repository.load(thread_id)
|
|
69
|
+
snapshot = workflow_contract_snapshot(record)
|
|
70
|
+
fields = workflow_contract_value(snapshot, :fields)
|
|
71
|
+
|
|
72
|
+
expect(workflow_contract_revision(record)).to eq(1)
|
|
73
|
+
expect(workflow_contract_value(fields, :value)).to eq(1)
|
|
74
|
+
expect(workflow_contract_value(snapshot, :phase)).to eq("pause")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "isolates stored snapshots from caller mutation" do
|
|
78
|
+
thread_id = "t1-#{SecureRandom.uuid}"
|
|
79
|
+
repository.save(
|
|
80
|
+
thread_id,
|
|
81
|
+
expected_revision: nil,
|
|
82
|
+
snapshot: {fields: {values: [1]}, phase: "pause"}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
loaded = repository.load(thread_id)
|
|
86
|
+
snapshot = workflow_contract_snapshot(loaded)
|
|
87
|
+
fields = workflow_contract_value(snapshot, :fields)
|
|
88
|
+
values = workflow_contract_value(fields, :values)
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
values << 2
|
|
92
|
+
rescue FrozenError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
reloaded = repository.load(thread_id)
|
|
97
|
+
reloaded_fields = workflow_contract_value(
|
|
98
|
+
workflow_contract_snapshot(reloaded),
|
|
99
|
+
:fields
|
|
100
|
+
)
|
|
101
|
+
expect(workflow_contract_value(reloaded_fields, :values)).to eq([1])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "deletes only at the expected revision" do
|
|
105
|
+
thread_id = "t1-#{SecureRandom.uuid}"
|
|
106
|
+
repository.save(
|
|
107
|
+
thread_id,
|
|
108
|
+
expected_revision: nil,
|
|
109
|
+
snapshot: {fields: {}, phase: "pause"}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
expect do
|
|
113
|
+
repository.delete(thread_id, expected_revision: 99)
|
|
114
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
115
|
+
|
|
116
|
+
repository.delete(thread_id, expected_revision: 1)
|
|
117
|
+
expect(repository.load(thread_id)).to be_nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
RSpec.shared_examples "an Agent repository" do
|
|
6
|
+
let(:agent_repository) { persistence.agents }
|
|
7
|
+
let(:agent_root) do
|
|
8
|
+
Phronomy::Agent::AgentRoot.create(
|
|
9
|
+
agent_id: "contract-agent-#{SecureRandom.uuid}",
|
|
10
|
+
agent_definition_id: "contract-agent",
|
|
11
|
+
definition_version: 1
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "creates and loads an AgentRoot" do
|
|
16
|
+
expect(agent_repository.create(agent_root).to_h).to eq(agent_root.to_h)
|
|
17
|
+
expect(agent_repository.load(agent_root.agent_id).to_h).to eq(agent_root.to_h)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "rejects duplicate create" do
|
|
21
|
+
agent_repository.create(agent_root)
|
|
22
|
+
|
|
23
|
+
expect do
|
|
24
|
+
agent_repository.create(agent_root)
|
|
25
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "raises NotFoundError for a missing Agent" do
|
|
29
|
+
expect do
|
|
30
|
+
agent_repository.load("missing-#{SecureRandom.uuid}")
|
|
31
|
+
end.to raise_error(Phronomy::Persistence::NotFoundError)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "saves only at the expected revision" do
|
|
35
|
+
agent_repository.create(agent_root)
|
|
36
|
+
updated = agent_root.with(agent_revision: agent_root.agent_revision + 1)
|
|
37
|
+
|
|
38
|
+
expect(
|
|
39
|
+
agent_repository.save(
|
|
40
|
+
agent_root.agent_id,
|
|
41
|
+
expected_revision: agent_root.agent_revision,
|
|
42
|
+
root: updated
|
|
43
|
+
).to_h
|
|
44
|
+
).to eq(updated.to_h)
|
|
45
|
+
|
|
46
|
+
expect(agent_repository.load(agent_root.agent_id).agent_revision).to eq(1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "rejects a stale expected revision" do
|
|
50
|
+
agent_repository.create(agent_root)
|
|
51
|
+
first = agent_root.with(agent_revision: 1)
|
|
52
|
+
agent_repository.save(agent_root.agent_id, expected_revision: 0, root: first)
|
|
53
|
+
stale = agent_root.with(agent_revision: 1)
|
|
54
|
+
|
|
55
|
+
expect do
|
|
56
|
+
agent_repository.save(agent_root.agent_id, expected_revision: 0, root: stale)
|
|
57
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "rejects an Agent identity mismatch" do
|
|
61
|
+
agent_repository.create(agent_root)
|
|
62
|
+
other = Phronomy::Agent::AgentRoot.create(
|
|
63
|
+
agent_id: "other-#{SecureRandom.uuid}",
|
|
64
|
+
agent_definition_id: "contract-agent",
|
|
65
|
+
definition_version: 1
|
|
66
|
+
).with(agent_revision: 1)
|
|
67
|
+
|
|
68
|
+
expect do
|
|
69
|
+
agent_repository.save(
|
|
70
|
+
agent_root.agent_id,
|
|
71
|
+
expected_revision: 0,
|
|
72
|
+
root: other
|
|
73
|
+
)
|
|
74
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "requires revision to advance exactly once" do
|
|
78
|
+
agent_repository.create(agent_root)
|
|
79
|
+
skipped = agent_root.with(agent_revision: 2)
|
|
80
|
+
|
|
81
|
+
expect do
|
|
82
|
+
agent_repository.save(
|
|
83
|
+
agent_root.agent_id,
|
|
84
|
+
expected_revision: 0,
|
|
85
|
+
root: skipped
|
|
86
|
+
)
|
|
87
|
+
end.to raise_error(Phronomy::Persistence::ConflictError)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "deletes idempotently" do
|
|
91
|
+
agent_repository.create(agent_root)
|
|
92
|
+
agent_repository.delete(agent_root.agent_id)
|
|
93
|
+
expect { agent_repository.delete(agent_root.agent_id) }.not_to raise_error
|
|
94
|
+
|
|
95
|
+
expect do
|
|
96
|
+
agent_repository.load(agent_root.agent_id)
|
|
97
|
+
end.to raise_error(Phronomy::Persistence::NotFoundError)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -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"
|
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"
|
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.
|
|
4
|
+
version: 0.20.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-
|
|
11
|
+
date: 2026-08-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -126,6 +126,7 @@ files:
|
|
|
126
126
|
- docs/migrations/0.15.md
|
|
127
127
|
- docs/migrations/0.16.md
|
|
128
128
|
- docs/migrations/0.19.md
|
|
129
|
+
- docs/persistence-backends.md
|
|
129
130
|
- docs/runtime-and-concurrency.md
|
|
130
131
|
- examples/workflows/agent_event_mapping.rb
|
|
131
132
|
- examples/workflows/generic_task_event_mapping.rb
|
|
@@ -250,6 +251,13 @@ files:
|
|
|
250
251
|
- lib/phronomy/testing/eval/scorer/includes_scorer.rb
|
|
251
252
|
- lib/phronomy/testing/eval/scorer/llm_judge.rb
|
|
252
253
|
- lib/phronomy/testing/fake_clock.rb
|
|
254
|
+
- lib/phronomy/testing/persistence_contract.rb
|
|
255
|
+
- lib/phronomy/testing/persistence_contract/a_content_store.rb
|
|
256
|
+
- lib/phronomy/testing/persistence_contract/a_journal_repository.rb
|
|
257
|
+
- lib/phronomy/testing/persistence_contract/a_persistence_backend.rb
|
|
258
|
+
- lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb
|
|
259
|
+
- lib/phronomy/testing/persistence_contract/an_agent_repository.rb
|
|
260
|
+
- lib/phronomy/testing/persistence_contract/an_execution_repository.rb
|
|
253
261
|
- lib/phronomy/token_usage.rb
|
|
254
262
|
- lib/phronomy/tools/agent.rb
|
|
255
263
|
- lib/phronomy/tools/mcp.rb
|
|
@@ -283,7 +291,6 @@ files:
|
|
|
283
291
|
- scripts/add_to_h_unnamed_doubles.rb
|
|
284
292
|
- scripts/api_snapshot.rb
|
|
285
293
|
- scripts/check_api_annotations.rb
|
|
286
|
-
- scripts/check_private_enforcement.rb
|
|
287
294
|
- scripts/check_readme_ruby.rb
|
|
288
295
|
- scripts/check_readme_runnable.rb
|
|
289
296
|
- scripts/migrate_spec_agent_definition.rb
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# check_private_enforcement.rb
|
|
5
|
-
#
|
|
6
|
-
# Verifies that every instance method annotated @api private in lib/ is
|
|
7
|
-
# actually non-public at the Ruby level (i.e., NOT in Module#public_instance_methods).
|
|
8
|
-
#
|
|
9
|
-
# Class methods (def self.xxx) are excluded from this check because their
|
|
10
|
-
# visibility is managed separately on the singleton class and rarely causes
|
|
11
|
-
# accidental public exposure to consumers.
|
|
12
|
-
#
|
|
13
|
-
# Usage (run from the phronomy/ repository root):
|
|
14
|
-
# bundle exec ruby scripts/check_private_enforcement.rb
|
|
15
|
-
#
|
|
16
|
-
# Exit codes:
|
|
17
|
-
# 0 — all @api private instance methods are non-public (or have no Ruby def)
|
|
18
|
-
# 1 — one or more @api private instance methods are exposed as public
|
|
19
|
-
|
|
20
|
-
require "bundler/setup"
|
|
21
|
-
require_relative "../lib/phronomy"
|
|
22
|
-
|
|
23
|
-
lib_dir = File.expand_path("../lib", __dir__)
|
|
24
|
-
|
|
25
|
-
unless File.directory?(lib_dir)
|
|
26
|
-
warn "ERROR: lib directory not found at #{lib_dir}"
|
|
27
|
-
exit 1
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Step 1: Collect instance methods annotated @api private via static analysis.
|
|
31
|
-
api_private_entries = []
|
|
32
|
-
|
|
33
|
-
Dir.glob(File.join(lib_dir, "**", "*.rb")).sort.each do |file|
|
|
34
|
-
lines = File.readlines(file)
|
|
35
|
-
|
|
36
|
-
lines.each_with_index do |line, i|
|
|
37
|
-
next unless line.match?(/^\s*#\s*@api\s+private\s*$/)
|
|
38
|
-
|
|
39
|
-
# Advance past any further comment or blank lines to reach the def.
|
|
40
|
-
j = i + 1
|
|
41
|
-
j += 1 while j < lines.size && lines[j].match?(/^\s*(#|$)/)
|
|
42
|
-
next unless j < lines.size
|
|
43
|
-
|
|
44
|
-
# Skip class-level methods — they live on the singleton class, not as
|
|
45
|
-
# public instance methods accessible to consumers.
|
|
46
|
-
next if lines[j].match?(/def\s+self\./)
|
|
47
|
-
|
|
48
|
-
# Match both plain def and "private def".
|
|
49
|
-
m = lines[j].match(/^\s*(?:private\s+)?def\s+(\w+[!?=]?)/)
|
|
50
|
-
next unless m
|
|
51
|
-
|
|
52
|
-
rel_path = file.sub("#{lib_dir}/../", "")
|
|
53
|
-
api_private_entries << {name: m[1].to_sym, file: rel_path, line: j + 1}
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
if api_private_entries.empty?
|
|
58
|
-
puts "No @api private instance methods found."
|
|
59
|
-
exit 0
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Step 2: Build a map of publicly exposed instance methods across all
|
|
63
|
-
# Phronomy-namespaced modules/classes (own methods only, no inheritance).
|
|
64
|
-
all_phronomy_modules = ObjectSpace.each_object(Module).select do |mod|
|
|
65
|
-
mod.name&.start_with?("Phronomy")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
public_exposure_map = {}
|
|
69
|
-
all_phronomy_modules.each do |mod|
|
|
70
|
-
mod.public_instance_methods(false).each do |meth|
|
|
71
|
-
(public_exposure_map[meth] ||= []) << mod.name
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Step 3: Report violations — @api private methods that are still public.
|
|
76
|
-
errors = []
|
|
77
|
-
|
|
78
|
-
api_private_entries.each do |entry|
|
|
79
|
-
exposing_modules = public_exposure_map[entry[:name]]
|
|
80
|
-
next unless exposing_modules
|
|
81
|
-
|
|
82
|
-
errors << "#{entry[:file]}:#{entry[:line]} def #{entry[:name]}" \
|
|
83
|
-
" (annotated @api private but public in: #{exposing_modules.join(", ")})"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
if errors.empty?
|
|
87
|
-
puts "OK: all #{api_private_entries.size} @api private instance methods are non-public."
|
|
88
|
-
exit 0
|
|
89
|
-
else
|
|
90
|
-
warn "ERROR: #{errors.size} @api private instance method(s) are exposed as public:"
|
|
91
|
-
errors.each { |e| warn " #{e}" }
|
|
92
|
-
exit 1
|
|
93
|
-
end
|