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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/CONTRIBUTING.md +30 -0
  4. data/README.md +2 -0
  5. data/docs/decisions/009-state-store-abstraction.md +1 -1
  6. data/docs/decisions/014-unified-persistence-durable-state.md +273 -0
  7. data/docs/features.md +27 -1
  8. data/docs/getting-started.md +37 -1
  9. data/docs/migrations/0.19.md +154 -0
  10. data/docs/persistence-backends.md +504 -0
  11. data/docs/runtime-and-concurrency.md +93 -2
  12. data/lib/phronomy/agent/agent_execution.rb +29 -0
  13. data/lib/phronomy/agent/base.rb +81 -36
  14. data/lib/phronomy/agent/context_assembler.rb +13 -3
  15. data/lib/phronomy/agent/execution_coordinator.rb +420 -249
  16. data/lib/phronomy/agent/journal_projection.rb +5 -1
  17. data/lib/phronomy/agent/llm_call_record.rb +20 -0
  18. data/lib/phronomy/configuration.rb +2 -1
  19. data/lib/phronomy/engine/event_loop.rb +86 -8
  20. data/lib/phronomy/engine/fsm_session.rb +6 -4
  21. data/lib/phronomy/engine/runtime.rb +7 -0
  22. data/lib/phronomy/persistence/in_memory.rb +113 -8
  23. data/lib/phronomy/persistence.rb +109 -6
  24. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +50 -0
  25. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +164 -0
  26. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +215 -0
  27. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +119 -0
  28. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +99 -0
  29. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +202 -0
  30. data/lib/phronomy/testing/persistence_contract.rb +41 -0
  31. data/lib/phronomy/version.rb +1 -1
  32. data/lib/phronomy/workflow.rb +10 -9
  33. data/lib/phronomy/workflow_runner.rb +361 -95
  34. data/lib/phronomy.rb +9 -0
  35. metadata +12 -5
  36. data/lib/phronomy/state_store/base.rb +0 -48
  37. data/lib/phronomy/state_store/in_memory.rb +0 -62
  38. 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