phronomy 0.16.0 → 0.18.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/.mutant.yml +8 -9
- data/CHANGELOG.md +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +6 -7
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +89 -190
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +49 -377
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -265
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
5
|
class ContextAssembler
|
|
6
|
-
ASSEMBLY_POLICY_VERSION =
|
|
6
|
+
ASSEMBLY_POLICY_VERSION = 6
|
|
7
7
|
SEGMENT_ORIGIN_METADATA_KEY = "phronomy_origin"
|
|
8
8
|
BEFORE_LLM_INPUT_ORIGIN = "before_llm_input"
|
|
9
|
+
EARLY_CONTEXT_CATEGORIES = %i[
|
|
10
|
+
instruction structured_state knowledge memory summary
|
|
11
|
+
].freeze
|
|
9
12
|
|
|
10
13
|
def initialize(
|
|
11
14
|
agent:,
|
|
@@ -45,14 +48,13 @@ module Phronomy
|
|
|
45
48
|
model_config: model_cfg,
|
|
46
49
|
tool_definitions: tool_set.definitions,
|
|
47
50
|
tool_definitions_ref: nil,
|
|
48
|
-
prior_records: projection.
|
|
51
|
+
prior_records: projection.context_records,
|
|
49
52
|
working_records: execution.working_records,
|
|
50
53
|
excluded_record_ids: excluded,
|
|
51
54
|
base_segments: base_segments,
|
|
52
55
|
hook_candidates: hook_candidates,
|
|
53
56
|
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) }
|
|
57
|
+
mandatory_values: [system_text, current_input, tool_set.definitions]
|
|
56
58
|
)
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -81,14 +83,13 @@ module Phronomy
|
|
|
81
83
|
model_config: model_cfg,
|
|
82
84
|
tool_definitions: tool_definitions,
|
|
83
85
|
tool_definitions_ref: base_manifest.tool_definitions_ref,
|
|
84
|
-
prior_records: projection.
|
|
86
|
+
prior_records: projection.context_records,
|
|
85
87
|
working_records: execution.working_records,
|
|
86
88
|
excluded_record_ids: [],
|
|
87
89
|
base_segments: system_segments.map { |existing| segment_hash(existing) },
|
|
88
90
|
hook_candidates: hook_candidates,
|
|
89
91
|
current_input_segment: nil,
|
|
90
92
|
mandatory_values: system_segments.map { |segment| fetch_content(segment.content_ref) } +
|
|
91
|
-
hook_candidates.map { |candidate| candidate.fetch(:content) } +
|
|
92
93
|
[tool_definitions]
|
|
93
94
|
)
|
|
94
95
|
end
|
|
@@ -121,6 +122,14 @@ module Phronomy
|
|
|
121
122
|
working_records: eligible_working,
|
|
122
123
|
excluded_record_ids: excluded_record_ids
|
|
123
124
|
)
|
|
125
|
+
candidates = merge_hook_candidates(
|
|
126
|
+
candidates,
|
|
127
|
+
hook_candidates,
|
|
128
|
+
agent_root: agent_root,
|
|
129
|
+
execution: execution,
|
|
130
|
+
call_sequence: call_sequence
|
|
131
|
+
)
|
|
132
|
+
|
|
124
133
|
token_budget = TokenBudgetResolver.new(agent: @agent).resolve(model_config)
|
|
125
134
|
mandatory_token_estimate = estimate_values(mandatory_values)
|
|
126
135
|
parts = context_parts
|
|
@@ -144,12 +153,12 @@ module Phronomy
|
|
|
144
153
|
"Derived Context persistence is not enabled in Context Policy phase 1-4"
|
|
145
154
|
end
|
|
146
155
|
|
|
156
|
+
selected_candidates = validated.selected_candidates.sort_by do |candidate|
|
|
157
|
+
[candidate.sequence || 0, candidate.candidate_id]
|
|
158
|
+
end
|
|
147
159
|
segments = Array(base_segments).dup
|
|
148
|
-
|
|
149
|
-
|
|
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)
|
|
160
|
+
append_selected_candidates(segments, selected_candidates, before_history: true)
|
|
161
|
+
append_selected_candidates(segments, selected_candidates, before_history: false)
|
|
153
162
|
segments << current_input_segment if current_input_segment
|
|
154
163
|
|
|
155
164
|
ContextParts::Validators::FinalBudgetValidator.new(
|
|
@@ -231,20 +240,55 @@ module Phronomy
|
|
|
231
240
|
content = hash.fetch(:content) { hash.fetch("content") }
|
|
232
241
|
category = (hash[:category] || hash["category"] || :knowledge).to_sym
|
|
233
242
|
role = (hash[:role] || hash["role"] || default_role(category)).to_sym
|
|
234
|
-
|
|
243
|
+
metadata = hash[:metadata] || hash["metadata"] || {}
|
|
244
|
+
{
|
|
245
|
+
content: content.to_s,
|
|
246
|
+
category: category,
|
|
247
|
+
role: role,
|
|
248
|
+
metadata: metadata.to_h.transform_keys(&:to_s)
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def merge_hook_candidates(candidates, hooks, agent_root:, execution:, call_sequence:)
|
|
254
|
+
next_sequence = Array(candidates).filter_map(&:sequence).max.to_i
|
|
255
|
+
generated = hooks.each_with_index.map do |hook, index|
|
|
256
|
+
content_ref = @persistence.contents.put_text(hook.fetch(:content))
|
|
257
|
+
metadata = hook.fetch(:metadata).merge(
|
|
258
|
+
SEGMENT_ORIGIN_METADATA_KEY => BEFORE_LLM_INPUT_ORIGIN,
|
|
259
|
+
"estimated_tokens" => Phronomy::LlmContextWindow::TokenEstimator.estimate(
|
|
260
|
+
fetch_content(content_ref)
|
|
261
|
+
),
|
|
262
|
+
"source_kind" => "hook"
|
|
263
|
+
)
|
|
264
|
+
ContextCandidate.new(
|
|
265
|
+
candidate_id: "hook:#{execution.execution_id}:#{call_sequence}:#{index}",
|
|
266
|
+
source_kind: :hook,
|
|
267
|
+
category: hook.fetch(:category),
|
|
268
|
+
role: hook.fetch(:role),
|
|
269
|
+
content_ref: content_ref,
|
|
270
|
+
record_id: nil,
|
|
271
|
+
agent_id: agent_root.agent_id,
|
|
272
|
+
execution_id: execution.execution_id,
|
|
273
|
+
llm_call_id: nil,
|
|
274
|
+
tool_call_id: nil,
|
|
275
|
+
sequence: next_sequence + index + 1,
|
|
276
|
+
requirement: :optional,
|
|
277
|
+
priority: 0,
|
|
278
|
+
metadata: metadata
|
|
279
|
+
)
|
|
235
280
|
end
|
|
281
|
+
(Array(candidates) + generated)
|
|
282
|
+
.sort_by { |candidate| [candidate.sequence || 0, candidate.candidate_id] }
|
|
283
|
+
.freeze
|
|
236
284
|
end
|
|
237
285
|
|
|
238
|
-
def
|
|
286
|
+
def append_selected_candidates(segments, candidates, before_history:)
|
|
239
287
|
candidates.each do |candidate|
|
|
240
|
-
early =
|
|
288
|
+
early = EARLY_CONTEXT_CATEGORIES.include?(candidate.category)
|
|
241
289
|
next unless early == before_history
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
candidate[:role],
|
|
245
|
-
candidate[:content],
|
|
246
|
-
metadata: {SEGMENT_ORIGIN_METADATA_KEY => BEFORE_LLM_INPUT_ORIGIN}
|
|
247
|
-
)
|
|
290
|
+
|
|
291
|
+
segments << segment_from_candidate(candidate)
|
|
248
292
|
end
|
|
249
293
|
end
|
|
250
294
|
|
|
@@ -306,15 +350,7 @@ module Phronomy
|
|
|
306
350
|
end
|
|
307
351
|
|
|
308
352
|
def build_system_text(input)
|
|
309
|
-
|
|
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")
|
|
353
|
+
@agent.send(:build_instructions, input)
|
|
318
354
|
end
|
|
319
355
|
end
|
|
320
356
|
end
|
|
@@ -8,33 +8,29 @@ module Phronomy
|
|
|
8
8
|
module UnitBuilders
|
|
9
9
|
class DependencyAwareUnitBuilder
|
|
10
10
|
def build(candidates)
|
|
11
|
-
ordered = Array(candidates).sort_by
|
|
12
|
-
|
|
11
|
+
ordered = Array(candidates).sort_by do |candidate|
|
|
12
|
+
[candidate.sequence || 0, candidate.candidate_id]
|
|
13
|
+
end
|
|
13
14
|
|
|
14
|
-
canonical_assistants = ordered.select
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
canonical_assistants = ordered.select do |candidate|
|
|
16
|
+
candidate.category == :assistant_message
|
|
17
|
+
end
|
|
18
|
+
canonical_tool_messages = ordered.select do |candidate|
|
|
19
|
+
candidate.category == :tool_message
|
|
20
|
+
end.group_by(&:tool_call_id)
|
|
17
21
|
assistant_by_tool_call_id = canonical_assistants.each_with_object({}) do |candidate, result|
|
|
18
22
|
canonical_tool_call_ids(candidate).each do |tool_call_id|
|
|
19
23
|
if result.key?(tool_call_id)
|
|
20
|
-
raise ArgumentError,
|
|
24
|
+
raise ArgumentError,
|
|
25
|
+
"duplicate assistant Tool Call id in Context candidates: #{tool_call_id}"
|
|
21
26
|
end
|
|
22
27
|
result[tool_call_id] = candidate
|
|
23
28
|
end
|
|
24
29
|
end
|
|
25
30
|
|
|
26
|
-
legacy_tool_results = ordered.select { |candidate| candidate.category == :tool_result }
|
|
27
|
-
.group_by(&:tool_call_id)
|
|
28
|
-
legacy_calls_by_llm = ordered.select do |candidate|
|
|
29
|
-
candidate.category == :tool_call && candidate.llm_call_id
|
|
30
|
-
end.group_by(&:llm_call_id)
|
|
31
|
-
legacy_assistant_by_llm = ordered.select do |candidate|
|
|
32
|
-
candidate.llm_call_id && %i[llm_message tool_call].include?(candidate.category)
|
|
33
|
-
end.group_by(&:llm_call_id)
|
|
34
|
-
|
|
35
31
|
claimed = Set.new
|
|
36
32
|
units = []
|
|
37
|
-
ordered.
|
|
33
|
+
ordered.each do |candidate|
|
|
38
34
|
next if claimed.include?(candidate.candidate_id)
|
|
39
35
|
|
|
40
36
|
group = canonical_tool_exchange(
|
|
@@ -42,23 +38,13 @@ module Phronomy
|
|
|
42
38
|
assistant_by_tool_call_id: assistant_by_tool_call_id,
|
|
43
39
|
tool_messages: canonical_tool_messages
|
|
44
40
|
)
|
|
45
|
-
group ||= if legacy_runtime_tool_exchange?(candidate, legacy_calls_by_llm)
|
|
46
|
-
legacy_runtime_tool_exchange(
|
|
47
|
-
candidate,
|
|
48
|
-
assistant_by_llm: legacy_assistant_by_llm,
|
|
49
|
-
calls_by_llm: legacy_calls_by_llm,
|
|
50
|
-
tool_results: legacy_tool_results
|
|
51
|
-
)
|
|
52
|
-
else
|
|
53
|
-
legacy_import_tool_exchange(ordered, index, legacy_tool_results)
|
|
54
|
-
end
|
|
55
41
|
|
|
56
42
|
if group && !group.empty?
|
|
57
43
|
group.each { |item| claimed << item.candidate_id }
|
|
58
44
|
units << build_unit(group, kind: :tool_exchange)
|
|
59
45
|
else
|
|
60
46
|
claimed << candidate.candidate_id
|
|
61
|
-
units << build_unit([
|
|
47
|
+
units << build_unit([candidate], kind: :message)
|
|
62
48
|
end
|
|
63
49
|
end
|
|
64
50
|
|
|
@@ -78,8 +64,12 @@ module Phronomy
|
|
|
78
64
|
call_ids = canonical_tool_call_ids(assistant)
|
|
79
65
|
return if call_ids.empty?
|
|
80
66
|
|
|
81
|
-
messages = call_ids.flat_map
|
|
82
|
-
|
|
67
|
+
messages = call_ids.flat_map do |tool_call_id|
|
|
68
|
+
tool_messages.fetch(tool_call_id, [])
|
|
69
|
+
end
|
|
70
|
+
([assistant] + messages).uniq.sort_by do |item|
|
|
71
|
+
[item.sequence || 0, item.candidate_id]
|
|
72
|
+
end
|
|
83
73
|
end
|
|
84
74
|
|
|
85
75
|
def canonical_tool_call_ids(candidate)
|
|
@@ -88,66 +78,6 @@ module Phronomy
|
|
|
88
78
|
.map(&:to_s)
|
|
89
79
|
end
|
|
90
80
|
|
|
91
|
-
def legacy_runtime_tool_exchange?(candidate, calls_by_llm)
|
|
92
|
-
candidate.llm_call_id &&
|
|
93
|
-
calls_by_llm.key?(candidate.llm_call_id) &&
|
|
94
|
-
%i[llm_message tool_call].include?(candidate.category)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def legacy_runtime_tool_exchange(candidate, assistant_by_llm:, calls_by_llm:, tool_results:)
|
|
98
|
-
llm_call_id = candidate.llm_call_id
|
|
99
|
-
calls = calls_by_llm.fetch(llm_call_id)
|
|
100
|
-
call_ids = calls.map(&:tool_call_id).compact
|
|
101
|
-
assistant = assistant_by_llm.fetch(llm_call_id, [])
|
|
102
|
-
results = call_ids.flat_map { |id| tool_results.fetch(id, []) }
|
|
103
|
-
(assistant + results).uniq.sort_by { |item| [item.sequence || 0, item.candidate_id] }
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def legacy_import_tool_exchange(ordered, index, tool_results)
|
|
107
|
-
candidate = ordered.fetch(index)
|
|
108
|
-
return unless %i[llm_message tool_call].include?(candidate.category)
|
|
109
|
-
return if candidate.llm_call_id
|
|
110
|
-
|
|
111
|
-
group = []
|
|
112
|
-
cursor = index
|
|
113
|
-
if candidate.category == :llm_message
|
|
114
|
-
next_candidate = ordered[cursor + 1]
|
|
115
|
-
return unless next_candidate &&
|
|
116
|
-
next_candidate.category == :tool_call &&
|
|
117
|
-
contiguous_source?(candidate, next_candidate) &&
|
|
118
|
-
next_candidate.llm_call_id.nil?
|
|
119
|
-
group << candidate
|
|
120
|
-
cursor += 1
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
calls = []
|
|
124
|
-
while (item = ordered[cursor]) && item.category == :tool_call && item.llm_call_id.nil?
|
|
125
|
-
if calls.any? && !contiguous_source?(calls.last, item)
|
|
126
|
-
break
|
|
127
|
-
end
|
|
128
|
-
if group.any? && calls.empty? && !contiguous_source?(group.last, item)
|
|
129
|
-
break
|
|
130
|
-
end
|
|
131
|
-
calls << item
|
|
132
|
-
cursor += 1
|
|
133
|
-
end
|
|
134
|
-
return if calls.empty?
|
|
135
|
-
|
|
136
|
-
call_ids = calls.map(&:tool_call_id).compact
|
|
137
|
-
results = call_ids.flat_map { |id| tool_results.fetch(id, []) }
|
|
138
|
-
(group + calls + results).uniq.sort_by { |item| [item.sequence || 0, item.candidate_id] }
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def contiguous_source?(left, right)
|
|
142
|
-
left_source = left.metadata["source_sequence"]
|
|
143
|
-
right_source = right.metadata["source_sequence"]
|
|
144
|
-
if left_source && right_source
|
|
145
|
-
Integer(right_source) == Integer(left_source) + 1
|
|
146
|
-
else
|
|
147
|
-
Integer(right.sequence) == Integer(left.sequence) + 1
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
81
|
def build_unit(candidates, kind:)
|
|
152
82
|
ids = candidates.map(&:candidate_id)
|
|
153
83
|
sequences = candidates.map(&:sequence).compact
|
|
@@ -58,7 +58,6 @@ module Phronomy
|
|
|
58
58
|
def validate_tool_dependencies!(all_candidates, selected_candidates)
|
|
59
59
|
selected_ids = selected_candidates.to_h { |candidate| [candidate.candidate_id, true] }
|
|
60
60
|
validate_canonical_tool_dependencies!(all_candidates, selected_ids)
|
|
61
|
-
validate_legacy_tool_dependencies!(all_candidates, selected_candidates, selected_ids)
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
def validate_canonical_tool_dependencies!(all_candidates, selected_ids)
|
|
@@ -113,38 +112,6 @@ module Phronomy
|
|
|
113
112
|
end
|
|
114
113
|
end
|
|
115
114
|
|
|
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
115
|
def canonical_tool_call_ids(candidate)
|
|
149
116
|
Array(candidate.metadata["tool_call_ids"] || candidate.metadata[:tool_call_ids])
|
|
150
117
|
.compact
|
|
@@ -19,7 +19,7 @@ module Phronomy
|
|
|
19
19
|
)
|
|
20
20
|
result_task = Phronomy::Task.deferred(name: "agent-#{@agent.agent_id}-#{mode}")
|
|
21
21
|
runtime = Phronomy::Runtime.instance
|
|
22
|
-
preparation = runtime.
|
|
22
|
+
preparation = runtime.offload.submit(on_full: :raise) do
|
|
23
23
|
prepare(input, thread_id: thread_id, config: config)
|
|
24
24
|
end
|
|
25
25
|
preparation.on_complete do |prepared, error|
|
|
@@ -52,7 +52,7 @@ module Phronomy
|
|
|
52
52
|
name: "agent-approval-resume:#{execution_id}"
|
|
53
53
|
)
|
|
54
54
|
runtime = Phronomy::Runtime.instance
|
|
55
|
-
preparation = runtime.
|
|
55
|
+
preparation = runtime.offload.submit(on_full: :raise) do
|
|
56
56
|
execution = @agent.persistence.executions.load(execution_id)
|
|
57
57
|
unless execution.agent_id == @agent.agent_id && execution.status == :suspended
|
|
58
58
|
raise ArgumentError,
|
|
@@ -86,7 +86,7 @@ module Phronomy
|
|
|
86
86
|
config: config
|
|
87
87
|
)
|
|
88
88
|
rescue => start_error
|
|
89
|
-
commit = runtime.
|
|
89
|
+
commit = runtime.offload.submit(on_full: :raise) do
|
|
90
90
|
commit_failed(activation, activation.invocation, start_error)
|
|
91
91
|
end
|
|
92
92
|
commit.on_complete do |terminal, commit_error|
|
|
@@ -107,7 +107,7 @@ module Phronomy
|
|
|
107
107
|
|
|
108
108
|
# Persists all Runtime events from the previous call, fixes the next
|
|
109
109
|
# Canonical LLM Input Manifest, and materializes the next Runtime Projection.
|
|
110
|
-
# This method is executed on
|
|
110
|
+
# This method is executed on an OffloadPool worker, never on EventLoop.
|
|
111
111
|
def prepare_next_llm_call(activation)
|
|
112
112
|
snapshot = activation.runtime_snapshot
|
|
113
113
|
manifest = manifest_ref = updated = root = nil
|
|
@@ -367,7 +367,6 @@ module Phronomy
|
|
|
367
367
|
session = Agent::AgentInvocationSessionBuilder.build(
|
|
368
368
|
agent: @agent,
|
|
369
369
|
input: prepared.filtered_input,
|
|
370
|
-
messages: prepared.runtime_projection.messages,
|
|
371
370
|
config: effective_config,
|
|
372
371
|
approval_policy: approval_policy,
|
|
373
372
|
approval_listener: approval_listener,
|
|
@@ -398,7 +397,7 @@ module Phronomy
|
|
|
398
397
|
|
|
399
398
|
def finish(activation, result_task, invocation, error)
|
|
400
399
|
runtime = Phronomy::Runtime.instance
|
|
401
|
-
operation = runtime.
|
|
400
|
+
operation = runtime.offload.submit(on_full: :raise) do
|
|
402
401
|
compute_terminal(activation, invocation, error)
|
|
403
402
|
end
|
|
404
403
|
operation.on_complete do |outcome, commit_error|
|
|
@@ -1070,7 +1069,7 @@ module Phronomy
|
|
|
1070
1069
|
def dispatch_approval_listener(invocation, request)
|
|
1071
1070
|
listener = invocation.approval_listener
|
|
1072
1071
|
return unless listener
|
|
1073
|
-
Phronomy::Runtime.instance.
|
|
1072
|
+
Phronomy::Runtime.instance.offload.submit(on_full: :raise) { listener.call(request) }
|
|
1074
1073
|
end
|
|
1075
1074
|
|
|
1076
1075
|
def json_value(value)
|
|
@@ -4,9 +4,11 @@ module Phronomy
|
|
|
4
4
|
module Agent
|
|
5
5
|
class JournalProjection
|
|
6
6
|
MESSAGE_KINDS = %i[
|
|
7
|
-
external_message
|
|
8
|
-
|
|
7
|
+
external_message
|
|
8
|
+
assistant_message
|
|
9
|
+
tool_message
|
|
9
10
|
].freeze
|
|
11
|
+
KNOWLEDGE_RESET_KINDS = %i[knowledge_cleared context_reset].freeze
|
|
10
12
|
|
|
11
13
|
def initialize(persistence:, agent_root:)
|
|
12
14
|
@persistence = persistence
|
|
@@ -22,6 +24,16 @@ module Phronomy
|
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
# Returns all persistent records eligible for Context selection.
|
|
28
|
+
# Knowledge has an independent logical lifetime from the transcript:
|
|
29
|
+
# clear_transcript! does not remove Knowledge, while knowledge_cleared and
|
|
30
|
+
# context_reset invalidate earlier Knowledge records by Journal position.
|
|
31
|
+
def context_records
|
|
32
|
+
(transcript_records + active_knowledge_records)
|
|
33
|
+
.sort_by { |record| [record.sequence || 0, record.record_id] }
|
|
34
|
+
.freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
25
37
|
def llm_call_records
|
|
26
38
|
records.select { |record| record.kind == :llm_call_recorded }
|
|
27
39
|
end
|
|
@@ -29,6 +41,20 @@ module Phronomy
|
|
|
29
41
|
def records
|
|
30
42
|
@records ||= @persistence.journals.read(@agent_root.agent_id)
|
|
31
43
|
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def active_knowledge_records
|
|
48
|
+
reset_sequence = records.filter_map do |record|
|
|
49
|
+
record.sequence if KNOWLEDGE_RESET_KINDS.include?(record.kind)
|
|
50
|
+
end.max.to_i
|
|
51
|
+
|
|
52
|
+
records.select do |record|
|
|
53
|
+
record.context_candidate &&
|
|
54
|
+
record.kind == :knowledge &&
|
|
55
|
+
record.sequence.to_i > reset_sequence
|
|
56
|
+
end
|
|
57
|
+
end
|
|
32
58
|
end
|
|
33
59
|
end
|
|
34
60
|
end
|
|
@@ -112,102 +112,7 @@ module Phronomy
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def materialize_message_segments(segments)
|
|
115
|
-
|
|
116
|
-
index = 0
|
|
117
|
-
while index < segments.length
|
|
118
|
-
segment = segments[index]
|
|
119
|
-
if legacy_assistant_segment?(segment)
|
|
120
|
-
grouped, consumed = legacy_assistant_group(segments, index)
|
|
121
|
-
result << materialize_legacy_assistant_group(grouped)
|
|
122
|
-
index += consumed
|
|
123
|
-
else
|
|
124
|
-
result << materialize_segment(segment)
|
|
125
|
-
index += 1
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
result
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# New canonical records preserve one logical assistant/tool message per
|
|
132
|
-
# record. These legacy helpers remain only so already-persisted state from
|
|
133
|
-
# the preceding stateful refactor can still be read during migration.
|
|
134
|
-
def legacy_assistant_segment?(segment)
|
|
135
|
-
segment.role&.to_sym == :assistant &&
|
|
136
|
-
%i[llm_message tool_call].include?(segment.category.to_sym)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def legacy_assistant_group(segments, start_index)
|
|
140
|
-
first = segments.fetch(start_index)
|
|
141
|
-
llm_call_id = metadata_value(first, "llm_call_id")
|
|
142
|
-
return legacy_runtime_assistant_group(segments, start_index, llm_call_id) if llm_call_id
|
|
143
|
-
|
|
144
|
-
legacy_import_assistant_group(segments, start_index)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def legacy_runtime_assistant_group(segments, start_index, llm_call_id)
|
|
148
|
-
group = []
|
|
149
|
-
index = start_index
|
|
150
|
-
while (segment = segments[index]) && legacy_assistant_segment?(segment) &&
|
|
151
|
-
metadata_value(segment, "llm_call_id").to_s == llm_call_id.to_s
|
|
152
|
-
group << segment
|
|
153
|
-
index += 1
|
|
154
|
-
end
|
|
155
|
-
[group, group.length]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def legacy_import_assistant_group(segments, start_index)
|
|
159
|
-
first = segments.fetch(start_index)
|
|
160
|
-
group = [first]
|
|
161
|
-
index = start_index + 1
|
|
162
|
-
last = first
|
|
163
|
-
|
|
164
|
-
while (segment = segments[index]) && legacy_assistant_segment?(segment)
|
|
165
|
-
break unless segment.category.to_sym == :tool_call
|
|
166
|
-
break unless contiguous_source?(last, segment)
|
|
167
|
-
break if metadata_value(segment, "llm_call_id")
|
|
168
|
-
|
|
169
|
-
group << segment
|
|
170
|
-
last = segment
|
|
171
|
-
index += 1
|
|
172
|
-
end
|
|
173
|
-
[group, group.length]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def contiguous_source?(left, right)
|
|
177
|
-
left_sequence = metadata_value(left, "journal_sequence")
|
|
178
|
-
right_sequence = metadata_value(right, "journal_sequence")
|
|
179
|
-
return false unless left_sequence && right_sequence
|
|
180
|
-
|
|
181
|
-
Integer(right_sequence) == Integer(left_sequence) + 1
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def materialize_legacy_assistant_group(segments)
|
|
185
|
-
content_segments = segments.select { |segment| segment.category.to_sym == :llm_message }
|
|
186
|
-
if content_segments.length > 1
|
|
187
|
-
raise ArgumentError, "ambiguous assistant message group contains multiple content records"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
tool_calls = {}
|
|
191
|
-
segments.select { |segment| segment.category.to_sym == :tool_call }.each do |segment|
|
|
192
|
-
payload = fetch_json(segment.content_ref)
|
|
193
|
-
tool_call_id = payload.fetch("id").to_s
|
|
194
|
-
if tool_calls.key?(tool_call_id)
|
|
195
|
-
raise ArgumentError, "duplicate Tool Call in assistant message: #{tool_call_id}"
|
|
196
|
-
end
|
|
197
|
-
tool_calls[tool_call_id] = materialize_tool_call(payload)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
content = if content_segments.empty?
|
|
201
|
-
""
|
|
202
|
-
else
|
|
203
|
-
@persistence.contents.fetch_text(content_segments.first.content_ref)
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
RubyLLM::Message.new(
|
|
207
|
-
role: :assistant,
|
|
208
|
-
content: content,
|
|
209
|
-
tool_calls: tool_calls.empty? ? nil : tool_calls
|
|
210
|
-
)
|
|
115
|
+
segments.map { |segment| materialize_segment(segment) }
|
|
211
116
|
end
|
|
212
117
|
|
|
213
118
|
def materialize_segment(segment)
|
|
@@ -216,18 +121,8 @@ module Phronomy
|
|
|
216
121
|
materialize_canonical_message(segment, expected_role: :assistant)
|
|
217
122
|
when :tool_message
|
|
218
123
|
materialize_canonical_message(segment, expected_role: :tool)
|
|
219
|
-
when :tool_call
|
|
220
|
-
materialize_legacy_assistant_group([segment])
|
|
221
124
|
when :tool_result
|
|
222
|
-
|
|
223
|
-
raise ArgumentError, "raw Tool result is not an LLM message"
|
|
224
|
-
end
|
|
225
|
-
RubyLLM::Message.new(
|
|
226
|
-
role: :tool,
|
|
227
|
-
content: @persistence.contents.fetch_text(segment.content_ref),
|
|
228
|
-
tool_call_id: segment.tool_call_id ||
|
|
229
|
-
segment.metadata["tool_call_id"] || segment.metadata[:tool_call_id]
|
|
230
|
-
)
|
|
125
|
+
raise ArgumentError, "raw Tool result is not an LLM message"
|
|
231
126
|
else
|
|
232
127
|
RubyLLM::Message.new(
|
|
233
128
|
role: segment.role,
|
|
@@ -289,10 +184,6 @@ module Phronomy
|
|
|
289
184
|
thought_signature: payload["thought_signature"]
|
|
290
185
|
)
|
|
291
186
|
end
|
|
292
|
-
|
|
293
|
-
def metadata_value(segment, name)
|
|
294
|
-
segment.metadata[name] || segment.metadata[name.to_sym]
|
|
295
|
-
end
|
|
296
187
|
end
|
|
297
188
|
end
|
|
298
189
|
end
|