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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -18
  3. data/README.md +300 -75
  4. data/benchmark/bench_agent_invoke.rb +3 -0
  5. data/benchmark/bench_regression.rb +2 -18
  6. data/benchmark/bench_tool_schema.rb +1 -0
  7. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
  8. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
  9. data/lib/phronomy/agent/activation_registry.rb +28 -0
  10. data/lib/phronomy/agent/agent_execution.rb +97 -0
  11. data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
  12. data/lib/phronomy/agent/agent_invocation.rb +42 -10
  13. data/lib/phronomy/agent/agent_invocation_session_builder.rb +50 -11
  14. data/lib/phronomy/agent/agent_root.rb +67 -0
  15. data/lib/phronomy/agent/async_event_api.rb +55 -393
  16. data/lib/phronomy/agent/base.rb +301 -641
  17. data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
  18. data/lib/phronomy/agent/context_assembler.rb +321 -0
  19. data/lib/phronomy/agent/context_candidate.rb +47 -0
  20. data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
  21. data/lib/phronomy/agent/context_importer.rb +217 -0
  22. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
  23. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
  24. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
  25. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +188 -0
  26. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
  27. data/lib/phronomy/agent/context_plan.rb +25 -0
  28. data/lib/phronomy/agent/context_plan_validator.rb +167 -0
  29. data/lib/phronomy/agent/context_policies/default.rb +53 -0
  30. data/lib/phronomy/agent/context_policy.rb +15 -0
  31. data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
  32. data/lib/phronomy/agent/context_policy_registry.rb +46 -0
  33. data/lib/phronomy/agent/context_request.rb +35 -0
  34. data/lib/phronomy/agent/context_selection_unit.rb +38 -0
  35. data/lib/phronomy/agent/derived_content_spec.rb +34 -0
  36. data/lib/phronomy/agent/execution_coordinator.rb +1123 -0
  37. data/lib/phronomy/agent/fsm_runtime_adapter.rb +210 -0
  38. data/lib/phronomy/agent/immutable.rb +31 -0
  39. data/lib/phronomy/agent/journal_projection.rb +34 -0
  40. data/lib/phronomy/agent/journal_record.rb +67 -0
  41. data/lib/phronomy/agent/llm_call_record.rb +51 -0
  42. data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
  43. data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
  44. data/lib/phronomy/agent/llm_input_patch.rb +21 -0
  45. data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
  46. data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
  47. data/lib/phronomy/agent/ruby_llm_materializer.rb +298 -0
  48. data/lib/phronomy/agent/token_budget_resolver.rb +69 -0
  49. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
  50. data/lib/phronomy/agent/tool_definition_set.rb +55 -0
  51. data/lib/phronomy/agent.rb +14 -16
  52. data/lib/phronomy/agent_busy_error.rb +5 -0
  53. data/lib/phronomy/canonical_json.rb +136 -0
  54. data/lib/phronomy/configuration.rb +9 -4
  55. data/lib/phronomy/content_store/base.rb +51 -0
  56. data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
  57. data/lib/phronomy/engine/event_loop.rb +3 -0
  58. data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
  59. data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
  60. data/lib/phronomy/llm_context_window/assembler.rb +8 -8
  61. data/lib/phronomy/multi_agent/orchestrator.rb +1 -0
  62. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +6 -2
  64. data/lib/phronomy/persistence/in_memory.rb +247 -0
  65. data/lib/phronomy/persistence.rb +39 -0
  66. data/lib/phronomy/tools/agent.rb +14 -36
  67. data/lib/phronomy/version.rb +1 -1
  68. data/lib/phronomy.rb +11 -0
  69. data/scripts/add_to_h_to_token_doubles.rb +33 -0
  70. data/scripts/add_to_h_unnamed_doubles.rb +27 -0
  71. data/scripts/migrate_spec_agent_definition.rb +108 -0
  72. data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
  73. data/scripts/migrate_spec_inline_pass3.rb +24 -0
  74. metadata +54 -47
  75. data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
  76. data/lib/phronomy/agent/before_completion_context.rb +0 -47
  77. data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Concerns
6
+ module BeforeLLMInput
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def before_llm_input(callable = nil, &block)
13
+ if callable.nil? && !block_given?
14
+ @before_llm_input
15
+ else
16
+ @before_llm_input = callable || block
17
+ end
18
+ end
19
+
20
+ def _before_llm_input = @before_llm_input
21
+ end
22
+
23
+ attr_accessor :before_llm_input
24
+
25
+ private
26
+
27
+ def run_before_llm_input_hooks(call_sequence:, config:)
28
+ hooks = [
29
+ Phronomy.configuration.before_llm_input,
30
+ self.class._before_llm_input,
31
+ @before_llm_input
32
+ ].compact
33
+ return LLMInputPatch.empty if hooks.empty?
34
+
35
+ definition = self.class.agent_definition
36
+ context = LLMInputBuildContext.new(
37
+ agent_id: agent_id,
38
+ agent_definition_id: definition.fetch(:id),
39
+ definition_version: definition.fetch(:version),
40
+ config: config,
41
+ call_sequence: call_sequence
42
+ )
43
+ model_patch = {}
44
+ segments = []
45
+
46
+ hooks.each do |hook|
47
+ result = hook.call(context)
48
+ check_cancellation!(config, "invocation cancelled during before_llm_input hook")
49
+ next if result.nil?
50
+ unless result.is_a?(LLMInputPatch)
51
+ raise TypeError,
52
+ "before_llm_input must return LLMInputPatch or nil, got #{result.class}"
53
+ end
54
+ model_patch.merge!(result.model_config_patch) if result.model_config_patch
55
+ segments.concat(Array(result.segment_candidates))
56
+ end
57
+
58
+ LLMInputPatch.new(
59
+ model_config_patch: model_patch.empty? ? nil : model_patch,
60
+ segment_candidates: segments.empty? ? nil : segments
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ class ContextAssembler
6
+ ASSEMBLY_POLICY_VERSION = 5
7
+ SEGMENT_ORIGIN_METADATA_KEY = "phronomy_origin"
8
+ BEFORE_LLM_INPUT_ORIGIN = "before_llm_input"
9
+
10
+ def initialize(
11
+ agent:,
12
+ persistence:,
13
+ policy: ContextPolicies::Default.new,
14
+ candidate_resolver: nil
15
+ )
16
+ @agent = agent
17
+ @persistence = persistence
18
+ @policy = policy
19
+ @candidate_resolver = candidate_resolver || ContextCandidateResolver.new(
20
+ content_loader: method(:fetch_content)
21
+ )
22
+ end
23
+
24
+ def build_initial(input:, agent_root:, execution:, config: {}, patch: LLMInputPatch.empty)
25
+ projection = JournalProjection.new(persistence: @persistence, agent_root: agent_root)
26
+ model_cfg = effective_model_config(config, patch)
27
+ tool_set = ToolDefinitionSet.build(@agent)
28
+ system_text = build_system_text(input)
29
+ hook_candidates = normalize_candidates(patch.segment_candidates)
30
+ input_ref = execution.metadata.fetch("current_input_ref")
31
+ current_input = @persistence.contents.fetch_text(input_ref)
32
+ excluded = [execution.metadata["current_input_record_id"]].compact
33
+
34
+ base_segments = []
35
+ unless system_text.to_s.empty?
36
+ base_segments << text_segment(:instruction, :system, system_text)
37
+ end
38
+
39
+ assemble(
40
+ agent_root: agent_root,
41
+ execution: execution,
42
+ call_sequence: 1,
43
+ call_mode: :ask,
44
+ previous_manifest: nil,
45
+ model_config: model_cfg,
46
+ tool_definitions: tool_set.definitions,
47
+ tool_definitions_ref: nil,
48
+ prior_records: projection.transcript_records,
49
+ working_records: execution.working_records,
50
+ excluded_record_ids: excluded,
51
+ base_segments: base_segments,
52
+ hook_candidates: hook_candidates,
53
+ current_input_segment: segment(:current_input, :user, input_ref, :ask_argument),
54
+ mandatory_values: [system_text, current_input, tool_set.definitions] +
55
+ hook_candidates.map { |candidate| candidate.fetch(:content) }
56
+ )
57
+ end
58
+
59
+ def build_followup(
60
+ base_manifest:,
61
+ agent_root:,
62
+ execution:,
63
+ config: {},
64
+ patch: LLMInputPatch.empty
65
+ )
66
+ projection = JournalProjection.new(persistence: @persistence, agent_root: agent_root)
67
+ model_cfg = effective_model_config(config, patch)
68
+ hook_candidates = normalize_candidates(patch.segment_candidates)
69
+ system_segments = base_manifest.segments.select do |segment|
70
+ segment.role == :system && !before_llm_input_segment?(segment)
71
+ end
72
+ tool_definitions = base_manifest.tool_definitions_ref ?
73
+ fetch_json(base_manifest.tool_definitions_ref) : []
74
+
75
+ assemble(
76
+ agent_root: agent_root,
77
+ execution: execution,
78
+ call_sequence: execution.llm_calls.length + 1,
79
+ call_mode: :complete,
80
+ previous_manifest: base_manifest,
81
+ model_config: model_cfg,
82
+ tool_definitions: tool_definitions,
83
+ tool_definitions_ref: base_manifest.tool_definitions_ref,
84
+ prior_records: projection.transcript_records,
85
+ working_records: execution.working_records,
86
+ excluded_record_ids: [],
87
+ base_segments: system_segments.map { |existing| segment_hash(existing) },
88
+ hook_candidates: hook_candidates,
89
+ current_input_segment: nil,
90
+ mandatory_values: system_segments.map { |segment| fetch_content(segment.content_ref) } +
91
+ hook_candidates.map { |candidate| candidate.fetch(:content) } +
92
+ [tool_definitions]
93
+ )
94
+ end
95
+
96
+ private
97
+
98
+ def assemble(
99
+ agent_root:,
100
+ execution:,
101
+ call_sequence:,
102
+ call_mode:,
103
+ previous_manifest:,
104
+ model_config:,
105
+ tool_definitions:,
106
+ tool_definitions_ref:,
107
+ prior_records:,
108
+ working_records:,
109
+ excluded_record_ids:,
110
+ base_segments:,
111
+ hook_candidates:,
112
+ current_input_segment:,
113
+ mandatory_values:
114
+ )
115
+ generation = agent_root.transcript_generation
116
+ eligible_working = Array(working_records).select do |record|
117
+ record.context_candidate && record.context_generation == generation
118
+ end
119
+ candidates = @candidate_resolver.resolve(
120
+ prior_records: prior_records,
121
+ working_records: eligible_working,
122
+ excluded_record_ids: excluded_record_ids
123
+ )
124
+ token_budget = TokenBudgetResolver.new(agent: @agent).resolve(model_config)
125
+ mandatory_token_estimate = estimate_values(mandatory_values)
126
+ parts = context_parts
127
+ request = ContextRequest.new(
128
+ agent_id: agent_root.agent_id,
129
+ execution_id: execution.execution_id,
130
+ call_sequence: call_sequence,
131
+ call_mode: call_mode,
132
+ candidates: candidates,
133
+ token_budget: token_budget,
134
+ model_config: model_config,
135
+ previous_manifest: previous_manifest,
136
+ required_coverage: [],
137
+ parts: parts,
138
+ metadata: {"mandatory_token_estimate" => mandatory_token_estimate}
139
+ )
140
+ plan = @policy.call(request)
141
+ validated = ContextPlanValidator.new.validate!(request: request, plan: plan)
142
+ unless validated.plan.derived_contents.empty?
143
+ raise Phronomy::ConfigurationError,
144
+ "Derived Context persistence is not enabled in Context Policy phase 1-4"
145
+ end
146
+
147
+ segments = Array(base_segments).dup
148
+ append_candidates(segments, hook_candidates, before_history: true)
149
+ validated.selected_candidates
150
+ .sort_by { |candidate| [candidate.sequence || 0, candidate.candidate_id] }
151
+ .each { |candidate| segments << segment_from_candidate(candidate) }
152
+ append_candidates(segments, hook_candidates, before_history: false)
153
+ segments << current_input_segment if current_input_segment
154
+
155
+ ContextParts::Validators::FinalBudgetValidator.new(
156
+ content_loader: method(:fetch_content)
157
+ ).validate!(
158
+ token_budget: token_budget,
159
+ segments: segments,
160
+ extra_values: [tool_definitions]
161
+ )
162
+
163
+ store_manifest(
164
+ call_sequence: call_sequence,
165
+ call_mode: call_mode,
166
+ segments: segments,
167
+ model_config_ref: @persistence.contents.put_json(model_config),
168
+ tool_definitions_ref: tool_definitions_ref ||
169
+ @persistence.contents.put_json(tool_definitions)
170
+ )
171
+ end
172
+
173
+ def context_parts
174
+ {
175
+ unit_builder: ContextParts::UnitBuilders::DependencyAwareUnitBuilder.new,
176
+ required_context_resolver: ContextParts::Requirements::RequiredContextResolver.new,
177
+ recent_first_selector: ContextParts::Selectors::RecentFirstSelector.new,
178
+ token_budget_packer: ContextParts::Budget::TokenBudgetPacker.new
179
+ }.freeze
180
+ end
181
+
182
+ def estimate_values(values)
183
+ Array(values).sum do |value|
184
+ bytes = value.is_a?(String) ? value : Phronomy::CanonicalJSON.dump(value)
185
+ Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes)
186
+ end
187
+ end
188
+
189
+ def store_manifest(call_sequence:, call_mode:, segments:, model_config_ref:, tool_definitions_ref:)
190
+ positioned = segments.each_with_index.map do |value, position|
191
+ LLMInputManifest::Segment.new(**value.merge(position: position))
192
+ end
193
+ manifest = LLMInputManifest.new(
194
+ call_sequence: call_sequence,
195
+ call_mode: call_mode,
196
+ segments: positioned,
197
+ model_config_ref: model_config_ref,
198
+ tool_definitions_ref: tool_definitions_ref,
199
+ assembly_policy_version: ASSEMBLY_POLICY_VERSION,
200
+ ruby_llm_version: defined?(RubyLLM::VERSION) ? RubyLLM::VERSION : nil,
201
+ adapter_name: Phronomy.configuration.llm_adapter.class.name
202
+ )
203
+ [manifest, @persistence.contents.put_json(manifest.to_h)]
204
+ end
205
+
206
+ def effective_model_config(config, patch)
207
+ apply_model_config_patch(model_config(config), patch.model_config_patch)
208
+ end
209
+
210
+ def model_config(config)
211
+ {
212
+ "model" => @agent.class.model&.to_s,
213
+ "provider" => @agent.class.provider&.to_s,
214
+ "temperature" => @agent.class.temperature,
215
+ "max_output_tokens" => @agent.class.max_output_tokens,
216
+ "context_window" => @agent.class.context_window,
217
+ "cache_instructions" => !!@agent.class.cache_instructions,
218
+ "parallel_tool_execution" => !!Phronomy.configuration.parallel_tool_execution,
219
+ "thread_id" => config[:thread_id]&.to_s
220
+ }.compact
221
+ end
222
+
223
+ def apply_model_config_patch(base, patch)
224
+ return base unless patch
225
+ base.merge(patch.to_h.transform_keys(&:to_s)).compact
226
+ end
227
+
228
+ def normalize_candidates(candidates)
229
+ Array(candidates).map do |candidate|
230
+ hash = candidate.to_h
231
+ content = hash.fetch(:content) { hash.fetch("content") }
232
+ category = (hash[:category] || hash["category"] || :knowledge).to_sym
233
+ role = (hash[:role] || hash["role"] || default_role(category)).to_sym
234
+ {content: content.to_s, category: category, role: role}
235
+ end
236
+ end
237
+
238
+ def append_candidates(segments, candidates, before_history:)
239
+ candidates.each do |candidate|
240
+ early = %i[instruction structured_state knowledge memory summary].include?(candidate[:category])
241
+ next unless early == before_history
242
+ segments << text_segment(
243
+ candidate[:category],
244
+ candidate[:role],
245
+ candidate[:content],
246
+ metadata: {SEGMENT_ORIGIN_METADATA_KEY => BEFORE_LLM_INPUT_ORIGIN}
247
+ )
248
+ end
249
+ end
250
+
251
+ def default_role(category)
252
+ (category == :instruction) ? :system : :user
253
+ end
254
+
255
+ def text_segment(category, role, content, metadata: {})
256
+ segment(
257
+ category, role, @persistence.contents.put_text(content.to_s), :chat_message,
258
+ metadata: metadata
259
+ )
260
+ end
261
+
262
+ def before_llm_input_segment?(segment)
263
+ segment.metadata[SEGMENT_ORIGIN_METADATA_KEY] == BEFORE_LLM_INPUT_ORIGIN
264
+ end
265
+
266
+ def segment_from_candidate(candidate)
267
+ metadata = candidate.metadata.reject do |key, _value|
268
+ %w[estimated_tokens source_kind source_sequence].include?(key.to_s)
269
+ end
270
+ metadata = metadata.merge(
271
+ "journal_record_id" => candidate.record_id,
272
+ "journal_sequence" => candidate.metadata["source_sequence"],
273
+ "llm_call_id" => candidate.llm_call_id
274
+ ).compact
275
+ segment(
276
+ candidate.category,
277
+ candidate.role,
278
+ candidate.content_ref,
279
+ :chat_message,
280
+ tool_call_id: candidate.tool_call_id,
281
+ metadata: metadata
282
+ )
283
+ end
284
+
285
+ def segment_hash(existing)
286
+ {
287
+ category: existing.category, role: existing.role,
288
+ content_ref: existing.content_ref, delivery: existing.delivery,
289
+ tool_call_id: existing.tool_call_id, metadata: existing.metadata
290
+ }
291
+ end
292
+
293
+ def segment(category, role, content_ref, delivery, tool_call_id: nil, metadata: {})
294
+ {category: category, role: role, content_ref: content_ref, delivery: delivery,
295
+ tool_call_id: tool_call_id, metadata: metadata}
296
+ end
297
+
298
+ def fetch_json(ref)
299
+ Phronomy::CanonicalJSON.load(@persistence.contents.fetch(ref))
300
+ end
301
+
302
+ def fetch_content(ref)
303
+ @persistence.contents.fetch_text(ref)
304
+ rescue
305
+ @persistence.contents.fetch(ref)
306
+ end
307
+
308
+ def build_system_text(input)
309
+ instruction = @agent.send(:build_instructions, input)
310
+ knowledge = @agent.class.static_knowledge_chunks + @agent.send(:instance_knowledge_chunks)
311
+ parts = [instruction]
312
+ knowledge.each do |chunk|
313
+ parts << Phronomy::LlmContextWindow::Assembler.xml_tag(
314
+ chunk[:content], type: chunk[:type] || :static, trusted: true
315
+ )
316
+ end
317
+ parts.compact.join("\n\n")
318
+ end
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ ContextCandidate = Data.define(
6
+ :candidate_id,
7
+ :source_kind,
8
+ :category,
9
+ :role,
10
+ :content_ref,
11
+ :record_id,
12
+ :agent_id,
13
+ :execution_id,
14
+ :llm_call_id,
15
+ :tool_call_id,
16
+ :sequence,
17
+ :requirement,
18
+ :priority,
19
+ :metadata
20
+ ) do
21
+ def initialize(**values)
22
+ normalized = values.merge(
23
+ candidate_id: values.fetch(:candidate_id).to_s.freeze,
24
+ source_kind: values.fetch(:source_kind).to_sym,
25
+ category: values.fetch(:category).to_sym,
26
+ role: values[:role]&.to_sym,
27
+ content_ref: values.fetch(:content_ref).to_s.freeze,
28
+ record_id: values[:record_id]&.to_s&.freeze,
29
+ agent_id: values[:agent_id]&.to_s&.freeze,
30
+ execution_id: values[:execution_id]&.to_s&.freeze,
31
+ llm_call_id: values[:llm_call_id]&.to_s&.freeze,
32
+ tool_call_id: values[:tool_call_id]&.to_s&.freeze,
33
+ sequence: values[:sequence] && Integer(values[:sequence]),
34
+ requirement: (values[:requirement] || :optional).to_sym,
35
+ priority: Integer(values[:priority] || 0),
36
+ metadata: Immutable.copy(values[:metadata] || {})
37
+ )
38
+ unless %i[protocol_required declared_required optional].include?(normalized[:requirement])
39
+ raise ArgumentError, "unknown ContextCandidate requirement: #{normalized[:requirement].inspect}"
40
+ end
41
+
42
+ super(**normalized)
43
+ freeze
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ class ContextCandidateResolver
6
+ def initialize(content_loader:)
7
+ @content_loader = content_loader
8
+ end
9
+
10
+ def resolve(prior_records:, working_records:, excluded_record_ids: [])
11
+ excluded = Array(excluded_record_ids).compact.map(&:to_s).to_h { |id| [id, true] }
12
+ prior = eligible(prior_records, excluded)
13
+ working = eligible(working_records, excluded)
14
+ next_sequence = prior.map(&:sequence).compact.max.to_i
15
+
16
+ candidates = []
17
+ prior.each do |record|
18
+ candidates << candidate_for(record, source_kind: :journal, sequence: record.sequence)
19
+ end
20
+ working.each do |record|
21
+ sequence = record.sequence || (next_sequence += 1)
22
+ candidates << candidate_for(record, source_kind: :working, sequence: sequence)
23
+ end
24
+ candidates.sort_by { |candidate| [candidate.sequence || 0, candidate.candidate_id] }.freeze
25
+ end
26
+
27
+ private
28
+
29
+ def eligible(records, excluded)
30
+ Array(records).select do |record|
31
+ record.context_candidate &&
32
+ record.content_ref &&
33
+ !excluded.key?(record.record_id.to_s)
34
+ end
35
+ end
36
+
37
+ def candidate_for(record, source_kind:, sequence:)
38
+ tool_call_id = record.metadata["tool_call_id"] || record.metadata[:tool_call_id]
39
+ bytes = @content_loader.call(record.content_ref)
40
+ metadata = record.metadata.merge(
41
+ "estimated_tokens" => Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes),
42
+ "source_kind" => source_kind.to_s,
43
+ "source_sequence" => record.sequence
44
+ )
45
+
46
+ ContextCandidate.new(
47
+ candidate_id: "record:#{record.record_id}",
48
+ source_kind: source_kind,
49
+ category: record.kind,
50
+ role: record.role,
51
+ content_ref: record.content_ref,
52
+ record_id: record.record_id,
53
+ agent_id: record.agent_id,
54
+ execution_id: record.execution_id,
55
+ llm_call_id: record.llm_call_id,
56
+ tool_call_id: tool_call_id,
57
+ sequence: sequence,
58
+ requirement: :optional,
59
+ priority: (source_kind == :working) ? 100 : 0,
60
+ metadata: metadata
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end