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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7eb1f72acaa35f02d5f848dac344ea971e6ef190544d2b200fe367409e48e92
4
- data.tar.gz: 26b2de048b30845bdbf973a5e26a6d5fa4b734295d31b81cec2530e9dd896dc8
3
+ metadata.gz: e696f701b22d91c4ccb8f566e28274b73e9fd69704a673e18a687a1d377b4583
4
+ data.tar.gz: 7a63749fb58c70fb6c74a843a601bb48401ffe35fa02de03a63fa9f8d6320552
5
5
  SHA512:
6
- metadata.gz: eaf92481864d37b545ccf7507541fbd64777eb6ff6369e2f8a674f31341bc2ec439883dfa2964d410c1becc2adc92c2794c9a7a2e6a7a2e5050a78e2ae4fa181
7
- data.tar.gz: 6ca6f91cc3bf270a2694cb5a8a194cc1f4fe8270440ada751a4082f0ae27489c0798743a351d8850e1fbf4e81eb0aac207bee563995e8aa821d2008b2b6916e2
6
+ metadata.gz: af029ecc2491d2912c48fef847f8501ce255040759bb5d8edba3949d2bea0352b2a6a89e731d70493570c5b72d26e094ed03d80aff412f6805af21a0da588219
7
+ data.tar.gz: 529ece7bc74b4096d332e9f39a3097933f1441797f5dc22b5bb9ec9d5b39fde2c42709d2b53e83b99a3e42ec6e2e9a227920f10371e6ac65772241cb54d0abc7
data/CHANGELOG.md CHANGED
@@ -12,6 +12,147 @@ Release history for 0.14.0 and earlier is archived in
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.33.0] - 2026-11-14
16
+
17
+ ### Added
18
+
19
+ - Add public `ContextPolicyInput#input_token_limit` and
20
+ `#with_input_token_limit(limit)` for immutable, non-expanding selection limits,
21
+ plus protected `ContextPolicy#estimate_tokens(content)` using the same
22
+ canonical estimator as generated Policy items. Applications can reserve input
23
+ capacity while retaining DefaultPolicy selection without constructing private
24
+ budget objects or calling private token-estimation helpers. These operations
25
+ do not change model capabilities, generation settings or final validation.
26
+ - Add `Agent::Base.with_tools(definitions, id:, version:)` to derive an Agent
27
+ class with a complete replacement Tool map and explicit definition identity.
28
+ The factory preserves Phronomy authoring settings and independently copies
29
+ filter lists; configured callable instances remain shared. Custom SharedState
30
+ uses this public operation after merging member and application Tools. General
31
+ Agent subclass inheritance remains unchanged.
32
+ - Add `MultiAgent::SharedState.sharing(provider, id:, version:)` and the public
33
+ `SharedState::Binding` value. A synchronous provider opens one application-owned
34
+ store for an invocation and supplies Tool definitions for each member/cycle.
35
+ Applications define their own records, search arguments and cleanup without
36
+ overriding SharedState private methods. Custom sharing requires an explicit
37
+ aggregation block and does not inject the default findings Tools or append
38
+ stored records to member prompts.
39
+ - Include the sharing provider's stable ID and semantic version in custom
40
+ generated Agent definition identity. Default SharedState retains its existing
41
+ built-in Tools, prompts and instrumentation revision 1. The extension adds no
42
+ database adapter, ContextPolicy implementation or durable sharing lifecycle.
43
+ - Release terminal custom-sharing members through the public Agent lifecycle
44
+ operation. Normal Agent approval waits keep the invocation and provider open.
45
+ `SharedState::MemberLifecycleError` reports failed release or a custom
46
+ `invoke` override returning a suspended result prematurely, with the Agent and
47
+ available result/error details. It does not automatically answer approvals or
48
+ release a pending execution; application providers retain responsibility for
49
+ resource cleanup and recovery policy.
50
+
51
+ ### Changed
52
+
53
+ - Reuse the immutable argument schema prepared by an explicit `parameters`
54
+ declaration. Each Tool instance compiles its own validator on first use.
55
+
56
+ ### Fixed
57
+
58
+ - Build each human-facing Tool approval item from the Tool's public
59
+ `approval_display(arguments:, facts:)` result. A Tool without this operation
60
+ contributes empty argument and fact maps to new approval requests. Tool
61
+ execution and policy evaluation still receive their original inputs.
62
+ - Apply the default approval display's sensitive-value check to values inside
63
+ a Tool's redacted hash or array arguments, including copies used as fact keys.
64
+
65
+ ### Security
66
+
67
+ - Use patched ActiveRecord 8.0 development dependencies, including ActiveSupport
68
+ 8.0.5.2 or later in the 8.0 series. Remove unused ActiveJob and SQLite3
69
+ development dependencies; the examples retain the dependencies they use.
70
+ The published gem's runtime dependencies are unchanged.
71
+ - Provide Bundler Audit in the development bundle and check the complete
72
+ dependency set in pull-request CI and immediately before release publication.
73
+
74
+ ### Compatibility and verification
75
+
76
+ - The new SharedState sharing, Agent Tool composition and Context budgeting APIs
77
+ require 0.33.0 or later and remain Experimental. Default sharing and ordinary
78
+ Agent subclass inheritance retain their existing behavior. Custom sharing is
79
+ synchronous and invocation-scoped; it does not add durable sharing recovery.
80
+ - Approval display is selected by each Tool. Custom Tool implementations without
81
+ `approval_display` expose empty display maps; execution and policy evaluation
82
+ still use the original arguments. Review custom display overrides for safe
83
+ human-facing fields when upgrading.
84
+ - Observed LM Studio/gpt-oss PEG-format failures remain unresolved.
85
+ The existing adapter identifies known errors and bounds retries; this release
86
+ does not claim to fix their root cause or establish full real-LLM acceptance.
87
+ The latest full examples verification recorded 102 passed, 1 failed and no
88
+ skips (Example 22: HTTP 400). A separate Qwen Example 22 run completed all three
89
+ cycles, but took 2,220 seconds and does not replace the full verifier result
90
+ or satisfy its 240-second per-example limit.
91
+
92
+ ## [0.32.0] - 2026-11-03
93
+
94
+ ### Added
95
+
96
+ - Add `Agent#start_hosted`, `Agent::HostedInvocation` and immutable
97
+ `Agent::ApprovalWait` for safely detaching an exclusively owned standalone Agent
98
+ at its first committed Tool approval wait. A new incarnation can use the
99
+ existing `load` and `approve` operations to continue the same execution.
100
+ - Add `Agent::Store#standalone_approval_wait` to query the committed approval fact without
101
+ transferring a live worker claim. Delivery deduplication, exclusive claims and
102
+ verified worker termination remain application responsibilities. Standalone
103
+ detachment does not transfer parent Workflows or shared children; supported
104
+ direct-child handoff and saved Workflow events are separate APIs below.
105
+
106
+ - Add `Workflow#send_saved_event` to restore a declared wait by Workflow instance
107
+ ID and expected revision before applying an event. The host can reject a
108
+ delayed delivery even when the same Workflow later returns to an identical wait.
109
+ - Allow an Orchestrator's hosted handle to detach its local parent and direct
110
+ child together at a saved child Tool approval. The host supplies the child's
111
+ execution and approval request IDs and retains the parent's execution ID for
112
+ resumption on another worker. Both workers can remain alive.
113
+ - Add `MultiAgent::Orchestrator#approve_hosted_child` to answer a restored
114
+ direct child's approval with a hosted handle. If the same execution asks for
115
+ another approval, the worker can release the pair at the new wait. A third
116
+ worker can restore that wait without replaying the earlier Tool call.
117
+ - Add `MultiAgent::Orchestrator#unload_with_completed_child!` for a worker
118
+ that has finished a direct child dispatch. It can release the local owners
119
+ while keeping the conversation, execution results and native slot history.
120
+
121
+ ### Fixed
122
+
123
+ - Bind native subagent dispatch context to each prepared Tool instance so that
124
+ cached Tool methods do not keep completed Orchestrators alive after unload
125
+ (Issue #440). Saved SQLite execution and conversation history is preserved.
126
+ - Keep distinct native Tool call/result pairs when a provider reuses its call ID
127
+ in later responses on the same Agent. A subsequent request can use the saved
128
+ conversation without executing either Tool again to rebuild context.
129
+ - Restore mutable Workflow context fields from durable checkpoints when
130
+ delivering a saved event or starting another invocation of the same instance.
131
+ Actions can update nested strings, arrays and hashes without changing the
132
+ original checkpoint.
133
+ - Continue using an Agent after a human rejects a Tool call. Model input for the
134
+ next turn omits the rejected, incomplete Tool exchange. The original journal
135
+ and public conversation history remain available for inspection.
136
+
137
+ ### Compatibility and verification
138
+
139
+ - Worker APIs require 0.32.0 or later; applications using `start_hosted` cannot
140
+ run against 0.31.0. The existing [0.31 storage and Agent Tool migration
141
+ requirements](docs/migrations/0.31.md) still apply. This release does not add a
142
+ persistence mapping migration or change those stored execution formats.
143
+ - Native handoff and completed-owner release cover one direct child in an
144
+ isolated dispatch batch. Delivery deduplication, exclusive claims, ACKs and
145
+ verified worker termination belong to the application. General graph transfer,
146
+ multi-host fencing, supervisor restart and recovery of unknown external Tool
147
+ effects are not established by these examples.
148
+ - Worker acceptance uses fixed HTTP responses, actual Ruby processes and SQLite,
149
+ including Ubuntu 24.04 and 26.04. It does not establish live-model acceptance.
150
+ - LM Studio/gpt-oss PEG parsing remains a known unresolved limitation. The latest
151
+ pre-release standard verifier reported 101 passed, 2 failed and 0 skipped;
152
+ Examples 01 and 22 received provider PEG errors, also reproduced before Worker
153
+ support. Known-error identification and bounded retry do not fix the provider's
154
+ root cause. These two failures are tracked separately from Worker acceptance.
155
+
15
156
  ## [0.31.0] - 2026-10-25
16
157
 
17
158
  ### Added
data/README.md CHANGED
@@ -37,18 +37,30 @@ tracking `main` directly.
37
37
 
38
38
  See [Features and Application Programming Interface (API) stability](docs/features.md) for the full feature matrix.
39
39
 
40
- The following table describes the application APIs available in v0.31.0.
40
+ The following table describes the application APIs available in v0.33.0.
41
+ The new sharing, Tool composition and Context budgeting operations remain
42
+ Experimental; their introduction version is shown below.
43
+ Worker hosting and completed parent/child release require v0.32.0 or later.
41
44
  Review the [0.31 migration notes](docs/migrations/0.31.md) before upgrading
42
45
  persistent applications or custom Agent Tool subclasses.
43
46
 
44
47
  | API | Stability | Contract |
45
48
  |---|---|---|
49
+ | `Context::ContextPolicyInput#input_token_limit / #with_input_token_limit` and protected `ContextPolicy#estimate_tokens` | Experimental (since v0.33.0) | Inspect and constrain selection budgets and estimate candidate content through Context-owned operations. [Policy budgeting](docs/context-policy-budgeting.md) |
50
+ | `Agent::Base.with_tools(definitions, id:, version:)` | Experimental (since v0.33.0) | Derive an Agent class with a complete replacement Tool map and explicit definition identity while preserving Phronomy authoring settings and filters. [Tool composition](docs/shared-state-sharing.md#deriving-an-agent-with-a-complete-tool-set) |
51
+ | `MultiAgent::SharedState.sharing(provider, id:, version:)`, `SharedState::Binding` and `SharedState::MemberLifecycleError` | Experimental (since v0.33.0) | Connect application-owned shared data and Tools to sequential coordination through a synchronous invocation-scoped provider. [Custom sharing contract and example](docs/shared-state-sharing.md) |
52
+ | `Tool::Base#approval_display(arguments:, facts:)` | Beta (since v0.33.0) | The Tool selects the arguments and derived facts a person can see in an approval request. [Approval display](docs/runtime-and-concurrency.md#tool-approval-display) |
46
53
  | `Phronomy::LLMAdapter::LMStudio` | Beta | Explicit known-output-error identification and bounded opt-in retry; [policy and evidence limits](docs/architecture/lm-studio-output-failures.md) |
47
54
  | Agent `output_schema:`, `schema_delivery:`, `Agent::OutputError` and `OutputParser::JsonSchemaParser` | Beta | Agent returns validated immutable JSON or an explicit output failure; [contract and migration](docs/architecture/structured-output-and-scope.md) |
48
55
  | Agent `transcript_messages` / `transcript_messages_async` | Beta | Read immutable conversation Message values with the same restoration rules as execution results; [reading conversation state](docs/getting-started.md) |
49
56
  | `Phronomy::GeneratorVerifier`, its approval/lifetime operations and immutable `Result` with output-failure diagnostics | Beta | [Generation and structured-output contract](docs/architecture/structured-output-and-scope.md) |
50
57
  | `Agent.open_scope`, `Agent::Scope`, `Agent::Base#unload_async / #unload!`, `AgentUnloadedError` and `Agent::ScopeCleanupError` | Beta | Runtime-bound ownership and safe release of idle Agent incarnations without changing saved history or closing the Store; [contract](docs/architecture/dx05-agent-scope.md) |
58
+ | `Agent::Base#start_hosted`, `Agent::HostedInvocation`, `Agent::ApprovalWait` and `Agent::Store#standalone_approval_wait` | Beta | Added in v0.32.0. Detach an exclusively owned standalone Agent at its first committed approval wait, then restore the same execution in another process; delivery claims and worker fencing remain application responsibilities; [contract](docs/runtime-and-concurrency.md#hosting-a-standalone-agent-through-approval) |
59
+ | `Agent::HostedInvocation#detach!(execution_id:, approval_request_id:)` | Beta | Added in v0.32.0. Release an exclusively owned Orchestrator and its single direct subagent at the child's approval wait. A second worker can resume both saved executions while the first worker handles other requests. [Contract](docs/runtime-and-concurrency.md#hosting-an-orchestrator-through-a-child-approval) |
60
+ | `MultiAgent::Orchestrator#approve_hosted_child` | Beta | Added in v0.32.0. Answer the current approval on a restored direct child. The returned handle can release the pair again if that child requests another approval during the same execution. [Contract](docs/runtime-and-concurrency.md#hosting-an-orchestrator-through-a-child-approval) |
61
+ | `MultiAgent::Orchestrator#unload_with_completed_child!` | Beta | Added in v0.32.0. Release local owners after a direct child result has been imported and the parent has finished. The saved executions, transcripts and coordination history remain available. [Contract](docs/runtime-and-concurrency.md#releasing-a-completed-orchestrator-and-child) |
51
62
  | `Tools::Agent::CleanupError` | Beta | Added in v0.31.0. Preserves the child, result and failure causes when safe release fails; retry `child_agent.unload_async` for release only; [contract](docs/architecture/agent-tool-child-lifetime.md) |
63
+ | `Workflow#send_saved_event` | Beta | Added in v0.32.0. Deliver an event to a specific revision of a persisted Workflow wait; [contract](docs/runtime-and-concurrency.md#delivering-an-event-to-a-saved-workflow-wait) |
52
64
  | `Workflow::Builder#task_state` and `WorkflowTaskResumeError` | Beta | State-entry-scoped Task completion, live cancellation and explicit restart rejection; [contract](docs/architecture/dx05-workflow-task-state.md) |
53
65
  | `Phronomy::Integrations::OrderedEventDelivery` | Beta | Optional ordered delivery with bounded admission and explicit drain lifetime; [usage](docs/ordered-event-delivery.md) |
54
66
  | `Phronomy::Error#code` and error constructor `code:` | Beta | Optional reason codes and bounded causes retained in saved failure diagnostics; [meaning and compatibility](docs/architecture/error-api-review.md) |
data/RELEASE_CHECKLIST.md CHANGED
@@ -38,6 +38,10 @@ Copy it into the GitHub Release draft and check off each item.
38
38
  - [ ] No new `requires_approval` tools missing the approval gate
39
39
  - [ ] No secrets, credentials, or PII in tool descriptions, schema strings, or spec fixtures
40
40
  - [ ] Dependency audit passes: `bundle exec bundler-audit check --update`
41
+ Run `bundle install` first; the development Gemfile provides the audit tool.
42
+ Audit the complete resolved bundle, including development and test dependencies,
43
+ without advisory exclusions. A runtime-only audit does not satisfy this gate.
44
+ The dependency-audit CI job and the release workflow enforce the same command.
41
45
 
42
46
  ---
43
47
 
@@ -83,10 +83,11 @@ with lazy grammar` is a known native schema/Tool rejection. It is classified as
83
83
  stream operations. Unrelated string errors and requests without a schema retain
84
84
  their ordinary transport-error behavior. The adapter neither downgrades schemas
85
85
  nor replays requests. RubyLLM's configured transport retry policy is independent.
86
- The minimum Faraday streaming callback omits the HTTP status, so RubyLLM may
87
- surface this same exact rejection as `ServerError`; its cause is preserved too.
88
- That synthetic server status can trigger SDK retries before the adapter receives
89
- the error. Adapter replay tests disable SDK retries to verify their own boundary.
86
+ On the supported HTTP stack, the known schema rejection retains its
87
+ `RubyLLM::BadRequestError` cause even when streaming. An HTTP 500 with the same
88
+ body retains its `RubyLLM::ServerError` cause. The
89
+ [HTTP dependency rationale](trust-acceptance-followup.md#http-dependency-correction)
90
+ documents the tested versions and request counts.
90
91
 
91
92
  `Response` records finish_reason (stop, tool_calls, length, content_filter, refusal,
92
93
  unknown), refusal and raw_finish_reason metadata. Only normal final completion can
@@ -37,6 +37,21 @@ retain the SDK server-error cause. Configured SDK retries for 429/500 remain;
37
37
  a 400 rejection is one HTTP attempt even with three retries configured.
38
38
  Partial-stream replay and callback identity regressions remain in place.
39
39
 
40
+ RubyLLM owns HTTP transport selection for model calls. Phronomy's LLM and
41
+ Embeddings adapters recognize Faraday exception classes at the SDK boundary.
42
+ The two gemspec declarations are a temporary workaround for the streaming
43
+ status bug. Remove both when all supported RubyLLM versions preserve the
44
+ status or exclude the affected HTTP stack through their own dependencies.
45
+
46
+ Verify removal with the two Phronomy declarations omitted. Resolve the minimum
47
+ supported RubyLLM and its permitted HTTP dependencies without the existing CI
48
+ pins. If the old pair still resolves, test it explicitly. If RubyLLM excludes
49
+ it, confirm resolution rejects it and test the new minimum. In both cases run
50
+ `spec/integration/transport_error_spec.rb` against the minimum and the normal
51
+ bundle. With three retries configured, a streaming HTTP 400 with a string
52
+ error body must retain its 400 cause and send one POST. An HTTP 500 with the
53
+ same body must retain its 500 cause and send four POSTs.
54
+
40
55
  Before and after, application handling uses the same public API:
41
56
 
42
57
  ```ruby
@@ -0,0 +1,108 @@
1
+ # Context Policy budgeting
2
+
3
+ Applications choose how much model input capacity to use for one selection.
4
+ Context exposes that capacity and candidate estimates, while the framework keeps
5
+ its internal budget representation and final input validation.
6
+
7
+ For example, a review Policy can reserve part of the available input for its own
8
+ safety margin, then let DefaultPolicy choose history within the smaller limit.
9
+ No application needs to construct a private budget object for that operation.
10
+
11
+ ## Reading and constraining input limits
12
+
13
+ A Policy input reports its current selection limit and can derive another input
14
+ with a smaller limit. Derivation preserves the original input and all its
15
+ material; it does not change model capabilities or generation settings.
16
+
17
+ For example, an input with a limit of 8192 can produce a selection input capped
18
+ at 5632. The original input still reports 8192. The derived input cannot later
19
+ increase its limit above 5632.
20
+
21
+ ```ruby
22
+ require "phronomy"
23
+
24
+ class ReservedInputPolicy < Phronomy::Context::ContextPolicy
25
+ def call(input)
26
+ limit = input.input_token_limit
27
+ unless limit
28
+ raise Phronomy::ConfigurationError, "Review requires known model input capacity"
29
+ end
30
+
31
+ selection_limit = limit - limit / 4 - limit / 16
32
+ selected_input = input.with_input_token_limit(selection_limit)
33
+ Phronomy::Context::DefaultPolicy.instance.call(selected_input)
34
+ end
35
+ end
36
+ ```
37
+
38
+ `input_token_limit` returns an Integer or `nil` for an unknown limit.
39
+ `with_input_token_limit(limit)` requires a positive Integer without coercion;
40
+ zero, negative values, strings and floating-point values raise `ArgumentError`.
41
+ Increasing a known limit also raises `ArgumentError`. The returned
42
+ `ContextPolicyInput` is immutable, and its other field values are unchanged.
43
+
44
+ When the original limit is unknown, an explicit positive selection cap is
45
+ allowed. That cap expresses application policy and does not establish that the
46
+ model can accept that input. Applications that require known capability must
47
+ reject `nil`, as the example above does.
48
+
49
+ The cap controls selection performed with the derived input. The framework's
50
+ final validation still uses the actual model budget; returning a different Plan
51
+ does not make the derived cap a new global validator. It does not set an output
52
+ token limit, alter a saved Manifest, or affect another Agent call.
53
+
54
+ ## Estimating candidate content
55
+
56
+ A Policy can estimate proposed content through the same canonical encoding and
57
+ estimator used by its generated items. The Policy remains responsible for
58
+ choosing complete items and preserving required conversation groups.
59
+
60
+ For example, a retrieval Policy can fit a JSON page before adding it as
61
+ knowledge. JSON escaping contributes to the estimate because the complete
62
+ serialized page is measured.
63
+
64
+ ```ruby
65
+ require "phronomy"
66
+ require "json"
67
+
68
+ class PageKnowledgePolicy < Phronomy::Context::ContextPolicy
69
+ def initialize(page:, allowance:)
70
+ @page = page
71
+ @allowance = allowance
72
+ end
73
+
74
+ def call(input)
75
+ content = JSON.generate(@page)
76
+ if estimate_tokens(content) > @allowance
77
+ raise Phronomy::Context::BudgetExceededError, "Retrieved page exceeds its allowance"
78
+ end
79
+
80
+ page = knowledge_item(content: content, required: true)
81
+ Phronomy::Context::DefaultPolicy.instance.call(
82
+ input.with(knowledge: input.knowledge + [page])
83
+ )
84
+ end
85
+ end
86
+ ```
87
+
88
+ `estimate_tokens(content)` is a protected subclass helper. Strings are estimated
89
+ as text; other values use the framework's canonical JSON encoding. The helper
90
+ uses the existing estimator and does not introduce a second token-counting
91
+ algorithm. Estimates are approximate unless the configured estimator provides
92
+ model-specific counts. A successful estimate does not guarantee provider
93
+ acceptance or replace final input validation.
94
+
95
+ ## Compatibility and ownership
96
+
97
+ These operations belong to the Context contract and reuse its existing budget
98
+ and estimation mechanisms. Applications own allocation ratios and retrieval
99
+ policy; DefaultPolicy retains its current selection algorithm.
100
+
101
+ For example, Example 22 can budget retrieved findings through these operations
102
+ while SharedState continues to coordinate Agents and application Tools continue
103
+ to own the finding store. No SharedState-to-Context dependency is introduced.
104
+
105
+ The operations are Experimental and available since v0.33.0. Existing private
106
+ budget and estimation classes remain implementation details. Existing ContextPolicy
107
+ factories and DefaultPolicy behavior are unchanged, and the protected helper
108
+ applies equally to normal Agent calls and SharedState member calls.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "phronomy"
5
+
6
+ module SharedStateCustomSharingExample
7
+ class IssueBoard
8
+ attr_reader :closed
9
+
10
+ def initialize
11
+ @records = []
12
+ @closed = false
13
+ end
14
+
15
+ def record(content:, category:, status:, author:, cycle:)
16
+ raise "Issue board is closed" if closed
17
+
18
+ row = {id: @records.length + 1, content: content.dup.freeze,
19
+ category: category.dup.freeze, status: status.dup.freeze,
20
+ author: author.dup.freeze, cycle: cycle}.freeze
21
+ @records << row
22
+ row
23
+ end
24
+
25
+ def search(category: nil, status: nil)
26
+ snapshot.select do |row|
27
+ (category.nil? || row.fetch(:category) == category) &&
28
+ (status.nil? || row.fetch(:status) == status)
29
+ end
30
+ end
31
+
32
+ def snapshot
33
+ raise "Issue board is closed" if closed
34
+
35
+ @records.dup.freeze
36
+ end
37
+
38
+ def close
39
+ @records.clear
40
+ @closed = true
41
+ end
42
+ end
43
+
44
+ class IssueSharing
45
+ def open(input:, config:)
46
+ board = IssueBoard.new
47
+ yield Phronomy::MultiAgent::SharedState::Binding.new(
48
+ store: board,
49
+ tools: ->(member:, cycle:) { tool_definitions(board, member, cycle) }
50
+ )
51
+ ensure
52
+ board&.close
53
+ end
54
+
55
+ private
56
+
57
+ def tool_definitions(board, member, cycle)
58
+ {
59
+ record_tool(board, member.agent_definition.fetch(:id), cycle) => nil,
60
+ search_tool(board) => nil
61
+ }
62
+ end
63
+
64
+ def record_tool(board, author, cycle)
65
+ Class.new(Phronomy::Tool::Base) do
66
+ tool_name "record_issue"
67
+ description "Record one issue with an explicit category and status."
68
+ execution_mode :cooperative
69
+ param :content, type: :string, desc: "The issue description."
70
+ param :category, type: :string, enum: %w[security performance],
71
+ desc: "The issue category."
72
+ param :status, type: :string, enum: %w[open closed],
73
+ desc: "The issue status."
74
+
75
+ define_method(:execute) do |content:, category:, status:|
76
+ board.record(content: content, category: category, status: status,
77
+ author: author, cycle: cycle).to_json
78
+ end
79
+ end
80
+ end
81
+
82
+ def search_tool(board)
83
+ Class.new(Phronomy::Tool::Base) do
84
+ tool_name "search_issues"
85
+ description "Read issues matching all supplied category and status filters."
86
+ execution_mode :cooperative
87
+ param :category, type: :string, required: false, enum: %w[security performance],
88
+ desc: "Optional exact category filter."
89
+ param :status, type: :string, required: false, enum: %w[open closed],
90
+ desc: "Optional exact status filter."
91
+
92
+ define_method(:execute) do |category: nil, status: nil|
93
+ board.search(category: category, status: status).to_json
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ class Researcher < Phronomy::Agent::Base
100
+ agent_definition id: "shared-issue-researcher", version: 1
101
+ model ENV.fetch("PHRONOMY_EXAMPLE_MODEL", "configured-model")
102
+ end
103
+
104
+ class ReviewTeam < Phronomy::MultiAgent::SharedState
105
+ sharing IssueSharing.new, id: "shared-issues", version: 1
106
+ member Researcher
107
+ max_cycles 2
108
+ coordination "Use search_issues to inspect relevant issues. " \
109
+ "Use record_issue to record each new issue with its category and status."
110
+ aggregate { |board| board.snapshot }
111
+ end
112
+
113
+ module OfflineDemo
114
+ def self.run
115
+ opened_board = nil
116
+ output = IssueSharing.new.open(input: "Review authentication", config: {}) do |binding|
117
+ opened_board = binding.store
118
+ definitions = binding.tool_definitions(member: Researcher, cycle: 1)
119
+ tools = definitions.keys.to_h { |tool| [tool.tool_name, tool.new] }
120
+ recorder = tools.fetch("record_issue")
121
+ recorder.call({"content" => "Missing token validation", "category" => "security", "status" => "open"})
122
+ recorder.call({"content" => "Slow cache lookup", "category" => "performance", "status" => "open"})
123
+ recorder.call({"content" => "Session expiry corrected", "category" => "security", "status" => "closed"})
124
+
125
+ matches = JSON.parse(tools.fetch("search_issues").call({"category" => "security", "status" => "open"}))
126
+ unless matches.map { |row| row.fetch("content") } == ["Missing token validation"]
127
+ raise "Search did not apply both filters"
128
+ end
129
+ all_records = binding.store.snapshot
130
+ raise "Search changed the board" unless all_records.length == 3
131
+
132
+ {matches: matches, total_records: all_records.length}
133
+ end
134
+ raise "Provider did not close the board" unless opened_board.closed
135
+
136
+ puts JSON.pretty_generate(output.merge(provider_closed: true))
137
+ end
138
+ end
139
+ end
140
+
141
+ if $PROGRAM_NAME == __FILE__
142
+ abort "Run with --offline, or require this file from a configured Phronomy application." unless ARGV == ["--offline"]
143
+
144
+ SharedStateCustomSharingExample::OfflineDemo.run
145
+ end
data/docs/features.md CHANGED
@@ -106,8 +106,12 @@ See the [migration guide](migrations/parallel-tool-chat-removal.md).
106
106
  | **Durable Handoff** — `MultiAgent::Handoff` and `MultiAgent::HandoffRunner` persist responsibility, immutable Context and exact Target reservation in one Persistence domain; current graph wiring is required for continuation | Beta |
107
107
  | **GeneratorVerifier** — Generator-Verifier loop with application prompts, Agent-validated output schemas and explicit output failures | Beta |
108
108
  | **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
109
+ | **Hosted child approvals** — An Orchestrator can release a single direct child and its parent at successive Tool approvals within the same child execution. The host transfers exclusive ownership between workers. [Contract](runtime-and-concurrency.md#hosting-an-orchestrator-through-a-child-approval) | Beta |
110
+ | **Completed native child release** — A host can unload the local Orchestrator and its finished direct child while preserving their saved results, conversation and coordination history. [Contract](runtime-and-concurrency.md#releasing-a-completed-orchestrator-and-child) | Beta |
109
111
  | **`Phronomy::MultiAgent::TeamCoordinator`** — LLM coordinator with stateful worker Agents | Beta |
110
112
  | **`Phronomy::MultiAgent::SharedState`** — Sequential peer-agent coordination through an invocation-local findings store; see the [namespace migration](migrations/shared-state-multi-agent.md) | Experimental |
113
+ | **Agent Tool composition** — `Agent::Base.with_tools(definitions, id:, version:)` derives a class with a complete replacement Tool map while preserving Phronomy authoring settings and filters; [contract](shared-state-sharing.md#deriving-an-agent-with-a-complete-tool-set) | Experimental (Unreleased) |
114
+ | **Custom shared data and Tools** — `SharedState.sharing(provider, id:, version:)` and `SharedState::Binding` connect application-owned stores and Tool definitions without private overrides; [contract and executable example](shared-state-sharing.md) | Experimental (Unreleased) |
111
115
  | **Human-in-the-loop approval** — durable suspension publishes `:approval_required` through the Agent listener; `approve` / `approve_async` resumes the same logical `execution_id` with a fresh FSMSession incarnation | Beta |
112
116
  | **`tool_approval_policy`** — Application-defined allow/approve/reject policy using a value-only `ApprovalEvaluationRequest` without live Agent/Tool references | Beta |
113
117
 
data/docs/mcp-client.md CHANGED
@@ -10,8 +10,10 @@ Phronomy supports the official `mcp` Ruby SDK **1.x**. MCP SDK 0.x is not suppor
10
10
  - `http://...`
11
11
  - `https://...`
12
12
 
13
- HTTP and SSE support are standard dependencies of Phronomy through `faraday` and
14
- `event_stream_parser`.
13
+ For HTTP and HTTPS servers, Phronomy delegates to the MCP SDK's
14
+ `MCP::Client::HTTP` transport. RubyLLM already brings in `event_stream_parser`
15
+ for server-sent events. See the [HTTP dependency rationale](architecture/trust-acceptance-followup.md#http-dependency-correction)
16
+ for Phronomy's Faraday and Net::HTTP adapter requirements.
15
17
 
16
18
  ## Input-schema subset
17
19
 
@@ -42,3 +42,8 @@ for one invocation only.
42
42
 
43
43
  See [ADR-053](../decisions/053-shared-state-coordination-ownership.md) for the
44
44
  ownership and compatibility decision.
45
+
46
+ The later [custom sharing extension](../shared-state-sharing.md) is opt-in and
47
+ leaves the default behavior described above intact. It gives an application
48
+ explicit ownership of its store and Tool definitions; its generated identity
49
+ also records the application's sharing ID and semantic version.