< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Generators.AgentDiscoveryHelper
Assembly: NexusLabs.Needlr.AgentFramework.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Generators/AgentDiscoveryHelper.cs
Line coverage
86%
Covered lines: 405
Uncovered lines: 62
Coverable lines: 467
Total lines: 906
Line coverage: 86.7%
Branch coverage
77%
Covered branches: 408
Total branches: 524
Branch coverage: 77.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Generators/AgentDiscoveryHelper.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System.Collections.Immutable;
 5using System.Globalization;
 6using System.Linq;
 7using System.Text;
 8using System.Threading;
 9
 10using Microsoft.CodeAnalysis;
 11using Microsoft.CodeAnalysis.CSharp;
 12using Microsoft.CodeAnalysis.CSharp.Syntax;
 13
 14namespace NexusLabs.Needlr.AgentFramework.Generators;
 15
 16internal static class AgentDiscoveryHelper
 17{
 18    private const string AgentFunctionAttributeName = "NexusLabs.Needlr.AgentFramework.AgentFunctionAttribute";
 19    private const string AgentFunctionGroupAttributeName = "NexusLabs.Needlr.AgentFramework.AgentFunctionGroupAttribute"
 20
 21    public static AgentFunctionTypeInfo? GetAgentFunctionTypeInfo(
 22        GeneratorSyntaxContext context,
 23        CancellationToken cancellationToken)
 24    {
 267525        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 267526        var typeSymbol = context.SemanticModel
 267527            .GetDeclaredSymbol(classDeclaration, cancellationToken) as INamedTypeSymbol;
 28
 267529        if (typeSymbol is null)
 030            return null;
 31
 267532        return TryGetTypeInfo(typeSymbol);
 33    }
 34
 35    public static AgentFunctionTypeInfo? TryGetTypeInfo(INamedTypeSymbol typeSymbol)
 36    {
 267537        if (typeSymbol.TypeKind != TypeKind.Class)
 038            return null;
 39
 267540        if (!IsAccessibleFromGeneratedCode(typeSymbol))
 041            return null;
 42
 267543        if (!typeSymbol.IsStatic && typeSymbol.IsAbstract)
 044            return null;
 45
 267546        bool hasAgentFunction = false;
 3100347        foreach (var member in typeSymbol.GetMembers())
 48        {
 1285549            if (member is not IMethodSymbol method)
 50                continue;
 51
 728552            if (method.MethodKind != MethodKind.Ordinary)
 53                continue;
 54
 61555            if (method.DeclaredAccessibility != Accessibility.Public)
 56                continue;
 57
 128758            foreach (var attribute in method.GetAttributes())
 59            {
 5760                if (attribute.AttributeClass?.ToDisplayString() == AgentFunctionAttributeName)
 61                {
 5762                    hasAgentFunction = true;
 5763                    break;
 64                }
 65            }
 66
 61567            if (hasAgentFunction)
 68                break;
 69        }
 70
 267571        if (!hasAgentFunction)
 261872            return null;
 73
 5774        var methodInfos = ImmutableArray.CreateBuilder<AgentFunctionMethodInfo>();
 33875        foreach (var member in typeSymbol.GetMembers())
 76        {
 11277            if (member is not IMethodSymbol method)
 78                continue;
 79
 11280            if (method.MethodKind != MethodKind.Ordinary)
 81                continue;
 82
 5783            if (method.DeclaredAccessibility != Accessibility.Public)
 84                continue;
 85
 5786            bool isAgentFunction = false;
 17187            foreach (var attribute in method.GetAttributes())
 88            {
 5789                if (attribute.AttributeClass?.ToDisplayString() == AgentFunctionAttributeName)
 90                {
 5791                    isAgentFunction = true;
 5792                    break;
 93                }
 94            }
 95
 5796            if (!isAgentFunction)
 97                continue;
 98
 5799            var returnType = method.ReturnType;
 57100            bool isVoid = returnType.SpecialType == SpecialType.System_Void;
 57101            bool isTask = returnType is INamedTypeSymbol nt &&
 57102                nt.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks" &&
 57103                (nt.MetadataName == "Task" || nt.MetadataName == "ValueTask");
 57104            bool isTaskOfT = returnType is INamedTypeSymbol nt2 &&
 57105                nt2.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks" &&
 57106                (nt2.MetadataName == "Task`1" || nt2.MetadataName == "ValueTask`1") &&
 57107                nt2.TypeArguments.Length == 1;
 57108            bool isAsync = isTask || isTaskOfT;
 57109            bool isVoidLike = isVoid || isTask;
 57110            string? returnValueTypeFQN = isVoidLike ? null : isTaskOfT
 57111                ? GetFullyQualifiedName(((INamedTypeSymbol)returnType).TypeArguments[0])
 57112                : GetFullyQualifiedName(returnType);
 113
 57114            string? returnJsonSchemaType = null;
 57115            string? returnObjectSchemaJson = null;
 57116            if (!isVoidLike)
 117            {
 53118                var unwrappedReturnType = isTaskOfT
 53119                    ? ((INamedTypeSymbol)returnType).TypeArguments[0]
 53120                    : returnType;
 53121                returnJsonSchemaType = GetJsonSchemaType(unwrappedReturnType, out _);
 53122                if (returnJsonSchemaType == "object")
 123                {
 1124                    returnObjectSchemaJson = BuildObjectSchemaJson(unwrappedReturnType);
 125                }
 126            }
 127
 57128            string? methodDesc = GetDescriptionFromAttributes(method.GetAttributes());
 129
 57130            var parameters = ImmutableArray.CreateBuilder<AgentFunctionParameterInfo>();
 224131            foreach (var param in method.Parameters)
 132            {
 55133                bool isCancellationToken = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "glob
 55134                bool isNullable = param.NullableAnnotation == NullableAnnotation.Annotated ||
 55135                    (param.Type is INamedTypeSymbol pnt && pnt.ConstructedFrom.SpecialType == SpecialType.System_Nullabl
 55136                bool hasDefault = param.HasExplicitDefaultValue;
 55137                string? defaultLiteral = hasDefault
 55138                    ? ConvertToCSharpLiteral(param.ExplicitDefaultValue, param.Type)
 55139                    : null;
 55140                bool isEnum = param.Type.TypeKind == TypeKind.Enum ||
 55141                    (param.Type is INamedTypeSymbol enumNullable &&
 55142                     enumNullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T &&
 55143                     enumNullable.TypeArguments.Length == 1 &&
 55144                     enumNullable.TypeArguments[0].TypeKind == TypeKind.Enum);
 55145                string? paramDesc = GetDescriptionFromAttributes(param.GetAttributes());
 55146                string jsonSchemaType = GetJsonSchemaType(param.Type, out string? itemJsonSchemaType);
 55147                string? jsonSchemaFormat = GetJsonSchemaFormat(param.Type);
 55148                string typeFullName = GetFullyQualifiedName(param.Type);
 149
 150                // Build object schema for complex array items (e.g., FaqEntry[] → properties of FaqEntry)
 55151                string? itemObjectSchemaJson = null;
 55152                IReadOnlyList<ObjectPropertyInfo>? itemObjectProperties = null;
 55153                if (jsonSchemaType == "array" && itemJsonSchemaType == "object")
 154                {
 5155                    ITypeSymbol? elementType = null;
 5156                    if (param.Type is IArrayTypeSymbol arrSym)
 5157                        elementType = arrSym.ElementType;
 0158                    else if (param.Type is INamedTypeSymbol namedSym && namedSym.IsGenericType && namedSym.TypeArguments
 0159                        elementType = namedSym.TypeArguments[0];
 160
 5161                    if (elementType != null)
 162                    {
 5163                        itemObjectSchemaJson = BuildObjectSchemaJson(elementType);
 5164                        itemObjectProperties = BuildObjectPropertyInfos(elementType);
 165                    }
 166                }
 167
 168                // Build object schema for top-level complex DTO parameters (e.g., MyDto dto).
 169                // Same machinery as array items but applied to the parameter type itself —
 170                // gives the LLM a proper {"type":"object","properties":{…},"required":[…]}
 171                // schema and lets the wrapper extract per-property via TryGetProperty + helper
 172                // calls instead of the broken as-cast that silently returns default(MyDto).
 55173                string? objectSchemaJson = null;
 55174                IReadOnlyList<ObjectPropertyInfo>? objectProperties = null;
 55175                if (jsonSchemaType == "object")
 176                {
 5177                    var dtoType = param.Type;
 5178                    if (dtoType is INamedTypeSymbol dtoNullable && dtoNullable.ConstructedFrom.SpecialType == SpecialTyp
 0179                        dtoType = dtoNullable.TypeArguments[0];
 5180                    objectSchemaJson = BuildObjectSchemaJson(dtoType);
 5181                    objectProperties = BuildObjectPropertyInfos(dtoType);
 182                }
 183
 55184                parameters.Add(new AgentFunctionParameterInfo(
 55185                    param.Name, typeFullName, jsonSchemaType, jsonSchemaFormat, itemJsonSchemaType,
 55186                    itemObjectSchemaJson, itemObjectProperties,
 55187                    objectSchemaJson, objectProperties,
 55188                    isCancellationToken, isNullable, hasDefault, defaultLiteral, isEnum, paramDesc));
 189            }
 190
 57191            methodInfos.Add(new AgentFunctionMethodInfo(
 57192                method.Name, isAsync, isVoidLike, returnValueTypeFQN,
 57193                returnJsonSchemaType, returnObjectSchemaJson,
 57194                parameters.ToImmutable(), methodDesc ?? ""));
 195        }
 196
 57197        return new AgentFunctionTypeInfo(
 57198            GetFullyQualifiedName(typeSymbol),
 57199            typeSymbol.ContainingAssembly?.Name ?? "Unknown",
 57200            typeSymbol.IsStatic,
 57201            methodInfos.ToImmutable());
 202    }
 203
 204    public static ImmutableArray<AgentFunctionGroupEntry> GetAgentFunctionGroupEntries(
 205        GeneratorSyntaxContext context,
 206        CancellationToken cancellationToken)
 207    {
 2675208        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 2675209        var typeSymbol = context.SemanticModel
 2675210            .GetDeclaredSymbol(classDeclaration, cancellationToken) as INamedTypeSymbol;
 211
 2675212        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0213            return ImmutableArray<AgentFunctionGroupEntry>.Empty;
 214
 2675215        var entries = ImmutableArray.CreateBuilder<AgentFunctionGroupEntry>();
 216
 9174217        foreach (var attr in typeSymbol.GetAttributes())
 218        {
 1912219            if (attr.AttributeClass?.ToDisplayString() != AgentFunctionGroupAttributeName)
 220                continue;
 221
 51222            if (attr.ConstructorArguments.Length != 1)
 223                continue;
 224
 51225            var groupName = attr.ConstructorArguments[0].Value as string;
 51226            if (string.IsNullOrWhiteSpace(groupName))
 227                continue;
 228
 51229            entries.Add(new AgentFunctionGroupEntry(GetFullyQualifiedName(typeSymbol), groupName!));
 230        }
 231
 2675232        return entries.ToImmutable();
 233    }
 234
 235    public static NeedlrAiAgentTypeInfo? GetNeedlrAiAgentTypeInfo(
 236        GeneratorAttributeSyntaxContext context,
 237        CancellationToken cancellationToken)
 238    {
 107239        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 107240        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0241            return null;
 242
 107243        var classDeclaration = context.TargetNode as ClassDeclarationSyntax;
 225244        var isPartial = classDeclaration?.Modifiers.Any(m => m.ValueText == "partial") ?? false;
 245
 107246        var namespaceName = typeSymbol.ContainingNamespace?.IsGlobalNamespace == true
 107247            ? null
 107248            : typeSymbol.ContainingNamespace?.ToDisplayString();
 249
 107250        var functionGroupNames = ImmutableArray<string>.Empty;
 107251        var explicitFunctionTypeFQNs = ImmutableArray<string>.Empty;
 107252        var hasExplicitFunctionTypes = false;
 253
 107254        var agentAttr = context.Attributes.FirstOrDefault();
 107255        if (agentAttr is not null)
 256        {
 117257            var groupsArg = agentAttr.NamedArguments.FirstOrDefault(a => a.Key == "FunctionGroups");
 107258            if (groupsArg.Key is not null && groupsArg.Value.Kind == TypedConstantKind.Array)
 259            {
 2260                functionGroupNames = groupsArg.Value.Values
 2261                    .Select(v => v.Value as string)
 2262                    .Where(s => !string.IsNullOrWhiteSpace(s))
 2263                    .Select(s => s!)
 2264                    .ToImmutableArray();
 265            }
 266
 117267            var typesArg = agentAttr.NamedArguments.FirstOrDefault(a => a.Key == "FunctionTypes");
 107268            if (typesArg.Key is not null && typesArg.Value.Kind == TypedConstantKind.Array)
 269            {
 2270                hasExplicitFunctionTypes = true;
 2271                explicitFunctionTypeFQNs = typesArg.Value.Values
 1272                    .Where(v => v.Kind == TypedConstantKind.Type && v.Value is INamedTypeSymbol)
 1273                    .Select(v => GetFullyQualifiedName((INamedTypeSymbol)v.Value!))
 2274                    .ToImmutableArray();
 275            }
 276        }
 277
 107278        return new NeedlrAiAgentTypeInfo(
 107279            GetFullyQualifiedName(typeSymbol),
 107280            typeSymbol.Name,
 107281            namespaceName,
 107282            isPartial,
 107283            functionGroupNames,
 107284            explicitFunctionTypeFQNs,
 107285            hasExplicitFunctionTypes);
 286    }
 287
 288    public static ImmutableArray<HandoffEntry> GetHandoffEntries(
 289        GeneratorAttributeSyntaxContext context,
 290        CancellationToken cancellationToken)
 291    {
 5292        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 5293        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0294            return ImmutableArray<HandoffEntry>.Empty;
 295
 5296        var initialTypeName = GetFullyQualifiedName(typeSymbol);
 5297        var entries = ImmutableArray.CreateBuilder<HandoffEntry>();
 298
 20299        foreach (var attr in context.Attributes)
 300        {
 5301            if (attr.ConstructorArguments.Length < 1)
 302                continue;
 303
 5304            var typeArg = attr.ConstructorArguments[0];
 5305            if (typeArg.Kind != TypedConstantKind.Type || typeArg.Value is not INamedTypeSymbol targetTypeSymbol)
 306                continue;
 307
 5308            var targetTypeName = GetFullyQualifiedName(targetTypeSymbol);
 5309            var reason = attr.ConstructorArguments.Length > 1 ? attr.ConstructorArguments[1].Value as string : null;
 310
 5311            entries.Add(new HandoffEntry(initialTypeName, typeSymbol.Name, targetTypeName, reason));
 312        }
 313
 5314        return entries.ToImmutable();
 315    }
 316
 317    public static ImmutableArray<GroupChatEntry> GetGroupChatEntries(
 318        GeneratorAttributeSyntaxContext context,
 319        CancellationToken cancellationToken)
 320    {
 8321        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 8322        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0323            return ImmutableArray<GroupChatEntry>.Empty;
 324
 8325        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 8326        var entries = ImmutableArray.CreateBuilder<GroupChatEntry>();
 327
 32328        foreach (var attr in context.Attributes)
 329        {
 8330            if (attr.ConstructorArguments.Length < 1)
 331                continue;
 332
 8333            var groupName = attr.ConstructorArguments[0].Value as string;
 8334            if (string.IsNullOrWhiteSpace(groupName))
 335                continue;
 336
 8337            var order = 0;
 16338            foreach (var named in attr.NamedArguments)
 339            {
 0340                if (named.Key == "Order" && named.Value.Value is int orderValue)
 341                {
 0342                    order = orderValue;
 0343                    break;
 344                }
 345            }
 346
 8347            entries.Add(new GroupChatEntry(agentTypeName, groupName!, order));
 348        }
 349
 8350        return entries.ToImmutable();
 351    }
 352
 353    public static ImmutableArray<SequenceEntry> GetSequenceEntries(
 354        GeneratorAttributeSyntaxContext context,
 355        CancellationToken cancellationToken)
 356    {
 24357        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 24358        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0359            return ImmutableArray<SequenceEntry>.Empty;
 360
 24361        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 24362        var entries = ImmutableArray.CreateBuilder<SequenceEntry>();
 363
 96364        foreach (var attr in context.Attributes)
 365        {
 24366            if (attr.ConstructorArguments.Length < 2)
 367                continue;
 368
 24369            var pipelineName = attr.ConstructorArguments[0].Value as string;
 24370            if (string.IsNullOrWhiteSpace(pipelineName))
 371                continue;
 372
 24373            if (attr.ConstructorArguments[1].Value is not int order)
 374                continue;
 375
 24376            entries.Add(new SequenceEntry(agentTypeName, pipelineName!, order));
 377        }
 378
 24379        return entries.ToImmutable();
 380    }
 381
 382    public static ImmutableArray<TerminationConditionEntry> GetTerminationConditionEntries(
 383        GeneratorAttributeSyntaxContext context,
 384        CancellationToken cancellationToken)
 385    {
 8386        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 8387        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0388            return ImmutableArray<TerminationConditionEntry>.Empty;
 389
 8390        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 8391        var entries = ImmutableArray.CreateBuilder<TerminationConditionEntry>();
 392
 32393        foreach (var attr in context.Attributes)
 394        {
 8395            if (attr.ConstructorArguments.Length < 1)
 396                continue;
 397
 8398            var typeArg = attr.ConstructorArguments[0];
 8399            if (typeArg.Kind != TypedConstantKind.Type || typeArg.Value is not INamedTypeSymbol condTypeSymbol)
 400                continue;
 401
 8402            var condTypeFQN = GetFullyQualifiedName(condTypeSymbol);
 8403            var ctorArgLiterals = ImmutableArray<string>.Empty;
 404
 8405            if (attr.ConstructorArguments.Length > 1)
 406            {
 8407                var paramsArg = attr.ConstructorArguments[1];
 8408                if (paramsArg.Kind == TypedConstantKind.Array)
 409                {
 8410                    ctorArgLiterals = paramsArg.Values
 8411                        .Select(SerializeTypedConstant)
 8412                        .Where(s => s is not null)
 8413                        .Select(s => s!)
 8414                        .ToImmutableArray();
 415                }
 416            }
 417
 8418            entries.Add(new TerminationConditionEntry(agentTypeName, condTypeFQN, ctorArgLiterals));
 419        }
 420
 8421        return entries.ToImmutable();
 422    }
 423
 424    public static string? SerializeTypedConstant(TypedConstant constant)
 425    {
 8426        return constant.Kind switch
 8427        {
 8428            TypedConstantKind.Primitive when constant.Value is string s =>
 8429                "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"",
 0430            TypedConstantKind.Primitive when constant.Value is int i => i.ToString(),
 0431            TypedConstantKind.Primitive when constant.Value is long l => l.ToString() + "L",
 0432            TypedConstantKind.Primitive when constant.Value is bool b => b ? "true" : "false",
 0433            TypedConstantKind.Primitive when constant.Value is null => "null",
 0434            TypedConstantKind.Type when constant.Value is ITypeSymbol ts =>
 0435                $"typeof({GetFullyQualifiedName(ts)})",
 0436            _ => null,
 8437        };
 438    }
 439
 440    public static ImmutableArray<ProgressSinksEntry> GetProgressSinksEntries(
 441        GeneratorAttributeSyntaxContext context,
 442        CancellationToken cancellationToken)
 443    {
 4444        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 4445        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0446            return ImmutableArray<ProgressSinksEntry>.Empty;
 447
 4448        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 449
 12450        foreach (var attr in context.Attributes)
 451        {
 452            // params Type[] is a single constructor arg of TypedConstantKind.Array
 4453            if (attr.ConstructorArguments.Length < 1)
 454                continue;
 455
 4456            var firstArg = attr.ConstructorArguments[0];
 4457            if (firstArg.Kind != TypedConstantKind.Array)
 458                continue;
 459
 4460            var sinkFQNs = ImmutableArray.CreateBuilder<string>();
 16461            foreach (var element in firstArg.Values)
 462            {
 4463                if (element.Kind == TypedConstantKind.Type && element.Value is INamedTypeSymbol sinkType)
 464                {
 4465                    sinkFQNs.Add(GetFullyQualifiedName(sinkType));
 466                }
 467            }
 468
 4469            if (sinkFQNs.Count > 0)
 470            {
 4471                return ImmutableArray.Create(
 4472                    new ProgressSinksEntry(agentTypeName, typeSymbol.Name, sinkFQNs.ToImmutable()));
 473            }
 474        }
 475
 0476        return ImmutableArray<ProgressSinksEntry>.Empty;
 477    }
 478
 479    public static string? GetDescriptionFromAttributes(ImmutableArray<AttributeData> attributes)
 480    {
 349481        foreach (var attr in attributes)
 482        {
 68483            if (attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ==
 68484                "global::System.ComponentModel.DescriptionAttribute" &&
 68485                attr.ConstructorArguments.Length == 1 &&
 68486                attr.ConstructorArguments[0].Value is string desc)
 11487                return desc;
 488        }
 101489        return null;
 490    }
 491
 492    public static string GetJsonSchemaType(ITypeSymbol type, out string? itemType)
 493    {
 166494        itemType = null;
 166495        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 2496            type = nullable.TypeArguments[0];
 497
 166498        switch (type.SpecialType)
 499        {
 84500            case SpecialType.System_String: return "string";
 11501            case SpecialType.System_Boolean: return "boolean";
 502            case SpecialType.System_Byte:
 503            case SpecialType.System_SByte:
 504            case SpecialType.System_Int16:
 505            case SpecialType.System_UInt16:
 506            case SpecialType.System_Int32:
 507            case SpecialType.System_UInt32:
 508            case SpecialType.System_Int64:
 23509            case SpecialType.System_UInt64: return "integer";
 510            case SpecialType.System_Single:
 511            case SpecialType.System_Double:
 8512            case SpecialType.System_Decimal: return "number";
 6513            case SpecialType.System_DateTime: return "string";
 514        }
 515
 34516        if (type is IArrayTypeSymbol arrayType)
 517        {
 8518            itemType = GetJsonSchemaType(arrayType.ElementType, out _);
 8519            if (string.IsNullOrEmpty(itemType))
 0520                itemType = "object";
 8521            return "array";
 522        }
 523
 26524        if (type is INamedTypeSymbol named && named.IsGenericType && named.TypeArguments.Length == 1)
 525        {
 0526            var baseName = named.ConstructedFrom.ToDisplayString();
 0527            if (baseName == "System.Collections.Generic.IEnumerable<T>" ||
 0528                baseName == "System.Collections.Generic.List<T>" ||
 0529                baseName == "System.Collections.Generic.IReadOnlyList<T>" ||
 0530                baseName == "System.Collections.Generic.ICollection<T>" ||
 0531                baseName == "System.Collections.Generic.IList<T>")
 532            {
 0533                itemType = GetJsonSchemaType(named.TypeArguments[0], out _);
 0534                if (string.IsNullOrEmpty(itemType))
 0535                    itemType = "object";
 0536                return "array";
 537            }
 538        }
 539
 540        // Stringified value types — no SpecialType assignment in Roslyn for these.
 541        // Identified by their full display name. The matching JSON Schema "format" comes
 542        // from GetJsonSchemaFormat.
 26543        var displayName = type.ToDisplayString();
 26544        if (displayName == "System.Guid" ||
 26545            displayName == "System.DateTimeOffset" ||
 26546            displayName == "System.TimeSpan")
 12547            return "string";
 548
 549        // Enum types map to string (LLMs pass enum values as strings)
 14550        if (type.TypeKind == TypeKind.Enum)
 2551            return "string";
 552
 553        // Complex types (classes, records, structs) → "object"
 12554        if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct)
 12555            return "object";
 556
 0557        return "";
 558    }
 559
 560    /// <summary>
 561    /// Returns the JSON Schema <c>format</c> hint for stringified value types: <c>"uuid"</c>
 562    /// for <see cref="System.Guid"/>, <c>"date-time"</c> for <see cref="System.DateTime"/> and
 563    /// <see cref="System.DateTimeOffset"/>, <c>"duration"</c> for <see cref="System.TimeSpan"/>.
 564    /// Returns <see langword="null"/> for any other type.
 565    /// </summary>
 566    public static string? GetJsonSchemaFormat(ITypeSymbol type)
 567    {
 105568        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 2569            type = nullable.TypeArguments[0];
 570
 105571        if (type.SpecialType == SpecialType.System_DateTime)
 6572            return "date-time";
 573
 99574        var displayName = type.ToDisplayString();
 99575        return displayName switch
 99576        {
 6577            "System.Guid" => "uuid",
 2578            "System.DateTimeOffset" => "date-time",
 4579            "System.TimeSpan" => "duration",
 87580            _ => null,
 99581        };
 582    }
 583
 584    /// <summary>
 585    /// Builds a JSON schema string for a complex object type's properties.
 586    /// Returns <see langword="null"/> if the type has no public properties or is not a complex type.
 587    /// </summary>
 588    public static string? BuildObjectSchemaJson(ITypeSymbol type)
 589    {
 11590        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 0591            type = nullable.TypeArguments[0];
 592
 593        // Get public instance properties (including inherited)
 11594        var properties = type.GetMembers()
 11595            .OfType<IPropertySymbol>()
 29596            .Where(p => p.DeclaredAccessibility == Accessibility.Public &&
 29597                        !p.IsStatic && !p.IsIndexer &&
 29598                        p.GetMethod != null)
 11599            .ToList();
 600
 11601        if (properties.Count == 0)
 0602            return null;
 603
 11604        var sb = new StringBuilder();
 11605        sb.Append("{\"type\":\"object\",\"properties\":{");
 606
 11607        var required = new List<string>();
 11608        var first = true;
 78609        foreach (var prop in properties)
 610        {
 45611            if (!first) sb.Append(",");
 28612            first = false;
 613
 614            // Use camelCase for JSON property names (standard convention)
 28615            var propName = char.ToLowerInvariant(prop.Name[0]) + prop.Name.Substring(1);
 28616            var propSchemaType = GetJsonSchemaType(prop.Type, out _);
 28617            if (string.IsNullOrEmpty(propSchemaType))
 0618                propSchemaType = "string"; // fallback
 28619            var propSchemaFormat = GetJsonSchemaFormat(prop.Type);
 620
 621            // Check for [Description] attribute on the property
 28622            string? propDesc = null;
 64623            foreach (var attr in prop.GetAttributes())
 624            {
 7625                if (attr.AttributeClass?.Name == "DescriptionAttribute")
 626                {
 6627                    propDesc = attr.ConstructorArguments.FirstOrDefault().Value?.ToString();
 6628                    break;
 629                }
 630            }
 631
 28632            sb.Append($"\"{propName}\":{{\"type\":\"{propSchemaType}\"");
 28633            if (!string.IsNullOrEmpty(propSchemaFormat))
 634            {
 5635                sb.Append($",\"format\":\"{propSchemaFormat}\"");
 636            }
 28637            if (!string.IsNullOrEmpty(propDesc))
 638            {
 6639                var escapedDesc = propDesc!.Replace("\\", "\\\\").Replace("\"", "\\\"");
 6640                sb.Append($",\"description\":\"{escapedDesc}\"");
 641            }
 28642            sb.Append("}");
 643
 644            // Non-nullable value types and non-nullable reference types are required
 28645            if (!prop.Type.IsValueType && prop.NullableAnnotation != NullableAnnotation.Annotated)
 13646                required.Add(propName);
 15647            else if (prop.Type.IsValueType && prop.NullableAnnotation != NullableAnnotation.Annotated
 15648                     && prop.Type is INamedTypeSymbol nt && nt.ConstructedFrom.SpecialType != SpecialType.System_Nullabl
 15649                required.Add(propName);
 650        }
 651
 11652        sb.Append("}");
 11653        if (required.Count > 0)
 654        {
 11655            sb.Append(",\"required\":[");
 39656            sb.Append(string.Join(",", required.Select(r => "\"" + r + "\"")));
 11657            sb.Append("]");
 658        }
 11659        sb.Append("}");
 660
 11661        return sb.ToString();
 662    }
 663
 664    /// <summary>
 665    /// Builds a list of <see cref="ObjectPropertyInfo"/> for manual property extraction
 666    /// from JsonElement. Used in generated code for AOT-safe deserialization of
 667    /// complex array element types.
 668    /// </summary>
 669    public static IReadOnlyList<ObjectPropertyInfo>? BuildObjectPropertyInfos(ITypeSymbol type)
 670    {
 10671        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 0672            type = nullable.TypeArguments[0];
 673
 10674        var properties = type.GetMembers()
 10675            .OfType<IPropertySymbol>()
 26676            .Where(p => p.DeclaredAccessibility == Accessibility.Public &&
 26677                        !p.IsStatic && !p.IsIndexer &&
 26678                        p.GetMethod != null && p.SetMethod != null)
 10679            .ToList();
 680
 10681        if (properties.Count == 0)
 1682            return null;
 683
 9684        var result = new List<ObjectPropertyInfo>();
 62685        foreach (var prop in properties)
 686        {
 22687            var jsonName = char.ToLowerInvariant(prop.Name[0]) + prop.Name.Substring(1);
 22688            var schemaType = GetJsonSchemaType(prop.Type, out _);
 22689            if (string.IsNullOrEmpty(schemaType))
 0690                schemaType = "string";
 22691            var schemaFormat = GetJsonSchemaFormat(prop.Type);
 22692            var csharpTypeFullName = GetFullyQualifiedName(prop.Type);
 22693            var isNullable = prop.NullableAnnotation == NullableAnnotation.Annotated ||
 22694                (prop.Type is INamedTypeSymbol pnt && pnt.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T);
 22695            var initDefaultLiteral = TryGetPropertyInitializerLiteral(prop);
 22696            result.Add(new ObjectPropertyInfo(
 22697                prop.Name, jsonName, csharpTypeFullName, schemaType, schemaFormat,
 22698                isNullable, initDefaultLiteral));
 699        }
 700
 9701        return result;
 702    }
 703
 704    public static bool IsAccessibleFromGeneratedCode(INamedTypeSymbol typeSymbol)
 705    {
 5551706        if (typeSymbol.DeclaredAccessibility == Accessibility.Private ||
 5551707            typeSymbol.DeclaredAccessibility == Accessibility.Protected)
 0708            return false;
 709
 5551710        var current = typeSymbol.ContainingType;
 5551711        while (current != null)
 712        {
 0713            if (current.DeclaredAccessibility == Accessibility.Private)
 0714                return false;
 0715            current = current.ContainingType;
 716        }
 717
 5551718        return true;
 719    }
 720
 721    public static string GetFullyQualifiedName(ITypeSymbol typeSymbol) =>
 499722        "global::" + typeSymbol.ToDisplayString(
 499723            SymbolDisplayFormat.FullyQualifiedFormat
 499724                .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
 499725                .WithMiscellaneousOptions(
 499726                    SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions
 499727                        & ~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
 728
 729    public static string SanitizeIdentifier(string name)
 730    {
 138731        if (string.IsNullOrEmpty(name))
 0732            return "Generated";
 733
 138734        var sb = new StringBuilder(name.Length);
 3588735        foreach (var c in name)
 736        {
 1656737            if (char.IsLetterOrDigit(c) || c == '_')
 1656738                sb.Append(c);
 0739            else if (c == '.' || c == '-' || c == ' ')
 0740                sb.Append(c == '.' ? '.' : '_');
 741        }
 742
 138743        var result = sb.ToString();
 138744        var segments = result.Split('.');
 552745        for (int i = 0; i < segments.Length; i++)
 746        {
 138747            if (segments[i].Length > 0 && char.IsDigit(segments[i][0]))
 0748                segments[i] = "_" + segments[i];
 749        }
 750
 276751        return string.Join(".", segments.Where(s => s.Length > 0));
 752    }
 753
 754    public static string GetShortName(string fqn)
 755    {
 191756        var stripped = fqn.StartsWith("global::", System.StringComparison.Ordinal) ? fqn.Substring(8) : fqn;
 191757        var lastDot = stripped.LastIndexOf('.');
 191758        return lastDot >= 0 ? stripped.Substring(lastDot + 1) : stripped;
 759    }
 760
 761    public static string StripAgentSuffix(string className)
 762    {
 763        const string suffix = "Agent";
 6764        return className.EndsWith(suffix, System.StringComparison.Ordinal) && className.Length > suffix.Length
 6765            ? className.Substring(0, className.Length - suffix.Length)
 6766            : className;
 767    }
 768
 769    public static string GroupNameToPascalCase(string groupName)
 770    {
 172771        var sb = new StringBuilder();
 172772        var capitalizeNext = true;
 2624773        foreach (var c in groupName)
 774        {
 1140775            if (c == '-' || c == '_' || c == ' ')
 776            {
 44777                capitalizeNext = true;
 778            }
 1096779            else if (char.IsLetterOrDigit(c))
 780            {
 1096781                sb.Append(capitalizeNext ? char.ToUpperInvariant(c) : c);
 1096782                capitalizeNext = false;
 783            }
 784        }
 172785        return sb.ToString();
 786    }
 787
 788    /// <summary>
 789    /// Converts a Roslyn-supplied parameter default value (boxed as <see cref="object"/>) into a
 790    /// C# literal expression suitable for direct emission into generated source.
 791    /// </summary>
 792    /// <remarks>
 793    /// <para>
 794    /// Handles the three cases that produce non-trivial output:
 795    /// </para>
 796    /// <list type="bullet">
 797    /// <item><description>
 798    /// <paramref name="value"/> is <see langword="null"/> for a non-nullable value type
 799    /// (e.g., <c>Guid id = default</c>) — emits <c>default(typeFullName)</c> rather than
 800    /// <c>"null"</c> which would not compile against the variable's declared type.
 801    /// </description></item>
 802    /// <item><description>
 803    /// <paramref name="parameterType"/> is an <see langword="enum"/> — Roslyn surfaces the
 804    /// underlying primitive value (e.g., <c>2</c> for <c>Mode.Append</c>); this method
 805    /// resolves the matching enum field by constant value and emits the typed literal
 806    /// (<c>global::MyApp.Mode.Append</c>). When no field matches (flags combination), emits
 807    /// a typed cast (<c>(global::MyApp.Mode)2</c>) which is still C#-legal.
 808    /// </description></item>
 809    /// <item><description>
 810    /// Primitive types — emits the standard C# literal form (e.g., <c>5L</c> for long,
 811    /// <c>9.99m</c> for decimal, escaped string literals).
 812    /// </description></item>
 813    /// </list>
 814    /// <para>
 815    /// Falls back to <c>default(typeFullName)</c> for any value the helper cannot render
 816    /// unambiguously.
 817    /// </para>
 818    /// </remarks>
 819    public static string ConvertToCSharpLiteral(object? value, ITypeSymbol parameterType)
 820    {
 18821        var typeFullName = GetFullyQualifiedName(parameterType);
 822
 18823        var underlyingType = parameterType is INamedTypeSymbol nullableNamed &&
 18824            nullableNamed.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T &&
 18825            nullableNamed.TypeArguments.Length == 1
 18826                ? nullableNamed.TypeArguments[0]
 18827                : parameterType;
 828
 18829        if (value is null)
 830        {
 6831            bool isNullableValueType = parameterType is INamedTypeSymbol nvt &&
 6832                nvt.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T;
 6833            bool isAnnotatedReference = !parameterType.IsValueType &&
 6834                parameterType.NullableAnnotation == NullableAnnotation.Annotated;
 835
 6836            if (parameterType.IsValueType && !isNullableValueType)
 4837                return "default(" + typeFullName + ")";
 838
 2839            if (!parameterType.IsValueType && !isAnnotatedReference)
 0840                return "default(" + typeFullName + ")";
 841
 2842            return "null";
 843        }
 844
 12845        if (underlyingType.TypeKind == TypeKind.Enum)
 846        {
 1847            var underlyingTypeFullName = GetFullyQualifiedName(underlyingType);
 7848            foreach (var member in underlyingType.GetMembers().OfType<IFieldSymbol>())
 849            {
 3850                if (member.HasConstantValue && Equals(member.ConstantValue, value))
 1851                    return underlyingTypeFullName + "." + member.Name;
 852            }
 853
 0854            var castValue = ConvertPrimitiveLiteral(value, underlyingTypeFullName);
 0855            return "(" + underlyingTypeFullName + ")(" + castValue + ")";
 856        }
 857
 11858        return ConvertPrimitiveLiteral(value, typeFullName);
 1859    }
 860
 861    private static string ConvertPrimitiveLiteral(object value, string typeFullName)
 862    {
 11863        return value switch
 11864        {
 3865            bool b => b ? "true" : "false",
 2866            string s => SymbolDisplay.FormatLiteral(s, quote: true),
 0867            char c => SymbolDisplay.FormatLiteral(c, quote: true),
 0868            byte n => n.ToString(CultureInfo.InvariantCulture),
 0869            sbyte n => n.ToString(CultureInfo.InvariantCulture),
 0870            short n => n.ToString(CultureInfo.InvariantCulture),
 0871            ushort n => n.ToString(CultureInfo.InvariantCulture),
 3872            int n => n.ToString(CultureInfo.InvariantCulture),
 0873            uint n => n.ToString(CultureInfo.InvariantCulture) + "U",
 0874            long n => n.ToString(CultureInfo.InvariantCulture) + "L",
 0875            ulong n => n.ToString(CultureInfo.InvariantCulture) + "UL",
 2876            float n when !float.IsNaN(n) && !float.IsInfinity(n) => n.ToString("R", CultureInfo.InvariantCulture) + "f",
 2877            double n when !double.IsNaN(n) && !double.IsInfinity(n) => n.ToString("R", CultureInfo.InvariantCulture) + "
 1878            decimal n => n.ToString(CultureInfo.InvariantCulture) + "m",
 0879            _ => "default(" + typeFullName + ")"
 11880        };
 881    }
 882
 883    /// <summary>
 884    /// Reads the property's source declaration to extract a simple-literal initializer (e.g.,
 885    /// <c>= "default"</c>, <c>= 5</c>, <c>= true</c>, <c>= null</c>) and returns it as a C#
 886    /// literal string suitable for direct emission. Returns <see langword="null"/> when the
 887    /// property has no initializer or the initializer is not a simple literal expression
 888    /// (e.g., a method call, an array creation, an interpolated string). Non-literal
 889    /// initializers are intentionally skipped — they are not safe to splice into generated
 890    /// code without semantic analysis.
 891    /// </summary>
 892    public static string? TryGetPropertyInitializerLiteral(IPropertySymbol prop)
 893    {
 76894        foreach (var syntaxRef in prop.DeclaringSyntaxReferences)
 895        {
 22896            var node = syntaxRef.GetSyntax();
 22897            if (node is PropertyDeclarationSyntax propDecl &&
 22898                propDecl.Initializer is { } init &&
 22899                init.Value is LiteralExpressionSyntax literal)
 900            {
 12901                return literal.ToString();
 902            }
 903        }
 10904        return null;
 905    }
 906}

Methods/Properties

GetAgentFunctionTypeInfo(Microsoft.CodeAnalysis.GeneratorSyntaxContext,System.Threading.CancellationToken)
TryGetTypeInfo(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetAgentFunctionGroupEntries(Microsoft.CodeAnalysis.GeneratorSyntaxContext,System.Threading.CancellationToken)
GetNeedlrAiAgentTypeInfo(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext,System.Threading.CancellationToken)
GetHandoffEntries(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext,System.Threading.CancellationToken)
GetGroupChatEntries(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext,System.Threading.CancellationToken)
GetSequenceEntries(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext,System.Threading.CancellationToken)
GetTerminationConditionEntries(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext,System.Threading.CancellationToken)
SerializeTypedConstant(Microsoft.CodeAnalysis.TypedConstant)
GetProgressSinksEntries(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext,System.Threading.CancellationToken)
GetDescriptionFromAttributes(System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.AttributeData>)
GetJsonSchemaType(Microsoft.CodeAnalysis.ITypeSymbol,System.String&)
GetJsonSchemaFormat(Microsoft.CodeAnalysis.ITypeSymbol)
BuildObjectSchemaJson(Microsoft.CodeAnalysis.ITypeSymbol)
BuildObjectPropertyInfos(Microsoft.CodeAnalysis.ITypeSymbol)
IsAccessibleFromGeneratedCode(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetFullyQualifiedName(Microsoft.CodeAnalysis.ITypeSymbol)
SanitizeIdentifier(System.String)
GetShortName(System.String)
StripAgentSuffix(System.String)
GroupNameToPascalCase(System.String)
ConvertToCSharpLiteral(System.Object,Microsoft.CodeAnalysis.ITypeSymbol)
ConvertPrimitiveLiteral(System.Object,System.String)
TryGetPropertyInitializerLiteral(Microsoft.CodeAnalysis.IPropertySymbol)