phronomy 0.15.0 → 0.16.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 +107 -18
- data/README.md +300 -75
- data/benchmark/bench_agent_invoke.rb +3 -0
- data/benchmark/bench_regression.rb +2 -18
- data/benchmark/bench_tool_schema.rb +1 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
- data/lib/phronomy/agent/activation_registry.rb +28 -0
- data/lib/phronomy/agent/agent_execution.rb +97 -0
- data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
- data/lib/phronomy/agent/agent_invocation.rb +42 -10
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +50 -11
- data/lib/phronomy/agent/agent_root.rb +67 -0
- data/lib/phronomy/agent/async_event_api.rb +55 -393
- data/lib/phronomy/agent/base.rb +301 -641
- data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
- data/lib/phronomy/agent/context_assembler.rb +321 -0
- data/lib/phronomy/agent/context_candidate.rb +47 -0
- data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
- data/lib/phronomy/agent/context_importer.rb +217 -0
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +188 -0
- data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
- data/lib/phronomy/agent/context_plan.rb +25 -0
- data/lib/phronomy/agent/context_plan_validator.rb +167 -0
- data/lib/phronomy/agent/context_policies/default.rb +53 -0
- data/lib/phronomy/agent/context_policy.rb +15 -0
- data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
- data/lib/phronomy/agent/context_policy_registry.rb +46 -0
- data/lib/phronomy/agent/context_request.rb +35 -0
- data/lib/phronomy/agent/context_selection_unit.rb +38 -0
- data/lib/phronomy/agent/derived_content_spec.rb +34 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1123 -0
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +210 -0
- data/lib/phronomy/agent/immutable.rb +31 -0
- data/lib/phronomy/agent/journal_projection.rb +34 -0
- data/lib/phronomy/agent/journal_record.rb +67 -0
- data/lib/phronomy/agent/llm_call_record.rb +51 -0
- data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
- data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
- data/lib/phronomy/agent/llm_input_patch.rb +21 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
- data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +298 -0
- data/lib/phronomy/agent/token_budget_resolver.rb +69 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
- data/lib/phronomy/agent/tool_definition_set.rb +55 -0
- data/lib/phronomy/agent.rb +14 -16
- data/lib/phronomy/agent_busy_error.rb +5 -0
- data/lib/phronomy/canonical_json.rb +136 -0
- data/lib/phronomy/configuration.rb +9 -4
- data/lib/phronomy/content_store/base.rb +51 -0
- data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
- data/lib/phronomy/engine/event_loop.rb +3 -0
- data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
- data/lib/phronomy/llm_context_window/assembler.rb +8 -8
- data/lib/phronomy/multi_agent/orchestrator.rb +1 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
- data/lib/phronomy/multi_agent/team_coordinator.rb +6 -2
- data/lib/phronomy/persistence/in_memory.rb +247 -0
- data/lib/phronomy/persistence.rb +39 -0
- data/lib/phronomy/tools/agent.rb +14 -36
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +11 -0
- data/scripts/add_to_h_to_token_doubles.rb +33 -0
- data/scripts/add_to_h_unnamed_doubles.rb +27 -0
- data/scripts/migrate_spec_agent_definition.rb +108 -0
- data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
- data/scripts/migrate_spec_inline_pass3.rb +24 -0
- metadata +54 -47
- data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
- data/lib/phronomy/agent/before_completion_context.rb +0 -47
- data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
class ContextPlanValidator
|
|
6
|
+
ValidatedContextPlan = Data.define(
|
|
7
|
+
:plan, :units, :selected_units, :selected_candidates
|
|
8
|
+
) do
|
|
9
|
+
def initialize(**values)
|
|
10
|
+
super(**values.merge(
|
|
11
|
+
units: Array(values[:units]).freeze,
|
|
12
|
+
selected_units: Array(values[:selected_units]).freeze,
|
|
13
|
+
selected_candidates: Array(values[:selected_candidates]).freeze
|
|
14
|
+
))
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate!(request:, plan:)
|
|
20
|
+
unless plan.is_a?(ContextPlan)
|
|
21
|
+
raise ArgumentError, "Context Policy returned #{plan.class}, expected #{ContextPlan}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
units = request.parts.fetch(:unit_builder).build(request.candidates)
|
|
25
|
+
units = request.parts.fetch(:required_context_resolver).resolve(
|
|
26
|
+
request: request,
|
|
27
|
+
units: units
|
|
28
|
+
)
|
|
29
|
+
unit_index = units.to_h { |unit| [unit.unit_id, unit] }
|
|
30
|
+
unknown = plan.selected_unit_ids.reject { |unit_id| unit_index.key?(unit_id) }
|
|
31
|
+
raise ArgumentError, "ContextPlan contains unknown units: #{unknown.inspect}" unless unknown.empty?
|
|
32
|
+
|
|
33
|
+
selected_units = plan.selected_unit_ids.map { |unit_id| unit_index.fetch(unit_id) }
|
|
34
|
+
required = units.reject { |unit| unit.requirement == :optional }.map(&:unit_id)
|
|
35
|
+
missing_required = required - plan.selected_unit_ids
|
|
36
|
+
unless missing_required.empty?
|
|
37
|
+
raise Phronomy::ContextBudgetExceededError,
|
|
38
|
+
"ContextPlan omitted required units: #{missing_required.inspect}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
candidate_index = request.candidates.to_h { |candidate| [candidate.candidate_id, candidate] }
|
|
42
|
+
selected_candidates = selected_units.flat_map(&:candidate_ids).uniq.map do |candidate_id|
|
|
43
|
+
candidate_index.fetch(candidate_id)
|
|
44
|
+
end
|
|
45
|
+
validate_tool_dependencies!(request.candidates, selected_candidates)
|
|
46
|
+
validate_derived_contents!(request, plan)
|
|
47
|
+
|
|
48
|
+
ValidatedContextPlan.new(
|
|
49
|
+
plan: plan,
|
|
50
|
+
units: units,
|
|
51
|
+
selected_units: selected_units,
|
|
52
|
+
selected_candidates: selected_candidates
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def validate_tool_dependencies!(all_candidates, selected_candidates)
|
|
59
|
+
selected_ids = selected_candidates.to_h { |candidate| [candidate.candidate_id, true] }
|
|
60
|
+
validate_canonical_tool_dependencies!(all_candidates, selected_ids)
|
|
61
|
+
validate_legacy_tool_dependencies!(all_candidates, selected_candidates, selected_ids)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_canonical_tool_dependencies!(all_candidates, selected_ids)
|
|
65
|
+
assistants = Array(all_candidates).select { |candidate| candidate.category == :assistant_message }
|
|
66
|
+
tool_messages = Array(all_candidates).select { |candidate| candidate.category == :tool_message }
|
|
67
|
+
.group_by(&:tool_call_id)
|
|
68
|
+
assistant_by_tool_call_id = {}
|
|
69
|
+
|
|
70
|
+
tool_messages.each do |tool_call_id, messages|
|
|
71
|
+
if messages.length > 1
|
|
72
|
+
raise ArgumentError, "duplicate Tool message in Context candidates: #{tool_call_id}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
assistants.each do |assistant|
|
|
77
|
+
canonical_tool_call_ids(assistant).each do |tool_call_id|
|
|
78
|
+
if assistant_by_tool_call_id.key?(tool_call_id)
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"duplicate assistant Tool Call id in Context candidates: #{tool_call_id}"
|
|
81
|
+
end
|
|
82
|
+
assistant_by_tool_call_id[tool_call_id] = assistant
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
assistants.each do |assistant|
|
|
87
|
+
next unless selected_ids[assistant.candidate_id]
|
|
88
|
+
|
|
89
|
+
canonical_tool_call_ids(assistant).each do |tool_call_id|
|
|
90
|
+
messages = tool_messages.fetch(tool_call_id, [])
|
|
91
|
+
if messages.empty?
|
|
92
|
+
raise ArgumentError,
|
|
93
|
+
"ContextPlan selected assistant Tool Call without a Tool message: #{tool_call_id}"
|
|
94
|
+
end
|
|
95
|
+
missing = messages.reject { |message| selected_ids[message.candidate_id] }
|
|
96
|
+
unless missing.empty?
|
|
97
|
+
raise ArgumentError,
|
|
98
|
+
"ContextPlan split assistant/Tool message dependency: #{tool_call_id}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
tool_messages.each do |tool_call_id, messages|
|
|
104
|
+
messages.each do |message|
|
|
105
|
+
next unless selected_ids[message.candidate_id]
|
|
106
|
+
|
|
107
|
+
assistant = assistant_by_tool_call_id[tool_call_id]
|
|
108
|
+
unless assistant && selected_ids[assistant.candidate_id]
|
|
109
|
+
raise ArgumentError,
|
|
110
|
+
"ContextPlan selected orphan Tool message: #{tool_call_id}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Compatibility for Journal state written by the preceding stateful
|
|
117
|
+
# refactor, where assistant content, Tool Calls and Tool Results were
|
|
118
|
+
# flattened into separate Context candidates.
|
|
119
|
+
def validate_legacy_tool_dependencies!(all_candidates, selected_candidates, selected_ids)
|
|
120
|
+
all_calls = Array(all_candidates).select { |candidate| candidate.category == :tool_call }
|
|
121
|
+
.to_h { |candidate| [candidate.tool_call_id, candidate] }
|
|
122
|
+
all_results = Array(all_candidates).select { |candidate| candidate.category == :tool_result }
|
|
123
|
+
.group_by(&:tool_call_id)
|
|
124
|
+
|
|
125
|
+
selected_candidates.each do |candidate|
|
|
126
|
+
case candidate.category
|
|
127
|
+
when :tool_result
|
|
128
|
+
call = all_calls[candidate.tool_call_id]
|
|
129
|
+
unless call && selected_ids[call.candidate_id]
|
|
130
|
+
raise ArgumentError,
|
|
131
|
+
"ContextPlan selected orphan Tool Result: #{candidate.tool_call_id}"
|
|
132
|
+
end
|
|
133
|
+
when :tool_call
|
|
134
|
+
results = all_results.fetch(candidate.tool_call_id, [])
|
|
135
|
+
if results.empty?
|
|
136
|
+
raise ArgumentError,
|
|
137
|
+
"ContextPlan selected Tool Call without a Tool Result: #{candidate.tool_call_id}"
|
|
138
|
+
end
|
|
139
|
+
missing = results.reject { |result| selected_ids[result.candidate_id] }
|
|
140
|
+
unless missing.empty?
|
|
141
|
+
raise ArgumentError,
|
|
142
|
+
"ContextPlan split Tool Call/Result dependency: #{candidate.tool_call_id}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def canonical_tool_call_ids(candidate)
|
|
149
|
+
Array(candidate.metadata["tool_call_ids"] || candidate.metadata[:tool_call_ids])
|
|
150
|
+
.compact
|
|
151
|
+
.map(&:to_s)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def validate_derived_contents!(request, plan)
|
|
155
|
+
known = request.candidates.to_h { |candidate| [candidate.candidate_id, true] }
|
|
156
|
+
plan.derived_contents.each do |derived|
|
|
157
|
+
unless derived.is_a?(DerivedContentSpec)
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
"ContextPlan derived content must be DerivedContentSpec, got #{derived.class}"
|
|
160
|
+
end
|
|
161
|
+
unknown = derived.coverage_candidate_ids.reject { |id| known[id] }
|
|
162
|
+
raise ArgumentError, "Derived content covers unknown candidates: #{unknown.inspect}" unless unknown.empty?
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module ContextPolicies
|
|
6
|
+
class Default < ContextPolicy
|
|
7
|
+
DESCRIPTOR = ContextPolicyDescriptor.new(
|
|
8
|
+
id: "default-recent-v1",
|
|
9
|
+
version: 1,
|
|
10
|
+
config: {}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def initialize(config = {})
|
|
14
|
+
@config = Immutable.copy(config || {})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def descriptor
|
|
18
|
+
return DESCRIPTOR if @config.empty?
|
|
19
|
+
ContextPolicyDescriptor.new(id: DESCRIPTOR.id, version: DESCRIPTOR.version, config: @config)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(request)
|
|
23
|
+
units = request.parts.fetch(:unit_builder).build(request.candidates)
|
|
24
|
+
units = request.parts.fetch(:required_context_resolver).resolve(
|
|
25
|
+
request: request,
|
|
26
|
+
units: units
|
|
27
|
+
)
|
|
28
|
+
ordered = request.parts.fetch(:recent_first_selector).order(
|
|
29
|
+
request: request,
|
|
30
|
+
units: units
|
|
31
|
+
)
|
|
32
|
+
selected = request.parts.fetch(:token_budget_packer).pack(
|
|
33
|
+
request: request,
|
|
34
|
+
units: ordered
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ContextPlan.new(
|
|
38
|
+
selected_unit_ids: selected.map(&:unit_id),
|
|
39
|
+
derived_contents: [],
|
|
40
|
+
selected_tool_ids: [],
|
|
41
|
+
ordering_hints: {"history" => "chronological"},
|
|
42
|
+
policy_descriptor: descriptor,
|
|
43
|
+
metadata: {
|
|
44
|
+
"candidate_count" => request.candidates.length,
|
|
45
|
+
"unit_count" => units.length,
|
|
46
|
+
"selected_unit_count" => selected.length
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
class ContextPolicy
|
|
6
|
+
def call(_request)
|
|
7
|
+
raise NotImplementedError, "#{self.class}#call is not implemented"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def descriptor
|
|
11
|
+
raise NotImplementedError, "#{self.class}#descriptor is not implemented"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Agent
|
|
7
|
+
ContextPolicyDescriptor = Data.define(:id, :version, :config) do
|
|
8
|
+
def initialize(id:, version:, config: {})
|
|
9
|
+
normalized_config = Immutable.copy(config || {})
|
|
10
|
+
Immutable.validate_canonical_json!(normalized_config, label: "Context Policy config")
|
|
11
|
+
super(
|
|
12
|
+
id: id.to_s.freeze,
|
|
13
|
+
version: Integer(version),
|
|
14
|
+
config: normalized_config
|
|
15
|
+
)
|
|
16
|
+
raise ArgumentError, "Context Policy id must not be empty" if id.empty?
|
|
17
|
+
raise ArgumentError, "Context Policy version must be positive" unless version.positive?
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_h(hash)
|
|
22
|
+
descriptor = new(
|
|
23
|
+
id: hash.fetch("id") { hash.fetch(:id) },
|
|
24
|
+
version: hash.fetch("version") { hash.fetch(:version) },
|
|
25
|
+
config: hash["config"] || hash[:config] || {}
|
|
26
|
+
)
|
|
27
|
+
expected_digest = hash["config_digest"] || hash[:config_digest]
|
|
28
|
+
if expected_digest && expected_digest.to_s != descriptor.config_digest
|
|
29
|
+
raise Phronomy::ConfigurationError,
|
|
30
|
+
"Context Policy config digest mismatch for #{descriptor.id}@#{descriptor.version}"
|
|
31
|
+
end
|
|
32
|
+
descriptor
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def config_digest
|
|
36
|
+
"sha256:#{Digest::SHA256.hexdigest(Phronomy::CanonicalJSON.dump(config))}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
"id" => id,
|
|
42
|
+
"version" => version,
|
|
43
|
+
"config" => config,
|
|
44
|
+
"config_digest" => config_digest
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
class ContextPolicyRegistry
|
|
6
|
+
class << self
|
|
7
|
+
def default
|
|
8
|
+
@default ||= new.tap do |registry|
|
|
9
|
+
registry.register(id: "default-recent-v1", version: 1) do |config|
|
|
10
|
+
ContextPolicies::Default.new(config)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@factories = {}
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register(id:, version:, &factory)
|
|
22
|
+
raise ArgumentError, "Context Policy factory is required" unless factory
|
|
23
|
+
|
|
24
|
+
key = [id.to_s, Integer(version)].freeze
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
raise ArgumentError, "Context Policy already registered: #{key.join("@")}" if @factories.key?(key)
|
|
27
|
+
@factories[key] = factory
|
|
28
|
+
end
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve(descriptor)
|
|
33
|
+
key = [descriptor.id.to_s, Integer(descriptor.version)]
|
|
34
|
+
factory = @mutex.synchronize { @factories[key] }
|
|
35
|
+
raise Phronomy::ConfigurationError, "Unknown Context Policy: #{key.join("@")}" unless factory
|
|
36
|
+
|
|
37
|
+
policy = factory.call(descriptor.config)
|
|
38
|
+
unless policy.is_a?(ContextPolicy)
|
|
39
|
+
raise Phronomy::ConfigurationError,
|
|
40
|
+
"Context Policy factory returned #{policy.class}, expected #{ContextPolicy}"
|
|
41
|
+
end
|
|
42
|
+
policy
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
ContextRequest = Data.define(
|
|
6
|
+
:agent_id,
|
|
7
|
+
:execution_id,
|
|
8
|
+
:call_sequence,
|
|
9
|
+
:call_mode,
|
|
10
|
+
:candidates,
|
|
11
|
+
:token_budget,
|
|
12
|
+
:model_config,
|
|
13
|
+
:previous_manifest,
|
|
14
|
+
:required_coverage,
|
|
15
|
+
:parts,
|
|
16
|
+
:metadata
|
|
17
|
+
) do
|
|
18
|
+
def initialize(**values)
|
|
19
|
+
super(**values.merge(
|
|
20
|
+
agent_id: values.fetch(:agent_id).to_s.freeze,
|
|
21
|
+
execution_id: values[:execution_id]&.to_s&.freeze,
|
|
22
|
+
call_sequence: Integer(values.fetch(:call_sequence)),
|
|
23
|
+
call_mode: values.fetch(:call_mode).to_sym,
|
|
24
|
+
candidates: Array(values[:candidates]).freeze,
|
|
25
|
+
model_config: Immutable.copy(values[:model_config] || {}),
|
|
26
|
+
required_coverage: Array(values[:required_coverage]).map(&:to_s).freeze,
|
|
27
|
+
parts: (values[:parts] || {}).dup.freeze,
|
|
28
|
+
metadata: Immutable.copy(values[:metadata] || {})
|
|
29
|
+
))
|
|
30
|
+
raise ArgumentError, "ContextRequest call_sequence must be positive" unless call_sequence.positive?
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
ContextSelectionUnit = Data.define(
|
|
6
|
+
:unit_id,
|
|
7
|
+
:candidate_ids,
|
|
8
|
+
:dependency_unit_ids,
|
|
9
|
+
:kind,
|
|
10
|
+
:requirement,
|
|
11
|
+
:priority,
|
|
12
|
+
:sequence_range,
|
|
13
|
+
:metadata
|
|
14
|
+
) do
|
|
15
|
+
def initialize(**values)
|
|
16
|
+
normalized = values.merge(
|
|
17
|
+
unit_id: values.fetch(:unit_id).to_s.freeze,
|
|
18
|
+
candidate_ids: Array(values[:candidate_ids]).map(&:to_s).freeze,
|
|
19
|
+
dependency_unit_ids: Array(values[:dependency_unit_ids]).map(&:to_s).freeze,
|
|
20
|
+
kind: values.fetch(:kind).to_sym,
|
|
21
|
+
requirement: (values[:requirement] || :optional).to_sym,
|
|
22
|
+
priority: Integer(values[:priority] || 0),
|
|
23
|
+
sequence_range: Array(values[:sequence_range]).map { |v| Integer(v) }.freeze,
|
|
24
|
+
metadata: Immutable.copy(values[:metadata] || {})
|
|
25
|
+
)
|
|
26
|
+
unless %i[protocol_required declared_required optional].include?(normalized[:requirement])
|
|
27
|
+
raise ArgumentError, "unknown ContextSelectionUnit requirement: #{normalized[:requirement].inspect}"
|
|
28
|
+
end
|
|
29
|
+
unless normalized[:sequence_range].length == 2
|
|
30
|
+
raise ArgumentError, "ContextSelectionUnit sequence_range must contain two integers"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
super(**normalized)
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
DerivedContentSpec = Data.define(
|
|
6
|
+
:content,
|
|
7
|
+
:content_format,
|
|
8
|
+
:category,
|
|
9
|
+
:role,
|
|
10
|
+
:coverage_candidate_ids,
|
|
11
|
+
:source_record_ids,
|
|
12
|
+
:source_content_refs,
|
|
13
|
+
:transformer_id,
|
|
14
|
+
:transformer_version,
|
|
15
|
+
:metadata
|
|
16
|
+
) do
|
|
17
|
+
def initialize(**values)
|
|
18
|
+
super(**values.merge(
|
|
19
|
+
content: Immutable.copy(values[:content]),
|
|
20
|
+
content_format: values.fetch(:content_format).to_sym,
|
|
21
|
+
category: values.fetch(:category).to_sym,
|
|
22
|
+
role: values[:role]&.to_sym,
|
|
23
|
+
coverage_candidate_ids: Array(values[:coverage_candidate_ids]).map(&:to_s).freeze,
|
|
24
|
+
source_record_ids: Array(values[:source_record_ids]).map(&:to_s).freeze,
|
|
25
|
+
source_content_refs: Array(values[:source_content_refs]).map(&:to_s).freeze,
|
|
26
|
+
transformer_id: values.fetch(:transformer_id).to_s.freeze,
|
|
27
|
+
transformer_version: Integer(values.fetch(:transformer_version)),
|
|
28
|
+
metadata: Immutable.copy(values[:metadata] || {})
|
|
29
|
+
))
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|