From 6c01ddf8f47bb24764f29cdd797ba15376a8b8aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:53:59 +0000 Subject: [PATCH 1/2] Initial plan From b9191031f7637ec5e6d9622077ee0a7d96af9299 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:04:45 +0000 Subject: [PATCH 2/2] Fix YAML tools collection deserialization in C# runtime Agent-Logs-Url: https://github.com/microsoft/AgentSchema/sessions/938d50c0-80a7-42a6-bf79-b1d58978a7d9 Co-authored-by: sethjuarez <115409+sethjuarez@users.noreply.github.com> --- agentschema-emitter/package-lock.json | 7 -- .../src/templates/csharp/file.cs.njk | 11 +++ .../src/templates/csharp/utils.cs.njk | 39 ++++++++ .../PromptAgentToolsYamlTests.cs | 99 +++++++++++++++++++ runtime/csharp/AgentSchema/AgentManifest.cs | 11 +++ runtime/csharp/AgentSchema/ContainerAgent.cs | 22 +++++ runtime/csharp/AgentSchema/ObjectProperty.cs | 11 +++ runtime/csharp/AgentSchema/PromptAgent.cs | 11 +++ runtime/csharp/AgentSchema/PropertySchema.cs | 11 +++ runtime/csharp/AgentSchema/Tool.cs | 11 +++ runtime/csharp/AgentSchema/Utils.cs | 39 ++++++++ 11 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 runtime/csharp/AgentSchema.Tests/PromptAgentToolsYamlTests.cs diff --git a/agentschema-emitter/package-lock.json b/agentschema-emitter/package-lock.json index 1eb5ec68..1fc0a863 100644 --- a/agentschema-emitter/package-lock.json +++ b/agentschema-emitter/package-lock.json @@ -799,7 +799,6 @@ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -853,7 +852,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -1071,7 +1069,6 @@ "integrity": "sha512-FeLb7Q0z6Bh5dDpqtnU2RlWiIWWWF7rujx2xGMta5dcTuIOZ4jbdyz1hVdxk4iM4qadvaSV4ey/qrSuffNoh3w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "~7.27.1", "@inquirer/prompts": "^8.0.1", @@ -1128,7 +1125,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1531,7 +1527,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3552,7 +3547,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3605,7 +3599,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/agentschema-emitter/src/templates/csharp/file.cs.njk b/agentschema-emitter/src/templates/csharp/file.cs.njk index af817945..ac3eef9f 100644 --- a/agentschema-emitter/src/templates/csharp/file.cs.njk +++ b/agentschema-emitter/src/templates/csharp/file.cs.njk @@ -76,6 +76,12 @@ public{% if node.isAbstract %} abstract{% endif %} class {{ node.typeName.name } { var result = new List<{{ collection.prop.typeName.name }}>(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -107,6 +113,11 @@ public{% if node.isAbstract %} abstract{% endif %} class {{ node.typeName.name } { result.Add({{ collection.prop.typeName.name }}.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add({{ collection.prop.typeName.name }}.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/agentschema-emitter/src/templates/csharp/utils.cs.njk b/agentschema-emitter/src/templates/csharp/utils.cs.njk index 9eab56be..f7431fcd 100644 --- a/agentschema-emitter/src/templates/csharp/utils.cs.njk +++ b/agentschema-emitter/src/templates/csharp/utils.cs.njk @@ -397,4 +397,43 @@ internal static class Utils return dict; } + + /// + /// Normalizes a YamlDotNet Dictionary<object, object> to Dictionary<string, object?>, + /// recursively normalizing all nested values. + /// + /// The YamlDotNet dictionary to normalize. + /// A normalized Dictionary<string, object?>. + public static Dictionary NormalizeDictionary(this Dictionary dict) + { + var result = new Dictionary(); + foreach (var kvp in dict) + { + result[kvp.Key?.ToString() ?? ""] = NormalizeValue(kvp.Value); + } + return result; + } + + /// + /// Recursively normalizes a value from YamlDotNet deserialization, + /// converting Dictionary<object, object> to Dictionary<string, object?> + /// and normalizing items within lists. + /// + /// The value to normalize. + /// The normalized value. + public static object? NormalizeValue(this object? value) + { + if (value is Dictionary dictObjObj) + return dictObjObj.NormalizeDictionary(); + + if (value is IEnumerable enumerable) + { + var list = new List(); + foreach (var item in enumerable) + list.Add(NormalizeValue(item)); + return list; + } + + return value; + } } diff --git a/runtime/csharp/AgentSchema.Tests/PromptAgentToolsYamlTests.cs b/runtime/csharp/AgentSchema.Tests/PromptAgentToolsYamlTests.cs new file mode 100644 index 00000000..e0446e5a --- /dev/null +++ b/runtime/csharp/AgentSchema.Tests/PromptAgentToolsYamlTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +using Xunit; + +#pragma warning disable IDE0130 +namespace AgentSchema; +#pragma warning restore IDE0130 + +/// +/// Regression tests verifying that tools are correctly deserialized from YAML. +/// YamlDotNet 16.x deserializes nested YAML mappings as Dictionary<object, object> +/// rather than Dictionary<string, object?>, which previously caused the Tools +/// collection to always be empty after loading from YAML. +/// +public class PromptAgentToolsYamlTests +{ + [Fact] + public void LoadYaml_ListFormat_ToolsAreDeserializedCorrectly() + { + string yaml = """ +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.NotNull(agent); + Assert.NotNull(agent.Tools); + Assert.NotEmpty(agent.Tools); + Assert.Single(agent.Tools); + Assert.Equal("search", agent.Tools[0].Name); + Assert.Equal("function", agent.Tools[0].Kind); + Assert.Equal("Search the web", agent.Tools[0].Description); + } + + [Fact] + public void LoadYaml_DictionaryFormat_ToolsAreDeserializedCorrectly() + { + string yaml = """ +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 +"""; + + var agent = AgentDefinition.FromYaml(yaml) as PromptAgent; + + Assert.NotNull(agent); + Assert.NotNull(agent.Tools); + Assert.NotEmpty(agent.Tools); + Assert.Equal(2, agent.Tools.Count); + + var searchTool = agent.Tools.FirstOrDefault(t => t.Name == "search"); + Assert.NotNull(searchTool); + Assert.Equal("function", searchTool.Kind); + Assert.Equal("Search the web", searchTool.Description); + + var calcTool = agent.Tools.FirstOrDefault(t => t.Name == "calculate"); + Assert.NotNull(calcTool); + Assert.Equal("function", calcTool.Kind); + Assert.Equal("Run a calculation", calcTool.Description); + } + + [Fact] + public void LoadYaml_ListFormat_MultipleTool_CountIsCorrect() + { + string yaml = """ +kind: prompt +name: my-agent +model: gpt-4o +template: Hello +tools: + - name: search + kind: function + description: Search the web + - name: calculate + kind: function + description: Run a calculation +"""; + + var agent = AgentDefinition.FromYaml(yaml) as PromptAgent; + + Assert.NotNull(agent); + Assert.NotNull(agent.Tools); + Assert.Equal(2, agent.Tools.Count); + } +} diff --git a/runtime/csharp/AgentSchema/AgentManifest.cs b/runtime/csharp/AgentSchema/AgentManifest.cs index ab901797..81fe3921 100644 --- a/runtime/csharp/AgentSchema/AgentManifest.cs +++ b/runtime/csharp/AgentSchema/AgentManifest.cs @@ -142,6 +142,12 @@ public static IList LoadResources(object data, LoadContext? context) { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -173,6 +179,11 @@ public static IList LoadResources(object data, LoadContext? context) { result.Add(Resource.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(Resource.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/runtime/csharp/AgentSchema/ContainerAgent.cs b/runtime/csharp/AgentSchema/ContainerAgent.cs index a7468174..df6bf451 100644 --- a/runtime/csharp/AgentSchema/ContainerAgent.cs +++ b/runtime/csharp/AgentSchema/ContainerAgent.cs @@ -123,6 +123,12 @@ public static IList LoadProtocols(object data, LoadContex { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -154,6 +160,11 @@ public static IList LoadProtocols(object data, LoadContex { result.Add(ProtocolVersionRecord.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(ProtocolVersionRecord.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } @@ -168,6 +179,12 @@ public static IList LoadEnvironmentVariables(object data, L { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -199,6 +216,11 @@ public static IList LoadEnvironmentVariables(object data, L { result.Add(EnvironmentVariable.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(EnvironmentVariable.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/runtime/csharp/AgentSchema/ObjectProperty.cs b/runtime/csharp/AgentSchema/ObjectProperty.cs index 0738408f..9a44ef74 100644 --- a/runtime/csharp/AgentSchema/ObjectProperty.cs +++ b/runtime/csharp/AgentSchema/ObjectProperty.cs @@ -82,6 +82,12 @@ public static IList LoadProperties(object data, LoadContext? context) { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -113,6 +119,11 @@ public static IList LoadProperties(object data, LoadContext? context) { result.Add(Property.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(Property.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/runtime/csharp/AgentSchema/PromptAgent.cs b/runtime/csharp/AgentSchema/PromptAgent.cs index 91e85815..59a4b7a9 100644 --- a/runtime/csharp/AgentSchema/PromptAgent.cs +++ b/runtime/csharp/AgentSchema/PromptAgent.cs @@ -123,6 +123,12 @@ public static IList LoadTools(object data, LoadContext? context) { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -154,6 +160,11 @@ public static IList LoadTools(object data, LoadContext? context) { result.Add(Tool.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(Tool.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/runtime/csharp/AgentSchema/PropertySchema.cs b/runtime/csharp/AgentSchema/PropertySchema.cs index d3deb6c2..4a1355d1 100644 --- a/runtime/csharp/AgentSchema/PropertySchema.cs +++ b/runtime/csharp/AgentSchema/PropertySchema.cs @@ -92,6 +92,12 @@ public static IList LoadProperties(object data, LoadContext? context) { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -123,6 +129,11 @@ public static IList LoadProperties(object data, LoadContext? context) { result.Add(Property.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(Property.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/runtime/csharp/AgentSchema/Tool.cs b/runtime/csharp/AgentSchema/Tool.cs index b5962bf2..bff0dca0 100644 --- a/runtime/csharp/AgentSchema/Tool.cs +++ b/runtime/csharp/AgentSchema/Tool.cs @@ -101,6 +101,12 @@ public static IList LoadBindings(object data, LoadContext? context) { var result = new List(); + // Normalize YamlDotNet's Dictionary to Dictionary + if (data is Dictionary rawYamlDict) + { + data = rawYamlDict.NormalizeDictionary(); + } + if (data is Dictionary dict) { // Convert named dictionary to list @@ -132,6 +138,11 @@ public static IList LoadBindings(object data, LoadContext? context) { result.Add(Binding.Load(itemDict, context)); } + else if (item is Dictionary yamlItemDict) + { + // Handle YamlDotNet's Dictionary list items + result.Add(Binding.Load(yamlItemDict.NormalizeDictionary(), context)); + } } } diff --git a/runtime/csharp/AgentSchema/Utils.cs b/runtime/csharp/AgentSchema/Utils.cs index a4509993..feb746b0 100644 --- a/runtime/csharp/AgentSchema/Utils.cs +++ b/runtime/csharp/AgentSchema/Utils.cs @@ -397,4 +397,43 @@ public static IDictionary ToParamDictionary(this object obj) return dict; } + + /// + /// Normalizes a YamlDotNet Dictionary<object, object> to Dictionary<string, object?>, + /// recursively normalizing all nested values. + /// + /// The YamlDotNet dictionary to normalize. + /// A normalized Dictionary<string, object?>. + public static Dictionary NormalizeDictionary(this Dictionary dict) + { + var result = new Dictionary(); + foreach (var kvp in dict) + { + result[kvp.Key?.ToString() ?? ""] = NormalizeValue(kvp.Value); + } + return result; + } + + /// + /// Recursively normalizes a value from YamlDotNet deserialization, + /// converting Dictionary<object, object> to Dictionary<string, object?> + /// and normalizing items within lists. + /// + /// The value to normalize. + /// The normalized value. + public static object? NormalizeValue(this object? value) + { + if (value is Dictionary dictObjObj) + return dictObjObj.NormalizeDictionary(); + + if (value is IEnumerable enumerable) + { + var list = new List(); + foreach (var item in enumerable) + list.Add(NormalizeValue(item)); + return list; + } + + return value; + } }