phronomy 0.18.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/CHANGELOG.md +25 -0
- data/CONTRIBUTING.md +30 -0
- data/README.md +2 -0
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/014-unified-persistence-durable-state.md +273 -0
- data/docs/features.md +27 -1
- data/docs/getting-started.md +37 -1
- data/docs/migrations/0.19.md +154 -0
- data/docs/persistence-backends.md +504 -0
- data/docs/runtime-and-concurrency.md +93 -2
- data/lib/phronomy/agent/agent_execution.rb +29 -0
- data/lib/phronomy/agent/base.rb +81 -36
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +420 -249
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/llm_call_record.rb +20 -0
- data/lib/phronomy/configuration.rb +2 -1
- data/lib/phronomy/engine/event_loop.rb +86 -8
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime.rb +7 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +109 -6
- 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/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +361 -95
- data/lib/phronomy.rb +9 -0
- metadata +12 -5
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/scripts/check_private_enforcement.rb +0 -93
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module StateStore
|
|
5
|
-
# Abstract base class for workflow state persistence backends.
|
|
6
|
-
#
|
|
7
|
-
# Subclasses must implement {#load}, {#save}, and {#delete}.
|
|
8
|
-
# A snapshot is a plain +Hash+ with two keys:
|
|
9
|
-
# +:fields+ — output of +context.to_h+
|
|
10
|
-
# +:phase+ — +context.phase.to_s+
|
|
11
|
-
#
|
|
12
|
-
# @example Implementing a custom backend
|
|
13
|
-
# class MyStore < Phronomy::StateStore::Base
|
|
14
|
-
# def load(thread_id) = MyRecord.find_by(thread_id:)&.to_h
|
|
15
|
-
# def save(thread_id, snapshot) = MyRecord.upsert(thread_id:, data: snapshot)
|
|
16
|
-
# def delete(thread_id) = MyRecord.where(thread_id:).delete_all
|
|
17
|
-
# end
|
|
18
|
-
class Base
|
|
19
|
-
# Load the stored snapshot for +thread_id+.
|
|
20
|
-
#
|
|
21
|
-
# @param thread_id [String]
|
|
22
|
-
# @return [Hash, nil] stored snapshot hash, or +nil+ if absent
|
|
23
|
-
# @api public
|
|
24
|
-
def load(thread_id)
|
|
25
|
-
raise NotImplementedError, "#{self.class}#load is not implemented"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Persist +snapshot+ for +thread_id+. Overwrites any existing snapshot.
|
|
29
|
-
#
|
|
30
|
-
# @param thread_id [String]
|
|
31
|
-
# @param snapshot [Hash] serialisable hash of workflow state
|
|
32
|
-
# @return [void]
|
|
33
|
-
# @api public
|
|
34
|
-
def save(thread_id, snapshot)
|
|
35
|
-
raise NotImplementedError, "#{self.class}#save is not implemented"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Delete the stored snapshot for +thread_id+. No-op if absent.
|
|
39
|
-
#
|
|
40
|
-
# @param thread_id [String]
|
|
41
|
-
# @return [void]
|
|
42
|
-
# @api public
|
|
43
|
-
def delete(thread_id)
|
|
44
|
-
raise NotImplementedError, "#{self.class}#delete is not implemented"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module StateStore
|
|
5
|
-
# Thread-safe in-process state store backed by a plain Ruby Hash.
|
|
6
|
-
#
|
|
7
|
-
# Used as the recommended default for single-process applications and tests.
|
|
8
|
-
# State does not survive process restart.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# store = Phronomy::StateStore::InMemory.new
|
|
12
|
-
# store.save("t1", { fields: { count: 1 }, phase: "__end__" })
|
|
13
|
-
# store.load("t1") # => { fields: { count: 1 }, phase: "__end__" }
|
|
14
|
-
# store.delete("t1")
|
|
15
|
-
# store.load("t1") # => nil
|
|
16
|
-
class InMemory < Base
|
|
17
|
-
def initialize
|
|
18
|
-
@data = {}
|
|
19
|
-
@mutex = Mutex.new
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# @param thread_id [String]
|
|
23
|
-
# @return [Hash, nil]
|
|
24
|
-
# @api public
|
|
25
|
-
def load(thread_id)
|
|
26
|
-
@mutex.synchronize do
|
|
27
|
-
snap = @data[thread_id]
|
|
28
|
-
snap ? deep_dup(snap) : nil
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# @param thread_id [String]
|
|
33
|
-
# @param snapshot [Hash]
|
|
34
|
-
# @return [void]
|
|
35
|
-
# @api public
|
|
36
|
-
def save(thread_id, snapshot)
|
|
37
|
-
@mutex.synchronize { @data[thread_id] = deep_dup(snapshot) }
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @param thread_id [String]
|
|
42
|
-
# @return [void]
|
|
43
|
-
# @api public
|
|
44
|
-
def delete(thread_id)
|
|
45
|
-
@mutex.synchronize { @data.delete(thread_id) }
|
|
46
|
-
nil
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
# Recursively deep-duplicates a plain-data value (Hash, Array, or scalar).
|
|
52
|
-
# Sufficient for snapshot data which consists of JSON-compatible types.
|
|
53
|
-
def deep_dup(val)
|
|
54
|
-
case val
|
|
55
|
-
when Hash then val.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
|
|
56
|
-
when Array then val.map { |v| deep_dup(v) }
|
|
57
|
-
else val.frozen? ? val : (val.dup rescue val) # rubocop:disable Style/RescueModifier
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
@@ -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
|