phronomy 0.31.0 → 0.33.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +13 -1
  4. data/RELEASE_CHECKLIST.md +4 -0
  5. data/docs/architecture/structured-output-and-scope.md +5 -4
  6. data/docs/architecture/trust-acceptance-followup.md +15 -0
  7. data/docs/context-policy-budgeting.md +108 -0
  8. data/docs/examples/shared_state_custom_sharing.rb +145 -0
  9. data/docs/features.md +4 -0
  10. data/docs/mcp-client.md +4 -2
  11. data/docs/migrations/shared-state-multi-agent.md +5 -0
  12. data/docs/runtime-and-concurrency.md +195 -0
  13. data/docs/shared-state-sharing.md +223 -0
  14. data/lib/phronomy/agent/approval_wait.rb +8 -0
  15. data/lib/phronomy/agent/async_event_api.rb +60 -0
  16. data/lib/phronomy/agent/base.rb +54 -1
  17. data/lib/phronomy/agent/execution/exact_execution.rb +4 -0
  18. data/lib/phronomy/agent/execution/execution_coordinator.rb +100 -2
  19. data/lib/phronomy/agent/journal/journal_projection.rb +24 -1
  20. data/lib/phronomy/agent/lifecycle/hosted_invocation.rb +67 -0
  21. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +30 -0
  22. data/lib/phronomy/agent/lifecycle/operation_lease.rb +7 -0
  23. data/lib/phronomy/agent/lifecycle/operation_result.rb +12 -0
  24. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +86 -14
  25. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +1 -1
  26. data/lib/phronomy/agent/store.rb +52 -0
  27. data/lib/phronomy/agent/tool_execution/tool_approval_request.rb +3 -2
  28. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +12 -36
  29. data/lib/phronomy/context/context_policy.rb +11 -2
  30. data/lib/phronomy/context/context_policy_input.rb +26 -0
  31. data/lib/phronomy/context/context_policy_input_builder.rb +27 -19
  32. data/lib/phronomy/engine/fsm_session.rb +9 -3
  33. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +87 -5
  34. data/lib/phronomy/multi_agent/orchestrator.rb +30 -18
  35. data/lib/phronomy/multi_agent/shared_state/binding.rb +44 -0
  36. data/lib/phronomy/multi_agent/shared_state/member_lifecycle_error.rb +32 -0
  37. data/lib/phronomy/multi_agent/shared_state.rb +147 -2
  38. data/lib/phronomy/tool/base.rb +69 -3
  39. data/lib/phronomy/tool/schema.rb +23 -11
  40. data/lib/phronomy/version.rb +1 -1
  41. data/lib/phronomy/workflow/execution/workflow.rb +9 -0
  42. data/lib/phronomy/workflow/execution/workflow_runner.rb +62 -14
  43. data/scripts/api_snapshot.rb +8 -3
  44. data/sig/phronomy/agent/execution_contract.rbs +13 -0
  45. data/sig/phronomy/agent/store.rbs +1 -0
  46. data/sig/phronomy/agent.rbs +2 -0
  47. data/sig/phronomy/context.rbs +3 -0
  48. data/sig/phronomy/multi_agent.rbs +28 -2
  49. data/sig/phronomy/tool.rbs +1 -0
  50. data/sig/phronomy/workflow.rbs +1 -0
  51. metadata +9 -2
@@ -363,10 +363,17 @@ module Phronomy
363
363
  # @api public
364
364
  def validate_arguments(args)
365
365
  schema = parameters_schema
366
- if !@validation_schema || @validation_schema.json_schema != schema
367
- @validation_schema = Schema.new(schema)
366
+ validator = @validation_schema
367
+ if !validator || validator.json_schema != schema
368
+ declaration = self.class.parameters_schema_definition
369
+ validator = if Schema === declaration && declaration.instance_of?(Schema) && declaration.json_schema.equal?(schema)
370
+ declaration.for_tool_instance
371
+ else
372
+ Schema.new(schema)
373
+ end
374
+ @validation_schema = validator
368
375
  end
369
- values, error = @validation_schema.validate(args || {}, coerce: self.class.on_schema_error == :coerce)
376
+ values, error = validator.validate(args || {}, coerce: self.class.on_schema_error == :coerce)
370
377
  if values && !self.class.parameters_schema_definition
371
378
  # Legacy optional param nil means omission, preserving Ruby defaults.
372
379
  self.class.declared_parameters.each do |name, parameter|
@@ -442,6 +449,24 @@ module Phronomy
442
449
  {}
443
450
  end
444
451
 
452
+ # Selects the arguments and policy facts that may appear in a human approval
453
+ # request. Arguments may be raw or validated. This does not change either input.
454
+ # @api public
455
+ def approval_display(arguments:, facts:)
456
+ sensitive = arguments.transform_keys(&:to_sym).slice(*self.class.redact_params).values
457
+ indexed = {}
458
+ sensitive.each do |value|
459
+ if Hash === value || Array === value
460
+ approval_comparison_key(value) { |key| indexed[key] = true }
461
+ end
462
+ end
463
+ display_facts = redact_approval_fact(facts) do |value|
464
+ sensitive_fact_value?(value, sensitive) || (!indexed.empty? && indexed.key?(approval_comparison_key(value)))
465
+ end
466
+ # If the entire facts Hash matches a sensitive argument, hide all of its keys.
467
+ {arguments: redacted_args(arguments), facts: display_facts.is_a?(Hash) ? display_facts : {}}
468
+ end
469
+
445
470
  # @api public
446
471
  def execute(**_args)
447
472
  raise NotImplementedError, "#{self.class}#execute is not implemented"
@@ -536,6 +561,47 @@ module Phronomy
536
561
  end
537
562
  end
538
563
 
564
+ # Index each sensitive subtree and scalar once. Object member names describe
565
+ # structure; its member values and array elements can be copied into facts.
566
+ def approval_comparison_key(value, &index)
567
+ key = case value
568
+ when Hash then value.transform_values { |item| approval_comparison_key(item, &index) }
569
+ when Array then value.map { |item| approval_comparison_key(item, &index) }
570
+ when Symbol then value.to_s
571
+ when Float then (value.finite? && value == value.to_i) ? value.to_i : value
572
+ else value
573
+ end
574
+ index&.call(key)
575
+ key
576
+ end
577
+
578
+ def sensitive_fact_value?(value, sensitive_values)
579
+ sensitive_values.any? do |sensitive|
580
+ sensitive == value ||
581
+ (Symbol === value && value.to_s == sensitive) ||
582
+ (Symbol === sensitive && sensitive.to_s == value)
583
+ end
584
+ end
585
+
586
+ def redact_approval_fact(value, &sensitive)
587
+ return "[REDACTED]" if sensitive.call(value)
588
+
589
+ case value
590
+ when Hash
591
+ value.each_with_object({}) do |(key, item), result|
592
+ next if sensitive.call(key)
593
+
594
+ result[key] = redact_approval_fact(item, &sensitive)
595
+ end.freeze
596
+ when Array
597
+ value.map { |item| redact_approval_fact(item, &sensitive) }.freeze
598
+ when String
599
+ "[REDACTED]"
600
+ else
601
+ value
602
+ end
603
+ end
604
+
539
605
  def nested_schema_to_json_schema(nested)
540
606
  nested.each_with_object({}) do |(prop_name, spec), result|
541
607
  type = spec[:type].to_s
@@ -43,19 +43,17 @@ module Phronomy
43
43
  unless @json_schema.is_a?(Hash) && @json_schema["type"] == "object"
44
44
  raise ArgumentError, "Tool schema must declare an object"
45
45
  end
46
+ @json_schema = Phronomy::Values::Immutable.copy(@json_schema)
46
47
  @references = []
47
48
  validate_keywords!(@json_schema)
48
- options = {ref_resolver: ->(uri) { raise ArgumentError, "external Tool schema reference: #{uri}" }}
49
- unless JSONSchemer.valid_schema?(@json_schema, **options)
50
- raise ArgumentError, "invalid Tool JSON Schema"
51
- end
52
- @json_schema = Phronomy::Values::Immutable.copy(@json_schema)
53
- @validator = JSONSchemer.schema(@json_schema, **options)
54
- @references.each { |reference| @validator.ref(reference) }
55
- @references = nil
56
- freeze
57
- rescue JSONSchemer::InvalidRefPointer, JSONSchemer::InvalidRefResolution => error
58
- raise ArgumentError, "invalid local Tool schema reference: #{error.message}"
49
+ @references.freeze
50
+ compile_validator!
51
+ end
52
+
53
+ # Build an independent validator from this immutable Tool definition.
54
+ # @api private
55
+ def for_tool_instance
56
+ dup.compile_validator!
59
57
  end
60
58
 
61
59
  # @api public
@@ -99,6 +97,20 @@ module Phronomy
99
97
  end
100
98
  end
101
99
 
100
+ protected
101
+
102
+ def compile_validator!
103
+ options = {ref_resolver: ->(uri) { raise ArgumentError, "external Tool schema reference: #{uri}" }}
104
+ unless JSONSchemer.valid_schema?(@json_schema, **options)
105
+ raise ArgumentError, "invalid Tool JSON Schema"
106
+ end
107
+ @validator = JSONSchemer.schema(@json_schema, **options)
108
+ @references.each { |reference| @validator.ref(reference) }
109
+ freeze
110
+ rescue JSONSchemer::InvalidRefPointer, JSONSchemer::InvalidRefResolution => error
111
+ raise ArgumentError, "invalid local Tool schema reference: #{error.message}"
112
+ end
113
+
102
114
  private
103
115
 
104
116
  def error_message(error)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.31.0"
4
+ VERSION = "0.33.0"
5
5
  end
@@ -45,6 +45,15 @@ module Phronomy
45
45
  @runner.send_event(state: state, event: event, input: input)
46
46
  end
47
47
 
48
+ # Applies an explicitly named event to one revision of a saved wait.
49
+ # The host holds its exclusive claim for this Workflow across the call.
50
+ # @return [WorkflowContext] the next halted or completed context
51
+ # @api public
52
+ def send_saved_event(workflow_instance_id:, expected_revision:, event:, input: nil)
53
+ @runner.send_saved_event(workflow_instance_id: workflow_instance_id,
54
+ expected_revision: expected_revision, event: event, input: input)
55
+ end
56
+
48
57
  # Sends an event to an active Workflow execution without blocking.
49
58
  #
50
59
  # +workflow_instance_id+ is the logical/durable Workflow identity. EventLoop resolves it
@@ -54,6 +54,7 @@ module Phronomy
54
54
  :input,
55
55
  :event_name,
56
56
  :current_phase,
57
+ :expected_revision,
57
58
  :result_task,
58
59
  :workflow_instance_id,
59
60
  :owner_token
@@ -141,13 +142,28 @@ module Phronomy
141
142
  end
142
143
 
143
144
  start_resume_execution(
144
- state,
145
+ workflow_instance_id,
146
+ state: state,
145
147
  input: input,
146
148
  event_name: event_name,
147
149
  current_phase: current_phase
148
150
  ).wait_result
149
151
  end
150
152
 
153
+ def send_saved_event(workflow_instance_id:, expected_revision:, event:, input: nil)
154
+ if Phronomy::WaitPolicy.blocking_forbidden?
155
+ raise Phronomy::EventLoopReentrancyError, "send_saved_event requires a host thread"
156
+ end
157
+ unless workflow_instance_id.is_a?(String) && !workflow_instance_id.empty?
158
+ raise ArgumentError, "workflow_instance_id must be a nonempty String"
159
+ end
160
+ unless expected_revision.is_a?(Integer) && expected_revision.positive?
161
+ raise ArgumentError, "expected_revision must be a positive Integer"
162
+ end
163
+ start_resume_execution(workflow_instance_id.dup, expected_revision: expected_revision,
164
+ input: input, event_name: event).wait_result
165
+ end
166
+
151
167
  # Posts an application-defined event to the currently live Workflow owner.
152
168
  # WorkflowExecutionRegistry resolves workflow_instance_id to the current concrete
153
169
  # fsm_session_id. The opaque admission owner token is deliberately not a
@@ -256,8 +272,8 @@ module Phronomy
256
272
  result_task || failed_task("workflow:preparation", error)
257
273
  end
258
274
 
259
- def start_resume_execution(state, input:, event_name:, current_phase:)
260
- workflow_instance_id = state.workflow_instance_id.to_s.freeze
275
+ def start_resume_execution(workflow_instance_id, input:, event_name:, state: nil, current_phase: nil, expected_revision: nil)
276
+ workflow_instance_id = workflow_instance_id.to_s.freeze
261
277
  result_task = Phronomy::TaskResult.deferred(name: "workflow-resume:#{workflow_instance_id}")
262
278
  Phronomy::Tracing::Automatic.observe_task(
263
279
  result_task,
@@ -272,7 +288,8 @@ module Phronomy
272
288
  state: state,
273
289
  input: input,
274
290
  event_name: event_name.to_sym,
275
- current_phase: current_phase.to_sym,
291
+ current_phase: current_phase,
292
+ expected_revision: expected_revision,
276
293
  result_task: result_task,
277
294
  workflow_instance_id: workflow_instance_id,
278
295
  owner_token: Object.new.freeze
@@ -345,6 +362,7 @@ module Phronomy
345
362
 
346
363
  repository = configured_repository
347
364
  unless repository
365
+ raise Phronomy::ConfigurationError, "send_saved_event requires Workflow persistence" if request.expected_revision
348
366
  context = request.input ? request.state.merge(request.input) : request.state
349
367
  execution = Execution.new(
350
368
  context: context,
@@ -445,12 +463,9 @@ module Phronomy
445
463
  return
446
464
  end
447
465
 
448
- expected_revision = validate_resume_snapshot!(
449
- request.state,
450
- ready.result.snapshot,
451
- ready.result.revision
452
- )
453
- context = request.input ? request.state.merge(request.input) : request.state
466
+ state = request.state || restore_saved_wait!(request, ready.result)
467
+ expected_revision = validate_resume_snapshot!(state, ready.result.snapshot, ready.result.revision)
468
+ context = request.input ? state.merge(request.input) : state
454
469
  execution = Execution.new(
455
470
  context: context,
456
471
  workflow_instance_id: request.workflow_instance_id,
@@ -465,12 +480,28 @@ module Phronomy
465
480
  execution,
466
481
  request.result_task,
467
482
  resume_event: request.event_name,
468
- resume_phase: request.current_phase
483
+ resume_phase: request.current_phase || state.phase,
484
+ require_resume_transition: !request.expected_revision.nil?
469
485
  )
470
486
  rescue => error
471
487
  release_and_fail(request, error)
472
488
  end
473
489
 
490
+ def restore_saved_wait!(request, loaded)
491
+ unless loaded.snapshot && loaded.revision == request.expected_revision
492
+ raise Phronomy::Persistence::StateConflictError,
493
+ "No saved Workflow at revision #{request.expected_revision} for #{request.workflow_instance_id.inspect}"
494
+ end
495
+ phase = record_value(loaded.snapshot, :phase)&.to_sym
496
+ unless @wait_state_names.include?(phase) && @external_events.fetch(request.event_name, []).any? { |edge| edge[:from] == phase }
497
+ raise ArgumentError, "No saved wait at #{phase.inspect} accepting #{request.event_name.inspect}"
498
+ end
499
+ fields = record_value(loaded.snapshot, :fields)
500
+ @state_class.new(**restore_context_fields(fields)).set_graph_metadata(
501
+ workflow_instance_id: request.workflow_instance_id, phase: phase
502
+ )
503
+ end
504
+
474
505
  def build_new_execution(
475
506
  input,
476
507
  workflow_instance_id:,
@@ -487,9 +518,7 @@ module Phronomy
487
518
  stored_fields = loaded_snapshot &&
488
519
  (loaded_snapshot[:fields] || loaded_snapshot["fields"])
489
520
  initial_fields = if stored_fields
490
- stored_fields
491
- .transform_keys(&:to_sym)
492
- .merge(input.transform_keys(&:to_sym))
521
+ restore_context_fields(stored_fields).merge(input.transform_keys(&:to_sym))
493
522
  else
494
523
  input
495
524
  end
@@ -509,6 +538,21 @@ module Phronomy
509
538
  )
510
539
  end
511
540
 
541
+ # The repository and the load message own immutable snapshots. Actions receive
542
+ # their own mutable field values, including strings nested in arrays or hashes.
543
+ def restore_context_fields(fields)
544
+ fields.to_h { |name, value| [name.to_sym, mutable_field_value(value)] }
545
+ end
546
+
547
+ def mutable_field_value(value)
548
+ case value
549
+ when Hash then value.transform_values { |child| mutable_field_value(child) }
550
+ when Array then value.map { |child| mutable_field_value(child) }
551
+ when String then value.dup
552
+ else value
553
+ end
554
+ end
555
+
512
556
  def validate_resume_snapshot!(state, durable_snapshot, durable_revision)
513
557
  return nil unless durable_snapshot
514
558
 
@@ -576,6 +620,7 @@ module Phronomy
576
620
  result_task,
577
621
  resume_event: nil,
578
622
  resume_phase: nil,
623
+ require_resume_transition: false,
579
624
  stable_observer: nil
580
625
  )
581
626
  assert_execution_owner!
@@ -588,6 +633,7 @@ module Phronomy
588
633
  execution: execution,
589
634
  resume_event: resume_event,
590
635
  resume_phase: resume_phase,
636
+ require_resume_transition: require_resume_transition,
591
637
  stable_observer: stable_observer,
592
638
  task_execution: task_execution
593
639
  )
@@ -769,6 +815,7 @@ module Phronomy
769
815
  execution:,
770
816
  resume_event: nil,
771
817
  resume_phase: nil,
818
+ require_resume_transition: false,
772
819
  stable_observer: nil,
773
820
  task_execution: nil
774
821
  )
@@ -787,6 +834,7 @@ module Phronomy
787
834
  recursion_limit: execution.recursion_limit,
788
835
  resume_event: resume_event,
789
836
  resume_phase: resume_phase,
837
+ require_resume_transition: require_resume_transition,
790
838
  stable_observer: stable_observer,
791
839
  persist: terminal_persistence_callback(execution),
792
840
  task_execution: task_execution
@@ -30,6 +30,8 @@ PUBLIC_API_ENTRIES = [
30
30
  Phronomy::Embeddings::AuthenticationError,
31
31
  Phronomy::Embeddings::RateLimitError,
32
32
  Phronomy::Agent::Base,
33
+ Phronomy::Agent::HostedInvocation,
34
+ Phronomy::Agent::ApprovalWait,
33
35
  Phronomy::Agent::ReservedExecution,
34
36
  Phronomy::Agent::ExecutionObservation,
35
37
  Phronomy::Agent::KnowledgeItem,
@@ -104,9 +106,12 @@ def snapshot_entry(klass)
104
106
  "public_class_methods" => (klass.public_methods(false) - BASELINE_CLASS_METHODS).sort
105
107
  }
106
108
  else
107
- internal_context_methods = (klass == Phronomy::InvocationContext) ?
108
- %i[__bind_execution __execution_scope] : []
109
- instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS - internal_context_methods).sort
109
+ internal_methods = case klass.name
110
+ when "Phronomy::InvocationContext" then %i[__bind_execution __execution_scope]
111
+ when "Phronomy::Tool::Schema" then %i[for_tool_instance]
112
+ else []
113
+ end
114
+ instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS - internal_methods).sort
110
115
  internal_class_methods = (klass == Phronomy::Storage::AsyncClient) ? %i[submit] : []
111
116
  class_methods = (klass.public_methods(false) - BASELINE_CLASS_METHODS - internal_class_methods).sort
112
117
  {
@@ -4,6 +4,19 @@ module Phronomy
4
4
  type execution_wiring = { ?phronomy_execution_participant: _ExecutionParticipant, ?phronomy_control_bindings: Array[_ControlCapability], ?phronomy_transfer_context: TransferContext, ?phronomy_admission: _AdmissionOwner, ?phronomy_reservation: Hash[String, json_value] }
5
5
  type execution_result = { agent_id: String, execution_id: String, status: Symbol, result: json_value, output: json_value, ?raw_output: String?, error: Hash[String, json_value]? }
6
6
 
7
+ class ApprovalWait < Data
8
+ attr_reader execution_id: String
9
+ attr_reader approval_request_id: String
10
+ attr_reader execution_revision: Integer
11
+ def self.new: (execution_id: String, approval_request_id: String, execution_revision: Integer) -> instance
12
+ end
13
+
14
+ class HostedInvocation
15
+ def on_complete: () { (untyped, StandardError?) -> untyped } -> self
16
+ def detach!: (approval_request_id: String, ?execution_id: String) -> ApprovalWait
17
+ def wait_result: (?timeout: Numeric?) -> (execution_result | ApprovalWait)
18
+ end
19
+
7
20
  class ReservedExecution < Data
8
21
  attr_reader agent_id: String
9
22
  attr_reader execution_id: String
@@ -54,6 +54,7 @@ module Phronomy
54
54
  def authorized_operations: (::Phronomy::Persistence::Transaction scope, agent_id: String, execution_id: String, invocation_id: String, name: String, arguments: Hash[String, untyped], names: Array[String]) -> Array[AuthorizedOperation]
55
55
  def result: (String execution_id) -> Hash[Symbol, untyped]
56
56
  def runs: (String owner_id, ?after: String?, ?limit: Integer) -> Array[untyped]
57
+ def standalone_approval_wait: (agent_id: String, execution_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> ApprovalWait?
57
58
  def observe_execution: (agent_id: String, execution_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> ExecutionObservation
58
59
  def active_executions: (agent_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> Array[ExecutionObservation]
59
60
  def definition: (agent_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> { id: String, version: Integer }
@@ -83,6 +83,7 @@ module Phronomy
83
83
  def self.instructions: (?untyped text) { (untyped) -> untyped } -> untyped
84
84
  | (?untyped text) -> untyped
85
85
  def self.tools: (?Hash[Class, String?] definitions) -> untyped
86
+ def self.with_tools: (Hash[Class, String?] definitions, id: String, version: Integer) -> singleton(Base)
86
87
  def self.tool_aliases: () -> Hash[Class, String]
87
88
  def self.provider: (?Symbol name) -> Symbol?
88
89
  def self.temperature: (?Float value) -> Float?
@@ -108,6 +109,7 @@ module Phronomy
108
109
  attr_reader persistence: Store
109
110
  def knowledge_snapshot: () -> Array[KnowledgeItem]
110
111
  def event_listener: () -> (^(Object) -> void)?
112
+ def start_hosted: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> HostedInvocation
111
113
  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
114
  def resume_async: (String execution_id, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
113
115
  def resume: (String execution_id, ?config: Hash[Symbol, Object]) -> execution_result
@@ -110,6 +110,8 @@ module Phronomy
110
110
  attr_reader previous_manifest: LLMInputManifest?
111
111
  attr_reader metadata: Hash[untyped, untyped]
112
112
  def initialize: (agent_id: String, ?execution_id: String?, call_sequence: Integer, call_mode: Symbol, ?instruction: Array[InstructionItem], ?knowledge: Array[KnowledgeItem], ?tools: Array[ToolItem], ?conversation: Array[Array[ConversationItem]], token_budget: untyped, ?model_config: Hash[untyped, untyped], previous_manifest: LLMInputManifest?, ?metadata: Hash[untyped, untyped]) -> void
113
+ def input_token_limit: () -> Integer?
114
+ def with_input_token_limit: (Integer limit) -> ContextPolicyInput
113
115
  end
114
116
 
115
117
  class ContextPlan
@@ -124,6 +126,7 @@ module Phronomy
124
126
  class ContextPolicy
125
127
  def call: (ContextPolicyInput input) -> ContextPlan
126
128
 
129
+ def estimate_tokens: (untyped content) -> Integer
127
130
  def instruction_item: (content: untyped, ?role: Symbol, ?kind: Symbol, ?content_format: Symbol?, ?required: bool, ?metadata: Hash[untyped, untyped]) -> ContextPolicyInput::InstructionItem
128
131
  def knowledge_item: (content: untyped, ?role: Symbol, ?kind: Symbol, ?content_format: Symbol?, ?required: bool, ?metadata: Hash[untyped, untyped]) -> ContextPolicyInput::KnowledgeItem
129
132
  def conversation_item: (content: untyped, role: Symbol, ?kind: Symbol, ?content_format: Symbol?, ?sequence: Integer?, ?required: bool, ?tool_call_id: String?, ?tool_call_ids: Array[String], ?delivery: Symbol, ?metadata: Hash[untyped, untyped]) -> ContextPolicyInput::ConversationItem
@@ -1,6 +1,29 @@
1
1
  module Phronomy
2
2
  module MultiAgent
3
3
  class SharedState
4
+ interface _SharingTools
5
+ def call: (member: singleton(::Phronomy::Agent::Base), cycle: Integer) -> Hash[singleton(::Phronomy::Tool::Base), untyped]
6
+ end
7
+
8
+ interface _SharingProvider
9
+ def open: (input: untyped, config: Hash[Symbol, untyped]) { (Binding) -> untyped } -> untyped
10
+ end
11
+
12
+ class Binding
13
+ def self.new: (store: untyped, tools: _SharingTools) -> instance
14
+ attr_reader store: untyped
15
+ def tool_definitions: (member: singleton(::Phronomy::Agent::Base), cycle: Integer) -> Hash[singleton(::Phronomy::Tool::Base), String?]
16
+ end
17
+
18
+ class MemberLifecycleError < ::Phronomy::Error
19
+ attr_reader agent: ::Phronomy::Agent::Base
20
+ attr_reader result: untyped
21
+ attr_reader reason: Symbol
22
+ attr_reader primary_error: Exception?
23
+ attr_reader release_error: Exception?
24
+ def self.new: (agent: ::Phronomy::Agent::Base, result: untyped, reason: Symbol, ?primary_error: Exception?, ?release_error: Exception?) -> instance
25
+ end
26
+
4
27
  class KnowledgeStore
5
28
  def self.new: () -> instance
6
29
  def read_all: () -> Array[Hash[Symbol, untyped]]
@@ -13,8 +36,9 @@ module Phronomy
13
36
  def self.coordination: (?untyped text) -> untyped
14
37
  def self.max_cycles: (?untyped value) -> Integer?
15
38
  def self.timeout: (?untyped value) -> Float?
16
- def self.terminate_when: () ?{ (KnowledgeStore) -> untyped } -> untyped
17
- def self.aggregate: () ?{ (KnowledgeStore) -> untyped } -> untyped
39
+ def self.terminate_when: () ?{ (untyped) -> untyped } -> untyped
40
+ def self.aggregate: () ?{ (untyped) -> untyped } -> untyped
41
+ def self.sharing: (_SharingProvider provider, id: String, version: Integer) -> Hash[Symbol, untyped]
18
42
  def invoke: (untyped input, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
19
43
  end
20
44
 
@@ -23,6 +47,8 @@ module Phronomy
23
47
  def self.create: (?coordination_store: Store?, **Object options) ?{ (Object) -> void } -> instance
24
48
  def self.load: (String agent_id, persistence: Agent::Store, coordination_store: Store, **Object options) ?{ (Object) -> void } -> instance
25
49
  def self.subagent: (Symbol | String name, singleton(::Phronomy::Agent::Base) agent_class, ?on_error: Symbol, ?inherit_knowledge: bool) -> untyped
50
+ def unload_with_completed_child!: (String parent_execution_id, execution_id: String) -> bool
51
+ def approve_hosted_child: (String parent_execution_id, execution_id: String, approval_request_id: String, ?approved: bool, ?config: Hash[Symbol, untyped]) -> ::Phronomy::Agent::HostedInvocation
26
52
  def self.registered_subagents: () -> Hash[Symbol | String, untyped]
27
53
  def resume: (String execution_id, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
28
54
  def dispatch_parallel: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> Array[untyped]
@@ -55,6 +55,7 @@ module Phronomy
55
55
  def requires_approval?: () -> untyped
56
56
  def tool_origin: () -> Symbol
57
57
  def approval_metadata: () -> Hash[untyped, untyped]
58
+ def approval_display: (arguments: Hash[String | Symbol, untyped], facts: Hash[untyped, untyped]) -> { arguments: Hash[String | Symbol, untyped], facts: Hash[untyped, untyped] }
58
59
  def execute: (**untyped args) -> untyped
59
60
 
60
61
  private
@@ -18,6 +18,7 @@ module Phronomy
18
18
  def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) { (untyped) -> void } -> untyped
19
19
  def resume: (state: untyped, ?input: untyped) -> untyped
20
20
  def send_event: (state: untyped, event: Symbol, ?input: untyped) -> untyped
21
+ def send_saved_event: (workflow_instance_id: String, expected_revision: Integer, event: Symbol, ?input: untyped) -> untyped
21
22
  def signal: (workflow_instance_id: String, event: Symbol, ?payload: untyped) -> bool
22
23
 
23
24
  class Builder
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.31.0
4
+ version: 0.33.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-25 00:00:00.000000000 Z
11
+ date: 2026-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -257,6 +257,7 @@ files:
257
257
  - docs/archive/design/historical/06_design_decisions.md
258
258
  - docs/async-composition.md
259
259
  - docs/changelog/0.14-and-earlier.md
260
+ - docs/context-policy-budgeting.md
260
261
  - docs/decisions/001-rubyllm-as-provider-layer.md
261
262
  - docs/decisions/002-workflow-context-immutability.md
262
263
  - docs/decisions/003-event-loop-singleton.md
@@ -333,6 +334,7 @@ files:
333
334
  - docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md
334
335
  - docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md
335
336
  - docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md
337
+ - docs/examples/shared_state_custom_sharing.rb
336
338
  - docs/execution-boundaries-preview.md
337
339
  - docs/failure-reason-codes.md
338
340
  - docs/features.md
@@ -376,6 +378,7 @@ files:
376
378
  - docs/ordered-event-delivery.md
377
379
  - docs/persistence-backends.md
378
380
  - docs/runtime-and-concurrency.md
381
+ - docs/shared-state-sharing.md
379
382
  - docs/workflow-completion.md
380
383
  - examples/README.md
381
384
  - gemfiles/http_minimum.gemfile
@@ -389,6 +392,7 @@ files:
389
392
  - lib/phronomy/agent/agent_unloaded_error.rb
390
393
  - lib/phronomy/agent/api/agent.rb
391
394
  - lib/phronomy/agent/api/execution_rehydration_required_error.rb
395
+ - lib/phronomy/agent/approval_wait.rb
392
396
  - lib/phronomy/agent/async_event_api.rb
393
397
  - lib/phronomy/agent/base.rb
394
398
  - lib/phronomy/agent/before_llm_input.rb
@@ -429,6 +433,7 @@ files:
429
433
  - lib/phronomy/agent/knowledge_item.rb
430
434
  - lib/phronomy/agent/lifecycle/agent_root.rb
431
435
  - lib/phronomy/agent/lifecycle/default_persistence.rb
436
+ - lib/phronomy/agent/lifecycle/hosted_invocation.rb
432
437
  - lib/phronomy/agent/lifecycle/lifetime_api.rb
433
438
  - lib/phronomy/agent/lifecycle/operation_lease.rb
434
439
  - lib/phronomy/agent/lifecycle/operation_result.rb
@@ -614,6 +619,8 @@ files:
614
619
  - lib/phronomy/multi_agent/reserved_child_admission.rb
615
620
  - lib/phronomy/multi_agent/runtime_binding/engine_environment.rb
616
621
  - lib/phronomy/multi_agent/shared_state.rb
622
+ - lib/phronomy/multi_agent/shared_state/binding.rb
623
+ - lib/phronomy/multi_agent/shared_state/member_lifecycle_error.rb
617
624
  - lib/phronomy/multi_agent/store.rb
618
625
  - lib/phronomy/multi_agent/team_busy_error.rb
619
626
  - lib/phronomy/multi_agent/team_coordinator.rb