Skip to content

Yaml parsing for csharp returns empty tools collection #65

Description

(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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions