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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -0
- data/CONTRIBUTING.md +5 -5
- data/README.md +20 -1
- data/RELEASE_CHECKLIST.md +7 -1
- data/SECURITY.md +1 -1
- data/VERIFY.sh +1 -9
- data/docs/architecture/error-api-review.md +6 -2
- data/docs/architecture/structured-output-and-scope.md +8 -8
- data/docs/architecture/trust-acceptance-followup.md +15 -0
- data/docs/context-policy-budgeting.md +108 -0
- data/docs/decisions/016-semantic-multi-agent-handoff.md +6 -5
- data/docs/examples/shared_state_custom_sharing.rb +145 -0
- data/docs/features.md +4 -2
- data/docs/mcp-client.md +4 -2
- data/docs/migrations/cleanup-audit.md +89 -0
- data/docs/migrations/legacy-format-removal.md +55 -0
- data/docs/migrations/r8-unit6.md +4 -2
- data/docs/migrations/shared-state-multi-agent.md +5 -0
- data/docs/migrations/vector-async-clients.md +6 -1
- data/docs/runtime-and-concurrency.md +36 -0
- data/docs/shared-state-sharing.md +233 -0
- data/lib/phronomy/agent/base.rb +53 -0
- data/lib/phronomy/agent/execution/agent_invocation.rb +0 -4
- data/lib/phronomy/agent/execution/execution_coordinator.rb +11 -0
- data/lib/phronomy/agent/handoff/transfer_projection.rb +0 -9
- data/lib/phronomy/agent/journal/journal_record.rb +2 -2
- data/lib/phronomy/agent/lifecycle/hosted_invocation.rb +2 -5
- data/lib/phronomy/agent/recovery/recovery_rules.rb +0 -12
- data/lib/phronomy/agent/store.rb +3 -8
- data/lib/phronomy/agent/tool_execution/tool_approval_request.rb +3 -2
- data/lib/phronomy/agent/tool_execution/tool_invocation.rb +9 -36
- data/lib/phronomy/context/context_policy.rb +13 -8
- data/lib/phronomy/context/context_policy_input.rb +26 -0
- data/lib/phronomy/context/llm_input_manifest.rb +3 -3
- data/lib/phronomy/engine/runtime/shutdown_result.rb +0 -5
- data/lib/phronomy/execution/execution.rb +0 -11
- data/lib/phronomy/multi_agent/shared_state/binding.rb +44 -0
- data/lib/phronomy/multi_agent/shared_state/member_lifecycle_error.rb +32 -0
- data/lib/phronomy/multi_agent/shared_state.rb +152 -10
- data/lib/phronomy/tool/base.rb +69 -3
- data/lib/phronomy/tool/schema.rb +23 -11
- data/lib/phronomy/version.rb +1 -1
- data/scripts/api_snapshot.rb +6 -3
- data/sig/phronomy/agent.rbs +1 -0
- data/sig/phronomy/context.rbs +3 -0
- data/sig/phronomy/multi_agent.rbs +26 -2
- data/sig/phronomy/tool.rbs +1 -0
- metadata +9 -11
- data/lib/phronomy/execution/scheduler_reentrancy_error.rb +0 -9
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +0 -226
- data/scripts/add_to_h_to_token_doubles.rb +0 -33
- data/scripts/add_to_h_unnamed_doubles.rb +0 -27
- data/scripts/migrate_generator_verifier_result.rb +0 -18
- data/scripts/migrate_spec_agent_definition.rb +0 -108
- data/scripts/migrate_spec_agent_definition_pass2.rb +0 -53
- data/scripts/migrate_spec_inline_pass3.rb +0 -24
- 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
|
|
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.
|
data/docs/migrations/r8-unit6.md
CHANGED
|
@@ -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).
|
|
95
|
-
|
|
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
|
|
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.
|
data/lib/phronomy/agent/base.rb
CHANGED
|
@@ -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)
|
|
@@ -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
|
-
#
|
|
61
|
-
#
|
|
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
|