phronomy 0.16.0 → 0.18.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/.mutant.yml +8 -9
- data/CHANGELOG.md +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +6 -7
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +89 -190
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +49 -377
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -265
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -2,79 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
# Implements
|
|
6
|
-
#
|
|
7
|
-
# @see https://claude.com/blog/multi-agent-coordination-patterns
|
|
8
|
-
#
|
|
9
|
-
# Multiple peer agents collaborate through a shared {KnowledgeStore}.
|
|
10
|
-
# There is no central coordinator. Each agent reads the store, acts on what it
|
|
11
|
-
# finds, and writes new findings back. Later agents in a cycle immediately see
|
|
12
|
-
# findings written by earlier agents in the same cycle.
|
|
13
|
-
#
|
|
14
|
-
# Two tools are automatically injected into every member agent at runtime:
|
|
15
|
-
# - +read_store+ — returns all current findings as a JSON string
|
|
16
|
-
# - +write_finding+ — appends a Hash finding to the store
|
|
17
|
-
#
|
|
18
|
-
# Use +member+ to register agents and optionally provide per-agent coordination
|
|
19
|
-
# instructions. Use +coordination+ to define the team-level protocol that all
|
|
20
|
-
# members receive instead of the built-in default guide.
|
|
21
|
-
#
|
|
22
|
-
# @example Basic usage with per-agent instructions
|
|
23
|
-
# class CodeReviewTeam < Phronomy::Agent::SharedState
|
|
24
|
-
# member StructureAnalyst
|
|
25
|
-
# member SecurityAuditor, instruction: "Focus on authentication and injection risks."
|
|
26
|
-
# member QualityReviewer, instruction: "Flag methods longer than 10 lines."
|
|
27
|
-
# max_cycles 3
|
|
28
|
-
# aggregate { |store| { findings: store.read_all, total: store.size } }
|
|
29
|
-
# end
|
|
30
|
-
#
|
|
31
|
-
# result = CodeReviewTeam.new.invoke("Review the files in ./src")
|
|
32
|
-
# # => { output: { findings: [...], total: N }, cycles: 3, terminated_by: :max_cycles }
|
|
33
|
-
#
|
|
34
|
-
# @example With custom team-level coordination protocol
|
|
35
|
-
# class ResearchTeam < Phronomy::Agent::SharedState
|
|
36
|
-
# coordination <<~TEXT
|
|
37
|
-
# Shared store tools: read_store (no params), write_finding(content:).
|
|
38
|
-
# Workflow: read first, then write one finding per insight.
|
|
39
|
-
# TEXT
|
|
40
|
-
# member LiteratureAgent
|
|
41
|
-
# member IndustryAgent
|
|
42
|
-
# max_cycles 10
|
|
43
|
-
# terminate_when { |store| store.size >= 20 }
|
|
44
|
-
# end
|
|
5
|
+
# Implements peer coordination through a shared KnowledgeStore.
|
|
45
6
|
class SharedState
|
|
46
|
-
# Thread-safe (serialised by sequential execution) knowledge store shared
|
|
47
|
-
# across all researcher agents within a single {SharedState#invoke} call.
|
|
48
|
-
#
|
|
49
|
-
# Each finding is stored as a Hash with the keys:
|
|
50
|
-
# :agent — Symbol derived from the researcher class name
|
|
51
|
-
# :content — String written by the researcher
|
|
52
|
-
# :cycle — Integer cycle number in which the finding was recorded
|
|
53
7
|
class KnowledgeStore
|
|
54
8
|
def initialize
|
|
55
9
|
@findings = []
|
|
56
10
|
end
|
|
57
11
|
|
|
58
|
-
# Returns a shallow copy of all findings in insertion order.
|
|
59
|
-
# @return [Array<Hash>]
|
|
60
12
|
# @api public
|
|
61
13
|
def read_all
|
|
62
14
|
@findings.dup
|
|
63
15
|
end
|
|
64
16
|
|
|
65
|
-
# Appends a new finding to the store.
|
|
66
|
-
# @param agent [Symbol] researcher identifier
|
|
67
|
-
# @param content [String] the finding text
|
|
68
|
-
# @param cycle [Integer] the current cycle number
|
|
69
|
-
# @return [nil]
|
|
70
17
|
# @api public
|
|
71
18
|
def write(agent:, content:, cycle:)
|
|
72
19
|
@findings << {agent: agent, content: content, cycle: cycle}
|
|
73
20
|
nil
|
|
74
21
|
end
|
|
75
22
|
|
|
76
|
-
# Returns the number of findings recorded so far.
|
|
77
|
-
# @return [Integer]
|
|
78
23
|
# @api public
|
|
79
24
|
def size
|
|
80
25
|
@findings.size
|
|
@@ -82,96 +27,45 @@ module Phronomy
|
|
|
82
27
|
end
|
|
83
28
|
|
|
84
29
|
class << self
|
|
85
|
-
# Registers a member agent class that will collaborate via the shared store.
|
|
86
|
-
# Members are invoked sequentially within each cycle in declaration order.
|
|
87
|
-
#
|
|
88
|
-
# @param klass [Class] an Agent::Base subclass
|
|
89
|
-
# @param instruction [String, nil] optional per-agent coordination instruction
|
|
90
|
-
# appended to the team coordination text in this agent's prompt
|
|
91
30
|
# @api public
|
|
92
31
|
def member(klass, instruction: nil)
|
|
93
32
|
@members ||= []
|
|
94
33
|
@members << {klass: klass, instruction: instruction}
|
|
95
34
|
end
|
|
96
35
|
|
|
97
|
-
# Backward-compatible alias. Registers each class as a member without a
|
|
98
|
-
# per-agent instruction. Prefer {.member} for new code.
|
|
99
|
-
#
|
|
100
|
-
# @param classes [Array<Class>] Agent::Base subclasses
|
|
101
|
-
# @api public
|
|
102
|
-
def researchers(*classes)
|
|
103
|
-
classes.flatten.each { |klass| member(klass) }
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Defines the team-level coordination protocol text injected into every
|
|
107
|
-
# member's prompt. When omitted the built-in default guide is used, which
|
|
108
|
-
# explains +read_store+ / +write_finding+ usage and enforces the standard
|
|
109
|
-
# workflow. Override this when you need a different protocol or tone.
|
|
110
|
-
#
|
|
111
|
-
# @param text [String, nil] the coordination instructions
|
|
112
36
|
# @api public
|
|
113
37
|
def coordination(text = nil)
|
|
114
38
|
text ? @coordination = text : @coordination
|
|
115
39
|
end
|
|
116
40
|
|
|
117
|
-
# Sets the maximum number of cycles to run.
|
|
118
|
-
# At least one of +max_cycles+ or +timeout+ must be configured.
|
|
119
|
-
#
|
|
120
|
-
# @param value [Integer, nil]
|
|
121
41
|
# @api public
|
|
122
42
|
def max_cycles(value = nil)
|
|
123
43
|
value ? @max_cycles = Integer(value) : @max_cycles
|
|
124
44
|
end
|
|
125
45
|
|
|
126
|
-
# Sets the maximum wall-clock seconds for the entire invocation.
|
|
127
|
-
# At least one of +max_cycles+ or +timeout+ must be configured.
|
|
128
|
-
#
|
|
129
|
-
# @param value [Numeric, nil]
|
|
130
46
|
# @api public
|
|
131
47
|
def timeout(value = nil)
|
|
132
48
|
value ? @timeout = value.to_f : @timeout
|
|
133
49
|
end
|
|
134
50
|
|
|
135
|
-
# Registers an optional convergence block. Evaluated after each completed
|
|
136
|
-
# cycle; when it returns +true+ the loop terminates early.
|
|
137
|
-
#
|
|
138
|
-
# @yield [KnowledgeStore] receives the store; return +true+ to stop
|
|
139
51
|
# @api public
|
|
140
52
|
def terminate_when(&block)
|
|
141
53
|
block ? @terminate_when = block : @terminate_when
|
|
142
54
|
end
|
|
143
55
|
|
|
144
|
-
# Defines how the final store is converted into the +:output+ of the result.
|
|
145
|
-
# When omitted, +store.read_all+ is used as-is.
|
|
146
|
-
#
|
|
147
|
-
# @yield [KnowledgeStore] receives the final store; return value becomes +:output+
|
|
148
56
|
# @api public
|
|
149
57
|
def aggregate(&block)
|
|
150
58
|
block ? @aggregator = block : @aggregator
|
|
151
59
|
end
|
|
152
60
|
|
|
153
|
-
# @!visibility private
|
|
154
61
|
def _members = Array(@members)
|
|
155
|
-
# @!visibility private — derives class list from _members for backward compat
|
|
156
|
-
def _researchers = _members.map { |m| m[:klass] }
|
|
157
|
-
# @!visibility private
|
|
158
62
|
def _coordination = @coordination
|
|
159
|
-
# @!visibility private
|
|
160
63
|
def _max_cycles = @max_cycles
|
|
161
|
-
# @!visibility private
|
|
162
64
|
def _timeout = @timeout
|
|
163
|
-
# @!visibility private
|
|
164
65
|
def _terminate_when = @terminate_when
|
|
165
|
-
# @!visibility private
|
|
166
66
|
def _aggregator = @aggregator
|
|
167
67
|
end
|
|
168
68
|
|
|
169
|
-
# Runs the shared-state coordination loop.
|
|
170
|
-
#
|
|
171
|
-
# @param input [String] the seed question or task description
|
|
172
|
-
# @param config [Hash] reserved for future use
|
|
173
|
-
# @return [Hash] +:output+, +:cycles+, +:terminated_by+
|
|
174
|
-
# @raise [ArgumentError] when neither +max_cycles+ nor +timeout+ is configured
|
|
175
69
|
# @api public
|
|
176
70
|
def invoke(input, config: {})
|
|
177
71
|
validate_termination!
|
|
@@ -181,12 +75,17 @@ module Phronomy
|
|
|
181
75
|
deadline = self.class._timeout ? Time.now + self.class._timeout : nil
|
|
182
76
|
terminated_by = :max_cycles
|
|
183
77
|
completed_cycles = 0
|
|
184
|
-
|
|
185
78
|
cycle_limit = max_cycles || Float::INFINITY
|
|
186
79
|
|
|
187
80
|
(1..cycle_limit).each do |cycle|
|
|
188
81
|
self.class._members.each do |member_config|
|
|
189
|
-
invoke_researcher(
|
|
82
|
+
invoke_researcher(
|
|
83
|
+
member_config[:klass],
|
|
84
|
+
store,
|
|
85
|
+
cycle,
|
|
86
|
+
input,
|
|
87
|
+
member_config[:instruction]
|
|
88
|
+
)
|
|
190
89
|
end
|
|
191
90
|
completed_cycles = cycle
|
|
192
91
|
|
|
@@ -199,8 +98,6 @@ module Phronomy
|
|
|
199
98
|
terminated_by = :timeout
|
|
200
99
|
break
|
|
201
100
|
end
|
|
202
|
-
|
|
203
|
-
terminated_by = :max_cycles
|
|
204
101
|
end
|
|
205
102
|
|
|
206
103
|
output = if self.class._aggregator
|
|
@@ -216,33 +113,38 @@ module Phronomy
|
|
|
216
113
|
|
|
217
114
|
def validate_termination!
|
|
218
115
|
return if self.class._max_cycles || self.class._timeout
|
|
116
|
+
|
|
219
117
|
raise ArgumentError,
|
|
220
118
|
"max_cycles or timeout must be configured before invoking SharedState"
|
|
221
119
|
end
|
|
222
120
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
121
|
+
def invoke_researcher(
|
|
122
|
+
researcher_class,
|
|
123
|
+
store,
|
|
124
|
+
cycle,
|
|
125
|
+
original_input,
|
|
126
|
+
per_agent_instruction = nil
|
|
127
|
+
)
|
|
228
128
|
instrumented = build_instrumented_researcher(researcher_class, store, cycle)
|
|
229
129
|
extra_tools = researcher_class.tools
|
|
230
|
-
prompt = build_prompt(
|
|
130
|
+
prompt = build_prompt(
|
|
131
|
+
original_input,
|
|
132
|
+
store,
|
|
133
|
+
cycle,
|
|
231
134
|
extra_tools: extra_tools,
|
|
232
|
-
per_agent_instruction: per_agent_instruction
|
|
135
|
+
per_agent_instruction: per_agent_instruction
|
|
136
|
+
)
|
|
233
137
|
instrumented.new.invoke(prompt)
|
|
234
138
|
end
|
|
235
139
|
|
|
236
|
-
# Builds an anonymous subclass of +researcher_class+ with two store tools
|
|
237
|
-
# injected. The tools close over the +store+ instance so that writes and
|
|
238
|
-
# reads are reflected in the live store.
|
|
239
140
|
def build_instrumented_researcher(researcher_class, store, cycle)
|
|
240
141
|
agent_key = researcher_class.name&.to_sym || researcher_class.object_id.to_s.to_sym
|
|
241
142
|
|
|
242
143
|
read_tool = Class.new(Phronomy::Agent::Context::Capability::Base) do
|
|
243
144
|
tool_name "read_store"
|
|
244
145
|
description "Read all current findings from the shared knowledge store. " \
|
|
245
|
-
|
|
146
|
+
"Call this to see what other researchers have discovered."
|
|
147
|
+
execution_mode :cooperative
|
|
246
148
|
|
|
247
149
|
define_method(:execute) { store.read_all.to_json }
|
|
248
150
|
end
|
|
@@ -250,7 +152,8 @@ module Phronomy
|
|
|
250
152
|
write_tool = Class.new(Phronomy::Agent::Context::Capability::Base) do
|
|
251
153
|
tool_name "write_finding"
|
|
252
154
|
description "Record a new finding into the shared knowledge store so " \
|
|
253
|
-
|
|
155
|
+
"that other researchers can build on your discovery."
|
|
156
|
+
execution_mode :cooperative
|
|
254
157
|
param :content, type: :string, desc: "The finding to record"
|
|
255
158
|
|
|
256
159
|
define_method(:execute) do |content:|
|
|
@@ -259,16 +162,22 @@ module Phronomy
|
|
|
259
162
|
end
|
|
260
163
|
end
|
|
261
164
|
|
|
262
|
-
|
|
263
|
-
|
|
165
|
+
definitions = researcher_class.tools.to_h do |tool_class|
|
|
166
|
+
[tool_class, researcher_class.tool_aliases[tool_class]]
|
|
167
|
+
end
|
|
168
|
+
definitions[read_tool] = nil
|
|
169
|
+
definitions[write_tool] = nil
|
|
170
|
+
|
|
171
|
+
Class.new(researcher_class) { tools(definitions) }
|
|
264
172
|
end
|
|
265
173
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
174
|
+
def build_prompt(
|
|
175
|
+
original_input,
|
|
176
|
+
store,
|
|
177
|
+
cycle,
|
|
178
|
+
extra_tools: [],
|
|
179
|
+
per_agent_instruction: nil
|
|
180
|
+
)
|
|
272
181
|
guide = self.class._coordination || default_coordination_guide(extra_tools)
|
|
273
182
|
|
|
274
183
|
prompt_parts = [guide]
|
|
@@ -281,20 +190,21 @@ module Phronomy
|
|
|
281
190
|
return base if store.size == 0
|
|
282
191
|
|
|
283
192
|
findings_text = store.read_all
|
|
284
|
-
.map { |
|
|
193
|
+
.map { |finding|
|
|
194
|
+
"- [#{finding[:agent]} / cycle #{finding[:cycle]}] #{finding[:content]}"
|
|
195
|
+
}
|
|
285
196
|
.join("\n")
|
|
286
197
|
|
|
287
198
|
"#{header}\n\nTask: #{original_input}\n\nFindings so far:\n#{findings_text}"
|
|
288
199
|
end
|
|
289
200
|
|
|
290
|
-
# Builds the default tool-usage guide for member agents.
|
|
291
|
-
# Describes +read_store+ / +write_finding+ and the required workflow.
|
|
292
|
-
# When extra_tools are present, lists their names so the agent knows
|
|
293
|
-
# what additional tools are available.
|
|
294
201
|
def default_coordination_guide(extra_tools)
|
|
295
202
|
extra_line = if extra_tools.any?
|
|
296
|
-
tool_names = extra_tools.map
|
|
297
|
-
|
|
203
|
+
tool_names = extra_tools.map do |tool|
|
|
204
|
+
tool.respond_to?(:tool_name) ? tool.tool_name : tool.name.to_s
|
|
205
|
+
end.join(", ")
|
|
206
|
+
" You also have access to additional tools (#{tool_names}) — " \
|
|
207
|
+
"use them to gather information before writing findings.\n"
|
|
298
208
|
else
|
|
299
209
|
""
|
|
300
210
|
end
|
|
@@ -43,9 +43,8 @@ module Phronomy
|
|
|
43
43
|
"set max_output_tokens or Phronomy.configuration.default_output_reserve"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# instructions and tool definitions a second time.
|
|
46
|
+
# Mandatory Manifest content is accounted separately by ContextAssembler
|
|
47
|
+
# and the final budget validator. TokenBudget reserves output capacity only.
|
|
49
48
|
Phronomy::LlmContextWindow::TokenBudget.new(
|
|
50
49
|
context_window: context_window,
|
|
51
50
|
max_output_tokens: reserve
|
|
@@ -62,7 +61,9 @@ module Phronomy
|
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
def stringify_keys(hash)
|
|
65
|
-
hash.to_h.each_with_object({})
|
|
64
|
+
hash.to_h.each_with_object({}) do |(key, value), result|
|
|
65
|
+
result[key.to_s] = value
|
|
66
|
+
end
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
end
|
|
@@ -2,90 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
#
|
|
5
|
+
# Routes Tool work according to the Tool execution contract.
|
|
6
6
|
#
|
|
7
|
-
# Tool
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# :cooperative Tool calls execute inline and must return quickly. call_async
|
|
8
|
+
# wraps their result in an already-settled Task and never consumes an
|
|
9
|
+
# OffloadPool worker.
|
|
10
|
+
#
|
|
11
|
+
# :offloaded Tool calls route synchronous work through OffloadPool. Phronomy
|
|
12
|
+
# does not distinguish whether the reason is blocking I/O, CPU-bound work, or
|
|
13
|
+
# another long synchronous operation.
|
|
11
14
|
module ToolExecutor
|
|
12
|
-
WARNED_MODES = Set.new
|
|
13
|
-
WARNED_MODES_MUTEX = Mutex.new
|
|
14
|
-
private_constant :WARNED_MODES, :WARNED_MODES_MUTEX
|
|
15
|
-
|
|
16
|
-
# Agent-owned execution boundary. Only a ToolInvocation that has consumed
|
|
17
|
-
# authorization may enter this method.
|
|
18
|
-
def self.call_invocation_async(
|
|
19
|
-
tool_invocation:,
|
|
20
|
-
cancellation_token: nil,
|
|
21
|
-
config: {},
|
|
22
|
-
runtime: Phronomy::Runtime.instance
|
|
23
|
-
)
|
|
24
|
-
unless tool_invocation.dispatchable?
|
|
25
|
-
raise Phronomy::ToolError,
|
|
26
|
-
"ToolInvocation #{tool_invocation.id} is not authorized for dispatch"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
call_async(
|
|
30
|
-
tool: tool_invocation.tool,
|
|
31
|
-
args: tool_invocation.arguments,
|
|
32
|
-
cancellation_token: cancellation_token,
|
|
33
|
-
config: config,
|
|
34
|
-
runtime: runtime
|
|
35
|
-
)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Low-level Tool API used by direct Tool#call_async callers. Agent execution
|
|
39
|
-
# must use .call_invocation_async so authorization cannot be bypassed.
|
|
40
|
-
#
|
|
41
|
-
# +config+ remains available for invocation metadata, but Phronomy does not
|
|
42
|
-
# interpret it as a Tool timeout or retry policy.
|
|
43
15
|
def self.call_async(
|
|
44
16
|
tool:,
|
|
45
17
|
args:,
|
|
46
18
|
cancellation_token: nil,
|
|
47
19
|
config: {},
|
|
48
|
-
runtime:
|
|
20
|
+
runtime: nil,
|
|
21
|
+
on_full: :raise
|
|
49
22
|
)
|
|
50
|
-
ct = cancellation_token
|
|
51
23
|
mode = tool.class.execution_mode
|
|
52
24
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"[Phronomy] Tool #{tool.class.name} declares execution_mode :external_process, " \
|
|
63
|
-
"which has no dedicated process manager. Falling back to blocking_io " \
|
|
64
|
-
"(BlockingAdapterPool)."
|
|
65
|
-
end
|
|
66
|
-
if Phronomy.configuration.logger
|
|
67
|
-
Phronomy.configuration.logger.warn(message)
|
|
68
|
-
else
|
|
69
|
-
warn message
|
|
70
|
-
end
|
|
25
|
+
case mode
|
|
26
|
+
when :cooperative
|
|
27
|
+
task = Phronomy::Task.deferred(name: "tool-#{tool.name}")
|
|
28
|
+
begin
|
|
29
|
+
task.complete(
|
|
30
|
+
tool.call(args, cancellation_token: cancellation_token)
|
|
31
|
+
)
|
|
32
|
+
rescue => error
|
|
33
|
+
task.fail(error)
|
|
71
34
|
end
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if mode == :cooperative || pool.nil?
|
|
82
|
-
runtime.spawn(name: "tool-#{tool.class.name.to_s.split("::").last}") do
|
|
83
|
-
tool.call(args, cancellation_token: ct)
|
|
35
|
+
task
|
|
36
|
+
when :offloaded
|
|
37
|
+
runtime ||= Phronomy::Runtime.instance
|
|
38
|
+
runtime.offload.submit(
|
|
39
|
+
cancellation_token: cancellation_token,
|
|
40
|
+
on_full: on_full
|
|
41
|
+
) do
|
|
42
|
+
tool.call(args, cancellation_token: cancellation_token)
|
|
84
43
|
end
|
|
85
44
|
else
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
45
|
+
raise Phronomy::ConfigurationError,
|
|
46
|
+
"unknown Tool execution_mode: #{mode.inspect}"
|
|
89
47
|
end
|
|
90
48
|
end
|
|
91
49
|
end
|