(In vain hope, I attempted to push a branch so I could propose a PR, but I didn't have the rights. The following is a summary of the issue.)
(Also, this issue may affect other child object graphs in csharp yaml parsing, but I didn't verify.)
(Finally, I also recognize the actual fix is in the code emitter(s); I just fixed the emitted code.)
Bug: Tools collection empty when loading PromptAgent from YAML
Summary
When loading a PromptAgent from a YAML agent definition file, the Tools collection is always empty regardless of what tools are specified. This affects both the dictionary format (tools: { name: { ... } }) and the list format (tools: [ {...} ]).
Environment
- Runtime: C# / .NET 10.0
- Library: YamlDotNet 16.3.0
- Project:
AgentSchema (runtime/csharp)
Reproduction
Both YAML formats below result in an empty Tools list:
Dictionary format
kind: prompt
name: my-agent
model: gpt-4o
template: Hello
tools:
search:
kind: function
description: Search the web
calculate:
kind: function
description: Run a calculation
List format
kind: prompt
name: my-agent
model: gpt-4o
template: Hello
tools:
- name: search
kind: function
description: Search the web
var agent = AgentDefinition.FromYaml(yaml) as PromptAgent;
Assert.NotEmpty(agent.Tools); // FAILS — count is 0
Root Cause
YamlDotNet 16.x deserializes nested YAML mappings as Dictionary<object, object>, not Dictionary<string, object?>. The LoadTools method in PromptAgent.cs and LoadBindings in Tool.cs used strict is Dictionary<string, object?> type pattern matching to detect and process tool entries. Because the actual runtime type never matched, both branches fell through silently and returned an empty list.
This affects:
- The top-level
tools mapping node (dictionary format)
- Each individual tool entry within a list (list format)
- Nested
bindings entries within each tool
Fix
runtime/csharp/AgentSchema/Utils.cs — new methods at end of Utils class
Two extension methods were added to normalize YamlDotNet's output:
public static Dictionary<string, object?> NormalizeDictionary(this Dictionary<object, object> dict)
{
var result = new Dictionary<string, object?>();
foreach (var kvp in dict)
{
var key = kvp.Key?.ToString() ?? "";
var value = NormalizeValue(kvp.Value);
result[key] = value;
}
return result;
}
public static object? NormalizeValue(this object? value)
{
if (value == null)
return null;
if (value is Dictionary<object, object> dictObjObj)
return dictObjObj.NormalizeDictionary();
if (value is System.Collections.IEnumerable enumerable && !(value is string))
{
var list = new List<object?>();
foreach (var item in enumerable)
list.Add(NormalizeValue(item));
return list;
}
return value;
}
runtime/csharp/AgentSchema/PromptAgent.cs — LoadTools() line 122
Added a new else if (data is Dictionary<object, object>) branch. Within both branches, nested tool values that are also Dictionary<object, object> are normalized before being passed to Tool.Load(). The same normalization was applied to each item in the list format branch.
Before (no Dictionary<object, object> handling — always missed when loading from YAML):
if (data is Dictionary<string, object?> dict) { ... }
else if (data is IEnumerable<object> list) { ... }
After:
if (data is Dictionary<string, object?> dict) { ... }
else if (data is Dictionary<object, object> dictObjObj)
{
var normalizedDict = dictObjObj.NormalizeDictionary();
// process normalizedDict entries, normalizing nested values as needed
}
else if (data is IEnumerable<object> list)
{
// each item now also handles Dictionary<object, object>:
else if (item is Dictionary<object, object> itemDictObjObj)
{
var normalizedDict = itemDictObjObj.NormalizeDictionary();
result.Add(Tool.Load(normalizedDict, context));
}
}
runtime/csharp/AgentSchema/Tool.cs — LoadBindings() line 100
Identical fix applied to LoadBindings, which had the same type-matching gap for tool argument bindings.
runtime/csharp/AgentSchema.Tests/PromptAgentToolsYamlTests.cs — new test class
Two behavioral regression tests were added:
ToolsYamlListFormat_ShouldDeserializeCorrectly — verifies list-format tools are loaded
ToolsDictionaryFormat_ShouldDeserializeCorrectly — verifies dictionary-format tools are loaded
Four diagnostic tests document the observed YamlDotNet type behavior (confirming Dictionary<object, object> is what YamlDotNet 16.x actually produces).
Test Results
Test summary: total: 282, failed: 0, succeeded: 282, skipped: 0
(In vain hope, I attempted to push a branch so I could propose a PR, but I didn't have the rights. The following is a summary of the issue.)
(Also, this issue may affect other child object graphs in csharp yaml parsing, but I didn't verify.)
(Finally, I also recognize the actual fix is in the code emitter(s); I just fixed the emitted code.)
Bug: Tools collection empty when loading
PromptAgentfrom YAMLSummary
When loading a
PromptAgentfrom a YAML agent definition file, theToolscollection is always empty regardless of what tools are specified. This affects both the dictionary format (tools: { name: { ... } }) and the list format (tools: [ {...} ]).Environment
AgentSchema(runtime/csharp)Reproduction
Both YAML formats below result in an empty
Toolslist:Dictionary format
List format
Root Cause
YamlDotNet 16.x deserializes nested YAML mappings as
Dictionary<object, object>, notDictionary<string, object?>. TheLoadToolsmethod inPromptAgent.csandLoadBindingsinTool.csused strictis Dictionary<string, object?>type pattern matching to detect and process tool entries. Because the actual runtime type never matched, both branches fell through silently and returned an empty list.This affects:
toolsmapping node (dictionary format)bindingsentries within each toolFix
runtime/csharp/AgentSchema/Utils.cs— new methods at end ofUtilsclassTwo extension methods were added to normalize YamlDotNet's output:
runtime/csharp/AgentSchema/PromptAgent.cs—LoadTools()line 122Added a new
else if (data is Dictionary<object, object>)branch. Within both branches, nested tool values that are alsoDictionary<object, object>are normalized before being passed toTool.Load(). The same normalization was applied to each item in the list format branch.Before (no
Dictionary<object, object>handling — always missed when loading from YAML):After:
runtime/csharp/AgentSchema/Tool.cs—LoadBindings()line 100Identical fix applied to
LoadBindings, which had the same type-matching gap for tool argument bindings.runtime/csharp/AgentSchema.Tests/PromptAgentToolsYamlTests.cs— new test classTwo behavioral regression tests were added:
ToolsYamlListFormat_ShouldDeserializeCorrectly— verifies list-format tools are loadedToolsDictionaryFormat_ShouldDeserializeCorrectly— verifies dictionary-format tools are loadedFour diagnostic tests document the observed YamlDotNet type behavior (confirming
Dictionary<object, object>is what YamlDotNet 16.x actually produces).Test Results