agent-harness 0.11.0 → 0.11.1

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: 635aae919a5bbbf99af4a24199b45507370c01ccbf637bfd8d9d0fa18bdb3c22
4
- data.tar.gz: 30548d834ae0195030e98565007ced6ebf140f12f9a489ae10a6d423c40e087f
3
+ metadata.gz: cf9a5083c42a6675255f4c0ea0516ab99473813020e3a875b02eeda98b937db8
4
+ data.tar.gz: e070d74c397e02260d2994783c42124f45ac41b15a7c070834dd43c9c59e712a
5
5
  SHA512:
6
- metadata.gz: ac200425094b482ad90fd6492ba0bf4d612ed08560bacde517c988375d1d452b12aca7c34f8ed9519cf907daa87a07a96a4c044952126ce3cd9e37f2d6b9a788
7
- data.tar.gz: a871de9fcc11224506f4220025016b3b7201ef97bc1e1aca918562c1f983b9dd175a93dbd6315a71a3a270234d3b9cd019f7deaa82030b984e11434ca86328f8
6
+ metadata.gz: 7bce4c50dc634010ce8d7ac38c4adc661fdff51e2b055d9a585c0a7ef745f4a365a7aa9c68fedac63cfcbce7d7bea18f74a69b1622f2cde8fc5a6d38312ce2df
7
+ data.tar.gz: cd9adee894f938a0bb104cd4648766f2b6b6f89e1080874436dd774f8313213bdea4d63d3fed0a81b7bddeca319c8e54a39588726fb40f724bb8d858a85314fc
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.11.0"
2
+ ".": "0.11.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.11.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.0...agent-harness/v0.11.1) (2026-04-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 163: Support provider-agnostic sub-agent definitions ([#166](https://github.com/viamin/agent-harness/issues/166)) ([1a00c35](https://github.com/viamin/agent-harness/commit/1a00c35f61c624b23f8d37b0d1b41e007d007ad3))
9
+
3
10
  ## [0.11.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.10.0...agent-harness/v0.11.0) (2026-04-25)
4
11
 
5
12
 
@@ -23,6 +23,7 @@ module AgentHarness
23
23
  attr_writer :command_executor
24
24
 
25
25
  attr_reader :providers, :orchestration_config, :callbacks, :custom_provider_classes
26
+ attr_reader :sub_agents, :tool_registry, :mcp_servers
26
27
 
27
28
  def initialize
28
29
  @logger = nil # Will use null logger if not set
@@ -36,6 +37,9 @@ module AgentHarness
36
37
  @orchestration_config = OrchestrationConfig.new
37
38
  @callbacks = CallbackRegistry.new
38
39
  @custom_provider_classes = {}
40
+ @sub_agents = {}
41
+ @tool_registry = ToolRegistry.new
42
+ @mcp_servers = {}
39
43
  end
40
44
 
41
45
  # Get or lazily initialize the command executor
@@ -74,6 +78,78 @@ module AgentHarness
74
78
  @custom_provider_classes[name.to_sym] = klass
75
79
  end
76
80
 
81
+ # Configure a provider-agnostic sub-agent definition.
82
+ #
83
+ # @param name [Symbol, String] sub-agent name
84
+ # @param attributes [Hash] sub-agent attributes
85
+ # @yield [Hash] mutable attributes hash before validation
86
+ # @return [SubAgentConfig] registered sub-agent config
87
+ def sub_agent(name, attributes = {})
88
+ attributes = attributes.dup
89
+ yield(attributes) if block_given?
90
+ attributes[:name] = name
91
+
92
+ config = SubAgentConfig.from_hash(attributes)
93
+ @sub_agents[config.name] = config
94
+ end
95
+
96
+ # Load sub-agent definitions from a YAML or Markdown file.
97
+ #
98
+ # @param path [String] file path
99
+ # @return [Array<SubAgentConfig>] loaded sub-agents
100
+ def load_sub_agents(path)
101
+ SubAgentFileLoader.load(path).each do |sub_agent|
102
+ @sub_agents[sub_agent.name] = sub_agent
103
+ end
104
+ end
105
+
106
+ # Register a generic tool definition that sub-agents can reference.
107
+ #
108
+ # @param name [Symbol, String] tool name
109
+ # @param description [String, nil] tool description
110
+ # @param provider_mappings [Hash] provider-specific mappings
111
+ # @return [ToolDefinition]
112
+ def register_tool(name, description: nil, **provider_mappings)
113
+ @tool_registry.register(name, description: description, **provider_mappings)
114
+ end
115
+
116
+ # Register a named MCP server for later reference by sub-agents.
117
+ #
118
+ # @param name [Symbol, String] server name
119
+ # @param definition [Hash, McpServer, nil] server definition
120
+ # @param attributes [Hash] server attributes
121
+ # @return [McpServer]
122
+ def register_mcp_server(name, definition = nil, **attributes)
123
+ server = if definition.is_a?(McpServer)
124
+ definition
125
+ else
126
+ payload = (definition || attributes).dup
127
+ payload[:name] ||= name.to_s
128
+ McpServer.from_hash(payload)
129
+ end
130
+
131
+ @mcp_servers[name.to_sym] = server
132
+ end
133
+
134
+ # Resolve a named or inline sub-agent definition.
135
+ #
136
+ # @param reference [Symbol, String, Hash, SubAgentConfig, nil] sub-agent reference
137
+ # @return [SubAgentConfig, nil] resolved sub-agent config
138
+ def resolve_sub_agent(reference)
139
+ case reference
140
+ when nil
141
+ nil
142
+ when SubAgentConfig
143
+ reference
144
+ when Hash
145
+ SubAgentConfig.from_hash(reference)
146
+ else
147
+ @sub_agents.fetch(reference.to_sym) do
148
+ raise ConfigurationError, "Unknown sub-agent: #{reference}"
149
+ end
150
+ end
151
+ end
152
+
77
153
  # Register callback for token usage events
78
154
  #
79
155
  # @yield [TokenEvent] called when tokens are used
@@ -263,6 +339,67 @@ module AgentHarness
263
339
  end
264
340
  end
265
341
 
342
+ # Provider-agnostic tool definition used during sub-agent translation.
343
+ class ToolDefinition
344
+ attr_reader :name, :description, :provider_mappings
345
+
346
+ def initialize(name:, description: nil, provider_mappings: {})
347
+ @name = name.to_sym
348
+ @description = description
349
+ @provider_mappings = deep_dup(provider_mappings).each_with_object({}) do |(provider, value), mappings|
350
+ mappings[provider.to_sym] = value
351
+ end.freeze
352
+ end
353
+
354
+ def mapping_for(provider)
355
+ deep_dup(@provider_mappings[provider.to_sym])
356
+ end
357
+
358
+ def to_h
359
+ {name: @name, description: @description, provider_mappings: deep_dup(@provider_mappings)}
360
+ end
361
+
362
+ private
363
+
364
+ def deep_dup(value)
365
+ case value
366
+ when Array
367
+ value.map { |entry| deep_dup(entry) }
368
+ when Hash
369
+ value.each_with_object({}) { |(key, entry), copy| copy[key] = deep_dup(entry) }
370
+ else
371
+ value.dup
372
+ end
373
+ rescue TypeError
374
+ value
375
+ end
376
+ end
377
+
378
+ # Registry for canonical tool definitions referenced by sub-agents.
379
+ class ToolRegistry
380
+ def initialize
381
+ @tools = {}
382
+ end
383
+
384
+ def register(name, description: nil, **provider_mappings)
385
+ @tools[name.to_sym] = ToolDefinition.new(
386
+ name: name,
387
+ description: description,
388
+ provider_mappings: provider_mappings
389
+ )
390
+ end
391
+
392
+ def fetch(name)
393
+ @tools.fetch(name.to_sym) do
394
+ raise ConfigurationError, "Unknown tool: #{name}"
395
+ end
396
+ end
397
+
398
+ def registered?(name)
399
+ @tools.key?(name.to_sym)
400
+ end
401
+ end
402
+
266
403
  # Registry for event callbacks
267
404
  class CallbackRegistry
268
405
  def initialize
@@ -126,6 +126,8 @@ module AgentHarness
126
126
 
127
127
  # Coerce provider_runtime from Hash if needed
128
128
  options = normalize_provider_runtime(options)
129
+ options = normalize_sub_agent(options)
130
+ prompt = apply_sub_agent_to_prompt(prompt, options[:translated_sub_agent])
129
131
 
130
132
  # Normalize and validate MCP servers
131
133
  options = normalize_mcp_servers(options)
@@ -210,6 +212,7 @@ module AgentHarness
210
212
  end
211
213
 
212
214
  options = normalize_provider_runtime(options)
215
+ options = normalize_sub_agent(options)
213
216
  runtime = options[:provider_runtime]
214
217
  conversation ||= messages
215
218
  raise ArgumentError, "conversation or messages is required" unless conversation
@@ -217,6 +220,7 @@ module AgentHarness
217
220
 
218
221
  transport = resolve_chat_transport(options)
219
222
  messages = format_messages_for_transport(conversation, transport)
223
+ messages = apply_sub_agent_to_messages(messages, options[:translated_sub_agent])
220
224
  transport_opts = chat_transport_options(runtime, options)
221
225
  transport_opts[:on_chat_chunk] = on_chat_chunk if on_chat_chunk
222
226
  transport_opts[:observer] = observer if observer
@@ -446,6 +450,33 @@ module AgentHarness
446
450
  options.merge(mcp_servers: normalized)
447
451
  end
448
452
 
453
+ def normalize_sub_agent(options)
454
+ sub_agent = options[:sub_agent]
455
+ return options unless sub_agent
456
+
457
+ resolved = AgentHarness.configuration.resolve_sub_agent(sub_agent)
458
+ translated = SubAgentTranslator.for_provider(
459
+ self.class.provider_name,
460
+ resolved,
461
+ tool_registry: AgentHarness.configuration.tool_registry,
462
+ mcp_servers: AgentHarness.configuration.mcp_servers
463
+ )
464
+
465
+ options.merge(sub_agent: resolved, translated_sub_agent: translated)
466
+ end
467
+
468
+ def apply_sub_agent_to_prompt(prompt, translated_sub_agent)
469
+ return prompt unless translated_sub_agent
470
+
471
+ [translated_sub_agent[:runtime_instructions], "User task:\n#{prompt}"].join("\n\n")
472
+ end
473
+
474
+ def apply_sub_agent_to_messages(messages, translated_sub_agent)
475
+ return messages unless translated_sub_agent
476
+
477
+ [{role: "system", content: translated_sub_agent[:runtime_instructions]}] + messages
478
+ end
479
+
449
480
  def command_execution_options(options)
450
481
  execution_options = {
451
482
  idle_timeout: options[:idle_timeout],
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentHarness
4
+ # Canonical provider-agnostic sub-agent definition.
5
+ class SubAgentConfig
6
+ attr_reader :name, :description, :instructions, :model, :tools, :mcp_servers
7
+ attr_reader :constraints, :handoff_conditions, :type, :sub_agents, :routing
8
+
9
+ def initialize(name:, description:, instructions:, model: "default", tools: [],
10
+ mcp_servers: [], constraints: {}, handoff_conditions: [], type: nil,
11
+ sub_agents: [], routing: nil)
12
+ @name = normalize_name(name)
13
+ @description = validate_string!(:description, description)
14
+ @instructions = validate_string!(:instructions, instructions)
15
+ @model = normalize_model(model)
16
+ @tools = normalize_array(:tools, tools)
17
+ @mcp_servers = normalize_array(:mcp_servers, mcp_servers)
18
+ @constraints = normalize_hash(:constraints, constraints)
19
+ @handoff_conditions = normalize_array(:handoff_conditions, handoff_conditions)
20
+ @type = type&.to_sym
21
+ @sub_agents = normalize_array(:sub_agents, sub_agents)
22
+ @routing = routing.nil? ? nil : normalize_hash(:routing, routing)
23
+ end
24
+
25
+ def self.from_hash(hash)
26
+ unless hash.is_a?(Hash)
27
+ raise ConfigurationError, "Sub-agent definition must be a Hash, got #{hash.class}"
28
+ end
29
+
30
+ attrs = hash.each_with_object({}) do |(key, value), memo|
31
+ memo[key.to_sym] = value
32
+ end
33
+
34
+ %i[name description instructions].each do |field|
35
+ value = attrs[field]
36
+ next if value.is_a?(String) && !value.strip.empty?
37
+ next if value.is_a?(Symbol)
38
+
39
+ raise ConfigurationError, "#{field} is required"
40
+ end
41
+
42
+ new(**attrs)
43
+ end
44
+
45
+ def to_h
46
+ {
47
+ name: @name,
48
+ description: @description,
49
+ instructions: @instructions,
50
+ model: @model,
51
+ tools: deep_dup(@tools),
52
+ mcp_servers: deep_dup(@mcp_servers),
53
+ constraints: deep_dup(@constraints),
54
+ handoff_conditions: deep_dup(@handoff_conditions),
55
+ type: @type,
56
+ sub_agents: deep_dup(@sub_agents),
57
+ routing: deep_dup(@routing)
58
+ }.compact
59
+ end
60
+
61
+ private
62
+
63
+ def normalize_name(name)
64
+ value = validate_string!(:name, name)
65
+ value.tr(" ", "_").to_sym
66
+ end
67
+
68
+ def normalize_model(model)
69
+ return "default" if model.nil?
70
+
71
+ validate_string!(:model, model)
72
+ end
73
+
74
+ def validate_string!(field, value)
75
+ unless value.is_a?(String) || value.is_a?(Symbol)
76
+ raise ConfigurationError, "#{field} must be a String or Symbol"
77
+ end
78
+
79
+ string = value.to_s.strip
80
+ raise ConfigurationError, "#{field} is required" if string.empty?
81
+
82
+ string
83
+ end
84
+
85
+ def normalize_array(field, value)
86
+ return [].freeze if value.nil?
87
+
88
+ unless value.is_a?(Array)
89
+ raise ConfigurationError, "#{field} must be an Array"
90
+ end
91
+
92
+ deep_dup(value).freeze
93
+ end
94
+
95
+ def normalize_hash(field, value)
96
+ return {}.freeze if value.nil?
97
+
98
+ unless value.is_a?(Hash)
99
+ raise ConfigurationError, "#{field} must be a Hash"
100
+ end
101
+
102
+ deep_dup(value).freeze
103
+ end
104
+
105
+ def deep_dup(value)
106
+ case value
107
+ when Array
108
+ value.map { |entry| deep_dup(entry) }
109
+ when Hash
110
+ value.each_with_object({}) { |(key, entry), copy| copy[key] = deep_dup(entry) }
111
+ else
112
+ value.dup
113
+ end
114
+ rescue TypeError
115
+ value
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module AgentHarness
6
+ # Loads canonical sub-agent definitions from YAML or Markdown files.
7
+ class SubAgentFileLoader
8
+ class << self
9
+ def load(path)
10
+ case File.extname(path).downcase
11
+ when ".yml", ".yaml"
12
+ load_yaml(path)
13
+ when ".md", ".markdown"
14
+ [load_markdown(path)]
15
+ else
16
+ raise ConfigurationError, "Unsupported sub-agent definition format: #{path}"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def load_yaml(path)
23
+ parsed = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
24
+ unless parsed.is_a?(Hash)
25
+ raise ConfigurationError, "YAML sub-agent definition must be a Hash"
26
+ end
27
+
28
+ agents = parsed["agents"] || parsed[:agents] || [parsed]
29
+ unless agents.is_a?(Array)
30
+ raise ConfigurationError, "YAML sub-agent definitions must provide an agents Array"
31
+ end
32
+
33
+ agents.map { |entry| SubAgentConfig.from_hash(entry) }
34
+ rescue Psych::SyntaxError => e
35
+ raise ConfigurationError, "Invalid YAML in #{path}: #{e.message}"
36
+ end
37
+
38
+ def load_markdown(path)
39
+ content = File.read(path)
40
+ match = content.match(/\A---\s*\n(?<frontmatter>.*?)\n---\s*\n?(?<body>.*)\z/m)
41
+ raise ConfigurationError, "Markdown sub-agent definitions require YAML frontmatter" unless match
42
+
43
+ attrs = YAML.safe_load(match[:frontmatter], permitted_classes: [], aliases: false) || {}
44
+ unless attrs.is_a?(Hash)
45
+ raise ConfigurationError, "Markdown frontmatter must be a Hash"
46
+ end
47
+
48
+ attrs["instructions"] ||= match[:body].strip
49
+ SubAgentConfig.from_hash(attrs)
50
+ rescue Psych::SyntaxError => e
51
+ raise ConfigurationError, "Invalid frontmatter in #{path}: #{e.message}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module AgentHarness
6
+ # Translates canonical sub-agent definitions into provider-specific formats.
7
+ module SubAgentTranslator
8
+ class << self
9
+ def for_provider(provider, sub_agent_config, tool_registry: AgentHarness.configuration.tool_registry,
10
+ mcp_servers: AgentHarness.configuration.mcp_servers)
11
+ config = normalize_sub_agent_config(sub_agent_config)
12
+ normalized_provider = normalize_provider(provider)
13
+ tools = resolve_tools(config.tools, provider: normalized_provider, tool_registry: tool_registry)
14
+ servers = resolve_mcp_servers(config.mcp_servers, mcp_servers: mcp_servers)
15
+
16
+ case normalized_provider
17
+ when :anthropic
18
+ translate_for_anthropic(config, tools: tools, mcp_servers: servers)
19
+ when :openai
20
+ translate_for_openai(config, tools: tools, mcp_servers: servers)
21
+ when :google
22
+ translate_for_google(config, tools: tools, mcp_servers: servers)
23
+ when :claude_code
24
+ translate_for_claude_code(config, tools: tools, mcp_servers: servers)
25
+ when :codex
26
+ translate_for_codex(config, tools: tools, mcp_servers: servers)
27
+ when :pi
28
+ translate_for_pi(config, tools: tools, mcp_servers: servers)
29
+ else
30
+ translate_generic(normalized_provider, config, tools: tools, mcp_servers: servers)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_sub_agent_config(sub_agent_config)
37
+ case sub_agent_config
38
+ when SubAgentConfig
39
+ sub_agent_config
40
+ when Hash
41
+ SubAgentConfig.from_hash(sub_agent_config)
42
+ else
43
+ raise ConfigurationError, "Sub-agent config must be a SubAgentConfig or Hash"
44
+ end
45
+ end
46
+
47
+ def normalize_provider(provider)
48
+ case provider.to_sym
49
+ when :claude then :anthropic
50
+ when :gemini then :google
51
+ else provider.to_sym
52
+ end
53
+ end
54
+
55
+ def resolve_tools(tool_refs, provider:, tool_registry:)
56
+ tool_refs.map do |tool|
57
+ case tool
58
+ when Symbol, String
59
+ mapping = tool_registry.fetch(tool).mapping_for(provider)
60
+ mapping.nil? ? tool.to_s : mapping
61
+ when Hash
62
+ deep_dup(tool)
63
+ else
64
+ raise ConfigurationError, "Unsupported tool reference #{tool.inspect} in sub-agent definition"
65
+ end
66
+ end
67
+ end
68
+
69
+ def resolve_mcp_servers(server_refs, mcp_servers:)
70
+ server_refs.map do |server|
71
+ resolved = case server
72
+ when McpServer
73
+ server
74
+ when Symbol, String
75
+ mcp_servers.fetch(server.to_sym) do
76
+ raise ConfigurationError, "Unknown MCP server: #{server}"
77
+ end
78
+ when Hash
79
+ McpServer.from_hash(server)
80
+ else
81
+ raise ConfigurationError, "Unsupported MCP server reference #{server.inspect} in sub-agent definition"
82
+ end
83
+
84
+ resolved.to_h
85
+ end
86
+ end
87
+
88
+ def translate_for_anthropic(config, tools:, mcp_servers:)
89
+ {
90
+ provider: :anthropic,
91
+ format: :agent_sdk,
92
+ agent: {
93
+ name: config.name.to_s,
94
+ description: config.description,
95
+ instructions: config.instructions,
96
+ model: config.model,
97
+ tools: tools,
98
+ mcp_servers: mcp_servers,
99
+ constraints: deep_dup(config.constraints)
100
+ },
101
+ runtime_instructions: runtime_instructions(config)
102
+ }
103
+ end
104
+
105
+ def translate_for_openai(config, tools:, mcp_servers:)
106
+ {
107
+ provider: :openai,
108
+ format: :responses,
109
+ assistant: {
110
+ name: config.name.to_s,
111
+ description: config.description,
112
+ instructions: config.instructions,
113
+ model: config.model,
114
+ tools: tools,
115
+ metadata: {
116
+ mcp_servers: mcp_servers,
117
+ constraints: deep_dup(config.constraints)
118
+ }
119
+ },
120
+ runtime_instructions: runtime_instructions(config)
121
+ }
122
+ end
123
+
124
+ def translate_for_google(config, tools:, mcp_servers:)
125
+ {
126
+ provider: :google,
127
+ format: :adk,
128
+ agent: {
129
+ name: config.name.to_s,
130
+ description: config.description,
131
+ instruction: config.instructions,
132
+ model: config.model,
133
+ tools: tools,
134
+ mcp_servers: mcp_servers,
135
+ constraints: deep_dup(config.constraints)
136
+ },
137
+ runtime_instructions: runtime_instructions(config)
138
+ }
139
+ end
140
+
141
+ def translate_for_claude_code(config, tools:, mcp_servers:)
142
+ frontmatter = {
143
+ "name" => config.name.to_s,
144
+ "description" => config.description,
145
+ "model" => config.model,
146
+ "tools" => tools,
147
+ "mcp_servers" => mcp_servers,
148
+ "constraints" => deep_dup(config.constraints)
149
+ }.delete_if { |_key, value| value.respond_to?(:empty?) ? value.empty? : value.nil? }
150
+
151
+ {
152
+ provider: :claude_code,
153
+ format: :markdown,
154
+ content: build_markdown_definition(frontmatter, config.instructions),
155
+ runtime_instructions: runtime_instructions(config)
156
+ }
157
+ end
158
+
159
+ def translate_for_codex(config, tools:, mcp_servers:)
160
+ {
161
+ provider: :codex,
162
+ format: :delegated_prompt,
163
+ definition: {
164
+ name: config.name.to_s,
165
+ description: config.description,
166
+ instructions: config.instructions,
167
+ model: config.model,
168
+ tools: tools,
169
+ mcp_servers: mcp_servers,
170
+ constraints: deep_dup(config.constraints)
171
+ },
172
+ runtime_instructions: runtime_instructions(config)
173
+ }
174
+ end
175
+
176
+ def translate_for_pi(config, tools:, mcp_servers:)
177
+ frontmatter = {
178
+ "name" => config.name.to_s,
179
+ "description" => config.description,
180
+ "model" => config.model,
181
+ "tools" => tools,
182
+ "mcp_servers" => mcp_servers
183
+ }.delete_if { |_key, value| value.respond_to?(:empty?) ? value.empty? : value.nil? }
184
+
185
+ {
186
+ provider: :pi,
187
+ format: :skill_markdown,
188
+ content: build_markdown_definition(frontmatter, config.instructions),
189
+ runtime_instructions: runtime_instructions(config)
190
+ }
191
+ end
192
+
193
+ def translate_generic(provider, config, tools:, mcp_servers:)
194
+ {
195
+ provider: provider,
196
+ format: :generic,
197
+ definition: {
198
+ name: config.name.to_s,
199
+ description: config.description,
200
+ instructions: config.instructions,
201
+ model: config.model,
202
+ tools: tools,
203
+ mcp_servers: mcp_servers,
204
+ constraints: deep_dup(config.constraints)
205
+ },
206
+ runtime_instructions: runtime_instructions(config)
207
+ }
208
+ end
209
+
210
+ def runtime_instructions(config)
211
+ <<~TEXT.strip
212
+ Sub-agent role: #{config.name}
213
+ Description: #{config.description}
214
+
215
+ Follow these sub-agent instructions exactly:
216
+ #{config.instructions}
217
+ TEXT
218
+ end
219
+
220
+ def build_markdown_definition(frontmatter, instructions)
221
+ <<~MARKDOWN
222
+ ---
223
+ #{YAML.dump(frontmatter).sub(/\A---\s*\n/, "").strip}
224
+ ---
225
+ #{instructions}
226
+ MARKDOWN
227
+ end
228
+
229
+ def deep_dup(value)
230
+ case value
231
+ when Array
232
+ value.map { |entry| deep_dup(entry) }
233
+ when Hash
234
+ value.each_with_object({}) { |(key, entry), copy| copy[key] = deep_dup(entry) }
235
+ else
236
+ value.dup
237
+ end
238
+ rescue TypeError
239
+ value
240
+ end
241
+ end
242
+ end
243
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.11.0"
4
+ VERSION = "0.11.1"
5
5
  end
data/lib/agent_harness.rb CHANGED
@@ -77,6 +77,23 @@ module AgentHarness
77
77
  conductor.send_message(prompt, provider: provider, executor: executor, **options)
78
78
  end
79
79
 
80
+ # Resolve a canonical sub-agent definition by name or inline payload.
81
+ #
82
+ # @param reference [Symbol, String, Hash, SubAgentConfig]
83
+ # @return [SubAgentConfig]
84
+ def sub_agent(reference)
85
+ configuration.resolve_sub_agent(reference)
86
+ end
87
+
88
+ # Translate a canonical sub-agent definition into a provider-specific format.
89
+ #
90
+ # @param reference [Symbol, String, Hash, SubAgentConfig] sub-agent reference
91
+ # @param provider [Symbol, String] target provider
92
+ # @return [Hash] provider-specific sub-agent definition
93
+ def translate_sub_agent(reference, provider:)
94
+ SubAgentTranslator.for_provider(provider, sub_agent(reference))
95
+ end
96
+
80
97
  # Get a provider instance
81
98
  # @param name [Symbol] the provider name
82
99
  # @return [Providers::Base] the provider instance
@@ -261,6 +278,9 @@ end
261
278
  # Core components
262
279
  require_relative "agent_harness/errors"
263
280
  require_relative "agent_harness/mcp_server"
281
+ require_relative "agent_harness/sub_agent_config"
282
+ require_relative "agent_harness/sub_agent_file_loader"
283
+ require_relative "agent_harness/sub_agent_translator"
264
284
  require_relative "agent_harness/provider_runtime"
265
285
  require_relative "agent_harness/execution_preparation"
266
286
  require_relative "agent_harness/configuration"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -132,6 +132,9 @@ files:
132
132
  - lib/agent_harness/providers/registry.rb
133
133
  - lib/agent_harness/providers/token_usage_parsing.rb
134
134
  - lib/agent_harness/response.rb
135
+ - lib/agent_harness/sub_agent_config.rb
136
+ - lib/agent_harness/sub_agent_file_loader.rb
137
+ - lib/agent_harness/sub_agent_translator.rb
135
138
  - lib/agent_harness/text_transport.rb
136
139
  - lib/agent_harness/token_tracker.rb
137
140
  - lib/agent_harness/version.rb