phronomy 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -1
- data/README.md +32 -6
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +8 -0
- data/docs/architecture/agent-iteration-budget.md +81 -0
- data/docs/architecture/dx05-agent-scope.md +126 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/architecture/generator-verifier-lifecycle-result.md +126 -0
- data/docs/architecture/generator-verifier-ownership.md +18 -109
- data/docs/architecture/llm-continuation-metadata.md +88 -0
- data/docs/architecture/lm-studio-output-failures.md +111 -0
- data/docs/architecture/r8-unit15.md +3 -0
- data/docs/architecture/r8-unit7.md +2 -1
- data/docs/architecture/structured-output-and-scope.md +183 -0
- data/docs/architecture/tracing.md +22 -4
- data/docs/architecture/transport-error-boundaries.md +103 -0
- data/docs/architecture/trust-acceptance-followup.md +87 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +14 -2
- data/docs/getting-started.md +44 -43
- data/docs/migrations/r8-unit15.md +3 -0
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +10 -0
- data/lib/phronomy/agent/admission.rb +1 -1
- data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
- data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
- data/lib/phronomy/agent/api/agent.rb +13 -0
- data/lib/phronomy/agent/async_event_api.rb +121 -88
- data/lib/phronomy/agent/base.rb +82 -44
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/context_importer.rb +4 -0
- data/lib/phronomy/agent/execution/agent_invocation.rb +6 -2
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +6 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +20 -9
- data/lib/phronomy/agent/execution/execution_cancellation.rb +1 -1
- data/lib/phronomy/agent/execution/execution_coordinator.rb +79 -18
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +24 -4
- data/lib/phronomy/agent/execution/execution_registry.rb +5 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +11 -5
- data/lib/phronomy/agent/execution/invocation_actions.rb +1 -1
- data/lib/phronomy/agent/execution/invocation_transitions.rb +4 -4
- data/lib/phronomy/agent/execution/iteration_budget.rb +35 -0
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +4 -4
- data/lib/phronomy/agent/execution_observation.rb +5 -5
- data/lib/phronomy/agent/filterable.rb +21 -15
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
- data/lib/phronomy/agent/lifecycle/scope.rb +208 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +21 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/output_contract.rb +64 -0
- data/lib/phronomy/agent/output_error.rb +53 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +6 -6
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +8 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +6 -10
- data/lib/phronomy/agent/recovery/recovery_support.rb +6 -8
- data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -1
- data/lib/phronomy/agent/runtime_input.rb +15 -4
- data/lib/phronomy/agent/saved_context_reader.rb +2 -1
- data/lib/phronomy/agent/store.rb +10 -2
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/common/values/immutable.rb +33 -0
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +18 -22
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +8 -2
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- data/lib/phronomy/engine/fsm_session.rb +16 -1
- data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
- data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
- data/lib/phronomy/execution/task_result.rb +7 -2
- data/lib/phronomy/filter/filter_block_error.rb +2 -2
- data/lib/phronomy/generation/generator_verifier/agent_operation.rb +28 -5
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +14 -27
- data/lib/phronomy/generation/generator_verifier/approval.rb +30 -0
- data/lib/phronomy/generation/generator_verifier/cleanup_error.rb +15 -0
- data/lib/phronomy/generation/generator_verifier/invocation.rb +147 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +16 -0
- data/lib/phronomy/generation/generator_verifier/protocol.rb +55 -0
- data/lib/phronomy/generation/generator_verifier/result.rb +121 -0
- data/lib/phronomy/generation/generator_verifier/unaccepted_result_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +13 -11
- data/lib/phronomy/generation/generator_verifier.rb +170 -85
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/llm_adapter/backends/lm_studio.rb +139 -0
- data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +134 -12
- data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +41 -0
- data/lib/phronomy/llm_adapter/message.rb +4 -0
- data/lib/phronomy/llm_adapter/request.rb +17 -2
- data/lib/phronomy/llm_adapter/response.rb +11 -4
- data/lib/phronomy/llm_adapter/structured_output_unsupported_error.rb +14 -0
- data/lib/phronomy/llm_adapter/transport_error.rb +4 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- data/lib/phronomy/multi_agent/shared_state.rb +1 -0
- data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
- data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
- data/lib/phronomy/output_parser/json_schema_parser.rb +113 -0
- data/lib/phronomy/output_parser/parse_error.rb +10 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +4 -0
- data/lib/phronomy/storage/condition_failed_error.rb +2 -2
- data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
- data/lib/phronomy/tool/base.rb +25 -23
- data/lib/phronomy/tracing/automatic.rb +14 -8
- data/lib/phronomy/tracing/base.rb +37 -7
- data/lib/phronomy/tracing/langfuse_tracer.rb +3 -3
- data/lib/phronomy/tracing/observation.rb +2 -1
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +7 -32
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +30 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
- data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
- data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
- data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
- data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
- data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
- data/lib/phronomy.rb +7 -2
- data/scripts/api_snapshot.rb +15 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- data/scripts/migrate_generator_verifier_result.rb +18 -0
- data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
- data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
- data/sig/phronomy/agent/execution_contract.rbs +4 -3
- data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +36 -5
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +8 -2
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/generator_verifier.rbs +70 -0
- data/sig/phronomy/integrations.rbs +14 -0
- data/sig/phronomy/llm_adapter/backends/lm_studio.rbs +8 -0
- data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +1 -0
- data/sig/phronomy/llm_adapter/values.rbs +8 -2
- data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
- data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
- data/sig/phronomy/storage/contracts.rbs +2 -2
- data/sig/phronomy/tracing/observation.rbs +1 -1
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +6 -0
- metadata +103 -5
- data/lib/phronomy/generation/generator_verifier/default_parser.rb +0 -27
- data/lib/phronomy/generation/low_confidence_error.rb +0 -14
- data/lib/phronomy/runtime_composition/generation_defaults.rb +0 -6
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Agent-owned two-phase release. The worker only checks durable facts; the
|
|
6
|
+
# EventLoop completes the exact process-local reservation after physical I/O.
|
|
7
|
+
# @api private
|
|
8
|
+
class UnloadTransition
|
|
9
|
+
Command = Data.define(:coordinator, :phase)
|
|
10
|
+
private_constant :Command
|
|
11
|
+
|
|
12
|
+
attr_reader :result
|
|
13
|
+
|
|
14
|
+
def initialize(agent:, environment:, ownership:)
|
|
15
|
+
@agent, @environment, @ownership = agent, environment, ownership
|
|
16
|
+
@result = Phronomy::TaskResult.deferred(name: "agent-unload:#{agent.agent_id}")
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@finished = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
post(:check)
|
|
23
|
+
@result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def deliver_on_event_loop(command)
|
|
27
|
+
if command.phase == :check
|
|
28
|
+
if @environment.existing_registry&.agent_execution_admitted?(@agent.agent_id)
|
|
29
|
+
raise Phronomy::AgentBusyError, "Agent #{@agent.agent_id.inspect} has a nonterminal execution"
|
|
30
|
+
end
|
|
31
|
+
agent_id, persistence = @agent.agent_id, @agent.persistence
|
|
32
|
+
task = @environment.submit(on_full: :raise) do
|
|
33
|
+
persistence.transaction do |tx|
|
|
34
|
+
tx.executions.assert_idle!(agent_id)
|
|
35
|
+
tx.guard_agent!(agent_id)
|
|
36
|
+
unless tx.retentions.list(agent_id).empty?
|
|
37
|
+
raise Phronomy::AgentBusyError, "Agent #{agent_id.inspect} is retained by durable coordination"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
task.on_complete do |_value, error|
|
|
42
|
+
@read_error = error
|
|
43
|
+
task.on_physical_complete { post(:apply) }
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
finish(@read_error)
|
|
47
|
+
end
|
|
48
|
+
rescue => error
|
|
49
|
+
finish(error)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def post(phase)
|
|
55
|
+
accepted = @environment.registry.post(Command.new(coordinator: self, phase: phase), completion: @result)
|
|
56
|
+
finish(Phronomy::RuntimeShutdownError.new("EventLoop rejected Agent unload #{phase}")) unless accepted
|
|
57
|
+
rescue => error
|
|
58
|
+
finish(error)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def finish(error)
|
|
62
|
+
first = @mutex.synchronize do
|
|
63
|
+
next false if @finished
|
|
64
|
+
@finished = true
|
|
65
|
+
end
|
|
66
|
+
return unless first
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
@ownership.finish_unload(@agent, self, success: error.nil?)
|
|
70
|
+
rescue => ownership_error
|
|
71
|
+
error ||= ownership_error
|
|
72
|
+
end
|
|
73
|
+
@agent = @environment = @ownership = nil
|
|
74
|
+
error ? @result.fail(error) : @result.complete(true)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Agent
|
|
7
|
+
# Owns the execution's output promise; generic schema interpretation is injected.
|
|
8
|
+
# @api private
|
|
9
|
+
class OutputContract
|
|
10
|
+
FACTORIES = {}
|
|
11
|
+
private_constant :FACTORIES
|
|
12
|
+
attr_reader :schema, :digest
|
|
13
|
+
|
|
14
|
+
def self.install_factory(factory)
|
|
15
|
+
FACTORIES.replace(parser: factory).freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(schema)
|
|
19
|
+
@parser = FACTORIES.fetch(:parser).call(schema)
|
|
20
|
+
@schema = @parser.json_schema
|
|
21
|
+
@digest = Digest::SHA256.hexdigest(JSON.generate(@schema, sort_keys: true)).freeze
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def metadata(contents)
|
|
26
|
+
{"output_contract" => {"version" => 1, "schema_ref" => contents.put_text(JSON.generate(schema, sort_keys: true)), "schema_digest" => digest}}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.restore(execution, contents)
|
|
30
|
+
stored = execution.metadata["output_contract"]
|
|
31
|
+
return nil unless stored
|
|
32
|
+
raise ArgumentError, "unsupported Agent output contract version" unless stored.fetch("version") == 1
|
|
33
|
+
contract = new(contents.fetch_json(stored.fetch("schema_ref")))
|
|
34
|
+
unless contract.digest == stored.fetch("schema_digest")
|
|
35
|
+
raise Phronomy::ContentStore::IntegrityError, "Agent output schema digest mismatch"
|
|
36
|
+
end
|
|
37
|
+
contract
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate(output, response:, execution_id:)
|
|
41
|
+
raw = response&.content
|
|
42
|
+
reason = if response&.refusal || response&.finish_reason == :refusal
|
|
43
|
+
"model_refusal"
|
|
44
|
+
elsif !response || !%i[stop].include?(response.finish_reason) || !response.tool_calls.empty?
|
|
45
|
+
%i[length content_filter].include?(response&.finish_reason) ? response.finish_reason.to_s : "unknown_finish_reason"
|
|
46
|
+
elsif output.nil? || raw.nil?
|
|
47
|
+
"missing_output"
|
|
48
|
+
end
|
|
49
|
+
if reason
|
|
50
|
+
raise OutputError.new(execution_id: execution_id, schema_digest: digest, reason: reason, raw_output: raw)
|
|
51
|
+
end
|
|
52
|
+
@parser.parse(output)
|
|
53
|
+
rescue Phronomy::ParseError => error
|
|
54
|
+
reason = case error.code
|
|
55
|
+
when "output_parser.invalid_json" then "invalid_json"
|
|
56
|
+
when "output_parser.schema_mismatch" then "schema_mismatch"
|
|
57
|
+
else raise
|
|
58
|
+
end
|
|
59
|
+
raise OutputError.new(execution_id: execution_id, schema_digest: digest, reason: reason,
|
|
60
|
+
violations: error.details, raw_output: raw), cause: error
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# A declared output contract could not be fulfilled. Tool effects remain facts.
|
|
6
|
+
# @api public
|
|
7
|
+
class OutputError < Phronomy::Error
|
|
8
|
+
CODES = {"invalid_json" => "agent.output_invalid", "schema_mismatch" => "agent.output_invalid",
|
|
9
|
+
"missing_output" => "agent.output_invalid", "model_refusal" => "agent.output_refused",
|
|
10
|
+
"length" => "agent.output_incomplete", "content_filter" => "agent.output_incomplete",
|
|
11
|
+
"unknown_finish_reason" => "agent.output_incomplete"}.freeze
|
|
12
|
+
private_constant :CODES
|
|
13
|
+
|
|
14
|
+
# @api public
|
|
15
|
+
attr_reader :execution_id, :schema_digest, :reason, :violations, :raw_output
|
|
16
|
+
|
|
17
|
+
# @api public
|
|
18
|
+
def initialize(execution_id:, schema_digest:, reason:, violations: [], raw_output: nil)
|
|
19
|
+
@execution_id = execution_id.to_s.dup.freeze
|
|
20
|
+
@schema_digest = schema_digest.to_s.dup.freeze
|
|
21
|
+
@reason = reason.to_s.dup.freeze
|
|
22
|
+
@violations = Phronomy::Values::Immutable.json(violations)
|
|
23
|
+
@raw_output = Phronomy::Values::Immutable.copy(raw_output)
|
|
24
|
+
super("Agent output contract failed: #{@reason}", code: CODES.fetch(@reason))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Owner-defined facts for persistence and composition; no live exception values.
|
|
28
|
+
# @api private
|
|
29
|
+
def diagnostic(include_raw: true)
|
|
30
|
+
value = Phronomy::Error.diagnostic(self).merge("execution_id" => execution_id,
|
|
31
|
+
"schema_digest" => schema_digest, "reason" => reason, "violations" => violations)
|
|
32
|
+
value["raw_output"] = raw_output if include_raw
|
|
33
|
+
Phronomy::Values::Immutable.json(value)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @api private
|
|
37
|
+
def self.restore(value)
|
|
38
|
+
error = new(execution_id: value.fetch("execution_id"), schema_digest: value.fetch("schema_digest"),
|
|
39
|
+
reason: value.fetch("reason"), violations: value.fetch("violations"), raw_output: value["raw_output"])
|
|
40
|
+
raise ArgumentError, "invalid Agent output diagnostic code" unless error.code == value.fetch("code")
|
|
41
|
+
if value["cause"]
|
|
42
|
+
begin
|
|
43
|
+
raise error, cause: Phronomy::Error.from_diagnostic(value.fetch("cause"))
|
|
44
|
+
rescue OutputError => restored
|
|
45
|
+
restored.set_backtrace(nil)
|
|
46
|
+
return restored
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
error
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -39,9 +39,9 @@ module Phronomy
|
|
|
39
39
|
materializer = RuntimeInput.new(agent: agent, persistence: agent.persistence)
|
|
40
40
|
messages = records.map { |record| materializer.materialize_journal_record(record) }.freeze
|
|
41
41
|
assistant = messages.reverse.find { |message| message.role.to_sym == :assistant }
|
|
42
|
-
output, usage = SavedContextReader.provider_output_and_usage(agent, execution) if execution.phase.to_sym == :recovery_provider_completed
|
|
42
|
+
output, usage, response = SavedContextReader.provider_output_and_usage(agent, execution) if execution.phase.to_sym == :recovery_provider_completed
|
|
43
43
|
RecoveryMaterial.new(projection: projection, messages: messages,
|
|
44
|
-
assistant_message: assistant, output: output, usage: usage,
|
|
44
|
+
assistant_message: assistant, output: output, usage: usage, response: response,
|
|
45
45
|
cancellation_requested: agent.persistence.cancellation_requested?(agent_id: agent.agent_id, execution_id: execution.execution_id))
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -52,9 +52,9 @@ module Phronomy
|
|
|
52
52
|
environment.registry.mark_agent_execution_admission(agent.agent_id,
|
|
53
53
|
execution_id: execution.execution_id, state: :recovery_required)
|
|
54
54
|
deliver_resolution_required(execution, coordination_recovery_descriptor(execution))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
agent.send(:_complete_result_task, completion, {execution_id: execution.execution_id,
|
|
56
|
+
execution_revision: execution.execution_revision,
|
|
57
|
+
recovery: :resolution_required}.freeze)
|
|
58
58
|
return
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -73,7 +73,7 @@ module Phronomy
|
|
|
73
73
|
agent.send(:_phronomy_event_listener), messages: material.messages, cancellation_requested: material.cancellation_requested)
|
|
74
74
|
end
|
|
75
75
|
if execution.phase.to_sym == :recovery_provider_completed
|
|
76
|
-
invocation.
|
|
76
|
+
invocation.apply_llm_response!(material.response)
|
|
77
77
|
end
|
|
78
78
|
prepare_saved_provider_calls(execution, invocation, material.assistant_message) if action == :framework_calls
|
|
79
79
|
end
|
|
@@ -38,7 +38,7 @@ module Phronomy
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def prepare_plan(execution)
|
|
41
|
-
root = agent.
|
|
41
|
+
root = agent.send(:__root_for_operation)
|
|
42
42
|
manifest_ref = execution.metadata["manifest_ref"]
|
|
43
43
|
base_ref = execution.metadata["base_manifest_ref"] ||
|
|
44
44
|
manifest_ref
|
|
@@ -209,7 +209,7 @@ module Phronomy
|
|
|
209
209
|
}
|
|
210
210
|
)
|
|
211
211
|
end
|
|
212
|
-
command.completion
|
|
212
|
+
agent.send(:_complete_result_task, command.completion, agent)
|
|
213
213
|
return
|
|
214
214
|
end
|
|
215
215
|
|
|
@@ -246,7 +246,7 @@ module Phronomy
|
|
|
246
246
|
result: payload
|
|
247
247
|
)
|
|
248
248
|
end
|
|
249
|
-
command.completion
|
|
249
|
+
agent.send(:_complete_result_task, command.completion, agent)
|
|
250
250
|
when Phronomy::Agent::RecoveryRules::RESUMABLE
|
|
251
251
|
continue_resumable_on_event_loop(
|
|
252
252
|
execution,
|
|
@@ -288,7 +288,7 @@ module Phronomy
|
|
|
288
288
|
end
|
|
289
289
|
end
|
|
290
290
|
end
|
|
291
|
-
command.completion
|
|
291
|
+
agent.send(:_fail_result_task, command.completion, caught)
|
|
292
292
|
# simplecov:enable
|
|
293
293
|
end
|
|
294
294
|
|
|
@@ -296,6 +296,7 @@ module Phronomy
|
|
|
296
296
|
Phronomy::Tracing::Automatic.observe_task(
|
|
297
297
|
task,
|
|
298
298
|
"agent.execution",
|
|
299
|
+
usage: ->(value) { value.is_a?(Hash) ? value[:usage]&.to_h&.freeze : nil },
|
|
299
300
|
agent_id: agent.agent_id,
|
|
300
301
|
execution_id: execution.execution_id,
|
|
301
302
|
mode: (
|
|
@@ -317,7 +318,7 @@ module Phronomy
|
|
|
317
318
|
if framework_batch?(execution)
|
|
318
319
|
internal = Phronomy::TaskResult.deferred(name: "framework-tool-recovery:#{execution.execution_id}")
|
|
319
320
|
continue_recovery_on_event_loop(execution, internal, material: material)
|
|
320
|
-
|
|
321
|
+
agent.send(:_complete_result_task, completion, agent)
|
|
321
322
|
return
|
|
322
323
|
end
|
|
323
324
|
|
|
@@ -359,11 +360,11 @@ module Phronomy
|
|
|
359
360
|
result_task: internal_task,
|
|
360
361
|
error: nil
|
|
361
362
|
))
|
|
362
|
-
|
|
363
|
+
agent.send(:_complete_result_task, completion, agent)
|
|
363
364
|
when :recovery_tools_completed, :recovery_provider_completed, :recovery_resolved_failed
|
|
364
365
|
internal_task = Phronomy::TaskResult.deferred(name: "agent-recovery-auto:#{execution.execution_id}")
|
|
365
366
|
continue_recovery_on_event_loop(execution, internal_task, material: material)
|
|
366
|
-
|
|
367
|
+
agent.send(:_complete_result_task, completion, agent)
|
|
367
368
|
else
|
|
368
369
|
raise Phronomy::ExecutionRehydrationRequiredError,
|
|
369
370
|
"no automatic continuation for #{execution.phase.inspect}"
|
|
@@ -14,24 +14,22 @@ module Phronomy
|
|
|
14
14
|
request.execution_id
|
|
15
15
|
)
|
|
16
16
|
unless state && state.agent.equal?(agent)
|
|
17
|
-
request.completion
|
|
17
|
+
agent.send(:_fail_result_task, request.completion,
|
|
18
18
|
Phronomy::ExecutionRehydrationRequiredError.new(
|
|
19
19
|
"no live recovered execution #{request.execution_id}"
|
|
20
|
-
)
|
|
21
|
-
)
|
|
20
|
+
))
|
|
22
21
|
return
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
current = state.execution
|
|
26
25
|
unless current.execution_revision ==
|
|
27
26
|
request.expected_execution_revision
|
|
28
|
-
request.completion
|
|
27
|
+
agent.send(:_fail_result_task, request.completion,
|
|
29
28
|
Phronomy::Persistence::StateConflictError.new(
|
|
30
29
|
"Recovery resolution revision conflict: expected " \
|
|
31
30
|
"#{request.expected_execution_revision}, actual " \
|
|
32
31
|
"#{current.execution_revision}"
|
|
33
|
-
)
|
|
34
|
-
)
|
|
32
|
+
))
|
|
35
33
|
return
|
|
36
34
|
end
|
|
37
35
|
|
|
@@ -42,28 +40,26 @@ module Phronomy
|
|
|
42
40
|
descriptor.fetch(:subject),
|
|
43
41
|
request.subject
|
|
44
42
|
)
|
|
45
|
-
request.completion
|
|
43
|
+
agent.send(:_fail_result_task, request.completion,
|
|
46
44
|
ArgumentError.new(
|
|
47
45
|
"Recovery subject is not current for execution #{request.execution_id}"
|
|
48
|
-
)
|
|
49
|
-
)
|
|
46
|
+
))
|
|
50
47
|
return
|
|
51
48
|
end
|
|
52
49
|
|
|
53
50
|
unless Array(
|
|
54
51
|
descriptor.fetch(:allowed_outcomes)
|
|
55
52
|
).map(&:to_sym).include?(request.outcome)
|
|
56
|
-
request.completion
|
|
53
|
+
agent.send(:_fail_result_task, request.completion,
|
|
57
54
|
ArgumentError.new(
|
|
58
55
|
"Recovery outcome #{request.outcome.inspect} is not allowed for the current subject"
|
|
59
|
-
)
|
|
60
|
-
)
|
|
56
|
+
))
|
|
61
57
|
return
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
operation = ResolutionOperation.new(
|
|
65
61
|
execution: current,
|
|
66
|
-
root: agent.
|
|
62
|
+
root: agent.send(:__root_for_operation),
|
|
67
63
|
subject: request.subject,
|
|
68
64
|
outcome: request.outcome,
|
|
69
65
|
result: request.result,
|
|
@@ -77,6 +73,7 @@ module Phronomy
|
|
|
77
73
|
task = @environment.submit(on_full: :raise) do
|
|
78
74
|
prepare_resolution(operation)
|
|
79
75
|
end
|
|
76
|
+
request.completion.track(task) if request.completion.respond_to?(:track)
|
|
80
77
|
task.on_complete do |result, error|
|
|
81
78
|
ready = ResolveReady.new(
|
|
82
79
|
coordinator: self,
|
|
@@ -86,15 +83,14 @@ module Phronomy
|
|
|
86
83
|
error: error
|
|
87
84
|
)
|
|
88
85
|
unless post_control(ready)
|
|
89
|
-
request.completion
|
|
86
|
+
agent.send(:_fail_result_task, request.completion,
|
|
90
87
|
Phronomy::RuntimeShutdownError.new(
|
|
91
88
|
"EventLoop rejected Recovery resolution apply"
|
|
92
|
-
)
|
|
93
|
-
)
|
|
89
|
+
))
|
|
94
90
|
end
|
|
95
91
|
end
|
|
96
92
|
rescue => caught
|
|
97
|
-
request.completion
|
|
93
|
+
agent.send(:_fail_result_task, request.completion, caught)
|
|
98
94
|
end
|
|
99
95
|
|
|
100
96
|
def perform_resolution(operation)
|
|
@@ -498,7 +494,7 @@ module Phronomy
|
|
|
498
494
|
|
|
499
495
|
continue_recovery_on_event_loop(result.execution, request.completion, material: result.material)
|
|
500
496
|
rescue => caught
|
|
501
|
-
request.completion
|
|
497
|
+
agent.send(:_fail_result_task, request.completion, caught)
|
|
502
498
|
end
|
|
503
499
|
end
|
|
504
500
|
end
|
|
@@ -39,7 +39,7 @@ module Phronomy
|
|
|
39
39
|
)
|
|
40
40
|
ResolutionResult = Data.define(:execution)
|
|
41
41
|
ResolutionPreparation = Data.define(:execution, :material, :error)
|
|
42
|
-
RecoveryMaterial = Data.define(:projection, :messages, :assistant_message, :output, :usage, :cancellation_requested)
|
|
42
|
+
RecoveryMaterial = Data.define(:projection, :messages, :assistant_message, :output, :usage, :response, :cancellation_requested)
|
|
43
43
|
RecoveryPlan = Data.define(
|
|
44
44
|
:execution, :root, :manifest, :base_manifest,
|
|
45
45
|
:projection, :classification, :material
|
|
@@ -147,9 +147,8 @@ module Phronomy
|
|
|
147
147
|
failure = error_present ?
|
|
148
148
|
RecoverySupport.resolution_failure(error) : nil
|
|
149
149
|
|
|
150
|
-
completion =
|
|
151
|
-
name: "agent-recovery-resolve:#{execution_id}"
|
|
152
|
-
)
|
|
150
|
+
completion = OperationResult.new(agent: agent,
|
|
151
|
+
name: "agent-recovery-resolve:#{execution_id}")
|
|
153
152
|
command = ResolveCommand.new(
|
|
154
153
|
coordinator: self,
|
|
155
154
|
execution_id: execution_id.to_s.freeze,
|
|
@@ -162,18 +161,15 @@ module Phronomy
|
|
|
162
161
|
completion: completion
|
|
163
162
|
)
|
|
164
163
|
unless post_control(command)
|
|
165
|
-
|
|
166
|
-
Phronomy::RuntimeShutdownError.new(
|
|
167
|
-
"EventLoop rejected Agent Recovery resolution"
|
|
168
|
-
)
|
|
169
|
-
)
|
|
164
|
+
agent.send(:_fail_result_task, completion,
|
|
165
|
+
Phronomy::RuntimeShutdownError.new("EventLoop rejected Agent Recovery resolution"))
|
|
170
166
|
end
|
|
171
167
|
completion
|
|
172
168
|
rescue => caught
|
|
173
169
|
completion ||= Phronomy::TaskResult.deferred(
|
|
174
170
|
name: "agent-recovery-resolve:#{execution_id}"
|
|
175
171
|
)
|
|
176
|
-
|
|
172
|
+
agent.send(:_fail_result_task, completion, caught)
|
|
177
173
|
completion
|
|
178
174
|
end
|
|
179
175
|
|
|
@@ -26,17 +26,15 @@ module Phronomy
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def resolution_failure(error)
|
|
29
|
-
|
|
30
|
-
"class" => error.class.name.to_s,
|
|
31
|
-
"message" => error.message.to_s
|
|
32
|
-
}.freeze
|
|
29
|
+
Phronomy::Error.diagnostic(error).freeze
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
def error_from_failure(failure)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
if failure["class"] == "Phronomy::Agent::OutputError"
|
|
34
|
+
OutputError.restore(failure)
|
|
35
|
+
else
|
|
36
|
+
Phronomy::Error.from_diagnostic(failure)
|
|
37
|
+
end
|
|
40
38
|
end
|
|
41
39
|
|
|
42
40
|
def event_payload(execution, descriptor)
|
|
@@ -23,7 +23,8 @@ module Phronomy
|
|
|
23
23
|
declared_states: InvocationTransitions::DECLARED_STATES,
|
|
24
24
|
wait_state_names: InvocationTransitions::WAIT_STATES,
|
|
25
25
|
external_events: InvocationTransitions::EXTERNAL_EVENTS,
|
|
26
|
-
recursion_limit: InvocationTransitions
|
|
26
|
+
recursion_limit: InvocationTransitions::AUTOMATIC_RECURSION_LIMIT,
|
|
27
|
+
recursion_limit_scope: :automatic_chain,
|
|
27
28
|
event_loop: event_loop,
|
|
28
29
|
resume_event: resume_event,
|
|
29
30
|
resume_phase: resume_phase
|
|
@@ -5,8 +5,12 @@ module Phronomy
|
|
|
5
5
|
class RuntimeInput
|
|
6
6
|
RuntimeProjection = Data.define(
|
|
7
7
|
:system, :messages, :tool_classes, :ask_message,
|
|
8
|
-
:model_config, :manifest, :manifest_ref
|
|
9
|
-
)
|
|
8
|
+
:model_config, :manifest, :manifest_ref, :response_schema
|
|
9
|
+
) do
|
|
10
|
+
def initialize(response_schema: nil, **values)
|
|
11
|
+
super(**values, response_schema: response_schema)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
10
14
|
|
|
11
15
|
def initialize(agent:, persistence:, additional_tools: [])
|
|
12
16
|
@agent = agent
|
|
@@ -45,7 +49,8 @@ module Phronomy
|
|
|
45
49
|
ask_message: ask_message,
|
|
46
50
|
model_config: Phronomy::Values::Immutable.copy(model_config),
|
|
47
51
|
manifest: manifest,
|
|
48
|
-
manifest_ref: manifest_ref
|
|
52
|
+
manifest_ref: manifest_ref,
|
|
53
|
+
response_schema: manifest.response_schema_ref && Phronomy::Values::Immutable.json(fetch_json(manifest.response_schema_ref))
|
|
49
54
|
)
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -148,12 +153,18 @@ module Phronomy
|
|
|
148
153
|
end
|
|
149
154
|
|
|
150
155
|
tool_calls = materialize_tool_calls(payload["tool_calls"])
|
|
156
|
+
metadata = payload.fetch("metadata", {})
|
|
157
|
+
raise ArgumentError, "canonical message metadata must be a Hash" unless metadata.is_a?(Hash)
|
|
158
|
+
metadata = metadata.dup
|
|
159
|
+
# Preserve backend-owned values without interpreting them. Old records
|
|
160
|
+
# stored model_id only at the top level; retain that read compatibility.
|
|
161
|
+
metadata["model_id"] = payload["model_id"] if !metadata.key?("model_id") && payload["model_id"]
|
|
151
162
|
Phronomy::LLMAdapter::Message.new(
|
|
152
163
|
role: role,
|
|
153
164
|
content: payload.fetch("content", nil),
|
|
154
165
|
tool_calls: tool_calls,
|
|
155
166
|
tool_call_id: payload["tool_call_id"],
|
|
156
|
-
metadata:
|
|
167
|
+
metadata: metadata
|
|
157
168
|
)
|
|
158
169
|
end
|
|
159
170
|
|
|
@@ -48,7 +48,8 @@ module Phronomy
|
|
|
48
48
|
cached: usage_hash["cached"] || usage_hash[:cached] || usage_hash["cache_read_tokens"] || usage_hash[:cache_read_tokens] || usage_hash["cached_tokens"] || usage_hash[:cached_tokens],
|
|
49
49
|
cache_creation: usage_hash["cache_creation"] || usage_hash[:cache_creation] || usage_hash["cache_write_tokens"] || usage_hash[:cache_write_tokens] || usage_hash["cache_creation_tokens"] || usage_hash[:cache_creation_tokens]
|
|
50
50
|
)
|
|
51
|
-
|
|
51
|
+
response = Phronomy::LLMAdapter::Response.from_h(payload.merge("usage" => usage.to_h))
|
|
52
|
+
[output, usage, response]
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
end
|
data/lib/phronomy/agent/store.rb
CHANGED
|
@@ -79,7 +79,8 @@ module Phronomy
|
|
|
79
79
|
execution_id: execution.execution_id, agent_id: execution.agent_id,
|
|
80
80
|
status: execution.status, phase: execution.phase,
|
|
81
81
|
result_ref: execution.result_ref, error_ref: execution.error_ref,
|
|
82
|
-
result: execution
|
|
82
|
+
result: materialize_result(execution, contents),
|
|
83
|
+
raw_output: execution.metadata["raw_output_ref"] && contents.fetch_text(execution.metadata.fetch("raw_output_ref")),
|
|
83
84
|
error: execution.error_ref && contents.fetch_json(execution.error_ref)
|
|
84
85
|
}.then { |value| Phronomy::Values::Immutable.copy(value) }
|
|
85
86
|
end
|
|
@@ -97,7 +98,8 @@ module Phronomy
|
|
|
97
98
|
assert_execution_owner!(execution, agent_id)
|
|
98
99
|
ExecutionObservation.new(agent_id: execution.agent_id, execution_id: execution.execution_id,
|
|
99
100
|
status: execution.status,
|
|
100
|
-
result: execution
|
|
101
|
+
result: materialize_result(execution, records.contents),
|
|
102
|
+
raw_output: execution.metadata["raw_output_ref"] && records.contents.fetch_text(execution.metadata.fetch("raw_output_ref")),
|
|
101
103
|
error: execution.error_ref && Phronomy::Values::Immutable.copy(records.contents.fetch_json(execution.error_ref)),
|
|
102
104
|
reservation: execution.metadata["reservation"] && ReservedExecution.new(agent_id: agent_id,
|
|
103
105
|
execution_id: execution_id, correlation: execution.metadata["reservation"]),
|
|
@@ -226,6 +228,12 @@ module Phronomy
|
|
|
226
228
|
|
|
227
229
|
private
|
|
228
230
|
|
|
231
|
+
def materialize_result(execution, contents)
|
|
232
|
+
return nil unless execution.result_ref
|
|
233
|
+
(execution.metadata["result_format"] == "json") ?
|
|
234
|
+
Phronomy::Values::Immutable.json(contents.fetch_json(execution.result_ref)) : contents.fetch_text(execution.result_ref)
|
|
235
|
+
end
|
|
236
|
+
|
|
229
237
|
def authorized_argument_values(entry)
|
|
230
238
|
# Unit 6 persists the exact validated snapshot. Older records encoded
|
|
231
239
|
# legacy raw params, whose explicit optional nil meant omission.
|
|
@@ -22,14 +22,14 @@ module Phronomy
|
|
|
22
22
|
# @return [Exception] original Application callback exception
|
|
23
23
|
attr_reader :original_error
|
|
24
24
|
|
|
25
|
-
def initialize(event_type:, original_error:, result: nil)
|
|
25
|
+
def initialize(event_type:, original_error:, result: nil, code: nil)
|
|
26
26
|
@event_type = event_type
|
|
27
27
|
@original_error = original_error
|
|
28
28
|
@result = result
|
|
29
29
|
|
|
30
30
|
super(
|
|
31
31
|
"Stream callback failed while handling #{event_type.inspect}: " \
|
|
32
|
-
"#{original_error.class}: #{original_error.message}"
|
|
32
|
+
"#{original_error.class}: #{original_error.message}", code: code
|
|
33
33
|
)
|
|
34
34
|
set_backtrace(original_error.backtrace)
|
|
35
35
|
end
|
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
class Error < StandardError
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
# Optional stable reason supplied by the owner of the failed operation.
|
|
6
|
+
# It does not imply retry safety or replace execution status.
|
|
7
|
+
# @api public
|
|
8
|
+
attr_reader :code
|
|
9
|
+
|
|
10
|
+
# Existing message-only construction retains StandardError behavior.
|
|
11
|
+
# @api public
|
|
12
|
+
def initialize(message = nil, code: nil)
|
|
13
|
+
raise ArgumentError, "error code must be a String or nil" unless code.nil? || code.is_a?(String)
|
|
14
|
+
@code = code&.dup&.freeze
|
|
15
|
+
super(message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Generic diagnostic transport. No domain-specific interpretation.
|
|
19
|
+
# External exceptions are not extended with this protocol.
|
|
20
|
+
# @api private
|
|
21
|
+
def self.diagnostic(error)
|
|
22
|
+
diagnostic = {"class" => error.class.name, "message" => error.message}
|
|
23
|
+
diagnostic["code"] = error.code if error.is_a?(Phronomy::Error) && error.code
|
|
24
|
+
cause = error.cause
|
|
25
|
+
diagnostic["cause"] = cause_diagnostic(cause, remaining: 3, seen: [error.object_id]) if cause
|
|
26
|
+
diagnostic
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Restores data, never a saved class name or arbitrary instance attributes.
|
|
30
|
+
# Restored causes are generic errors as well. Old diagnostics remain valid.
|
|
31
|
+
# @api private
|
|
32
|
+
def self.from_diagnostic(value, remaining: 3)
|
|
33
|
+
hash = value.to_h { |key, item| [key.to_s, item] }
|
|
34
|
+
error = Phronomy::Error.new(
|
|
35
|
+
"#{hash.fetch("class", "Error")}: #{hash.fetch("message", "Recovery-resolved failure")}",
|
|
36
|
+
code: hash["code"]
|
|
37
|
+
)
|
|
38
|
+
return error unless remaining.positive? && hash["cause"].is_a?(Hash) && hash["cause"].key?("class")
|
|
39
|
+
|
|
40
|
+
cause = from_diagnostic(hash["cause"], remaining: remaining - 1)
|
|
41
|
+
begin
|
|
42
|
+
raise error, cause: cause
|
|
43
|
+
rescue Phronomy::Error => restored
|
|
44
|
+
restored.set_backtrace(nil)
|
|
45
|
+
restored
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.cause_diagnostic(error, remaining:, seen:)
|
|
50
|
+
return {"truncated" => true} if remaining.zero? || seen.include?(error.object_id)
|
|
51
|
+
|
|
52
|
+
# Bound newly added cause data; retain the existing top-level format.
|
|
53
|
+
fields = {"class" => error.class.name, "message" => error.message}
|
|
54
|
+
fields["code"] = error.code if error.is_a?(Phronomy::Error) && error.code
|
|
55
|
+
diagnostic = fields.to_h do |key, value|
|
|
56
|
+
[key, value.is_a?(String) ? value[0, 2048] : value]
|
|
57
|
+
end
|
|
58
|
+
# Never turn a truncated reason into a different, apparently valid code.
|
|
59
|
+
diagnostic.delete("code") if fields["code"] && fields["code"].length > 2048
|
|
60
|
+
diagnostic["truncated"] = true if diagnostic != fields
|
|
61
|
+
if error.cause
|
|
62
|
+
diagnostic["cause"] = cause_diagnostic(error.cause,
|
|
63
|
+
remaining: remaining - 1, seen: seen + [error.object_id])
|
|
64
|
+
end
|
|
65
|
+
diagnostic
|
|
66
|
+
end
|
|
67
|
+
private_class_method :cause_diagnostic
|
|
68
|
+
end
|
|
5
69
|
end
|