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 +4 -4
- data/CHANGELOG.md +77 -0
- data/README.md +7 -1
- data/RELEASE_CHECKLIST.md +4 -0
- data/docs/architecture/structured-output-and-scope.md +5 -4
- data/docs/architecture/trust-acceptance-followup.md +15 -0
- data/docs/context-policy-budgeting.md +108 -0
- data/docs/examples/shared_state_custom_sharing.rb +145 -0
- data/docs/features.md +2 -0
- data/docs/mcp-client.md +4 -2
- data/docs/migrations/shared-state-multi-agent.md +5 -0
- data/docs/runtime-and-concurrency.md +36 -0
- data/docs/shared-state-sharing.md +223 -0
- data/lib/phronomy/agent/base.rb +53 -0
- data/lib/phronomy/agent/execution/execution_coordinator.rb +11 -0
- data/lib/phronomy/agent/lifecycle/hosted_invocation.rb +2 -5
- data/lib/phronomy/agent/tool_execution/tool_approval_request.rb +3 -2
- data/lib/phronomy/agent/tool_execution/tool_invocation.rb +12 -36
- data/lib/phronomy/context/context_policy.rb +11 -2
- data/lib/phronomy/context/context_policy_input.rb +26 -0
- 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 +147 -2
- 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 +7 -2
|
@@ -0,0 +1,223 @@
|
|
|
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 and application Tools. It does
|
|
106
|
+
not need to read Agent private settings.
|
|
107
|
+
|
|
108
|
+
For example, inside the complete example's module, a provider can supply the
|
|
109
|
+
entire Tool map for a derived researcher class:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
IssueSharing.new.open(input: "Review authentication", config: {}) do |binding|
|
|
113
|
+
tool_map = binding.tool_definitions(member: Researcher, cycle: 1)
|
|
114
|
+
derived = Researcher.with_tools(
|
|
115
|
+
tool_map, id: "issue-researcher-with-board", version: 1
|
|
116
|
+
)
|
|
117
|
+
# `derived` is a class; no Agent instance or Runtime resource was created.
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`definitions` is the complete Hash of Tool classes to aliases or `nil`; this
|
|
122
|
+
factory does not merge Tools or detect naming collisions. SharedState performs
|
|
123
|
+
those operations before calling it. A missing parent Tool is removed, and a
|
|
124
|
+
`nil` alias uses the Tool's own name even when the parent assigned an alias.
|
|
125
|
+
The supplied ID and version identify the derived definition directly.
|
|
126
|
+
|
|
127
|
+
The factory preserves Phronomy-authored model, provider, instructions,
|
|
128
|
+
ContextPolicy, generation settings, LLM-input hook and filters. Filter lists
|
|
129
|
+
are independent while configured filter and callback instances remain shared.
|
|
130
|
+
The original class is unchanged. This explicit factory does not change general
|
|
131
|
+
Agent subclass inheritance or define copying rules for application-specific
|
|
132
|
+
class state.
|
|
133
|
+
|
|
134
|
+
## Application search and LLM input
|
|
135
|
+
|
|
136
|
+
Application Tools own record fields and search conditions. ContextPolicy and the
|
|
137
|
+
existing input-validation path continue to own LLM-input selection and budget
|
|
138
|
+
checks. A custom provider does not become another Context engine.
|
|
139
|
+
|
|
140
|
+
For example, `search_issues(category: "security", status: "open")` in the complete
|
|
141
|
+
example applies both field conditions and returns matching records as JSON. A
|
|
142
|
+
search can leave three records in the store while returning only one match. Its
|
|
143
|
+
Tool parameter types and enumerations use the existing Tool schema API.
|
|
144
|
+
|
|
145
|
+
The example has no result-size cap. A broad query may still produce a large Tool
|
|
146
|
+
result, and this extension does not make that result optional, rewrite recorded
|
|
147
|
+
Tool output or bypass the final input-budget check. Applications that need
|
|
148
|
+
paging, compaction or another acquisition policy must define that behavior
|
|
149
|
+
explicitly through the existing appropriate contracts.
|
|
150
|
+
|
|
151
|
+
SharedState continues to use the member's configured ContextPolicy. The binding
|
|
152
|
+
does not automatically populate `before_llm_input` candidates, inspect Context
|
|
153
|
+
internals or manage a provider message buffer. Example 22's adaptive reader is a
|
|
154
|
+
separate application implementation; this extension alone does not migrate it
|
|
155
|
+
or remove its existing private-API dependency.
|
|
156
|
+
|
|
157
|
+
## Lifetime and verification
|
|
158
|
+
|
|
159
|
+
The provider owns resources for one synchronous invocation. SharedState does not
|
|
160
|
+
define a database schema, durable binding identity or process-transfer protocol
|
|
161
|
+
for the application's store. A live binding does not become a persisted
|
|
162
|
+
continuation. Custom sharing requires each member invocation to reach a terminal
|
|
163
|
+
state before coordination proceeds.
|
|
164
|
+
|
|
165
|
+
For example, the application file can check its store and Tools without calling
|
|
166
|
+
an LLM:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
bundle exec ruby docs/examples/shared_state_custom_sharing.rb --offline
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The check opens the provider, records three issues through `Tool#call`, searches
|
|
173
|
+
with both category and status, verifies one match and three retained records,
|
|
174
|
+
and verifies provider cleanup. It uses the real Tool argument-validation path.
|
|
175
|
+
It does not exercise the SharedState Agent loop or establish live-model,
|
|
176
|
+
database or multi-process acceptance.
|
|
177
|
+
|
|
178
|
+
In custom mode, SharedState releases terminal member instances through the
|
|
179
|
+
Agent's public `unload!` operation. A normal Agent approval wait keeps the public
|
|
180
|
+
`invoke` call pending. SharedState consequently stays inside the provider's
|
|
181
|
+
`open` block and retains its resources; it does not return a suspended result or
|
|
182
|
+
close the provider merely because an internal execution reaches an approval
|
|
183
|
+
wait. The application remains responsible for answering or cancelling approvals.
|
|
184
|
+
|
|
185
|
+
For example, when a member waits for approval of `record_issue`, the board stays
|
|
186
|
+
open while that invocation waits. An approval allows the Tool and member to
|
|
187
|
+
complete before aggregation and provider cleanup. A cancellation that ends the
|
|
188
|
+
member invocation also allows cleanup to run. The in-memory example's `ensure`
|
|
189
|
+
is scoped to the end of the invocation, not the start of an approval wait. This
|
|
190
|
+
is a live wait in the current process, not a way to release a Worker and restore
|
|
191
|
+
the board on another one.
|
|
192
|
+
|
|
193
|
+
The [SharedState integration suite](../spec/integration/shared_state_spec.rb)
|
|
194
|
+
separately exercises this approval lifecycle using a real Agent and fixed LLM
|
|
195
|
+
responses. It checks that the provider stays open and aggregation has not run
|
|
196
|
+
during the wait; after a public approval response, the Tool writes its record,
|
|
197
|
+
aggregation runs, the provider closes and the member is released. Fixed
|
|
198
|
+
responses verify the execution path, not a live model's Tool-selection behavior.
|
|
199
|
+
|
|
200
|
+
The lifecycle guard also detects a custom `invoke` override that returns a Hash
|
|
201
|
+
with a suspended status before reaching completion. That exceptional early
|
|
202
|
+
return raises `SharedState::MemberLifecycleError` with `reason: :suspended` and
|
|
203
|
+
stops coordination before later members or aggregation. It is not the behavior
|
|
204
|
+
of normal Agent HITL. SharedState does not approve, deny, cancel or unload the
|
|
205
|
+
pending execution on the application's behalf. Providers handling this case
|
|
206
|
+
must decide whether to retain their resources; merely retaining a binding adds
|
|
207
|
+
no SharedState resume operation.
|
|
208
|
+
|
|
209
|
+
`MemberLifecycleError#reason` also reports `:release_failed`. The error exposes
|
|
210
|
+
`agent`, the available `result`, `primary_error` and `release_error`. A failed
|
|
211
|
+
release preserves the Agent reference and both failure details when invocation
|
|
212
|
+
and release fail together, so the application can inspect the original failure
|
|
213
|
+
and decide whether to retry release. The provider's `open` scope can rescue this
|
|
214
|
+
error before its cleanup policy runs. Rescuing a coordination failure inside
|
|
215
|
+
`open` does not turn the invocation into success: SharedState still reports the
|
|
216
|
+
failure if the provider returns normally.
|
|
217
|
+
|
|
218
|
+
To run the team from an application, configure Phronomy's LLM backend normally,
|
|
219
|
+
set `PHRONOMY_EXAMPLE_MODEL` to that backend's model ID before requiring the file,
|
|
220
|
+
then invoke `SharedStateCustomSharingExample::ReviewTeam.new.invoke(task)`.
|
|
221
|
+
The example's fallback model name is a placeholder, not a remotely available
|
|
222
|
+
model. A DB-backed provider may replace the example's in-memory board, but its
|
|
223
|
+
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.
|
|
@@ -37,15 +37,12 @@ module Phronomy
|
|
|
37
37
|
unless wait && wait.approval_request_id == approval_request_id
|
|
38
38
|
raise Phronomy::AgentBusyError, "No matching hosted approval wait"
|
|
39
39
|
end
|
|
40
|
-
completion = Phronomy::TaskResult.deferred(name: "detach-hosted-approval")
|
|
41
40
|
owner = @environment.registry.agent_execution_owner(execution_id)
|
|
42
41
|
unless owner && (child ? owner.agent.persistence.equal?(@agent.persistence) : owner.agent.equal?(@agent))
|
|
43
42
|
raise Phronomy::AgentBusyError, "Hosted execution has no matching local owner"
|
|
44
43
|
end
|
|
45
|
-
|
|
46
|
-
task: (child && !@resumed_from) ? nil : @task, parent: child ? @agent : nil
|
|
47
|
-
accepted = @environment.registry.post(command, completion: completion)
|
|
48
|
-
raise Phronomy::RuntimeShutdownError, "EventLoop rejected hosted detach" unless accepted
|
|
44
|
+
completion = owner.coordinator.request_hosted_detach(wait,
|
|
45
|
+
task: (child && !@resumed_from) ? nil : @task, parent: child ? @agent : nil)
|
|
49
46
|
completion.wait_result
|
|
50
47
|
end
|
|
51
48
|
|
|
@@ -101,12 +101,13 @@ module Phronomy
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def self.build_item(invocation)
|
|
104
|
+
display = invocation.approval_display
|
|
104
105
|
Item.new(
|
|
105
106
|
tool_invocation_id: invocation.id,
|
|
106
107
|
tool_call_id: invocation.tool_call_id,
|
|
107
108
|
tool_name: invocation.tool_name,
|
|
108
|
-
arguments:
|
|
109
|
-
facts:
|
|
109
|
+
arguments: display.fetch(:arguments),
|
|
110
|
+
facts: display.fetch(:facts),
|
|
110
111
|
reason: invocation.authorization_reason,
|
|
111
112
|
origin: invocation.origin,
|
|
112
113
|
metadata: invocation.metadata
|
|
@@ -333,14 +333,21 @@ module Phronomy
|
|
|
333
333
|
@tool&.respond_to?(:parameters_schema) ? @tool.parameters_schema : {}
|
|
334
334
|
end
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
# The approval Item snapshots these fields at its own construction boundary.
|
|
337
|
+
# @api private
|
|
338
|
+
def approval_display
|
|
339
|
+
return {arguments: {}.freeze, facts: {}.freeze}.freeze unless @tool&.respond_to?(:approval_display)
|
|
339
340
|
|
|
340
|
-
|
|
341
|
-
|
|
341
|
+
display = @tool.approval_display(arguments: @arguments || @raw_arguments, facts: @facts)
|
|
342
|
+
unless Hash === display && Hash === display.fetch(:arguments, nil) && Hash === display.fetch(:facts, nil)
|
|
343
|
+
raise Phronomy::ConfigurationError, "Tool approval_display must return :arguments and :facts Hashes"
|
|
344
|
+
end
|
|
345
|
+
display
|
|
342
346
|
end
|
|
343
347
|
|
|
348
|
+
def display_arguments = immutable_copy(approval_display.fetch(:arguments))
|
|
349
|
+
def display_facts = immutable_copy(approval_display.fetch(:facts))
|
|
350
|
+
|
|
344
351
|
private
|
|
345
352
|
|
|
346
353
|
def restore_saved_state!(status, result)
|
|
@@ -544,37 +551,6 @@ module Phronomy
|
|
|
544
551
|
def immutable_copy(value)
|
|
545
552
|
Phronomy::Values::Immutable.copy(value)
|
|
546
553
|
end
|
|
547
|
-
|
|
548
|
-
def redact_for_display(value)
|
|
549
|
-
if @tool&.respond_to?(:redacted_args, true)
|
|
550
|
-
immutable_copy(@tool.send(:redacted_args, value || {}))
|
|
551
|
-
else
|
|
552
|
-
immutable_copy(value || {})
|
|
553
|
-
end
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
def sensitive_argument_values
|
|
557
|
-
return [] unless @tool&.class&.respond_to?(:redact_params)
|
|
558
|
-
normalized = (@arguments || @raw_arguments || {}).transform_keys(&:to_sym)
|
|
559
|
-
@tool.class.redact_params.filter_map { |name| normalized[name] }
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
def redact_value(value, sensitive_values)
|
|
563
|
-
return "[REDACTED]" if sensitive_values.any? { |sensitive| sensitive == value }
|
|
564
|
-
|
|
565
|
-
case value
|
|
566
|
-
when Hash
|
|
567
|
-
value.each_with_object({}) do |(key, item), result|
|
|
568
|
-
result[key] = redact_value(item, sensitive_values)
|
|
569
|
-
end.freeze
|
|
570
|
-
when Array
|
|
571
|
-
value.map { |item| redact_value(item, sensitive_values) }.freeze
|
|
572
|
-
when String
|
|
573
|
-
"[REDACTED]"
|
|
574
|
-
else
|
|
575
|
-
value
|
|
576
|
-
end
|
|
577
|
-
end
|
|
578
554
|
end
|
|
579
555
|
end
|
|
580
556
|
end
|
|
@@ -11,6 +11,16 @@ module Phronomy
|
|
|
11
11
|
|
|
12
12
|
protected
|
|
13
13
|
|
|
14
|
+
# Uses the same canonical representation and estimator as generated Policy
|
|
15
|
+
# items. Estimates are planning values, not a model tokenizer guarantee.
|
|
16
|
+
# @param content [Object] text or JSON-serializable content
|
|
17
|
+
# @return [Integer] estimated token count
|
|
18
|
+
# @api public
|
|
19
|
+
def estimate_tokens(content)
|
|
20
|
+
bytes = content.is_a?(String) ? content : Phronomy::CanonicalJSON.dump(content)
|
|
21
|
+
Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes)
|
|
22
|
+
end
|
|
23
|
+
|
|
14
24
|
def instruction_item(
|
|
15
25
|
content:,
|
|
16
26
|
role: :system,
|
|
@@ -113,8 +123,7 @@ module Phronomy
|
|
|
113
123
|
end
|
|
114
124
|
|
|
115
125
|
def estimate_content(content)
|
|
116
|
-
|
|
117
|
-
Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes)
|
|
126
|
+
estimate_tokens(content)
|
|
118
127
|
end
|
|
119
128
|
end
|
|
120
129
|
end
|
|
@@ -56,6 +56,32 @@ module Phronomy
|
|
|
56
56
|
freeze
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Reports the input limit supplied to this Policy without exposing the
|
|
60
|
+
# framework's budget representation. Unknown capability remains nil.
|
|
61
|
+
# @return [Integer, nil]
|
|
62
|
+
# @api public
|
|
63
|
+
def input_token_limit
|
|
64
|
+
token_budget&.effective_input_limit
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns a selection input with an application limit. A known limit may
|
|
68
|
+
# only be retained or reduced. An unknown limit accepts an explicit cap
|
|
69
|
+
# without asserting that the model can accept that many tokens.
|
|
70
|
+
# @param limit [Integer] positive selection limit
|
|
71
|
+
# @return [ContextPolicyInput]
|
|
72
|
+
# @raise [ArgumentError] for an invalid limit or a known-limit increase
|
|
73
|
+
# @api public
|
|
74
|
+
def with_input_token_limit(limit)
|
|
75
|
+
unless limit.is_a?(Integer) && limit.positive?
|
|
76
|
+
raise ArgumentError, "input token limit must be a positive Integer"
|
|
77
|
+
end
|
|
78
|
+
if input_token_limit && limit > input_token_limit
|
|
79
|
+
raise ArgumentError, "input token limit must not exceed the current limit"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
with(token_budget: Phronomy::LlmContextWindow::TokenBudget.new(max_input_tokens: limit))
|
|
83
|
+
end
|
|
84
|
+
|
|
59
85
|
private
|
|
60
86
|
|
|
61
87
|
def validate_items!(items, expected_class, label)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module MultiAgent
|
|
5
|
+
class SharedState
|
|
6
|
+
# Connects one application's shared data to the Tools offered to members.
|
|
7
|
+
# The application owns the store's shape, operations and resource lifetime.
|
|
8
|
+
# @api public
|
|
9
|
+
class Binding
|
|
10
|
+
# @param store [Object] opaque application data passed to coordination callbacks
|
|
11
|
+
# @param tools [#call] builder accepting member: (Agent class) and cycle:
|
|
12
|
+
# @api public
|
|
13
|
+
def initialize(store:, tools:)
|
|
14
|
+
raise ArgumentError, "sharing tools must respond to call" unless tools.respond_to?(:call)
|
|
15
|
+
|
|
16
|
+
@store = store
|
|
17
|
+
@tools = tools
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api public
|
|
22
|
+
attr_reader :store
|
|
23
|
+
|
|
24
|
+
# Build invocation-bound Tools using the existing Agent.tools declaration shape.
|
|
25
|
+
# The builder receives the registered Agent class, not an Agent instance.
|
|
26
|
+
# @return [Hash<Class, String, nil>] Tool classes mapped to aliases or nil
|
|
27
|
+
# @api public
|
|
28
|
+
def tool_definitions(member:, cycle:)
|
|
29
|
+
definitions = @tools.call(member: member, cycle: cycle)
|
|
30
|
+
unless definitions.is_a?(Hash)
|
|
31
|
+
raise ArgumentError, "sharing tools must return a Hash of ToolClass => alias_or_nil"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
definitions.to_h do |tool_class, alias_name|
|
|
35
|
+
unless tool_class.is_a?(Class) && tool_class <= Phronomy::Tool::Base
|
|
36
|
+
raise ArgumentError, "sharing tools must contain Phronomy::Tool::Base classes"
|
|
37
|
+
end
|
|
38
|
+
[tool_class, alias_name&.to_s&.dup&.freeze]
|
|
39
|
+
end.freeze
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module MultiAgent
|
|
5
|
+
class SharedState
|
|
6
|
+
# Keeps the member handle when invocation-scoped sharing cannot finish safely.
|
|
7
|
+
# Callers choose approval, cancellation or release through existing Agent APIs.
|
|
8
|
+
# @api public
|
|
9
|
+
class MemberLifecycleError < Phronomy::Error
|
|
10
|
+
# @api public
|
|
11
|
+
attr_reader :agent, :result, :reason, :primary_error, :release_error
|
|
12
|
+
|
|
13
|
+
# The live Agent and application resources are process-local, not a
|
|
14
|
+
# durable recipe for rebuilding the sharing provider or its Tools.
|
|
15
|
+
# @api public
|
|
16
|
+
def initialize(agent:, result:, reason:, primary_error: nil, release_error: nil)
|
|
17
|
+
@agent = agent
|
|
18
|
+
@result = result
|
|
19
|
+
@reason = reason
|
|
20
|
+
@primary_error = primary_error
|
|
21
|
+
@release_error = release_error
|
|
22
|
+
message = if reason == :suspended
|
|
23
|
+
"SharedState sharing cannot complete while a member is suspended"
|
|
24
|
+
else
|
|
25
|
+
"SharedState sharing could not release a member: #{release_error&.message}"
|
|
26
|
+
end
|
|
27
|
+
super(message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|