phronomy 0.22.0 → 0.23.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 +3 -4
- data/CHANGELOG.md +200 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +152 -0
- data/docs/architecture/persistence.md +175 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +82 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/README.md +172 -0
- data/docs/features.md +31 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/persistence-backends.md +88 -38
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +19 -15
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +145 -72
- data/lib/phronomy/agent/base.rb +388 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
- data/lib/phronomy/agent/recovery_support.rb +512 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +336 -102
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime.rb +77 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
- data/lib/phronomy/multi_agent/coordinator.rb +154 -0
- data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/handoff.rb +24 -45
- data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
- data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
- data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
- data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
- data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
- data/lib/phronomy/multi_agent/runner.rb +98 -0
- data/lib/phronomy/persistence/durable_codec.rb +646 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +210 -134
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +316 -0
- data/lib/phronomy/persistence.rb +81 -41
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/multi_agent.rbs +39 -0
- data/sig/phronomy/persistence.rbs +62 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +65 -17
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
|
@@ -3,64 +3,156 @@
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
5
|
class ContextAssembler
|
|
6
|
-
ASSEMBLY_POLICY_VERSION =
|
|
6
|
+
ASSEMBLY_POLICY_VERSION = 8
|
|
7
7
|
SEGMENT_ORIGIN_METADATA_KEY = "phronomy_origin"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
POLICY_ORIGIN_METADATA_KEY = "context_policy_origin"
|
|
9
|
+
POLICY_ITEM_ID_METADATA_KEY = "context_policy_item_id"
|
|
10
|
+
SEMANTIC_CATEGORY_METADATA_KEY = "context_policy_semantic_category"
|
|
11
|
+
CONTENT_FORMAT_METADATA_KEY = "context_policy_content_format"
|
|
12
|
+
CONVERSATION_GROUP_ID_METADATA_KEY = "context_policy_conversation_group_id"
|
|
13
|
+
TRUSTED_HANDOFF_METADATA_KEYS = %w[
|
|
14
|
+
handoff_policy_category
|
|
15
|
+
handoff_provenance
|
|
11
16
|
].freeze
|
|
17
|
+
HANDOFF_POLICY_CATEGORY_METADATA_KEY = "handoff_policy_category"
|
|
18
|
+
BEFORE_LLM_INPUT_ORIGIN = "before_llm_input"
|
|
19
|
+
HANDOFF_CONTEXT_ORIGIN = "handoff_context"
|
|
20
|
+
|
|
21
|
+
Prepared = Data.define(:input, :plan, :model_config, :call_sequence, :call_mode) do
|
|
22
|
+
def initialize(**values)
|
|
23
|
+
super
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
end
|
|
12
27
|
|
|
13
28
|
def initialize(
|
|
14
29
|
agent:,
|
|
15
30
|
persistence:,
|
|
16
|
-
policy: ContextPolicies::Default.new,
|
|
17
31
|
candidate_resolver: nil,
|
|
18
32
|
journal_records: nil
|
|
19
33
|
)
|
|
20
34
|
@agent = agent
|
|
21
35
|
@persistence = persistence
|
|
22
|
-
@policy =
|
|
36
|
+
@policy = agent.class.context_policy
|
|
23
37
|
@journal_records = journal_records
|
|
24
38
|
@candidate_resolver = candidate_resolver || ContextCandidateResolver.new(
|
|
25
39
|
content_loader: method(:fetch_content)
|
|
26
40
|
)
|
|
41
|
+
@input_builder = ContextPolicyInputBuilder.new(
|
|
42
|
+
content_loader: method(:fetch_content)
|
|
43
|
+
)
|
|
27
44
|
end
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
# Builds one immutable Policy input snapshot and executes Application Policy.
|
|
47
|
+
# This method must run outside a Phronomy Persistence transaction.
|
|
48
|
+
def prepare_initial(input:, agent_root:, execution:, config: {}, patch: LLMInputPatch.empty)
|
|
30
49
|
projection = journal_projection(agent_root)
|
|
31
50
|
model_cfg = effective_model_config(config, patch)
|
|
32
|
-
tool_set = ToolDefinitionSet.build(
|
|
51
|
+
tool_set = ToolDefinitionSet.build(
|
|
52
|
+
@agent,
|
|
53
|
+
additional_tools: handoff_tool_classes(config)
|
|
54
|
+
)
|
|
33
55
|
system_text = build_system_text(input)
|
|
34
56
|
hook_candidates = normalize_candidates(patch.segment_candidates)
|
|
35
57
|
input_ref = execution.metadata.fetch("current_input_ref")
|
|
36
|
-
|
|
58
|
+
current_input_content = @persistence.contents.fetch_text(input_ref)
|
|
37
59
|
excluded = [execution.metadata["current_input_record_id"]].compact
|
|
60
|
+
handoff_context = config[:phronomy_handoff_context]
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
instructions = []
|
|
40
63
|
unless system_text.to_s.empty?
|
|
41
|
-
|
|
64
|
+
instructions << ContextPolicyInput::InstructionItem.new(
|
|
65
|
+
id: "instruction:agent:#{execution.execution_id}:1",
|
|
66
|
+
kind: :instruction,
|
|
67
|
+
role: :system,
|
|
68
|
+
content: system_text.to_s,
|
|
69
|
+
content_format: :text,
|
|
70
|
+
estimated_tokens: estimate_value(system_text.to_s),
|
|
71
|
+
required: true,
|
|
72
|
+
provenance: ContextPolicyInput::Provenance.new(origin: :agent_configuration),
|
|
73
|
+
metadata: {}
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
if handoff_context
|
|
77
|
+
instructions << ContextPolicyInput::InstructionItem.new(
|
|
78
|
+
id: "instruction:handoff:#{execution.execution_id}:1",
|
|
79
|
+
kind: :handoff_responsibility,
|
|
80
|
+
role: :user,
|
|
81
|
+
content: handoff_context.responsibility.to_s,
|
|
82
|
+
content_format: :text,
|
|
83
|
+
estimated_tokens: estimate_value(handoff_context.responsibility.to_s),
|
|
84
|
+
required: true,
|
|
85
|
+
provenance: ContextPolicyInput::Provenance.new(origin: :handoff_context),
|
|
86
|
+
metadata: {SEGMENT_ORIGIN_METADATA_KEY => HANDOFF_CONTEXT_ORIGIN}
|
|
87
|
+
)
|
|
42
88
|
end
|
|
43
89
|
|
|
44
|
-
|
|
90
|
+
generation = agent_root.transcript_generation
|
|
91
|
+
eligible_working = Array(execution.working_records).select do |record|
|
|
92
|
+
record.context_candidate && record.context_generation == generation
|
|
93
|
+
end
|
|
94
|
+
candidates = @candidate_resolver.resolve(
|
|
95
|
+
prior_records: projection.context_records,
|
|
96
|
+
working_records: eligible_working,
|
|
97
|
+
excluded_record_ids: excluded
|
|
98
|
+
)
|
|
99
|
+
candidates = merge_hook_candidates(
|
|
100
|
+
candidates,
|
|
101
|
+
hook_candidates,
|
|
102
|
+
agent_root: agent_root,
|
|
103
|
+
execution: execution,
|
|
104
|
+
call_sequence: 1
|
|
105
|
+
)
|
|
106
|
+
candidates = merge_handoff_candidates(
|
|
107
|
+
candidates,
|
|
108
|
+
handoff_context,
|
|
109
|
+
execution: execution
|
|
110
|
+
)
|
|
111
|
+
next_sequence = Array(candidates).filter_map(&:sequence).max.to_i + 1
|
|
112
|
+
current_input = ContextPolicyInput::ConversationItem.new(
|
|
113
|
+
id: "current-input:#{execution.execution_id}",
|
|
114
|
+
kind: :current_input,
|
|
115
|
+
role: :user,
|
|
116
|
+
content: current_input_content,
|
|
117
|
+
content_format: :text,
|
|
118
|
+
sequence: next_sequence,
|
|
119
|
+
estimated_tokens: estimate_value(current_input_content),
|
|
120
|
+
required: true,
|
|
121
|
+
provenance: ContextPolicyInput::Provenance.new(
|
|
122
|
+
origin: :working,
|
|
123
|
+
content_ref: input_ref,
|
|
124
|
+
record_id: execution.metadata["current_input_record_id"],
|
|
125
|
+
agent_id: agent_root.agent_id,
|
|
126
|
+
execution_id: execution.execution_id
|
|
127
|
+
),
|
|
128
|
+
tool_call_id: nil,
|
|
129
|
+
tool_call_ids: [],
|
|
130
|
+
delivery: :ask_argument,
|
|
131
|
+
metadata: {
|
|
132
|
+
"source_agent_id" => agent_root.agent_id,
|
|
133
|
+
"source_execution_id" => execution.execution_id,
|
|
134
|
+
"handoff_policy_category" => "current_request"
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
prepare(
|
|
45
139
|
agent_root: agent_root,
|
|
46
140
|
execution: execution,
|
|
47
141
|
call_sequence: 1,
|
|
48
142
|
call_mode: :ask,
|
|
49
143
|
previous_manifest: nil,
|
|
50
144
|
model_config: model_cfg,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
excluded_record_ids: excluded,
|
|
56
|
-
base_segments: base_segments,
|
|
57
|
-
hook_candidates: hook_candidates,
|
|
58
|
-
current_input_segment: segment(:current_input, :user, input_ref, :ask_argument),
|
|
59
|
-
mandatory_values: [system_text, current_input, tool_set.definitions]
|
|
145
|
+
candidates: candidates,
|
|
146
|
+
instruction: instructions,
|
|
147
|
+
tools: tool_items(tool_set),
|
|
148
|
+
current_input: current_input
|
|
60
149
|
)
|
|
61
150
|
end
|
|
62
151
|
|
|
63
|
-
|
|
152
|
+
# Builds one immutable follow-up Policy input snapshot and executes Policy.
|
|
153
|
+
# The previous finalized Manifest is read-only input; Policy-generated items
|
|
154
|
+
# from a prior call are not promoted into future candidates automatically.
|
|
155
|
+
def prepare_followup(
|
|
64
156
|
base_manifest:,
|
|
65
157
|
agent_root:,
|
|
66
158
|
execution:,
|
|
@@ -70,68 +162,27 @@ module Phronomy
|
|
|
70
162
|
projection = journal_projection(agent_root)
|
|
71
163
|
model_cfg = effective_model_config(config, patch)
|
|
72
164
|
hook_candidates = normalize_candidates(patch.segment_candidates)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
fetch_json(base_manifest.tool_definitions_ref) : []
|
|
78
|
-
|
|
79
|
-
assemble(
|
|
80
|
-
agent_root: agent_root,
|
|
81
|
-
execution: execution,
|
|
82
|
-
call_sequence: execution.llm_calls.length + 1,
|
|
83
|
-
call_mode: :complete,
|
|
84
|
-
previous_manifest: base_manifest,
|
|
85
|
-
model_config: model_cfg,
|
|
86
|
-
tool_definitions: tool_definitions,
|
|
87
|
-
tool_definitions_ref: base_manifest.tool_definitions_ref,
|
|
88
|
-
prior_records: projection.context_records,
|
|
89
|
-
working_records: execution.working_records,
|
|
90
|
-
excluded_record_ids: [],
|
|
91
|
-
base_segments: system_segments.map { |existing| segment_hash(existing) },
|
|
92
|
-
hook_candidates: hook_candidates,
|
|
93
|
-
current_input_segment: nil,
|
|
94
|
-
mandatory_values: system_segments.map { |segment| fetch_content(segment.content_ref) } +
|
|
95
|
-
[tool_definitions]
|
|
165
|
+
handoff_context = config[:phronomy_handoff_context]
|
|
166
|
+
tool_set = ToolDefinitionSet.build(
|
|
167
|
+
@agent,
|
|
168
|
+
additional_tools: handoff_tool_classes(config)
|
|
96
169
|
)
|
|
97
|
-
end
|
|
98
170
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if @journal_records
|
|
103
|
-
JournalProjection.new(agent_root: agent_root, records: @journal_records)
|
|
104
|
-
else
|
|
105
|
-
JournalProjection.new(persistence: @persistence, agent_root: agent_root)
|
|
171
|
+
instructions = base_manifest.segments.filter_map do |segment|
|
|
172
|
+
next unless retained_base_instruction?(segment)
|
|
173
|
+
instruction_item_from_manifest(segment)
|
|
106
174
|
end
|
|
107
|
-
end
|
|
108
175
|
|
|
109
|
-
def assemble(
|
|
110
|
-
agent_root:,
|
|
111
|
-
execution:,
|
|
112
|
-
call_sequence:,
|
|
113
|
-
call_mode:,
|
|
114
|
-
previous_manifest:,
|
|
115
|
-
model_config:,
|
|
116
|
-
tool_definitions:,
|
|
117
|
-
tool_definitions_ref:,
|
|
118
|
-
prior_records:,
|
|
119
|
-
working_records:,
|
|
120
|
-
excluded_record_ids:,
|
|
121
|
-
base_segments:,
|
|
122
|
-
hook_candidates:,
|
|
123
|
-
current_input_segment:,
|
|
124
|
-
mandatory_values:
|
|
125
|
-
)
|
|
126
176
|
generation = agent_root.transcript_generation
|
|
127
|
-
eligible_working = Array(working_records).select do |record|
|
|
177
|
+
eligible_working = Array(execution.working_records).select do |record|
|
|
128
178
|
record.context_candidate && record.context_generation == generation
|
|
129
179
|
end
|
|
130
180
|
candidates = @candidate_resolver.resolve(
|
|
131
|
-
prior_records:
|
|
181
|
+
prior_records: projection.context_records,
|
|
132
182
|
working_records: eligible_working,
|
|
133
|
-
excluded_record_ids:
|
|
183
|
+
excluded_record_ids: []
|
|
134
184
|
)
|
|
185
|
+
call_sequence = execution.llm_calls.length + 1
|
|
135
186
|
candidates = merge_hook_candidates(
|
|
136
187
|
candidates,
|
|
137
188
|
hook_candidates,
|
|
@@ -139,73 +190,214 @@ module Phronomy
|
|
|
139
190
|
execution: execution,
|
|
140
191
|
call_sequence: call_sequence
|
|
141
192
|
)
|
|
193
|
+
candidates = merge_handoff_candidates(
|
|
194
|
+
candidates,
|
|
195
|
+
handoff_context,
|
|
196
|
+
execution: execution
|
|
197
|
+
)
|
|
142
198
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
request = ContextRequest.new(
|
|
147
|
-
agent_id: agent_root.agent_id,
|
|
148
|
-
execution_id: execution.execution_id,
|
|
199
|
+
prepare(
|
|
200
|
+
agent_root: agent_root,
|
|
201
|
+
execution: execution,
|
|
149
202
|
call_sequence: call_sequence,
|
|
150
|
-
call_mode:
|
|
203
|
+
call_mode: :complete,
|
|
204
|
+
previous_manifest: base_manifest,
|
|
205
|
+
model_config: model_cfg,
|
|
151
206
|
candidates: candidates,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
required_coverage: [],
|
|
156
|
-
parts: parts,
|
|
157
|
-
metadata: {"mandatory_token_estimate" => mandatory_token_estimate}
|
|
207
|
+
instruction: instructions,
|
|
208
|
+
tools: tool_items(tool_set),
|
|
209
|
+
current_input: nil
|
|
158
210
|
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Validates and canonicalizes a previously prepared Policy decision.
|
|
214
|
+
# This method contains no ContextPolicy invocation and is safe to execute in
|
|
215
|
+
# the short commit transaction after the caller revalidates durable revision.
|
|
216
|
+
def finalize(prepared, persistence: @persistence)
|
|
217
|
+
unless prepared.is_a?(Prepared)
|
|
218
|
+
raise ArgumentError, "ContextAssembler#finalize expected ContextAssembler::Prepared"
|
|
164
219
|
end
|
|
165
220
|
|
|
166
|
-
|
|
167
|
-
|
|
221
|
+
plan = ContextPlanValidator.new.validate!(input: prepared.input, plan: prepared.plan)
|
|
222
|
+
segments = []
|
|
223
|
+
plan.instruction.each do |item|
|
|
224
|
+
segments << segment_from_content_item(
|
|
225
|
+
item,
|
|
226
|
+
persistence: persistence,
|
|
227
|
+
additional_metadata: {SEMANTIC_CATEGORY_METADATA_KEY => "instruction"}
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
plan.knowledge.each do |item|
|
|
231
|
+
segments << segment_from_content_item(
|
|
232
|
+
item,
|
|
233
|
+
persistence: persistence,
|
|
234
|
+
additional_metadata: {SEMANTIC_CATEGORY_METADATA_KEY => "knowledge"}
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
plan.conversation.each_with_index do |group, group_index|
|
|
238
|
+
group_metadata = {
|
|
239
|
+
SEMANTIC_CATEGORY_METADATA_KEY => "conversation",
|
|
240
|
+
CONVERSATION_GROUP_ID_METADATA_KEY =>
|
|
241
|
+
"conversation:#{prepared.call_sequence}:#{group_index}"
|
|
242
|
+
}
|
|
243
|
+
if tool_exchange_group?(group)
|
|
244
|
+
group_metadata[HANDOFF_POLICY_CATEGORY_METADATA_KEY] = "tool_exchanges"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
group.each do |item|
|
|
248
|
+
segments << segment_from_content_item(
|
|
249
|
+
item,
|
|
250
|
+
persistence: persistence,
|
|
251
|
+
additional_metadata: group_metadata
|
|
252
|
+
)
|
|
253
|
+
end
|
|
168
254
|
end
|
|
169
|
-
segments = Array(base_segments).dup
|
|
170
|
-
append_selected_candidates(segments, selected_candidates, before_history: true)
|
|
171
|
-
append_selected_candidates(segments, selected_candidates, before_history: false)
|
|
172
|
-
segments << current_input_segment if current_input_segment
|
|
173
255
|
|
|
256
|
+
selected_tool_definitions = plan.tools.map(&:definition)
|
|
174
257
|
ContextParts::Validators::FinalBudgetValidator.new(
|
|
175
|
-
content_loader:
|
|
258
|
+
content_loader: lambda { |ref| fetch_content_from(persistence, ref) }
|
|
176
259
|
).validate!(
|
|
177
|
-
token_budget: token_budget,
|
|
260
|
+
token_budget: prepared.input.token_budget,
|
|
178
261
|
segments: segments,
|
|
179
|
-
extra_values: [
|
|
262
|
+
extra_values: [selected_tool_definitions]
|
|
180
263
|
)
|
|
181
264
|
|
|
182
265
|
store_manifest(
|
|
266
|
+
persistence: persistence,
|
|
267
|
+
call_sequence: prepared.call_sequence,
|
|
268
|
+
call_mode: prepared.call_mode,
|
|
269
|
+
segments: segments,
|
|
270
|
+
model_config_ref: persistence.contents.put_json(prepared.model_config),
|
|
271
|
+
tool_definitions_ref: persistence.contents.put_json(selected_tool_definitions)
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def prepare(
|
|
278
|
+
agent_root:,
|
|
279
|
+
execution:,
|
|
280
|
+
call_sequence:,
|
|
281
|
+
call_mode:,
|
|
282
|
+
previous_manifest:,
|
|
283
|
+
model_config:,
|
|
284
|
+
candidates:,
|
|
285
|
+
instruction:,
|
|
286
|
+
tools:,
|
|
287
|
+
current_input:
|
|
288
|
+
)
|
|
289
|
+
token_budget = TokenBudgetResolver.new(agent: @agent).resolve(model_config)
|
|
290
|
+
policy_input = @input_builder.build(
|
|
291
|
+
agent_id: agent_root.agent_id,
|
|
292
|
+
execution_id: execution.execution_id,
|
|
183
293
|
call_sequence: call_sequence,
|
|
184
294
|
call_mode: call_mode,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
295
|
+
candidates: candidates,
|
|
296
|
+
instruction: instruction,
|
|
297
|
+
tools: tools,
|
|
298
|
+
current_input: current_input,
|
|
299
|
+
token_budget: token_budget,
|
|
300
|
+
model_config: model_config,
|
|
301
|
+
previous_manifest: previous_manifest,
|
|
302
|
+
metadata: {
|
|
303
|
+
"agent_revision" => agent_root.agent_revision,
|
|
304
|
+
"context_revision" => agent_root.context_revision,
|
|
305
|
+
"journal_position" => agent_root.journal_position,
|
|
306
|
+
"execution_revision" => execution.execution_revision
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
plan = invoke_policy(policy_input)
|
|
310
|
+
ContextPlanValidator.new.validate!(input: policy_input, plan: plan)
|
|
311
|
+
Prepared.new(
|
|
312
|
+
input: policy_input,
|
|
313
|
+
plan: plan,
|
|
314
|
+
model_config: model_config,
|
|
315
|
+
call_sequence: call_sequence,
|
|
316
|
+
call_mode: call_mode
|
|
189
317
|
)
|
|
190
318
|
end
|
|
191
319
|
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
320
|
+
def invoke_policy(policy_input)
|
|
321
|
+
tracer = Phronomy.configuration.tracer
|
|
322
|
+
span = tracer.start_span(
|
|
323
|
+
"context_policy",
|
|
324
|
+
agent_id: policy_input.agent_id,
|
|
325
|
+
execution_id: policy_input.execution_id,
|
|
326
|
+
call_sequence: policy_input.call_sequence,
|
|
327
|
+
policy_class: @policy.class.name || @policy.class.to_s,
|
|
328
|
+
instruction_count: policy_input.instruction.length,
|
|
329
|
+
knowledge_count: policy_input.knowledge.length,
|
|
330
|
+
tool_count: policy_input.tools.length,
|
|
331
|
+
conversation_group_count: policy_input.conversation.length
|
|
332
|
+
)
|
|
333
|
+
result = @policy.call(policy_input)
|
|
334
|
+
tracer.finish_span(span)
|
|
335
|
+
result
|
|
336
|
+
rescue => error
|
|
337
|
+
tracer&.finish_span(span, error: error) if defined?(span) && span
|
|
338
|
+
raise
|
|
199
339
|
end
|
|
200
340
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
341
|
+
def journal_projection(agent_root)
|
|
342
|
+
if @journal_records
|
|
343
|
+
JournalProjection.new(agent_root: agent_root, records: @journal_records)
|
|
344
|
+
else
|
|
345
|
+
JournalProjection.new(persistence: @persistence, agent_root: agent_root)
|
|
205
346
|
end
|
|
206
347
|
end
|
|
207
348
|
|
|
208
|
-
def
|
|
349
|
+
def retained_base_instruction?(segment)
|
|
350
|
+
origin = segment.metadata[POLICY_ORIGIN_METADATA_KEY]
|
|
351
|
+
return %w[agent_configuration handoff_context].include?(origin) if origin
|
|
352
|
+
|
|
353
|
+
# Current-format manifests created before ACS-04 do not have the origin
|
|
354
|
+
# marker. Preserve only the legacy base instruction shapes.
|
|
355
|
+
(segment.role == :system &&
|
|
356
|
+
segment.metadata[SEGMENT_ORIGIN_METADATA_KEY] != BEFORE_LLM_INPUT_ORIGIN) ||
|
|
357
|
+
segment.category.to_sym == :handoff_responsibility
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def instruction_item_from_manifest(segment)
|
|
361
|
+
origin = segment.metadata[POLICY_ORIGIN_METADATA_KEY] ||
|
|
362
|
+
((segment.category.to_sym == :handoff_responsibility) ? "handoff_context" : "agent_configuration")
|
|
363
|
+
ContextPolicyInput::InstructionItem.new(
|
|
364
|
+
id: segment.metadata[POLICY_ITEM_ID_METADATA_KEY] || "manifest-instruction:#{segment.position}",
|
|
365
|
+
kind: segment.category,
|
|
366
|
+
role: segment.role,
|
|
367
|
+
content: fetch_content(segment.content_ref),
|
|
368
|
+
content_format: :text,
|
|
369
|
+
estimated_tokens: estimate_value(fetch_content(segment.content_ref)),
|
|
370
|
+
required: true,
|
|
371
|
+
provenance: ContextPolicyInput::Provenance.new(
|
|
372
|
+
origin: origin.to_sym,
|
|
373
|
+
content_ref: segment.content_ref
|
|
374
|
+
),
|
|
375
|
+
metadata: segment.metadata
|
|
376
|
+
)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def tool_items(tool_set)
|
|
380
|
+
tool_set.definitions.map do |definition|
|
|
381
|
+
name = definition.fetch("name")
|
|
382
|
+
ContextPolicyInput::ToolItem.new(
|
|
383
|
+
id: "tool:#{name}",
|
|
384
|
+
definition: definition,
|
|
385
|
+
estimated_tokens: estimate_value(definition),
|
|
386
|
+
required: false,
|
|
387
|
+
provenance: ContextPolicyInput::Provenance.new(origin: :agent_configuration),
|
|
388
|
+
metadata: {}
|
|
389
|
+
)
|
|
390
|
+
end.freeze
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def store_manifest(
|
|
394
|
+
persistence:,
|
|
395
|
+
call_sequence:,
|
|
396
|
+
call_mode:,
|
|
397
|
+
segments:,
|
|
398
|
+
model_config_ref:,
|
|
399
|
+
tool_definitions_ref:
|
|
400
|
+
)
|
|
209
401
|
positioned = segments.each_with_index.map do |value, position|
|
|
210
402
|
LLMInputManifest::Segment.new(**value.merge(position: position))
|
|
211
403
|
end
|
|
@@ -219,7 +411,57 @@ module Phronomy
|
|
|
219
411
|
ruby_llm_version: defined?(RubyLLM::VERSION) ? RubyLLM::VERSION : nil,
|
|
220
412
|
adapter_name: Phronomy.configuration.llm_adapter.class.name
|
|
221
413
|
)
|
|
222
|
-
[manifest,
|
|
414
|
+
[manifest, persistence.contents.put_json(manifest.to_h)]
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def segment_from_content_item(item, persistence:, additional_metadata: {})
|
|
418
|
+
content_ref = item.provenance.content_ref || store_item_content(item, persistence)
|
|
419
|
+
metadata = sanitized_item_metadata(item).merge(additional_metadata).merge(
|
|
420
|
+
CONTENT_FORMAT_METADATA_KEY => item.content_format.to_s,
|
|
421
|
+
POLICY_ITEM_ID_METADATA_KEY => item.id,
|
|
422
|
+
POLICY_ORIGIN_METADATA_KEY => item.provenance.origin.to_s,
|
|
423
|
+
"journal_record_id" => item.provenance.record_id,
|
|
424
|
+
"source_agent_id" => item.provenance.agent_id,
|
|
425
|
+
"source_execution_id" => item.provenance.execution_id,
|
|
426
|
+
"llm_call_id" => item.provenance.llm_call_id
|
|
427
|
+
).compact
|
|
428
|
+
|
|
429
|
+
{
|
|
430
|
+
category: item.kind,
|
|
431
|
+
role: item.role,
|
|
432
|
+
content_ref: content_ref,
|
|
433
|
+
delivery: item.respond_to?(:delivery) ? item.delivery : :chat_message,
|
|
434
|
+
tool_call_id: item.respond_to?(:tool_call_id) ? item.tool_call_id : nil,
|
|
435
|
+
metadata: metadata
|
|
436
|
+
}
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def sanitized_item_metadata(item)
|
|
440
|
+
raw = item.metadata.to_h.transform_keys(&:to_s)
|
|
441
|
+
# :working origin is Phronomy-controlled (from perform_initial_preparation),
|
|
442
|
+
# so handoff routing metadata on working records is equally trusted.
|
|
443
|
+
trusted_handoff = if item.provenance.origin == :handoff || item.provenance.origin == :working
|
|
444
|
+
raw.slice(*TRUSTED_HANDOFF_METADATA_KEYS)
|
|
445
|
+
else
|
|
446
|
+
{}
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
raw.except(*ContextPolicyInput::FRAMEWORK_METADATA_KEYS)
|
|
450
|
+
.merge(trusted_handoff)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def tool_exchange_group?(group)
|
|
454
|
+
Array(group).any? do |item|
|
|
455
|
+
item.kind == :assistant_message && !item.tool_call_ids.empty?
|
|
456
|
+
end && Array(group).any? { |item| item.kind == :tool_message }
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def store_item_content(item, persistence)
|
|
460
|
+
if item.content_format == :json
|
|
461
|
+
persistence.contents.put_json(item.content)
|
|
462
|
+
else
|
|
463
|
+
persistence.contents.put_text(item.content.to_s)
|
|
464
|
+
end
|
|
223
465
|
end
|
|
224
466
|
|
|
225
467
|
def effective_model_config(config, patch)
|
|
@@ -234,8 +476,7 @@ module Phronomy
|
|
|
234
476
|
"max_output_tokens" => @agent.class.max_output_tokens,
|
|
235
477
|
"context_window" => @agent.class.context_window,
|
|
236
478
|
"cache_instructions" => !!@agent.class.cache_instructions,
|
|
237
|
-
"parallel_tool_execution" => !!Phronomy.configuration.parallel_tool_execution
|
|
238
|
-
"thread_id" => config[:thread_id]&.to_s
|
|
479
|
+
"parallel_tool_execution" => !!Phronomy.configuration.parallel_tool_execution
|
|
239
480
|
}.compact
|
|
240
481
|
end
|
|
241
482
|
|
|
@@ -250,28 +491,43 @@ module Phronomy
|
|
|
250
491
|
content = hash.fetch(:content) { hash.fetch("content") }
|
|
251
492
|
category = (hash[:category] || hash["category"] || :knowledge).to_sym
|
|
252
493
|
role = (hash[:role] || hash["role"] || default_role(category)).to_sym
|
|
253
|
-
metadata = hash[:metadata] || hash["metadata"] || {}
|
|
494
|
+
metadata = (hash[:metadata] || hash["metadata"] || {})
|
|
495
|
+
.to_h
|
|
496
|
+
.transform_keys(&:to_s)
|
|
497
|
+
validate_application_metadata!(metadata, source: "LLMInputPatch segment candidate")
|
|
254
498
|
{
|
|
255
|
-
content: content
|
|
499
|
+
content: content,
|
|
256
500
|
category: category,
|
|
257
501
|
role: role,
|
|
258
|
-
metadata: metadata
|
|
502
|
+
metadata: metadata
|
|
259
503
|
}
|
|
260
504
|
end
|
|
261
505
|
end
|
|
262
506
|
|
|
507
|
+
def validate_application_metadata!(metadata, source:)
|
|
508
|
+
conflicts = metadata.keys.map(&:to_s) & ContextPolicyInput::FRAMEWORK_METADATA_KEYS
|
|
509
|
+
return if conflicts.empty?
|
|
510
|
+
|
|
511
|
+
raise ArgumentError,
|
|
512
|
+
"#{source} metadata uses Framework-reserved key(s): #{conflicts.sort.inspect}"
|
|
513
|
+
end
|
|
514
|
+
|
|
263
515
|
def merge_hook_candidates(candidates, hooks, agent_root:, execution:, call_sequence:)
|
|
264
516
|
next_sequence = Array(candidates).filter_map(&:sequence).max.to_i
|
|
265
517
|
generated = hooks.each_with_index.map do |hook, index|
|
|
266
|
-
|
|
518
|
+
content = hook.fetch(:content)
|
|
519
|
+
content_ref = if content.is_a?(String)
|
|
520
|
+
@persistence.contents.put_text(content)
|
|
521
|
+
else
|
|
522
|
+
@persistence.contents.put_json(content)
|
|
523
|
+
end
|
|
267
524
|
metadata = hook.fetch(:metadata).merge(
|
|
268
525
|
SEGMENT_ORIGIN_METADATA_KEY => BEFORE_LLM_INPUT_ORIGIN,
|
|
269
|
-
"estimated_tokens" =>
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
"source_kind" => "hook"
|
|
526
|
+
"estimated_tokens" => estimate_value(content),
|
|
527
|
+
"source_kind" => "hook",
|
|
528
|
+
"content_format" => content.is_a?(String) ? "text" : "json"
|
|
273
529
|
)
|
|
274
|
-
|
|
530
|
+
Selection::Candidate.new(
|
|
275
531
|
candidate_id: "hook:#{execution.execution_id}:#{call_sequence}:#{index}",
|
|
276
532
|
source_kind: :hook,
|
|
277
533
|
category: hook.fetch(:category),
|
|
@@ -283,7 +539,7 @@ module Phronomy
|
|
|
283
539
|
llm_call_id: nil,
|
|
284
540
|
tool_call_id: nil,
|
|
285
541
|
sequence: next_sequence + index + 1,
|
|
286
|
-
|
|
542
|
+
constraint: Selection::Constraint.selectable(origin: :context_policy),
|
|
287
543
|
priority: 0,
|
|
288
544
|
metadata: metadata
|
|
289
545
|
)
|
|
@@ -293,70 +549,73 @@ module Phronomy
|
|
|
293
549
|
.freeze
|
|
294
550
|
end
|
|
295
551
|
|
|
296
|
-
def
|
|
297
|
-
candidates.
|
|
298
|
-
early = EARLY_CONTEXT_CATEGORIES.include?(candidate.category)
|
|
299
|
-
next unless early == before_history
|
|
552
|
+
def merge_handoff_candidates(candidates, handoff_context, execution:)
|
|
553
|
+
return Array(candidates).freeze unless handoff_context
|
|
300
554
|
|
|
301
|
-
|
|
555
|
+
unless handoff_context.is_a?(Phronomy::MultiAgent::HandoffContext)
|
|
556
|
+
raise ArgumentError, "phronomy_handoff_context must be a HandoffContext"
|
|
302
557
|
end
|
|
303
|
-
end
|
|
304
558
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
559
|
+
next_sequence = Array(candidates).filter_map(&:sequence).max.to_i
|
|
560
|
+
transferred = handoff_context.items.each_with_index.map do |item, index|
|
|
561
|
+
content_ref = if item.content_format == :json
|
|
562
|
+
@persistence.contents.put_json(item.content)
|
|
563
|
+
else
|
|
564
|
+
@persistence.contents.put_text(item.content.to_s)
|
|
565
|
+
end
|
|
566
|
+
metadata = item.metadata.merge(
|
|
567
|
+
SEGMENT_ORIGIN_METADATA_KEY => HANDOFF_CONTEXT_ORIGIN,
|
|
568
|
+
"estimated_tokens" => estimate_value(item.content),
|
|
569
|
+
"source_kind" => "handoff",
|
|
570
|
+
"content_format" => item.content_format.to_s,
|
|
571
|
+
"handoff_policy_category" => item.policy_category.to_s,
|
|
572
|
+
"handoff_provenance" => item.provenance.to_h
|
|
573
|
+
)
|
|
308
574
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
575
|
+
Selection::Candidate.new(
|
|
576
|
+
candidate_id: "handoff:#{execution.execution_id}:#{index}:#{item.provenance.origin_record_id || item.provenance.origin_tool_call_id || index}",
|
|
577
|
+
source_kind: :handoff,
|
|
578
|
+
category: item.candidate_category,
|
|
579
|
+
role: item.role,
|
|
580
|
+
content_ref: content_ref,
|
|
581
|
+
record_id: nil,
|
|
582
|
+
agent_id: item.provenance.origin_agent_id,
|
|
583
|
+
execution_id: execution.execution_id,
|
|
584
|
+
llm_call_id: item.provenance.origin_llm_call_id,
|
|
585
|
+
tool_call_id: item.tool_call_id,
|
|
586
|
+
sequence: next_sequence + index + 1,
|
|
587
|
+
constraint: Selection::Constraint.selectable(origin: :handoff_context),
|
|
588
|
+
priority: 50,
|
|
589
|
+
metadata: metadata
|
|
590
|
+
)
|
|
591
|
+
end
|
|
315
592
|
|
|
316
|
-
|
|
317
|
-
|
|
593
|
+
(Array(candidates) + transferred)
|
|
594
|
+
.sort_by { |candidate| [candidate.sequence || 0, candidate.candidate_id] }
|
|
595
|
+
.freeze
|
|
318
596
|
end
|
|
319
597
|
|
|
320
|
-
def
|
|
321
|
-
|
|
322
|
-
%w[estimated_tokens source_kind source_sequence].include?(key.to_s)
|
|
323
|
-
end
|
|
324
|
-
metadata = metadata.merge(
|
|
325
|
-
"journal_record_id" => candidate.record_id,
|
|
326
|
-
"journal_sequence" => candidate.metadata["source_sequence"],
|
|
327
|
-
"llm_call_id" => candidate.llm_call_id
|
|
328
|
-
).compact
|
|
329
|
-
segment(
|
|
330
|
-
candidate.category,
|
|
331
|
-
candidate.role,
|
|
332
|
-
candidate.content_ref,
|
|
333
|
-
:chat_message,
|
|
334
|
-
tool_call_id: candidate.tool_call_id,
|
|
335
|
-
metadata: metadata
|
|
336
|
-
)
|
|
598
|
+
def default_role(category)
|
|
599
|
+
(category == :instruction) ? :system : :user
|
|
337
600
|
end
|
|
338
601
|
|
|
339
|
-
def
|
|
340
|
-
|
|
341
|
-
category: existing.category, role: existing.role,
|
|
342
|
-
content_ref: existing.content_ref, delivery: existing.delivery,
|
|
343
|
-
tool_call_id: existing.tool_call_id, metadata: existing.metadata
|
|
344
|
-
}
|
|
602
|
+
def handoff_tool_classes(config)
|
|
603
|
+
Array(config[:phronomy_handoff_bindings]).map(&:tool_class).freeze
|
|
345
604
|
end
|
|
346
605
|
|
|
347
|
-
def
|
|
348
|
-
|
|
349
|
-
|
|
606
|
+
def estimate_value(value)
|
|
607
|
+
bytes = value.is_a?(String) ? value : Phronomy::CanonicalJSON.dump(value)
|
|
608
|
+
Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes)
|
|
350
609
|
end
|
|
351
610
|
|
|
352
|
-
def
|
|
353
|
-
|
|
611
|
+
def fetch_content(ref)
|
|
612
|
+
fetch_content_from(@persistence, ref)
|
|
354
613
|
end
|
|
355
614
|
|
|
356
|
-
def
|
|
357
|
-
|
|
615
|
+
def fetch_content_from(persistence, ref)
|
|
616
|
+
persistence.contents.fetch_text(ref)
|
|
358
617
|
rescue
|
|
359
|
-
|
|
618
|
+
persistence.contents.fetch(ref)
|
|
360
619
|
end
|
|
361
620
|
|
|
362
621
|
def build_system_text(input)
|