omni_agent 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53319cab79a557426f4a7a109a1627c425cffe49d56dc6f2d0baba58ab74b24d
4
- data.tar.gz: 541ff86413a12fbf65f95c12945e45a74d9baac18f9f7d446f5dce3281ae93ce
3
+ metadata.gz: ea0324a20bc12e064b994b4fcdf7af700cd0280ba7b0609dc75724717e9fc881
4
+ data.tar.gz: dfda03e711340e59dfbb1cd803bc785a51bbe39d972992c6a26fb5b2d4d8761a
5
5
  SHA512:
6
- metadata.gz: 996ba6f8c9679fea087dccc4fc202de1b4fb2e8debece3fa63438719447819590294b0e965228ff4fb2e93eb25556283cce000dde2f04bba8fb2ee14a3b3ccfd
7
- data.tar.gz: 8620ac25e792aa0213f00abbf717b814c4b3cca8a09af45a8659f9e557d9df5c0e33363ff109615f139403595630832cf8ff0c370d88a1a26bdfc74bb97ccd6e
6
+ metadata.gz: 72739c339838d0f38c172455226ff79244ab7e739920ec9e7b89f16182d220a1526886bff1f85b7b943bc6426676d5cc4dbd727f903c612df401587fc627f3f0
7
+ data.tar.gz: b90d265c701efaf90d5d5f5e5f9aac3b337095e998d413bf5e872e05d794a9793a44e0bd669425d30ee13eabd50ded2f0fdc2c19cadbc1ff976ec02d0e8d888f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.8](https://github.com/ACR1209/omni_agent/compare/omni_agent/v0.1.7...omni_agent/v0.1.8) (2026-07-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * implement multi-agent delegation with context forwarding and depth control ([e9b5497](https://github.com/ACR1209/omni_agent/commit/e9b5497b0af89172701bf4c5d93b8d5dccc77a26))
11
+
5
12
  ## [0.1.7](https://github.com/ACR1209/omni_agent/compare/omni_agent-v0.1.6...omni_agent/v0.1.7) (2026-06-22)
6
13
 
7
14
 
data/README.md CHANGED
@@ -97,6 +97,38 @@ module ResearchAgent::Tools
97
97
  end
98
98
  ```
99
99
 
100
+ ## Multi-Agent Delegation
101
+
102
+ `delegate_to` turns an agent into a supervisor: it wraps another agent class as a tool, so the supervisor's LLM can decide when to hand off to it. The delegated agent is a normal, independently defined agent (e.g. `app/agents/research_agent.rb`) — no manual tool file needed.
103
+
104
+ ```ruby
105
+ class SupervisorAgent < OmniAgent::Agent
106
+ use_model "gpt-4o"
107
+
108
+ delegate_to ResearchAgent, as: :research, description: "Look up factual info"
109
+ delegate_to MathAgent, as: :calculate, description: "Do arithmetic"
110
+ end
111
+ ```
112
+
113
+ Each delegated agent runs in isolation (its own fresh instance, no shared context) and returns its final answer as the tool result. Delegation depth is capped by `OmniAgent.configuration.max_delegation_depth` (default `5`) to guard against runaway recursive delegation; exceeding it raises `OmniAgent::MaxDelegationDepthError`.
114
+
115
+ Pass `run_alias:` to call a `run_aliases` method (or any zero-arg run entrypoint) on the delegated agent instead of its default `#run` — useful when the sub-agent should render a different prompt file for delegated calls:
116
+
117
+ ```ruby
118
+ class SupervisorAgent < OmniAgent::Agent
119
+ delegate_to SupportAgent, as: :triage_ticket, run_alias: :triage
120
+ end
121
+ ```
122
+
123
+ Pass `forward:` to share part (or all) of the supervisor's context with the delegated agent — an array of context keys, or `true` to forward everything. Omitted keys, and the default (`forward: []`), keep the delegated agent fully isolated:
124
+
125
+ ```ruby
126
+ class SupervisorAgent < OmniAgent::Agent
127
+ delegate_to ResearchAgent, as: :research, forward: [ :user, :locale ]
128
+ delegate_to MathAgent, as: :calculate, forward: true
129
+ end
130
+ ```
131
+
100
132
  ## Evals
101
133
 
102
134
  `OmniAgent::Eval` lets you test agent quality: deterministic assertions (tool calls, output matching) and pluggable LLM-as-judge scoring.
@@ -84,6 +84,21 @@ module OmniAgent
84
84
  end
85
85
  end
86
86
 
87
+ def delegate_to(agent_class, as:, description: nil, run_alias: nil, forward: [])
88
+ unless agent_class.is_a?(Class) && agent_class <= OmniAgent::Agent
89
+ raise ArgumentError, "delegate_to requires an OmniAgent::Agent subclass"
90
+ end
91
+
92
+ tool_class = build_delegated_tool_class(agent_class, description: description, run_alias: run_alias, forward: forward)
93
+ delegated_tools_module.const_set(delegated_tool_const_name(as), tool_class)
94
+
95
+ @delegated_tool_classes = configured_delegated_tool_classes + [ tool_class ]
96
+ end
97
+
98
+ def configured_delegated_tool_classes
99
+ @delegated_tool_classes || []
100
+ end
101
+
87
102
  def with(context = nil, provider_override: nil, model_override: nil, options_override: {}, **context_keywords)
88
103
  merged_context = {}
89
104
  merged_context.merge!(context) if context.is_a?(Hash)
@@ -112,6 +127,58 @@ module OmniAgent
112
127
 
113
128
  private
114
129
 
130
+ def delegated_tools_module
131
+ @delegated_tools_module ||= const_set(:DelegatedTools, Module.new)
132
+ end
133
+
134
+ def delegated_tool_const_name(as)
135
+ as.to_s.split(/[_\s]+/).reject(&:empty?).map { |part| part[0].upcase + part[1..] }.join
136
+ end
137
+
138
+ def build_delegated_tool_class(agent_class, description:, run_alias:, forward:)
139
+ tool_description = description || "Delegate to #{agent_class.name}."
140
+
141
+ Class.new(OmniAgent::Tool) do
142
+ description tool_description
143
+
144
+ input do
145
+ string :input, description: "Input/question to send to the delegated agent."
146
+ end
147
+
148
+ define_method(:execute) do |input:|
149
+ forwarded_context = OmniAgent::Agent.__send__(:filter_forwarded_context, context, forward)
150
+ OmniAgent::Agent.__send__(:run_delegated_agent, agent_class, input, run_alias, forwarded_context)
151
+ end
152
+ end
153
+ end
154
+
155
+ def filter_forwarded_context(context, forward)
156
+ return {} unless context.is_a?(Hash)
157
+ return context.dup if forward == true
158
+ return {} if forward.nil? || forward == false
159
+
160
+ keys = Array(forward).map(&:to_sym)
161
+ context.select { |key, _| keys.include?(key.to_sym) }
162
+ end
163
+
164
+ def run_delegated_agent(agent_class, input, run_alias, forwarded_context)
165
+ depth = (Thread.current[:omni_agent_delegation_depth] ||= 0)
166
+ max_depth = OmniAgent.configuration.max_delegation_depth
167
+
168
+ if depth >= max_depth
169
+ raise OmniAgent::MaxDelegationDepthError,
170
+ "Exceeded max_delegation_depth (#{max_depth}) while delegating to #{agent_class.name}."
171
+ end
172
+
173
+ Thread.current[:omni_agent_delegation_depth] = depth + 1
174
+ begin
175
+ entrypoint = run_alias || :run
176
+ agent_class.new.public_send(entrypoint, input, context: forwarded_context).answer
177
+ ensure
178
+ Thread.current[:omni_agent_delegation_depth] = depth
179
+ end
180
+ end
181
+
115
182
  def normalize_callbacks(callback_type, callbacks)
116
183
  raise ArgumentError, "#{callback_type} requires at least one method name" if callbacks.empty?
117
184
 
@@ -196,6 +263,7 @@ module OmniAgent
196
263
 
197
264
  if tool_class
198
265
  tool_instance = tool_class.new
266
+ tool_instance.context = context if tool_instance.respond_to?(:context=)
199
267
 
200
268
  begin
201
269
  result = tool_instance.invoke(tool_args)
@@ -238,12 +306,17 @@ module OmniAgent
238
306
 
239
307
  def available_tools
240
308
  tool_namespace = "#{self.class.name}::Tools".safe_constantize
241
- return [] unless tool_namespace
242
309
 
243
- tool_namespace.constants.filter_map do |const_name|
244
- const = tool_namespace.const_get(const_name)
245
- const if const.is_a?(Class) && const < OmniAgent::Tool
310
+ namespace_tools = if tool_namespace
311
+ tool_namespace.constants.filter_map do |const_name|
312
+ const = tool_namespace.const_get(const_name)
313
+ const if const.is_a?(Class) && const < OmniAgent::Tool
314
+ end
315
+ else
316
+ []
246
317
  end
318
+
319
+ namespace_tools + self.class.configured_delegated_tool_classes
247
320
  end
248
321
 
249
322
  private
@@ -1,7 +1,7 @@
1
1
  module OmniAgent
2
2
  class Configuration
3
3
  attr_accessor :default_provider, :default_model, :max_retries, :retry_base_delay, :max_tool_iterations,
4
- :eval_judge_provider, :eval_judge_model, :eval_cache_enabled, :eval_cache_path
4
+ :max_delegation_depth, :eval_judge_provider, :eval_judge_model, :eval_cache_enabled, :eval_cache_path
5
5
 
6
6
  def initialize
7
7
  @default_provider = :openai
@@ -9,6 +9,7 @@ module OmniAgent
9
9
  @max_retries = 3
10
10
  @retry_base_delay = 0.5
11
11
  @max_tool_iterations = 10
12
+ @max_delegation_depth = 5
12
13
  @eval_judge_provider = nil
13
14
  @eval_judge_model = nil
14
15
  @eval_cache_enabled = true
@@ -4,6 +4,7 @@ module OmniAgent
4
4
  class MissingDependencyError < Error; end
5
5
  class UnknownProviderError < Error; end
6
6
  class MaxToolIterationsError < Error; end
7
+ class MaxDelegationDepthError < Error; end
7
8
  class EvalAssertionError < Error; end
8
9
  end
9
10
 
@@ -11,5 +12,6 @@ module OmniAgent
11
12
  MissingDependencyError = Errors::MissingDependencyError
12
13
  UnknownProviderError = Errors::UnknownProviderError
13
14
  MaxToolIterationsError = Errors::MaxToolIterationsError
15
+ MaxDelegationDepthError = Errors::MaxDelegationDepthError
14
16
  EvalAssertionError = Errors::EvalAssertionError
15
17
  end
@@ -69,6 +69,12 @@ module OmniAgent
69
69
  end
70
70
  end
71
71
 
72
+ attr_accessor :context
73
+
74
+ def initialize
75
+ @context = {}
76
+ end
77
+
72
78
  def invoke(arguments_hash)
73
79
  filtered_kwargs = self.class.parse_arguments(arguments_hash)
74
80
  execute(**filtered_kwargs)
@@ -1,3 +1,3 @@
1
1
  module OmniAgent
2
- VERSION = "0.1.7"
2
+ VERSION = "0.1.8"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omni_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - ACR1209