phronomy 0.32.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8eeef6dddca310da11fcdcaf7ffc2490191151cfa070384be8d04f84366efb08
4
- data.tar.gz: 2b43c66ff8c5484360b2a953bcdd4aa754ef99c3b7bc944285993d866ee34007
3
+ metadata.gz: e696f701b22d91c4ccb8f566e28274b73e9fd69704a673e18a687a1d377b4583
4
+ data.tar.gz: 7a63749fb58c70fb6c74a843a601bb48401ffe35fa02de03a63fa9f8d6320552
5
5
  SHA512:
6
- metadata.gz: 9ebee2313bc7d60b27c6e42e0909fbb1102cca3c220f25575d1c755b18f3d1c00b7ea0dfec36646155a7aff325ba47c99af202c9b7fbf226d45c4fbb07b8bff9
7
- data.tar.gz: 3b089fdc8447a0a56c66d10d64c809eb26ddc31be6800a55c095409e87e649047afbc42fa1872eab8fd68b7939bd6845edc194b5ec4ab03beca602408f320596
6
+ metadata.gz: af029ecc2491d2912c48fef847f8501ce255040759bb5d8edba3949d2bea0352b2a6a89e731d70493570c5b72d26e094ed03d80aff412f6805af21a0da588219
7
+ data.tar.gz: 529ece7bc74b4096d332e9f39a3097933f1441797f5dc22b5bb9ec9d5b39fde2c42709d2b53e83b99a3e42ec6e2e9a227920f10371e6ac65772241cb54d0abc7
data/CHANGELOG.md CHANGED
@@ -12,6 +12,83 @@ 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
+
15
92
  ## [0.32.0] - 2026-11-03
16
93
 
17
94
  ### Added
data/README.md CHANGED
@@ -37,13 +37,19 @@ 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.32.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.
41
43
  Worker hosting and completed parent/child release require v0.32.0 or later.
42
44
  Review the [0.31 migration notes](docs/migrations/0.31.md) before upgrading
43
45
  persistent applications or custom Agent Tool subclasses.
44
46
 
45
47
  | API | Stability | Contract |
46
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) |
47
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) |
48
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) |
49
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) |
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
@@ -110,6 +110,8 @@ See the [migration guide](migrations/parallel-tool-chat-removal.md).
110
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 |
111
111
  | **`Phronomy::MultiAgent::TeamCoordinator`** — LLM coordinator with stateful worker Agents | Beta |
112
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) |
113
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 |
114
116
  | **`tool_approval_policy`** — Application-defined allow/approve/reject policy using a value-only `ApprovalEvaluationRequest` without live Agent/Tool references | Beta |
115
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.
@@ -211,6 +211,37 @@ Worker authorization/execution outcomes return as values carrying
211
211
  `tool_invocation_id`; the Tool FSMSession consumes a mismatched semantic result
212
212
  without advancing its current state.
213
213
 
214
+ ## Tool approval display
215
+
216
+ A Tool chooses which arguments and derived facts a person may see. Agent asks
217
+ the Tool for both when it builds a new `ToolApprovalRequest` item and takes a
218
+ snapshot of the result. Approval policy and Tool execution use their own inputs.
219
+
220
+ For example, a Tool can declare `redact_params :token`. The default
221
+ `Tool::Base#approval_display(arguments:, facts:)` masks `token` in the
222
+ argument display and masks string values in facts. A numeric fact such as
223
+ `recipient_count: 3` can still be shown. Use fixed fact names and choose
224
+ aggregates that are appropriate to disclose.
225
+
226
+ If `redact_params :auth` covers `auth: {pin: 627018}`, the default also
227
+ masks a fact such as `echo: 627018`. It checks JSON values copied from inside
228
+ sensitive hashes and arrays. A matching fact key causes that entry to be
229
+ omitted. The declared
230
+ argument's own hash keys are treated as field names. If those names are
231
+ themselves confidential, keep them out of fact keys. Equality also masks
232
+ unrelated facts with the same value. The Tool author chooses which computed
233
+ values may be shown. For example, an arithmetic transformation of a PIN can
234
+ still disclose it even if the result differs from the original. Override
235
+ `approval_display` to explicitly select safe fields when publishing such facts.
236
+
237
+ To customize both fields, return a Hash with the symbol keys `:arguments` and
238
+ `:facts`, each holding a displayable Hash. The hook runs on EventLoop while
239
+ Agent prepares the approval. Build the result from the supplied values without
240
+ blocking or performing I/O. A wrapper can forward the operation to its
241
+ underlying Tool. If a Tool omits the operation, the item contains an
242
+ empty Hash for each field. Existing saved approval items retain the display
243
+ data recorded when they were created.
244
+
214
245
  ## Approval suspension and live owner lookup
215
246
 
216
247
  Approval suspension retains the same process-local Agent and AgentInvocation but
@@ -672,6 +703,11 @@ Runtime shutdown before resetting configuration.
672
703
 
673
704
  A host can release one suspended Agent and continue using its Ruby process for other Agents. Hold one external claim per mutable Agent while loading, executing, detaching and recording delivery. Persist a new request's reserved execution ID before dispatch. A host uses the existing incarnation-level listener to receive the approval request.
674
705
 
706
+ The hosted handle matches the requested approval to its saved wait and local
707
+ owner. That owner's execution coordinator submits the detach command to
708
+ EventLoop, which rechecks ownership before releasing the Agent. The host waits
709
+ for the result before handing the execution to another worker.
710
+
675
711
  For example, worker A starts a fresh reservation with `agent.start_hosted(reservation:, input:)`. If the run finishes directly, `handle.wait_result` returns the ordinary execution result. On an approval notification, call `handle.detach!(approval_request_id: request.id)` outside EventLoop. Success returns an immutable `ApprovalWait` with the execution ID, approval request ID and committed revision. The same handle then returns that wait. The released Agent object cannot be reused. Existing `invoke_async` handles keep their terminal-result contract.
676
712
 
677
713
  An incorrect or stale approval, outstanding physical work, concurrent operation, live scope or durable retention prevents transfer. A busy response leaves the host responsible for the claim. A spent reservation is rejected. After a verified worker exit, a host can query `store.standalone_approval_wait(agent_id:, execution_id:)` without loading the Agent. A missing or ineligible wait returns nil. Store errors and owner mismatches propagate. Only reclaim a delivery if the original worker is known to have stopped and its claim still matches.