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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/lib/agent_harness/configuration.rb +137 -0
- data/lib/agent_harness/providers/base.rb +31 -0
- data/lib/agent_harness/sub_agent_config.rb +118 -0
- data/lib/agent_harness/sub_agent_file_loader.rb +55 -0
- data/lib/agent_harness/sub_agent_translator.rb +243 -0
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +20 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf9a5083c42a6675255f4c0ea0516ab99473813020e3a875b02eeda98b937db8
|
|
4
|
+
data.tar.gz: e070d74c397e02260d2994783c42124f45ac41b15a7c070834dd43c9c59e712a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bce4c50dc634010ce8d7ac38c4adc661fdff51e2b055d9a585c0a7ef745f4a365a7aa9c68fedac63cfcbce7d7bea18f74a69b1622f2cde8fc5a6d38312ce2df
|
|
7
|
+
data.tar.gz: cd9adee894f938a0bb104cd4648766f2b6b6f89e1080874436dd774f8313213bdea4d63d3fed0a81b7bddeca319c8e54a39588726fb40f724bb8d858a85314fc
|
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
|
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.
|
|
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
|