phronomy 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +7 -2
  4. data/docs/architecture/agent-tool-child-lifetime.md +141 -0
  5. data/docs/architecture/dx05-agent-scope.md +4 -2
  6. data/docs/architecture/structured-output-and-scope.md +73 -13
  7. data/docs/decisions/044-agent-default-and-one-shot-composition.md +5 -0
  8. data/docs/getting-started.md +26 -0
  9. data/docs/migrations/0.31.md +72 -0
  10. data/lib/phronomy/agent/async_event_api.rb +17 -8
  11. data/lib/phronomy/agent/base.rb +27 -2
  12. data/lib/phronomy/agent/composition/run_once.rb +11 -10
  13. data/lib/phronomy/agent/context_preparation.rb +1 -0
  14. data/lib/phronomy/agent/execution/dispatch_preparation.rb +1 -1
  15. data/lib/phronomy/agent/execution/exact_execution.rb +5 -3
  16. data/lib/phronomy/agent/execution/execution_coordinator.rb +6 -0
  17. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +1 -6
  18. data/lib/phronomy/agent/execution/initial_preparation.rb +1 -1
  19. data/lib/phronomy/agent/execution/invocation_actions.rb +17 -15
  20. data/lib/phronomy/agent/execution_change.rb +4 -0
  21. data/lib/phronomy/agent/output_contract.rb +47 -5
  22. data/lib/phronomy/agent/persistence/codec.rb +16 -2
  23. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +1 -0
  24. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -2
  25. data/lib/phronomy/agent/runtime_input.rb +7 -0
  26. data/lib/phronomy/engine/fsm_session.rb +7 -0
  27. data/lib/phronomy/execution/async_operation.rb +16 -0
  28. data/lib/phronomy/execution/concurrency/result_finalization.rb +106 -0
  29. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +3 -3
  30. data/lib/phronomy/generation/generator_verifier.rb +6 -3
  31. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +1 -1
  32. data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +3 -1
  33. data/lib/phronomy/tools/agent.rb +58 -22
  34. data/lib/phronomy/version.rb +1 -1
  35. data/sig/phronomy/agent.rbs +7 -5
  36. data/sig/phronomy/execution/async_operation.rbs +1 -0
  37. data/sig/phronomy/generation/generator_verifier.rbs +1 -1
  38. data/sig/phronomy/tools/agent.rbs +14 -0
  39. metadata +6 -2
@@ -7,13 +7,30 @@ module Phronomy
7
7
  # Agent-backed Tools are logically asynchronous rather than offloaded
8
8
  # synchronous operations. Their ToolInvocation starts the child Agent and then
9
9
  # returns to EventLoop immediately; the Tool completion handle settles when the
10
- # child Agent FSMSession finishes. An OffloadPool worker is therefore never
11
- # consumed merely to wait for another Agent.
10
+ # child Agent's physical work and release finish. An OffloadPool worker is
11
+ # therefore never consumed merely to wait for another Agent.
12
12
  class Agent < Phronomy::Tool::Base
13
13
  execution_mode :cooperative
14
14
  description "Wraps a Phronomy::Agent as a Tool"
15
15
  param :input, type: :string, desc: "The input to forward to the wrapped Agent"
16
16
 
17
+ # A private child's operation ended, but its safe release failed. The live
18
+ # reference supports retrying unload_async without repeating business work.
19
+ # This error and its child reference are process-local, not durable state.
20
+ # @api public
21
+ class CleanupError < Phronomy::ToolError
22
+ attr_reader :child_agent, :agent_id, :result, :primary_error, :cleanup_error
23
+
24
+ def initialize(child_agent:, result:, primary_error:, cleanup_error:)
25
+ @child_agent = child_agent
26
+ @agent_id = child_agent.agent_id
27
+ @result = result
28
+ @primary_error = primary_error
29
+ @cleanup_error = cleanup_error
30
+ super("Agent Tool cleanup failed for #{@agent_id.inspect}", code: "tool.agent_cleanup_failed")
31
+ end
32
+ end
33
+
17
34
  class << self
18
35
  def from_agent(agent_class, tool_name: nil, description: nil)
19
36
  raise ArgumentError, "agent_class must be a Class" unless agent_class.is_a?(Class)
@@ -35,36 +52,34 @@ module Phronomy
35
52
  # Preserve the synchronous Tool API for top-level callers. ToolInvocation
36
53
  # never uses this path for Agent-backed Tools; it calls #call_async.
37
54
  klass.define_method(:execute) do |input:, cancellation_token: nil|
38
- invoke_options = {}
39
- if cancellation_token
40
- invoke_options[:config] = {cancellation_token: cancellation_token}
55
+ if Phronomy::WaitPolicy.blocking_forbidden?
56
+ raise Phronomy::EventLoopReentrancyError,
57
+ "Agent Tool execute cannot block EventLoop; use call_async"
41
58
  end
42
- # Use the same supplied store factory as the asynchronous path.
43
- # Preserve the creation arguments previously supplied by run_once.
44
- persistence = Phronomy::Agent::DefaultPersistence.build
45
- agent = agent_class.create(
46
- context: nil,
47
- knowledge: [],
48
- persistence: persistence,
49
- on_event: nil
50
- )
51
- result = agent.invoke(input, **invoke_options)
52
- result[:output].to_s
59
+ execute_async(input: input, cancellation_token: cancellation_token).wait_result
53
60
  end
54
61
 
55
62
  # Internal asynchronous execution protocol used by Agent#call_async.
56
- # The child Agent owns its own FSMSession/EventLoop lifecycle; this
57
- # method only returns its completion handle and performs a short map.
63
+ # C04 decides whether release is safe; C13 composes completion. The
64
+ # concrete Tool supplies only creation, output and cleanup policies.
58
65
  klass.define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
59
- persistence = Phronomy::Agent::DefaultPersistence.build
60
- agent = agent_class.create(persistence: persistence)
66
+ unless config.nil? || config.is_a?(Hash)
67
+ raise ArgumentError, "Agent Tool config must be a Hash"
68
+ end
61
69
  task_config = (config || {}).dup
62
70
  if cancellation_token && !task_config[:cancellation_token]
63
71
  task_config[:cancellation_token] = cancellation_token
64
72
  end
65
73
 
66
- agent.invoke_async(input, config: task_config).map do |result|
67
- result[:output].to_s
74
+ persistence = Phronomy::Agent::DefaultPersistence.build
75
+ agent = agent_class.create(persistence: persistence)
76
+ Phronomy::AsyncOperation.finalize(name: "agent-tool-#{name}-release",
77
+ cleanup: -> { agent.unload_async },
78
+ on_cleanup_error: ->(value, primary, cleanup) { child_cleanup_failed(agent, value, primary, cleanup) }) do
79
+ Phronomy::AsyncOperation.call(name: "agent-tool-#{name}-invoke",
80
+ transform: ->(result) { result[:output].to_s }) do
81
+ agent.invoke_async(input, config: task_config)
82
+ end
68
83
  end
69
84
  end
70
85
  klass.send(:private, :execute_async)
@@ -102,6 +117,27 @@ module Phronomy
102
117
 
103
118
  private
104
119
 
120
+ def child_cleanup_failed(agent, result, primary_error, cleanup_error)
121
+ failure = CleanupError.new(child_agent: agent, result: result,
122
+ primary_error: primary_error, cleanup_error: cleanup_error)
123
+ # Always report cleanup failures: an outer observer may already have
124
+ # cancelled its handle, so returning an error alone is not sufficient.
125
+ report_child_cleanup_failure(failure)
126
+ raise failure, cause: primary_error || cleanup_error
127
+ end
128
+
129
+ def report_child_cleanup_failure(failure)
130
+ details = {"agent_id" => failure.agent_id, "code" => failure.code,
131
+ "cleanup_error" => Phronomy::Error.diagnostic(failure.cleanup_error)}
132
+ details["primary_error"] = Phronomy::Error.diagnostic(failure.primary_error) if failure.primary_error
133
+ message = "[Phronomy] Agent Tool cleanup failed: #{details.inspect}"
134
+ logger = Phronomy::RuntimeSettings.current.logger
135
+ logger ? logger.error(message) : warn(message)
136
+ rescue
137
+ # Diagnostics cannot change the combined error or repeat the operation.
138
+ nil
139
+ end
140
+
105
141
  # Subclasses created by .from_agent override this method.
106
142
  # It deliberately remains private so it is not part of the public Tool API.
107
143
  def execute_async(input:, cancellation_token: nil, config: {})
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.30.1"
4
+ VERSION = "0.31.0"
5
5
  end
@@ -108,7 +108,7 @@ module Phronomy
108
108
  attr_reader persistence: Store
109
109
  def knowledge_snapshot: () -> Array[KnowledgeItem]
110
110
  def event_listener: () -> (^(Object) -> void)?
111
- def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[execution_result]
111
+ def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> TaskResult[execution_result]
112
112
  def resume_async: (String execution_id, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
113
113
  def resume: (String execution_id, ?config: Hash[Symbol, Object]) -> execution_result
114
114
  def cancel_async: (String execution_id) -> TaskResult[ExecutionObservation]
@@ -116,10 +116,10 @@ module Phronomy
116
116
 
117
117
  # Public operation methods no longer accept per-invocation event listeners
118
118
  # or event-listener blocks.
119
- def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> Hash[Symbol, untyped]
120
- def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[Hash[Symbol, untyped]]
121
- def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> Hash[Symbol, untyped]
122
- def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[Hash[Symbol, untyped]]
119
+ def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> Hash[Symbol, untyped]
120
+ def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> TaskResult[Hash[Symbol, untyped]]
121
+ def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> Hash[Symbol, untyped]
122
+ def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> TaskResult[Hash[Symbol, untyped]]
123
123
 
124
124
  def approve: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> untyped
125
125
  def approve_async: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> TaskResult[untyped]
@@ -131,6 +131,8 @@ module Phronomy
131
131
  def resolve_async: (String execution_id, expected_execution_revision: Integer, subject: Hash[Symbol, untyped], outcome: Symbol, ?result: untyped, ?error: Exception) -> TaskResult[untyped]
132
132
 
133
133
  def transcript: () -> Array[untyped]
134
+ def transcript_messages: () -> Array[LLMAdapter::Message]
135
+ def transcript_messages_async: () -> TaskResult[Array[LLMAdapter::Message]]
134
136
  def add_knowledge: (untyped content, ?metadata: Hash[untyped, untyped]) -> self
135
137
  def clear_transcript!: () -> untyped
136
138
  def clear_knowledge!: () -> untyped
@@ -3,5 +3,6 @@ module Phronomy
3
3
  def self.map: [A, B] (_CompletionHandle[A] source, name: String, ?on_error: (^(Exception) -> B)?) { (A) -> B } -> TaskResult[B]
4
4
  def self.call: [A, B] (name: String, ?on_error: (^(Exception) -> B)?, ?transform: ^(A) -> B) { () -> _CompletionHandle[A] } -> TaskResult[B]
5
5
  def self.capture: [V] (name: String, ?on_error: (^(Exception) -> V)?) { () -> V } -> TaskResult[V]
6
+ def self.finalize: [V] (name: String, cleanup: ^() -> TaskResult[untyped], on_cleanup_error: ^(V?, Exception?, Exception) -> Exception) { () -> TaskResult[V] } -> TaskResult[V]
6
7
  end
7
8
  end
@@ -10,7 +10,7 @@ module Phronomy
10
10
  def self.open: [R] (**untyped options) { (GeneratorVerifier pipeline) -> (R | TaskResult[R]) } -> R
11
11
  def initialize: (draft_agent: singleton(Agent::Base), review_agent: singleton(Agent::Base),
12
12
  draft_prompt_builder: untyped, review_prompt_builder: untyped,
13
- ?citation_schema: Hash[String | Symbol, untyped],
13
+ ?citation_schema: Hash[String | Symbol, untyped], ?schema_delivery: (:native | :none),
14
14
  ?confidence_threshold: Numeric, ?max_iterations: Integer,
15
15
  ?raise_if_unaccepted: bool, ?on_approval: _ApprovalListener?) -> void
16
16
  def invoke: (untyped input, ?config: Hash[Symbol, untyped]) -> Result
@@ -0,0 +1,14 @@
1
+ module Phronomy
2
+ module Tools
3
+ class Agent < Phronomy::Tool::Base
4
+ class CleanupError < Phronomy::ToolError
5
+ attr_reader child_agent: Phronomy::Agent::Base
6
+ attr_reader agent_id: String
7
+ attr_reader result: untyped
8
+ attr_reader primary_error: Exception?
9
+ attr_reader cleanup_error: Exception
10
+ def initialize: (child_agent: Phronomy::Agent::Base, result: untyped, primary_error: Exception?, cleanup_error: Exception) -> void
11
+ end
12
+ end
13
+ end
14
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.1
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-24 00:00:00.000000000 Z
11
+ date: 2026-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -193,6 +193,7 @@ files:
193
193
  - docs/architecture/agent-configuration-and-tool-binding.md
194
194
  - docs/architecture/agent-context.md
195
195
  - docs/architecture/agent-iteration-budget.md
196
+ - docs/architecture/agent-tool-child-lifetime.md
196
197
  - docs/architecture/agent-transition-ownership.md
197
198
  - docs/architecture/backend-groups.md
198
199
  - docs/architecture/before-llm-input.md
@@ -341,6 +342,7 @@ files:
341
342
  - docs/migrations/0.16.md
342
343
  - docs/migrations/0.19.md
343
344
  - docs/migrations/0.22.md
345
+ - docs/migrations/0.31.md
344
346
  - docs/migrations/durable-semantic-coordination-v2.md
345
347
  - docs/migrations/handoff-runner-multi-agent.md
346
348
  - docs/migrations/neutral-storage-spi.md
@@ -534,6 +536,7 @@ files:
534
536
  - lib/phronomy/execution/concurrency/physical_completion_task.rb
535
537
  - lib/phronomy/execution/concurrency/result_collector.rb
536
538
  - lib/phronomy/execution/concurrency/result_composition.rb
539
+ - lib/phronomy/execution/concurrency/result_finalization.rb
537
540
  - lib/phronomy/execution/concurrency/result_subscriptions.rb
538
541
  - lib/phronomy/execution/concurrency/subscriptions.rb
539
542
  - lib/phronomy/execution/concurrency/worker_input_restricted.rb
@@ -818,6 +821,7 @@ files:
818
821
  - sig/phronomy/storage/backends/in_memory.rbs
819
822
  - sig/phronomy/storage/contracts.rbs
820
823
  - sig/phronomy/tool.rbs
824
+ - sig/phronomy/tools/agent.rbs
821
825
  - sig/phronomy/tracing/observation.rbs
822
826
  - sig/phronomy/vector_store/async/async_client.rbs
823
827
  - sig/phronomy/vector_store/base.rbs