phronomy 0.32.0 → 0.34.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -0
  3. data/CONTRIBUTING.md +5 -5
  4. data/README.md +20 -1
  5. data/RELEASE_CHECKLIST.md +7 -1
  6. data/SECURITY.md +1 -1
  7. data/VERIFY.sh +1 -9
  8. data/docs/architecture/error-api-review.md +6 -2
  9. data/docs/architecture/structured-output-and-scope.md +8 -8
  10. data/docs/architecture/trust-acceptance-followup.md +15 -0
  11. data/docs/context-policy-budgeting.md +108 -0
  12. data/docs/decisions/016-semantic-multi-agent-handoff.md +6 -5
  13. data/docs/examples/shared_state_custom_sharing.rb +145 -0
  14. data/docs/features.md +4 -2
  15. data/docs/mcp-client.md +4 -2
  16. data/docs/migrations/cleanup-audit.md +89 -0
  17. data/docs/migrations/legacy-format-removal.md +55 -0
  18. data/docs/migrations/r8-unit6.md +4 -2
  19. data/docs/migrations/shared-state-multi-agent.md +5 -0
  20. data/docs/migrations/vector-async-clients.md +6 -1
  21. data/docs/runtime-and-concurrency.md +36 -0
  22. data/docs/shared-state-sharing.md +233 -0
  23. data/lib/phronomy/agent/base.rb +53 -0
  24. data/lib/phronomy/agent/execution/agent_invocation.rb +0 -4
  25. data/lib/phronomy/agent/execution/execution_coordinator.rb +11 -0
  26. data/lib/phronomy/agent/handoff/transfer_projection.rb +0 -9
  27. data/lib/phronomy/agent/journal/journal_record.rb +2 -2
  28. data/lib/phronomy/agent/lifecycle/hosted_invocation.rb +2 -5
  29. data/lib/phronomy/agent/recovery/recovery_rules.rb +0 -12
  30. data/lib/phronomy/agent/store.rb +3 -8
  31. data/lib/phronomy/agent/tool_execution/tool_approval_request.rb +3 -2
  32. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +9 -36
  33. data/lib/phronomy/context/context_policy.rb +13 -8
  34. data/lib/phronomy/context/context_policy_input.rb +26 -0
  35. data/lib/phronomy/context/llm_input_manifest.rb +3 -3
  36. data/lib/phronomy/engine/runtime/shutdown_result.rb +0 -5
  37. data/lib/phronomy/execution/execution.rb +0 -11
  38. data/lib/phronomy/multi_agent/shared_state/binding.rb +44 -0
  39. data/lib/phronomy/multi_agent/shared_state/member_lifecycle_error.rb +32 -0
  40. data/lib/phronomy/multi_agent/shared_state.rb +152 -10
  41. data/lib/phronomy/tool/base.rb +69 -3
  42. data/lib/phronomy/tool/schema.rb +23 -11
  43. data/lib/phronomy/version.rb +1 -1
  44. data/scripts/api_snapshot.rb +6 -3
  45. data/sig/phronomy/agent.rbs +1 -0
  46. data/sig/phronomy/context.rbs +3 -0
  47. data/sig/phronomy/multi_agent.rbs +26 -2
  48. data/sig/phronomy/tool.rbs +1 -0
  49. metadata +9 -11
  50. data/lib/phronomy/execution/scheduler_reentrancy_error.rb +0 -9
  51. data/lib/phronomy/persistence/migration/initial_format_migration.rb +0 -226
  52. data/scripts/add_to_h_to_token_doubles.rb +0 -33
  53. data/scripts/add_to_h_unnamed_doubles.rb +0 -27
  54. data/scripts/migrate_generator_verifier_result.rb +0 -18
  55. data/scripts/migrate_spec_agent_definition.rb +0 -108
  56. data/scripts/migrate_spec_agent_definition_pass2.rb +0 -53
  57. data/scripts/migrate_spec_inline_pass3.rb +0 -24
  58. data/sig/phronomy/persistence/migration.rbs +0 -13
data/docs/features.md CHANGED
@@ -90,7 +90,7 @@ Backend authors continue to implement the synchronous SPI only.
90
90
  | **OffloadPool sizing** — `offload_pool_size` / `offload_queue_size`; named pools available for application-owned isolation | Beta |
91
91
  | **InvocationContext** — Explicit cancellation/deadline/policy plus observability-correlation context for Agent and Workflow invocations | Beta |
92
92
  | **Metrics** — OffloadPool active/queue/abandoned metrics plus EventLoop queue/lag metrics | Beta |
93
- | **Runtime lifecycle** — Runtime-owned EventLoop and terminal `Runtime#shutdown` | Beta |
93
+ | **Runtime lifecycle** — Runtime-owned EventLoop and terminal `Runtime#shutdown`; use the result's `cleanup_complete?` for resource release and `clean?` for a graceful shutdown; [cleanup migration](migrations/cleanup-audit.md) | Beta |
94
94
 
95
95
  ## Agent and workflow patterns
96
96
 
@@ -109,7 +109,9 @@ See the [migration guide](migrations/parallel-tool-chat-removal.md).
109
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
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
- | **`Phronomy::MultiAgent::SharedState`** — Sequential peer-agent coordination through an invocation-local findings store; see the [namespace migration](migrations/shared-state-multi-agent.md) | Experimental |
112
+ | **`Phronomy::MultiAgent::SharedState`** — Sequential peer-agent coordination through an invocation-local findings store; default member derivation preserves Phronomy authoring settings through `Agent::Base.with_tools`; see the [sharing contract](shared-state-sharing.md#deriving-an-agent-with-a-complete-tool-set) and [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
 
@@ -0,0 +1,89 @@
1
+ # Cleanup audit migration
2
+
3
+ Phronomy 0.34.0 removes redundant internal operations and old public entry points
4
+ whose behavior is already provided by a current operation. Applications must
5
+ use the current names; the removed names have no compatibility aliases. Current
6
+ record writers are unchanged. The subsequent [legacy-format cleanup](legacy-format-removal.md)
7
+ also removes old read fallbacks and explicitly retires two data-conversion
8
+ entry points. It does not rewrite existing stores.
9
+
10
+ For example, an application checking whether Runtime resources were released
11
+ uses `cleanup_complete?` on the result returned by `Runtime#shutdown`. The
12
+ removed `success?` method delegated to that same predicate.
13
+
14
+ ## Runtime cleanup completion
15
+
16
+ Use `cleanup_complete?` to determine whether Runtime-owned resources have stopped.
17
+ Use `clean?` only when the application also needs a graceful, error-free shutdown;
18
+ that stricter predicate is retained and is not a replacement for `success?`.
19
+
20
+ Before:
21
+
22
+ ```ruby
23
+ result = runtime.shutdown
24
+ resources_released = result.success?
25
+ ```
26
+
27
+ After:
28
+
29
+ ```ruby
30
+ result = runtime.shutdown
31
+ resources_released = result.cleanup_complete?
32
+ ```
33
+
34
+ The same replacement applies to a shutdown result returned by
35
+ `Runtime.reset_default!`. Existing reset guards still reject incomplete cleanup.
36
+
37
+ ## EventLoop reentrancy errors
38
+
39
+ Rescue `Phronomy::EventLoopReentrancyError` when a synchronous operation would
40
+ block the EventLoop control thread. Framework operations already raise this
41
+ class. The removed `SchedulerReentrancyError` was an empty subclass and did not
42
+ add a distinct failure or recovery behavior.
43
+
44
+ Before:
45
+
46
+ ```ruby
47
+ begin
48
+ Phronomy::Diagnostics.assert_not_in_event_loop!
49
+ rescue Phronomy::SchedulerReentrancyError => error
50
+ warn error.message
51
+ end
52
+ ```
53
+
54
+ After:
55
+
56
+ ```ruby
57
+ begin
58
+ Phronomy::Diagnostics.assert_not_in_event_loop!
59
+ rescue Phronomy::EventLoopReentrancyError => error
60
+ warn error.message
61
+ end
62
+ ```
63
+
64
+ Ruby rescue matches a class and its subclasses, not its parent. The old rescue
65
+ above therefore did not catch a directly raised `EventLoopReentrancyError` even
66
+ before this cleanup. The current error name describes the actual failure.
67
+
68
+ ## Internal operations
69
+
70
+ Internal cleanup keeps behavior at its owning boundary. Context item factories
71
+ call the protected `ContextPolicy#estimate_tokens` extension operation directly.
72
+ Tool approval requests obtain one display projection and snapshot its arguments
73
+ and facts in the public approval Item.
74
+
75
+ For example, an application that renders approval details continues to use
76
+ `ToolApprovalRequest::Item#arguments` and `#facts`; it does not need the removed
77
+ internal `ToolInvocation#display_arguments` and `#display_facts` getters. The
78
+ public `Tool::Base#approval_display` extension operation remains available.
79
+
80
+ The cleanup also removes the unused internal `AgentInvocation#streaming?`,
81
+ `RecoveryRules.subject_key`, and `Execution#__while_open` and
82
+ `Execution#__cancellation_token` methods. Scope-owned operations used by result
83
+ composition remain in `ExecutionScopeState`. Five completed scripts for migrating
84
+ the repository's own test sources are removed. The subsequent legacy-format
85
+ cleanup separately retires the pre-S3 migration API and old Result JSON converter.
86
+
87
+ `JournalProjection#llm_call_records` and
88
+ `LLMInputManifest#referenced_content_refs` remain available. Lack of calls from
89
+ the repository does not make an application-facing operation removable.
@@ -0,0 +1,55 @@
1
+ # Legacy-format cleanup
2
+
3
+ Phronomy 0.34.0 keeps current saved facts and removes specific historical read
4
+ fallbacks and conversion facilities. It does not rewrite stores or introduce a
5
+ new record version. Retiring the conversion facilities is an intentional removal
6
+ of migration support, not a claim that another public API replaces them.
7
+
8
+ For example, a newly authorized Tool operation continues to store its validated
9
+ arguments, including type coercion and meaningful null values. Its reader no
10
+ longer reconstructs those arguments from an older raw-input field.
11
+
12
+ ## Handoff conversation groups
13
+
14
+ Handoff uses the conversation group recorded by the current Context assembly.
15
+ The historical selection-unit ID and kind no longer supply grouping or category
16
+ information. Segments without a current group still use the ordinary per-segment
17
+ fallback; reserved internal metadata is still removed from transferred content.
18
+
19
+ For example, current Tool-call and Tool-result segments share
20
+ `context_policy_conversation_group_id` and remain together when a Handoff policy
21
+ selects that exchange. A saved `selection_unit_id` is no longer an alternative
22
+ source for that relationship. The Manifest codec remains strict about its
23
+ supported version and required fields.
24
+
25
+ ## Authorized Tool arguments
26
+
27
+ Agent Store reads the exact validated argument snapshot for an authorized
28
+ operation. The raw request remains a separate fact and is not a substitute when
29
+ the validated snapshot is absent.
30
+
31
+ For example, raw `{"count":"2","optional":null}` may validate as
32
+ `{"count":2,"optional":null}`. Authorization matching must use the integer and
33
+ retain the explicit null. An authorized record missing `validated_arguments`
34
+ fails instead of falling back to `arguments` and dropping null values.
35
+
36
+ Provider-response recovery can temporarily record an `authorized` placeholder
37
+ without that snapshot. The normal `framework_calls` continuation validates its
38
+ Tool invocation and saves the prepared snapshot before an operation consumer
39
+ reads it. Directly projecting the intermediate placeholder is rejected; its
40
+ status alone is not proof that its arguments have been validated. A resumed
41
+ `framework_tools` continuation instead uses its previously saved dispatch
42
+ snapshot.
43
+
44
+ ## Retired conversion support
45
+
46
+ The pre-S3 `Persistence::Migration::InitialFormatMigration` API and the
47
+ GeneratorVerifier Result JSON converter are removed, along with the former
48
+ command's usage instructions. There is no compatibility alias or replacement
49
+ conversion command. Ordinary Agent and Workflow persistence, current Manifest
50
+ decoding and GeneratorVerifier Result validation remain in their owning domains.
51
+
52
+ For example, `GeneratorVerifier::Result.from_h` still requires `failure` even
53
+ when its value is null. It does not add missing fields to historical JSON.
54
+ The separate `PersistenceComposition::Unit4Migration` snapshot conversion is
55
+ retained and was not an implementation of either removed conversion facility.
@@ -91,8 +91,10 @@ an explicit items schema to restrict them.
91
91
  Storage SPI and SQL schemas are unchanged. New Tool batch metadata additionally
92
92
  records `validated_arguments`, separately from the unchanged raw request fields.
93
93
  Agent authorization projects and compares that exact snapshot (including explicit
94
- null in explicit schemas). Older snapshots without this field use the historical
95
- raw-argument/optional-nil-omission rule. Existing journal message boundaries,
94
+ null in explicit schemas). Authorization projection requires this validated
95
+ dispatch evidence; it does not infer approved values from raw request fields.
96
+ Provider-recovery placeholders are revalidated and replaced by the normal Tool
97
+ dispatch barrier before framework operations run. Existing journal message boundaries,
96
98
  Tool call identities and recorded usage remain readable. Legacy top-level
97
99
  thought_signature is decoded into opaque CallRequest metadata and rendered by
98
100
  the backend. Old SDK usage field spellings remain accepted by SavedContextReader.
@@ -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.
@@ -107,4 +107,9 @@ The public class names below are unchanged. Their implementation files move:
107
107
  Use `require "phronomy"` and the public constants. Direct partial-load paths are
108
108
  not the public loading contract; no require shim is kept in the contract
109
109
  directories. Backend Contracts have no execution or implementation dependency.
110
- P4 Storage/ContentStore work remains pending.
110
+ P4 Storage/ContentStore work is implemented; see the
111
+ [Storage asynchronous execution guide](storage-async-client.md).
112
+ [ADR-062](../decisions/062-content-store-service.md) subsequently classified
113
+ ContentStore as a content service over Storage, rather than a physical backend.
114
+ The final P5 cross-repository/API/documentation review remains pending, as
115
+ recorded in [Backend responsibilities](../architecture/backend-groups.md).
@@ -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.
@@ -0,0 +1,233 @@
1
+ # Custom sharing in SharedState
2
+
3
+ SharedState can coordinate member Agents while an application owns the shared
4
+ data and the Tools that access it. The application provides one invocation-scoped
5
+ binding; SharedState owns member order, cycles, stopping decisions and final
6
+ aggregation. This Experimental extension is available since v0.33.0.
7
+
8
+ For example, an issue-review team can share records with `category` and `status`
9
+ fields. Its search Tool accepts those fields as separate arguments instead of
10
+ forcing the application to encode every record and condition as finding text.
11
+
12
+ ## Application provider and binding
13
+
14
+ A provider scopes shared resources around one synchronous team invocation. Its
15
+ binding contains an opaque store and a Tool-definition builder. SharedState
16
+ passes the store to application callbacks and does not require store methods or
17
+ a particular record schema.
18
+
19
+ For example, an application provider can open an issue board, yield its binding,
20
+ and close the board in `ensure`:
21
+
22
+ ```ruby
23
+ class IssueSharing
24
+ def open(input:, config:)
25
+ board = IssueBoard.new
26
+ yield Phronomy::MultiAgent::SharedState::Binding.new(
27
+ store: board,
28
+ tools: ->(member:, cycle:) { build_issue_tools(board, member, cycle) }
29
+ )
30
+ ensure
31
+ board&.close
32
+ end
33
+ end
34
+ ```
35
+
36
+ This excerpt shows the provider boundary. The
37
+ [complete executable application example](examples/shared_state_custom_sharing.rb)
38
+ defines its store, Tool builder, member Agent, team and offline check.
39
+
40
+ The provider must respond to `open(input:, config:)` and yield exactly one
41
+ `Phronomy::MultiAgent::SharedState::Binding` synchronously during that call.
42
+ Zero yields, multiple yields or an invalid binding violate the protocol and
43
+ raise `ArgumentError`. Do not retain the block for deferred execution. The
44
+ provider owns cleanup, including failures raised by member execution or
45
+ aggregation; SharedState does not call `close` on the opaque store.
46
+
47
+ `Binding.new(store:, tools:)` accepts a callable Tool builder. The public
48
+ `binding.store` returns the opaque store, and
49
+ `binding.tool_definitions(member:, cycle:)` calls the builder. `member` is the
50
+ registered Agent class; `cycle` is the current positive cycle number. The builder
51
+ must return a Hash whose keys are Tool classes and whose values are aliases or
52
+ `nil`, using the same Tool-definition shape as the existing Agent Tool API.
53
+ Arrays are not accepted. Invalid configuration or protocol values raise
54
+ `ArgumentError`.
55
+
56
+ ## Team composition and definition identity
57
+
58
+ A team selects its provider and supplies the application operations that consume
59
+ the shared store. The provider's stable ID and version identify the meaning of
60
+ its injected Tools independently from an individual member's definition.
61
+
62
+ For example, the complete issue-review example declares:
63
+
64
+ ```ruby
65
+ class ReviewTeam < Phronomy::MultiAgent::SharedState
66
+ sharing IssueSharing.new, id: "shared-issues", version: 1
67
+ member Researcher
68
+ max_cycles 2
69
+ coordination "Use search_issues to inspect relevant issues. " \
70
+ "Use record_issue to record each new issue with its category and status."
71
+ aggregate { |board| board.snapshot }
72
+ end
73
+ ```
74
+
75
+ Custom sharing requires an explicit `aggregate` block because SharedState does
76
+ not know how to read the application store. An optional `terminate_when` block
77
+ also receives that same store. Existing cycle order, stopping rules and result
78
+ shape remain in place. `config:` is provided to the sharing provider; this does
79
+ not introduce forwarding of `config:` to member invocations.
80
+
81
+ In custom mode, SharedState adds only the Tools returned by the application's
82
+ builder. It does not add `read_store` or `write_finding`, and it does not append
83
+ stored records to member prompts. The prompt consists of coordination text, the
84
+ task input and any member instruction. The member's existing Tools and aliases
85
+ remain available. Conflicting effective Tool names are rejected rather than
86
+ silently replacing an existing Tool.
87
+
88
+ Use a stable sharing ID and increment its version whenever the injected Tool
89
+ schemas or behavior change. Custom generated Agent identity incorporates that
90
+ ID/version alongside the member definition. A new store instance per invocation
91
+ does not by itself require a new sharing ID. The application must not substitute
92
+ different Tool semantics under an unchanged sharing revision.
93
+ The ID must be a nonempty String and the version must be a positive Integer.
94
+
95
+ The default mode remains available when `.sharing` is not configured. It retains
96
+ the existing invocation-local KnowledgeStore, built-in Tools, full-store prompt
97
+ behavior and instrumentation revision 1. This extension does not add a `query`
98
+ argument to the default `read_store` Tool or a search method to that store.
99
+
100
+ ## Deriving an Agent with a complete Tool set
101
+
102
+ The public `Agent::Base.with_tools(definitions, id:, version:)` factory creates
103
+ an Agent subclass with a replacement Tool map and an explicit definition
104
+ identity. Agent owns preservation of its authoring settings; SharedState uses
105
+ this public operation after composing the member Tools with either its built-in
106
+ sharing Tools or the application's Tools. It does not need to read Agent private
107
+ settings.
108
+
109
+ For example, inside the complete example's module, a provider can supply the
110
+ entire Tool map for a derived researcher class:
111
+
112
+ ```ruby
113
+ IssueSharing.new.open(input: "Review authentication", config: {}) do |binding|
114
+ tool_map = binding.tool_definitions(member: Researcher, cycle: 1)
115
+ derived = Researcher.with_tools(
116
+ tool_map, id: "issue-researcher-with-board", version: 1
117
+ )
118
+ # `derived` is a class; no Agent instance or Runtime resource was created.
119
+ end
120
+ ```
121
+
122
+ `definitions` is the complete Hash of Tool classes to aliases or `nil`; this
123
+ factory does not merge Tools or detect naming collisions. SharedState performs
124
+ those operations before calling it. A missing parent Tool is removed, and a
125
+ `nil` alias uses the Tool's own name even when the parent assigned an alias.
126
+ The supplied ID and version identify the derived definition directly.
127
+
128
+ The factory preserves Phronomy-authored model, provider, instructions,
129
+ ContextPolicy, generation settings, LLM-input hook and filters. Filter lists
130
+ are independent while configured filter and callback instances remain shared.
131
+ The original class is unchanged. This explicit factory does not change general
132
+ Agent subclass inheritance or define copying rules for application-specific
133
+ class state.
134
+
135
+ The default findings-store mode now preserves these settings through the same
136
+ factory. For example, a member configured with `max_iterations 3` and
137
+ `temperature 0.25` keeps those settings when SharedState adds `read_store` and
138
+ `write_finding`. Member Tool aliases, the generated definition identity and the
139
+ default store/prompt behavior remain unchanged. This fixes the earlier default
140
+ derivation, which explicitly copied only the model setting.
141
+
142
+ ## Application search and LLM input
143
+
144
+ Application Tools own record fields and search conditions. ContextPolicy and the
145
+ existing input-validation path continue to own LLM-input selection and budget
146
+ checks. A custom provider does not become another Context engine.
147
+
148
+ For example, `search_issues(category: "security", status: "open")` in the complete
149
+ example applies both field conditions and returns matching records as JSON. A
150
+ search can leave three records in the store while returning only one match. Its
151
+ Tool parameter types and enumerations use the existing Tool schema API.
152
+
153
+ The example has no result-size cap. A broad query may still produce a large Tool
154
+ result, and this extension does not make that result optional, rewrite recorded
155
+ Tool output or bypass the final input-budget check. Applications that need
156
+ paging, compaction or another acquisition policy must define that behavior
157
+ explicitly through the existing appropriate contracts.
158
+
159
+ SharedState continues to use the member's configured ContextPolicy. The binding
160
+ does not automatically populate `before_llm_input` candidates, inspect Context
161
+ internals or manage a provider message buffer. Example 22's adaptive reader is a
162
+ separate application implementation. Its current version uses the public sharing
163
+ contract and Context budgeting APIs introduced in v0.33.0; its previous private
164
+ overrides have been removed. The application still owns adaptive retrieval and
165
+ budget allocation rather than delegating those policies to the sharing binding.
166
+
167
+ ## Lifetime and verification
168
+
169
+ The provider owns resources for one synchronous invocation. SharedState does not
170
+ define a database schema, durable binding identity or process-transfer protocol
171
+ for the application's store. A live binding does not become a persisted
172
+ continuation. Custom sharing requires each member invocation to reach a terminal
173
+ state before coordination proceeds.
174
+
175
+ For example, the application file can check its store and Tools without calling
176
+ an LLM:
177
+
178
+ ```sh
179
+ bundle exec ruby docs/examples/shared_state_custom_sharing.rb --offline
180
+ ```
181
+
182
+ The check opens the provider, records three issues through `Tool#call`, searches
183
+ with both category and status, verifies one match and three retained records,
184
+ and verifies provider cleanup. It uses the real Tool argument-validation path.
185
+ It does not exercise the SharedState Agent loop or establish live-model,
186
+ database or multi-process acceptance.
187
+
188
+ In custom mode, SharedState releases terminal member instances through the
189
+ Agent's public `unload!` operation. A normal Agent approval wait keeps the public
190
+ `invoke` call pending. SharedState consequently stays inside the provider's
191
+ `open` block and retains its resources; it does not return a suspended result or
192
+ close the provider merely because an internal execution reaches an approval
193
+ wait. The application remains responsible for answering or cancelling approvals.
194
+
195
+ For example, when a member waits for approval of `record_issue`, the board stays
196
+ open while that invocation waits. An approval allows the Tool and member to
197
+ complete before aggregation and provider cleanup. A cancellation that ends the
198
+ member invocation also allows cleanup to run. The in-memory example's `ensure`
199
+ is scoped to the end of the invocation, not the start of an approval wait. This
200
+ is a live wait in the current process, not a way to release a Worker and restore
201
+ the board on another one.
202
+
203
+ The [SharedState integration suite](../spec/integration/shared_state_spec.rb)
204
+ separately exercises this approval lifecycle using a real Agent and fixed LLM
205
+ responses. It checks that the provider stays open and aggregation has not run
206
+ during the wait; after a public approval response, the Tool writes its record,
207
+ aggregation runs, the provider closes and the member is released. Fixed
208
+ responses verify the execution path, not a live model's Tool-selection behavior.
209
+
210
+ The lifecycle guard also detects a custom `invoke` override that returns a Hash
211
+ with a suspended status before reaching completion. That exceptional early
212
+ return raises `SharedState::MemberLifecycleError` with `reason: :suspended` and
213
+ stops coordination before later members or aggregation. It is not the behavior
214
+ of normal Agent HITL. SharedState does not approve, deny, cancel or unload the
215
+ pending execution on the application's behalf. Providers handling this case
216
+ must decide whether to retain their resources; merely retaining a binding adds
217
+ no SharedState resume operation.
218
+
219
+ `MemberLifecycleError#reason` also reports `:release_failed`. The error exposes
220
+ `agent`, the available `result`, `primary_error` and `release_error`. A failed
221
+ release preserves the Agent reference and both failure details when invocation
222
+ and release fail together, so the application can inspect the original failure
223
+ and decide whether to retry release. The provider's `open` scope can rescue this
224
+ error before its cleanup policy runs. Rescuing a coordination failure inside
225
+ `open` does not turn the invocation into success: SharedState still reports the
226
+ failure if the provider returns normally.
227
+
228
+ To run the team from an application, configure Phronomy's LLM backend normally,
229
+ set `PHRONOMY_EXAMPLE_MODEL` to that backend's model ID before requiring the file,
230
+ then invoke `SharedStateCustomSharingExample::ReviewTeam.new.invoke(task)`.
231
+ The example's fallback model name is a placeholder, not a remotely available
232
+ model. A DB-backed provider may replace the example's in-memory board, but its
233
+ transactions, isolation and recovery behavior remain application responsibilities.
@@ -77,6 +77,8 @@ module Phronomy
77
77
 
78
78
  def tool_aliases
79
79
  own = @tool_aliases || {}
80
+ return own if @tool_aliases_replace_inherited
81
+
80
82
  if superclass.respond_to?(:tool_aliases)
81
83
  superclass.tool_aliases.merge(own)
82
84
  else
@@ -84,6 +86,29 @@ module Phronomy
84
86
  end
85
87
  end
86
88
 
89
+ # Derive an Agent definition with a complete replacement Tool map.
90
+ # Phronomy authoring settings and filters are preserved; mutable filter
91
+ # lists are independent, while configured callable instances are shared.
92
+ # No Agent instance or Runtime resource is created by this factory.
93
+ # @param definitions [Hash<Class, String, nil>] Tool classes and aliases
94
+ # @param id [String] identity for the derived Agent definition
95
+ # @param version [Integer] revision for the derived Agent definition
96
+ # @return [Class<Phronomy::Agent::Base>]
97
+ # @api public
98
+ def with_tools(definitions, id:, version:)
99
+ unless definitions.is_a?(Hash)
100
+ raise ArgumentError, "with_tools expects a Hash of ToolClass => alias_or_nil"
101
+ end
102
+
103
+ derived = Class.new(self)
104
+ derived.agent_definition(id: id, version: version)
105
+ copy_authoring_settings_to(derived)
106
+ copy_authoring_filters_to(derived)
107
+ derived.tools(definitions)
108
+ derived.instance_variable_set(:@tool_aliases_replace_inherited, true)
109
+ derived
110
+ end
111
+
87
112
  def provider(name = nil)
88
113
  if name
89
114
  @provider = name
@@ -263,6 +288,34 @@ module Phronomy
263
288
 
264
289
  private
265
290
 
291
+ def copy_authoring_settings_to(derived)
292
+ settings = {
293
+ model: model,
294
+ provider: provider,
295
+ instructions: instructions,
296
+ context_policy: context_policy,
297
+ temperature: temperature,
298
+ max_iterations: max_iterations,
299
+ cache_instructions: cache_instructions,
300
+ max_output_tokens: max_output_tokens,
301
+ before_llm_input: before_llm_input
302
+ }
303
+ settings.each do |name, value|
304
+ derived.public_send(name, value) unless value.nil?
305
+ end
306
+ # These getters inherit by ivar presence. An explicit nil preserves
307
+ # absence even if the source class is configured again later.
308
+ %i[provider instructions].each do |name|
309
+ derived.instance_variable_set(:"@#{name}", nil) if settings[name].nil?
310
+ end
311
+ end
312
+
313
+ def copy_authoring_filters_to(derived)
314
+ _class_input_filters.each { |filter| derived.input_filter(filter) }
315
+ _class_output_filters.each { |filter| derived.output_filter(filter) }
316
+ _class_tool_result_filters.each { |filter| derived.tool_result_filter(filter) }
317
+ end
318
+
266
319
  def __construct_owned_agent(owner_runtime, reserved_agent_id, *args, **kwargs, &block)
267
320
  instance = allocate
268
321
  instance.send(:__prepare_runtime_owner!, owner_runtime, reserved_agent_id)
@@ -114,10 +114,6 @@ module Phronomy
114
114
  @callback_failure = nil
115
115
  end
116
116
 
117
- def streaming?
118
- @mode == :stream
119
- end
120
-
121
117
  def set_graph_metadata(phase: nil)
122
118
  @phase = phase
123
119
  end
@@ -105,6 +105,7 @@ module Phronomy
105
105
  :coordinator, :operation, :delivery, :outcome, :error
106
106
  )
107
107
  DetachHostedWait = Data.define(:coordinator, :wait, :task, :parent, :completion)
108
+ private_constant :DetachHostedWait
108
109
  SuspensionCancelledCommand = Data.define(:coordinator, :execution_id)
109
110
 
110
111
  DeferredTerminalCommand = Data.define(
@@ -179,6 +180,16 @@ module Phronomy
179
180
  result_task
180
181
  end
181
182
 
183
+ # Submit the committed wait for final ownership checks on the EventLoop.
184
+ # @api private
185
+ def request_hosted_detach(wait, task:, parent:)
186
+ completion = Phronomy::TaskResult.deferred(name: "detach-hosted-approval")
187
+ command = DetachHostedWait.new(coordinator: self, wait: wait, task: task, parent: parent, completion: completion)
188
+ accepted = @agent.__execution_environment.registry.post(command, completion: completion)
189
+ raise Phronomy::RuntimeShutdownError, "EventLoop rejected hosted detach" unless accepted
190
+ completion
191
+ end
192
+
182
193
  # Called only from an Agent FSMSession external-operation dispatch state on
183
194
  # EventLoop. Each method captures one operation-specific immutable snapshot
184
195
  # and sends only that snapshot to OffloadPool.
@@ -83,9 +83,6 @@ module Phronomy
83
83
  return :current_request if segment.delivery.to_sym == :ask_argument
84
84
  return nil if CONTROL_CATEGORIES.include?(segment.category.to_sym)
85
85
  return :knowledge if segment.category.to_sym == :knowledge
86
- unit_kind = segment.metadata["selection_unit_kind"] ||
87
- segment.metadata[:selection_unit_kind]
88
- return :tool_exchanges if unit_kind.to_s == "tool_exchange"
89
86
 
90
87
  case segment.category.to_sym
91
88
  when :external_message, :assistant_message, :tool_message,
@@ -102,12 +99,6 @@ module Phronomy
102
99
  return "context-policy-conversation:#{conversation_group_id}"
103
100
  end
104
101
 
105
- # Compatibility for finalized pre-ACS-04 manifests. New manifests use
106
- # context_policy_conversation_group_id and do not recreate Phronomy::Context::Unit.
107
- unit_id = segment.metadata["selection_unit_id"] ||
108
- segment.metadata[:selection_unit_id]
109
- return "unit:#{unit_id}" if unit_id
110
-
111
102
  "segment:#{policy_category}:#{segment.position}"
112
103
  end
113
104
 
@@ -57,8 +57,8 @@ module Phronomy
57
57
  ATTRIBUTES.to_h { |name| [name.to_s, public_send(name)] }
58
58
  end
59
59
 
60
- # Historical durable fields are not accepted here. Explicit migration must
61
- # convert them to the current semantic payload first.
60
+ # Reads the current semantic payload only. Historical durable fields are
61
+ # rejected rather than converted.
62
62
  def self.from_h(hash)
63
63
  source = hash.to_h { |key, value| [key.to_s, value] }
64
64
  expected = ATTRIBUTES.map(&:to_s).sort