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
|
@@ -13,7 +13,7 @@ module Phronomy
|
|
|
13
13
|
class JournalRecord
|
|
14
14
|
ATTRIBUTES = %i[
|
|
15
15
|
record_id agent_id sequence execution_id llm_call_id kind channel role content_ref
|
|
16
|
-
parent_id causation_id
|
|
16
|
+
parent_id causation_id visibility context_generation
|
|
17
17
|
context_candidate occurred_at metadata
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
@@ -31,7 +31,6 @@ module Phronomy
|
|
|
31
31
|
content_ref: nil,
|
|
32
32
|
parent_id: nil,
|
|
33
33
|
causation_id: nil,
|
|
34
|
-
correlation_id: nil,
|
|
35
34
|
visibility: :agent,
|
|
36
35
|
context_generation: 0,
|
|
37
36
|
context_candidate: false,
|
|
@@ -52,14 +51,28 @@ module Phronomy
|
|
|
52
51
|
self.class.from_h(to_h.merge("sequence" => sequence))
|
|
53
52
|
end
|
|
54
53
|
|
|
54
|
+
# Current semantic payload representation. Persistence format identity and
|
|
55
|
+
# compatibility validation are owned by Persistence::DurableCodec.
|
|
55
56
|
def to_h
|
|
56
57
|
ATTRIBUTES.to_h { |name| [name.to_s, public_send(name)] }
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
# Historical durable fields are not accepted here. Explicit migration must
|
|
61
|
+
# convert them to the current semantic payload first.
|
|
59
62
|
def self.from_h(hash)
|
|
63
|
+
source = hash.to_h { |key, value| [key.to_s, value] }
|
|
64
|
+
expected = ATTRIBUTES.map(&:to_s).sort
|
|
65
|
+
actual = source.keys.sort
|
|
66
|
+
unless actual == expected
|
|
67
|
+
missing = expected - actual
|
|
68
|
+
unknown = actual - expected
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"JournalRecord payload schema mismatch: " \
|
|
71
|
+
"missing=#{missing.inspect}, unknown=#{unknown.inspect}"
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
new(**ATTRIBUTES.to_h do |name|
|
|
61
|
-
|
|
62
|
-
[name, hash[key]]
|
|
75
|
+
[name, source.fetch(name.to_s)]
|
|
63
76
|
end)
|
|
64
77
|
end
|
|
65
78
|
end
|
|
@@ -5,7 +5,7 @@ module Phronomy
|
|
|
5
5
|
# Immutable metadata passed to before_llm_input hooks. Runtime objects such
|
|
6
6
|
# as RubyLLM::Chat, messages and the mutable Agent instance are not exposed.
|
|
7
7
|
LLMInputBuildContext = Data.define(
|
|
8
|
-
:agent_id, :agent_definition_id, :
|
|
8
|
+
:agent_id, :agent_definition_id, :agent_definition_version,
|
|
9
9
|
:config, :call_sequence
|
|
10
10
|
) do
|
|
11
11
|
def initialize(**values)
|
|
@@ -2,19 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
+
# ContentStore-backed durable codec boundary for one Provider input manifest.
|
|
6
|
+
# It is intentionally separate from Persistence::DurableRecord.
|
|
5
7
|
class LLMInputManifest
|
|
6
|
-
VERSION = 1
|
|
8
|
+
VERSION = "0.1"
|
|
7
9
|
CALL_MODES = %i[ask complete].freeze
|
|
10
|
+
SEGMENT_DELIVERIES = %w[ask_argument chat_message].freeze
|
|
8
11
|
|
|
9
12
|
Segment = Data.define(
|
|
10
13
|
:position, :category, :role, :content_ref, :delivery,
|
|
11
14
|
:tool_call_id, :metadata
|
|
12
15
|
) do
|
|
13
16
|
def initialize(**values)
|
|
17
|
+
delivery = values[:delivery]
|
|
18
|
+
if delivery.nil? || !SEGMENT_DELIVERIES.include?(delivery.to_s)
|
|
19
|
+
raise ArgumentError, "unknown Segment delivery: #{delivery.inspect}"
|
|
20
|
+
end
|
|
14
21
|
super(**values.merge(metadata: Immutable.copy(values[:metadata] || {})))
|
|
15
22
|
freeze
|
|
16
23
|
end
|
|
17
24
|
|
|
25
|
+
def self.from_h(hash)
|
|
26
|
+
source = LLMInputManifest.send(
|
|
27
|
+
:strict_source!,
|
|
28
|
+
hash,
|
|
29
|
+
required: %w[position category content_ref delivery metadata],
|
|
30
|
+
optional: %w[role tool_call_id],
|
|
31
|
+
label: "LLMInputManifest segment"
|
|
32
|
+
)
|
|
33
|
+
label = "LLMInputManifest segment"
|
|
34
|
+
position = LLMInputManifest.send(
|
|
35
|
+
:require_nonnegative_integer_field!,
|
|
36
|
+
source,
|
|
37
|
+
"position",
|
|
38
|
+
label: label
|
|
39
|
+
)
|
|
40
|
+
category = LLMInputManifest.send(
|
|
41
|
+
:require_nonempty_string_field!,
|
|
42
|
+
source,
|
|
43
|
+
"category",
|
|
44
|
+
label: label
|
|
45
|
+
)
|
|
46
|
+
role = LLMInputManifest.send(
|
|
47
|
+
:require_optional_string_field!,
|
|
48
|
+
source,
|
|
49
|
+
"role",
|
|
50
|
+
label: label
|
|
51
|
+
)
|
|
52
|
+
content_ref = LLMInputManifest.send(
|
|
53
|
+
:require_nonempty_string_field!,
|
|
54
|
+
source,
|
|
55
|
+
"content_ref",
|
|
56
|
+
label: label
|
|
57
|
+
)
|
|
58
|
+
delivery = LLMInputManifest.send(
|
|
59
|
+
:require_nonempty_string_field!,
|
|
60
|
+
source,
|
|
61
|
+
"delivery",
|
|
62
|
+
label: label
|
|
63
|
+
)
|
|
64
|
+
unless SEGMENT_DELIVERIES.include?(delivery)
|
|
65
|
+
raise Phronomy::Persistence::SerializationError,
|
|
66
|
+
"LLMInputManifest segment delivery must be one of #{SEGMENT_DELIVERIES.inspect}, got #{delivery.inspect}"
|
|
67
|
+
end
|
|
68
|
+
tool_call_id = LLMInputManifest.send(
|
|
69
|
+
:require_optional_string_field!,
|
|
70
|
+
source,
|
|
71
|
+
"tool_call_id",
|
|
72
|
+
label: label
|
|
73
|
+
)
|
|
74
|
+
metadata = LLMInputManifest.send(
|
|
75
|
+
:require_hash_field!,
|
|
76
|
+
source,
|
|
77
|
+
"metadata",
|
|
78
|
+
label: label
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
new(
|
|
82
|
+
position: position,
|
|
83
|
+
category: category.to_sym,
|
|
84
|
+
role: role&.to_sym,
|
|
85
|
+
content_ref: content_ref,
|
|
86
|
+
delivery: delivery.to_sym,
|
|
87
|
+
tool_call_id: tool_call_id,
|
|
88
|
+
metadata: metadata
|
|
89
|
+
)
|
|
90
|
+
rescue Phronomy::Persistence::SerializationError
|
|
91
|
+
raise
|
|
92
|
+
rescue => error
|
|
93
|
+
raise Phronomy::Persistence::SerializationError,
|
|
94
|
+
"invalid LLMInputManifest segment: #{error.class}: #{error.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
18
97
|
def to_h
|
|
19
98
|
{
|
|
20
99
|
"position" => position,
|
|
@@ -28,6 +107,15 @@ module Phronomy
|
|
|
28
107
|
end
|
|
29
108
|
end
|
|
30
109
|
|
|
110
|
+
REQUIRED_KEYS = %w[
|
|
111
|
+
version call_sequence call_mode assembly_policy_version segments
|
|
112
|
+
model_config_ref
|
|
113
|
+
].freeze
|
|
114
|
+
OPTIONAL_KEYS = %w[
|
|
115
|
+
tool_definitions_ref response_schema_ref ruby_llm_version
|
|
116
|
+
adapter_name adapter_version
|
|
117
|
+
].freeze
|
|
118
|
+
|
|
31
119
|
attr_reader :version, :call_sequence, :call_mode,
|
|
32
120
|
:assembly_policy_version, :segments,
|
|
33
121
|
:model_config_ref, :tool_definitions_ref, :response_schema_ref,
|
|
@@ -46,7 +134,7 @@ module Phronomy
|
|
|
46
134
|
adapter_version: nil,
|
|
47
135
|
version: VERSION
|
|
48
136
|
)
|
|
49
|
-
@version =
|
|
137
|
+
@version = String(version).freeze
|
|
50
138
|
@call_sequence = Integer(call_sequence)
|
|
51
139
|
@call_mode = call_mode.to_sym
|
|
52
140
|
@assembly_policy_version = Integer(assembly_policy_version)
|
|
@@ -61,6 +149,93 @@ module Phronomy
|
|
|
61
149
|
freeze
|
|
62
150
|
end
|
|
63
151
|
|
|
152
|
+
# Current-format-only durable decoder. Historical format conversion is an
|
|
153
|
+
# explicit migration operation and is never attempted here. Unlike the
|
|
154
|
+
# Ruby construction API, this decoder never coerces durable field types.
|
|
155
|
+
def self.from_h(hash)
|
|
156
|
+
source = strict_source!(
|
|
157
|
+
hash,
|
|
158
|
+
required: REQUIRED_KEYS,
|
|
159
|
+
optional: OPTIONAL_KEYS,
|
|
160
|
+
label: "LLMInputManifest"
|
|
161
|
+
)
|
|
162
|
+
label = "LLMInputManifest"
|
|
163
|
+
version = source.fetch("version")
|
|
164
|
+
unless version.is_a?(String) && version == VERSION
|
|
165
|
+
raise Phronomy::Persistence::SerializationError,
|
|
166
|
+
"unsupported LLMInputManifest version: #{version.inspect}; " \
|
|
167
|
+
"current version is #{VERSION.inspect}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
call_sequence = require_positive_integer_field!(
|
|
171
|
+
source,
|
|
172
|
+
"call_sequence",
|
|
173
|
+
label: label
|
|
174
|
+
)
|
|
175
|
+
call_mode = require_enum_string_field!(
|
|
176
|
+
source,
|
|
177
|
+
"call_mode",
|
|
178
|
+
CALL_MODES.map(&:to_s),
|
|
179
|
+
label: label
|
|
180
|
+
)
|
|
181
|
+
assembly_policy_version = require_integer_field!(
|
|
182
|
+
source,
|
|
183
|
+
"assembly_policy_version",
|
|
184
|
+
label: label
|
|
185
|
+
)
|
|
186
|
+
segments = require_array_field!(source, "segments", label: label)
|
|
187
|
+
.map { |segment| Segment.from_h(segment) }
|
|
188
|
+
model_config_ref = require_nonempty_string_field!(
|
|
189
|
+
source,
|
|
190
|
+
"model_config_ref",
|
|
191
|
+
label: label
|
|
192
|
+
)
|
|
193
|
+
tool_definitions_ref = require_optional_string_field!(
|
|
194
|
+
source,
|
|
195
|
+
"tool_definitions_ref",
|
|
196
|
+
label: label
|
|
197
|
+
)
|
|
198
|
+
response_schema_ref = require_optional_string_field!(
|
|
199
|
+
source,
|
|
200
|
+
"response_schema_ref",
|
|
201
|
+
label: label
|
|
202
|
+
)
|
|
203
|
+
ruby_llm_version = require_optional_string_field!(
|
|
204
|
+
source,
|
|
205
|
+
"ruby_llm_version",
|
|
206
|
+
label: label
|
|
207
|
+
)
|
|
208
|
+
adapter_name = require_optional_string_field!(
|
|
209
|
+
source,
|
|
210
|
+
"adapter_name",
|
|
211
|
+
label: label
|
|
212
|
+
)
|
|
213
|
+
adapter_version = require_optional_string_field!(
|
|
214
|
+
source,
|
|
215
|
+
"adapter_version",
|
|
216
|
+
label: label
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
new(
|
|
220
|
+
version: version,
|
|
221
|
+
call_sequence: call_sequence,
|
|
222
|
+
call_mode: call_mode.to_sym,
|
|
223
|
+
assembly_policy_version: assembly_policy_version,
|
|
224
|
+
segments: segments,
|
|
225
|
+
model_config_ref: model_config_ref,
|
|
226
|
+
tool_definitions_ref: tool_definitions_ref,
|
|
227
|
+
response_schema_ref: response_schema_ref,
|
|
228
|
+
ruby_llm_version: ruby_llm_version,
|
|
229
|
+
adapter_name: adapter_name,
|
|
230
|
+
adapter_version: adapter_version
|
|
231
|
+
)
|
|
232
|
+
rescue Phronomy::Persistence::SerializationError
|
|
233
|
+
raise
|
|
234
|
+
rescue => error
|
|
235
|
+
raise Phronomy::Persistence::SerializationError,
|
|
236
|
+
"invalid LLMInputManifest: #{error.class}: #{error.message}"
|
|
237
|
+
end
|
|
238
|
+
|
|
64
239
|
def referenced_content_refs
|
|
65
240
|
([model_config_ref, tool_definitions_ref, response_schema_ref] +
|
|
66
241
|
segments.map(&:content_ref)).compact.uniq.freeze
|
|
@@ -82,9 +257,109 @@ module Phronomy
|
|
|
82
257
|
}.compact
|
|
83
258
|
end
|
|
84
259
|
|
|
260
|
+
class << self
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def strict_source!(hash, required:, optional:, label:)
|
|
264
|
+
unless hash.is_a?(Hash)
|
|
265
|
+
raise Phronomy::Persistence::SerializationError,
|
|
266
|
+
"#{label} must be a Hash"
|
|
267
|
+
end
|
|
268
|
+
unless hash.keys.all? { |key| key.is_a?(String) }
|
|
269
|
+
raise Phronomy::Persistence::SerializationError,
|
|
270
|
+
"#{label} keys must all be String"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
actual = hash.keys
|
|
274
|
+
missing = required - actual
|
|
275
|
+
unknown = actual - (required + optional)
|
|
276
|
+
unless missing.empty? && unknown.empty?
|
|
277
|
+
details = []
|
|
278
|
+
details << "missing=#{missing.inspect}" unless missing.empty?
|
|
279
|
+
details << "unknown=#{unknown.inspect}" unless unknown.empty?
|
|
280
|
+
raise Phronomy::Persistence::SerializationError,
|
|
281
|
+
"#{label} schema mismatch (#{details.join(", ")})"
|
|
282
|
+
end
|
|
283
|
+
Phronomy::CanonicalJSON.dump(hash)
|
|
284
|
+
hash
|
|
285
|
+
rescue ArgumentError => error
|
|
286
|
+
raise Phronomy::Persistence::SerializationError,
|
|
287
|
+
"#{label} is not canonical JSON compatible: #{error.message}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def require_integer_field!(hash, key, label:)
|
|
291
|
+
value = hash.fetch(key)
|
|
292
|
+
return value if value.is_a?(Integer)
|
|
293
|
+
|
|
294
|
+
raise Phronomy::Persistence::SerializationError,
|
|
295
|
+
"#{label} #{key} must be an Integer"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def require_positive_integer_field!(hash, key, label:)
|
|
299
|
+
value = hash.fetch(key)
|
|
300
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
301
|
+
|
|
302
|
+
raise Phronomy::Persistence::SerializationError,
|
|
303
|
+
"#{label} #{key} must be a positive Integer"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def require_nonnegative_integer_field!(hash, key, label:)
|
|
307
|
+
value = hash.fetch(key)
|
|
308
|
+
return value if value.is_a?(Integer) && value >= 0
|
|
309
|
+
|
|
310
|
+
raise Phronomy::Persistence::SerializationError,
|
|
311
|
+
"#{label} #{key} must be a non-negative Integer"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def require_nonempty_string_field!(hash, key, label:)
|
|
315
|
+
value = hash.fetch(key)
|
|
316
|
+
return value if value.is_a?(String) && !value.empty?
|
|
317
|
+
|
|
318
|
+
raise Phronomy::Persistence::SerializationError,
|
|
319
|
+
"#{label} #{key} must be a non-empty String"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def require_optional_string_field!(hash, key, label:)
|
|
323
|
+
value = hash[key]
|
|
324
|
+
return value if value.nil? || value.is_a?(String)
|
|
325
|
+
|
|
326
|
+
raise Phronomy::Persistence::SerializationError,
|
|
327
|
+
"#{label} #{key} must be a String or nil"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def require_enum_string_field!(hash, key, allowed, label:)
|
|
331
|
+
value = hash.fetch(key)
|
|
332
|
+
return value if value.is_a?(String) && allowed.include?(value)
|
|
333
|
+
|
|
334
|
+
raise Phronomy::Persistence::SerializationError,
|
|
335
|
+
"#{label} #{key} must be one of #{allowed.inspect}"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def require_array_field!(hash, key, label:)
|
|
339
|
+
value = hash.fetch(key)
|
|
340
|
+
return value if value.is_a?(Array)
|
|
341
|
+
|
|
342
|
+
raise Phronomy::Persistence::SerializationError,
|
|
343
|
+
"#{label} #{key} must be an Array"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def require_hash_field!(hash, key, label:)
|
|
347
|
+
value = hash.fetch(key)
|
|
348
|
+
return value if value.is_a?(Hash)
|
|
349
|
+
|
|
350
|
+
raise Phronomy::Persistence::SerializationError,
|
|
351
|
+
"#{label} #{key} must be a Hash"
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
85
355
|
private
|
|
86
356
|
|
|
87
357
|
def validate!
|
|
358
|
+
unless version == VERSION
|
|
359
|
+
raise Phronomy::ConfigurationError,
|
|
360
|
+
"unsupported LLMInputManifest version: #{version.inspect}; " \
|
|
361
|
+
"current version is #{VERSION.inspect}"
|
|
362
|
+
end
|
|
88
363
|
raise ArgumentError, "invalid manifest call mode: #{call_mode.inspect}" unless CALL_MODES.include?(call_mode)
|
|
89
364
|
raise ArgumentError, "call_sequence must be positive" unless call_sequence.positive?
|
|
90
365
|
expected = (0...segments.length).to_a
|
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
# Immutable carrier for one
|
|
5
|
+
# Immutable carrier for one Provider operation outcome.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# The Provider Call identity is allocated on EventLoop before transport starts.
|
|
8
|
+
# Worker completion carries that semantic identity back to the owning
|
|
9
|
+
# FSMSession, which validates it against the currently active call before
|
|
10
|
+
# applying any live state.
|
|
10
11
|
#
|
|
11
12
|
# @api private
|
|
12
13
|
class LLMOperationResult
|
|
13
|
-
attr_reader :response, :error, :streaming
|
|
14
|
+
attr_reader :llm_call_id, :response, :error, :streaming
|
|
14
15
|
|
|
15
|
-
def initialize(response: nil, error: nil, streaming: false)
|
|
16
|
+
def initialize(llm_call_id:, response: nil, error: nil, streaming: false)
|
|
17
|
+
id = llm_call_id&.to_s
|
|
18
|
+
raise ArgumentError, "LLMOperationResult requires llm_call_id" if id.nil? || id.empty?
|
|
19
|
+
|
|
20
|
+
@llm_call_id = id.freeze
|
|
16
21
|
@response = response
|
|
17
22
|
@error = error
|
|
18
|
-
@streaming = streaming
|
|
23
|
+
@streaming = !!streaming
|
|
19
24
|
freeze
|
|
20
25
|
end
|
|
21
26
|
end
|
|
@@ -4,12 +4,6 @@ require "state_machines"
|
|
|
4
4
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
module Agent
|
|
7
|
-
# Compiles AgentInvocation phase topology.
|
|
8
|
-
#
|
|
9
|
-
# Async completion is represented by explicit FSM events. This builder does
|
|
10
|
-
# not await Tasks or register Task callbacks.
|
|
11
|
-
#
|
|
12
|
-
# @api private
|
|
13
7
|
class PhaseMachineBuilder
|
|
14
8
|
TOOL_EVENTS = %i[
|
|
15
9
|
tool_authorized
|
|
@@ -43,6 +37,7 @@ module Phronomy
|
|
|
43
37
|
state :recording_tool_results
|
|
44
38
|
state :suspended
|
|
45
39
|
state :output_filtering
|
|
40
|
+
state :handed_off
|
|
46
41
|
state :completed
|
|
47
42
|
state :blocked
|
|
48
43
|
state :failed
|
|
@@ -70,7 +65,6 @@ module Phronomy
|
|
|
70
65
|
if: ->(machine) { machine.context&.ready_to_dispatch? }
|
|
71
66
|
transition evaluating_tools: :waiting_for_tools
|
|
72
67
|
|
|
73
|
-
transition dispatching_tools: :evaluating_tools
|
|
74
68
|
transition recording_tool_results: :calling_llm
|
|
75
69
|
|
|
76
70
|
transition output_filtering: :completed,
|
|
@@ -80,6 +74,12 @@ module Phronomy
|
|
|
80
74
|
end
|
|
81
75
|
|
|
82
76
|
event :llm_completed do
|
|
77
|
+
transition calling_llm: :failed,
|
|
78
|
+
if: ->(machine) { machine.context&.callback_failed? }
|
|
79
|
+
transition calling_llm: :failed,
|
|
80
|
+
if: ->(machine) { machine.context&.handoff_failed? }
|
|
81
|
+
transition calling_llm: :handed_off,
|
|
82
|
+
if: ->(machine) { machine.context&.handoff_requested? }
|
|
83
83
|
transition calling_llm: :starting_tools,
|
|
84
84
|
if: ->(machine) { machine.context&.tool_call_pending? }
|
|
85
85
|
transition calling_llm: :output_filtering
|
|
@@ -89,6 +89,18 @@ module Phronomy
|
|
|
89
89
|
transition calling_llm: :failed
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
event :llm_setup_failed do
|
|
93
|
+
transition calling_llm: :failed
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
event :tool_setup_failed do
|
|
97
|
+
transition dispatching_tools: :failed
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
event :tool_dispatch_prepared do
|
|
101
|
+
transition dispatching_tools: :evaluating_tools
|
|
102
|
+
end
|
|
103
|
+
|
|
92
104
|
TOOL_EVENTS.each do |event_name|
|
|
93
105
|
event event_name do
|
|
94
106
|
transition waiting_for_tools: :evaluating_tools
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
5
|
# Immutable, Phronomy-owned snapshot of one completed Provider assistant output.
|
|
6
|
-
# It is captured before Agent-owned Tool execution begins, so durable logging
|
|
7
|
-
# never depends on RubyLLM's later control flow or Application callbacks.
|
|
8
6
|
ProviderCallOutcome = Data.define(:role, :content, :tool_calls, :usage, :metadata) do
|
|
9
7
|
def self.capture(message)
|
|
10
8
|
return if message.nil?
|
|
9
|
+
return message if message.is_a?(self)
|
|
11
10
|
|
|
12
11
|
calls = if message.respond_to?(:tool_calls) && message.tool_calls
|
|
13
12
|
source = message.tool_calls.respond_to?(:values) ? message.tool_calls.values : Array(message.tool_calls)
|
|
@@ -32,6 +31,17 @@ module Phronomy
|
|
|
32
31
|
)
|
|
33
32
|
end
|
|
34
33
|
|
|
34
|
+
def self.from_h(hash)
|
|
35
|
+
source = hash.to_h { |key, value| [key.to_s, value] }
|
|
36
|
+
new(
|
|
37
|
+
role: source.fetch("role", "assistant"),
|
|
38
|
+
content: source["content"],
|
|
39
|
+
tool_calls: source.fetch("tool_calls", []),
|
|
40
|
+
usage: source.fetch("usage", {}),
|
|
41
|
+
metadata: source.fetch("metadata", {})
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
def self.tool_call_hash(call)
|
|
36
46
|
return call.to_h if call.respond_to?(:to_h)
|
|
37
47
|
|
|
@@ -54,11 +64,7 @@ module Phronomy
|
|
|
54
64
|
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
55
65
|
value
|
|
56
66
|
else
|
|
57
|
-
|
|
58
|
-
normalize(value.to_h)
|
|
59
|
-
else
|
|
60
|
-
value.to_s
|
|
61
|
-
end
|
|
67
|
+
value.respond_to?(:to_h) ? normalize(value.to_h) : value.to_s
|
|
62
68
|
end
|
|
63
69
|
end
|
|
64
70
|
private_class_method :normalize
|
|
@@ -78,6 +84,16 @@ module Phronomy
|
|
|
78
84
|
freeze
|
|
79
85
|
end
|
|
80
86
|
|
|
87
|
+
def to_h
|
|
88
|
+
{
|
|
89
|
+
"role" => role&.to_s,
|
|
90
|
+
"content" => content,
|
|
91
|
+
"tool_calls" => tool_calls,
|
|
92
|
+
"usage" => usage,
|
|
93
|
+
"metadata" => metadata
|
|
94
|
+
}.compact
|
|
95
|
+
end
|
|
96
|
+
|
|
81
97
|
def content_present?
|
|
82
98
|
!content.nil? && !(content.respond_to?(:empty?) && content.empty?)
|
|
83
99
|
end
|