Skip to content

Yaml parsing for csharp returns empty tools collection #65

@kerbybarrett

Description

@kerbybarrett

(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.csLoadTools() 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.csLoadBindings() 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

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions