< 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
85%
Covered lines: 320
Uncovered lines: 53
Coverable lines: 373
Total lines: 715
Line coverage: 85.7%
Branch coverage
71%
Covered branches: 288
Total branches: 401
Branch coverage: 71.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GetAgentFunctionTypeInfo(...)50%2283.33%
TryGetTypeInfo(...)85.41%989694.18%
GetAgentFunctionGroupEntries(...)78.57%141492.3%
GetNeedlrAiAgentTypeInfo(...)70.83%242497.14%
GetHandoffEntries(...)85.71%141492.3%
GetGroupChatEntries(...)68.75%201675%
GetSequenceEntries(...)83.33%121291.66%
GetTerminationConditionEntries(...)87.5%161695.23%
SerializeTypedConstant(...)11.11%821841.66%
GetProgressSinksEntries(...)83.33%191887.5%
GetDescriptionFromAttributes(...)90%1010100%
GetJsonSchemaType(...)57.44%3534748.27%
BuildObjectSchemaJson(...)80.95%434293.02%
BuildObjectPropertyInfos(...)59.09%242285%
IsAccessibleFromGeneratedCode(...)37.5%14855.55%
GetFullyQualifiedName(...)100%11100%
SanitizeIdentifier(...)50%332271.42%
GetShortName(...)50%44100%
StripAgentSuffix(...)50%44100%
GroupNameToPascalCase(...)100%1212100%

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.Linq;
 6using System.Text;
 7using System.Threading;
 8
 9using Microsoft.CodeAnalysis;
 10using Microsoft.CodeAnalysis.CSharp.Syntax;
 11
 12namespace NexusLabs.Needlr.AgentFramework.Generators;
 13
 14internal static class AgentDiscoveryHelper
 15{
 16    private const string AgentFunctionAttributeName = "NexusLabs.Needlr.AgentFramework.AgentFunctionAttribute";
 17    private const string AgentFunctionGroupAttributeName = "NexusLabs.Needlr.AgentFramework.AgentFunctionGroupAttribute"
 18
 19    public static AgentFunctionTypeInfo? GetAgentFunctionTypeInfo(
 20        GeneratorSyntaxContext context,
 21        CancellationToken cancellationToken)
 22    {
 196623        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 196624        var typeSymbol = context.SemanticModel
 196625            .GetDeclaredSymbol(classDeclaration, cancellationToken) as INamedTypeSymbol;
 26
 196627        if (typeSymbol is null)
 028            return null;
 29
 196630        return TryGetTypeInfo(typeSymbol);
 31    }
 32
 33    public static AgentFunctionTypeInfo? TryGetTypeInfo(INamedTypeSymbol typeSymbol)
 34    {
 196635        if (typeSymbol.TypeKind != TypeKind.Class)
 036            return null;
 37
 196638        if (!IsAccessibleFromGeneratedCode(typeSymbol))
 039            return null;
 40
 196641        if (!typeSymbol.IsStatic && typeSymbol.IsAbstract)
 042            return null;
 43
 196644        bool hasAgentFunction = false;
 2267445        foreach (var member in typeSymbol.GetMembers())
 46        {
 938147            if (member is not IMethodSymbol method)
 48                continue;
 49
 532350            if (method.MethodKind != MethodKind.Ordinary)
 51                continue;
 52
 43053            if (method.DeclaredAccessibility != Accessibility.Public)
 54                continue;
 55
 88056            foreach (var attribute in method.GetAttributes())
 57            {
 2058                if (attribute.AttributeClass?.ToDisplayString() == AgentFunctionAttributeName)
 59                {
 2060                    hasAgentFunction = true;
 2061                    break;
 62                }
 63            }
 64
 43065            if (hasAgentFunction)
 66                break;
 67        }
 68
 196669        if (!hasAgentFunction)
 194670            return null;
 71
 2072        var methodInfos = ImmutableArray.CreateBuilder<AgentFunctionMethodInfo>();
 11673        foreach (var member in typeSymbol.GetMembers())
 74        {
 3875            if (member is not IMethodSymbol method)
 76                continue;
 77
 3878            if (method.MethodKind != MethodKind.Ordinary)
 79                continue;
 80
 2081            if (method.DeclaredAccessibility != Accessibility.Public)
 82                continue;
 83
 2084            bool isAgentFunction = false;
 6085            foreach (var attribute in method.GetAttributes())
 86            {
 2087                if (attribute.AttributeClass?.ToDisplayString() == AgentFunctionAttributeName)
 88                {
 2089                    isAgentFunction = true;
 2090                    break;
 91                }
 92            }
 93
 2094            if (!isAgentFunction)
 95                continue;
 96
 2097            var returnType = method.ReturnType;
 2098            bool isVoid = returnType.SpecialType == SpecialType.System_Void;
 2099            bool isTask = returnType is INamedTypeSymbol nt &&
 20100                nt.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks" &&
 20101                (nt.MetadataName == "Task" || nt.MetadataName == "ValueTask");
 20102            bool isTaskOfT = returnType is INamedTypeSymbol nt2 &&
 20103                nt2.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks" &&
 20104                (nt2.MetadataName == "Task`1" || nt2.MetadataName == "ValueTask`1") &&
 20105                nt2.TypeArguments.Length == 1;
 20106            bool isAsync = isTask || isTaskOfT;
 20107            bool isVoidLike = isVoid || isTask;
 20108            string? returnValueTypeFQN = isVoidLike ? null : isTaskOfT
 20109                ? GetFullyQualifiedName(((INamedTypeSymbol)returnType).TypeArguments[0])
 20110                : GetFullyQualifiedName(returnType);
 111
 20112            string? returnJsonSchemaType = null;
 20113            string? returnObjectSchemaJson = null;
 20114            if (!isVoidLike)
 115            {
 16116                var unwrappedReturnType = isTaskOfT
 16117                    ? ((INamedTypeSymbol)returnType).TypeArguments[0]
 16118                    : returnType;
 16119                returnJsonSchemaType = GetJsonSchemaType(unwrappedReturnType, out _);
 16120                if (returnJsonSchemaType == "object")
 121                {
 1122                    returnObjectSchemaJson = BuildObjectSchemaJson(unwrappedReturnType);
 123                }
 124            }
 125
 20126            string? methodDesc = GetDescriptionFromAttributes(method.GetAttributes());
 127
 20128            var parameters = ImmutableArray.CreateBuilder<AgentFunctionParameterInfo>();
 74129            foreach (var param in method.Parameters)
 130            {
 17131                bool isCancellationToken = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "glob
 17132                bool isNullable = param.NullableAnnotation == NullableAnnotation.Annotated ||
 17133                    (param.Type is INamedTypeSymbol pnt && pnt.ConstructedFrom.SpecialType == SpecialType.System_Nullabl
 17134                bool hasDefault = param.HasExplicitDefaultValue;
 17135                string? paramDesc = GetDescriptionFromAttributes(param.GetAttributes());
 17136                string jsonSchemaType = GetJsonSchemaType(param.Type, out string? itemJsonSchemaType);
 17137                string typeFullName = GetFullyQualifiedName(param.Type);
 138
 139                // Build object schema for complex array items (e.g., FaqEntry[] â†’ properties of FaqEntry)
 17140                string? itemObjectSchemaJson = null;
 17141                IReadOnlyList<ObjectPropertyInfo>? itemObjectProperties = null;
 17142                if (jsonSchemaType == "array" && itemJsonSchemaType == "object")
 143                {
 3144                    ITypeSymbol? elementType = null;
 3145                    if (param.Type is IArrayTypeSymbol arrSym)
 3146                        elementType = arrSym.ElementType;
 0147                    else if (param.Type is INamedTypeSymbol namedSym && namedSym.IsGenericType && namedSym.TypeArguments
 0148                        elementType = namedSym.TypeArguments[0];
 149
 3150                    if (elementType != null)
 151                    {
 3152                        itemObjectSchemaJson = BuildObjectSchemaJson(elementType);
 3153                        itemObjectProperties = BuildObjectPropertyInfos(elementType);
 154                    }
 155                }
 156
 17157                parameters.Add(new AgentFunctionParameterInfo(
 17158                    param.Name, typeFullName, jsonSchemaType, itemJsonSchemaType,
 17159                    itemObjectSchemaJson, itemObjectProperties,
 17160                    isCancellationToken, isNullable, hasDefault, paramDesc));
 161            }
 162
 20163            methodInfos.Add(new AgentFunctionMethodInfo(
 20164                method.Name, isAsync, isVoidLike, returnValueTypeFQN,
 20165                returnJsonSchemaType, returnObjectSchemaJson,
 20166                parameters.ToImmutable(), methodDesc ?? ""));
 167        }
 168
 20169        return new AgentFunctionTypeInfo(
 20170            GetFullyQualifiedName(typeSymbol),
 20171            typeSymbol.ContainingAssembly?.Name ?? "Unknown",
 20172            typeSymbol.IsStatic,
 20173            methodInfos.ToImmutable());
 174    }
 175
 176    public static ImmutableArray<AgentFunctionGroupEntry> GetAgentFunctionGroupEntries(
 177        GeneratorSyntaxContext context,
 178        CancellationToken cancellationToken)
 179    {
 1966180        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 1966181        var typeSymbol = context.SemanticModel
 1966182            .GetDeclaredSymbol(classDeclaration, cancellationToken) as INamedTypeSymbol;
 183
 1966184        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0185            return ImmutableArray<AgentFunctionGroupEntry>.Empty;
 186
 1966187        var entries = ImmutableArray.CreateBuilder<AgentFunctionGroupEntry>();
 188
 6794189        foreach (var attr in typeSymbol.GetAttributes())
 190        {
 1431191            if (attr.AttributeClass?.ToDisplayString() != AgentFunctionGroupAttributeName)
 192                continue;
 193
 14194            if (attr.ConstructorArguments.Length != 1)
 195                continue;
 196
 14197            var groupName = attr.ConstructorArguments[0].Value as string;
 14198            if (string.IsNullOrWhiteSpace(groupName))
 199                continue;
 200
 14201            entries.Add(new AgentFunctionGroupEntry(GetFullyQualifiedName(typeSymbol), groupName!));
 202        }
 203
 1966204        return entries.ToImmutable();
 205    }
 206
 207    public static NeedlrAiAgentTypeInfo? GetNeedlrAiAgentTypeInfo(
 208        GeneratorAttributeSyntaxContext context,
 209        CancellationToken cancellationToken)
 210    {
 107211        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 107212        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0213            return null;
 214
 107215        var classDeclaration = context.TargetNode as ClassDeclarationSyntax;
 225216        var isPartial = classDeclaration?.Modifiers.Any(m => m.ValueText == "partial") ?? false;
 217
 107218        var namespaceName = typeSymbol.ContainingNamespace?.IsGlobalNamespace == true
 107219            ? null
 107220            : typeSymbol.ContainingNamespace?.ToDisplayString();
 221
 107222        var functionGroupNames = ImmutableArray<string>.Empty;
 107223        var explicitFunctionTypeFQNs = ImmutableArray<string>.Empty;
 107224        var hasExplicitFunctionTypes = false;
 225
 107226        var agentAttr = context.Attributes.FirstOrDefault();
 107227        if (agentAttr is not null)
 228        {
 117229            var groupsArg = agentAttr.NamedArguments.FirstOrDefault(a => a.Key == "FunctionGroups");
 107230            if (groupsArg.Key is not null && groupsArg.Value.Kind == TypedConstantKind.Array)
 231            {
 2232                functionGroupNames = groupsArg.Value.Values
 2233                    .Select(v => v.Value as string)
 2234                    .Where(s => !string.IsNullOrWhiteSpace(s))
 2235                    .Select(s => s!)
 2236                    .ToImmutableArray();
 237            }
 238
 117239            var typesArg = agentAttr.NamedArguments.FirstOrDefault(a => a.Key == "FunctionTypes");
 107240            if (typesArg.Key is not null && typesArg.Value.Kind == TypedConstantKind.Array)
 241            {
 2242                hasExplicitFunctionTypes = true;
 2243                explicitFunctionTypeFQNs = typesArg.Value.Values
 1244                    .Where(v => v.Kind == TypedConstantKind.Type && v.Value is INamedTypeSymbol)
 1245                    .Select(v => GetFullyQualifiedName((INamedTypeSymbol)v.Value!))
 2246                    .ToImmutableArray();
 247            }
 248        }
 249
 107250        return new NeedlrAiAgentTypeInfo(
 107251            GetFullyQualifiedName(typeSymbol),
 107252            typeSymbol.Name,
 107253            namespaceName,
 107254            isPartial,
 107255            functionGroupNames,
 107256            explicitFunctionTypeFQNs,
 107257            hasExplicitFunctionTypes);
 258    }
 259
 260    public static ImmutableArray<HandoffEntry> GetHandoffEntries(
 261        GeneratorAttributeSyntaxContext context,
 262        CancellationToken cancellationToken)
 263    {
 5264        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 5265        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0266            return ImmutableArray<HandoffEntry>.Empty;
 267
 5268        var initialTypeName = GetFullyQualifiedName(typeSymbol);
 5269        var entries = ImmutableArray.CreateBuilder<HandoffEntry>();
 270
 20271        foreach (var attr in context.Attributes)
 272        {
 5273            if (attr.ConstructorArguments.Length < 1)
 274                continue;
 275
 5276            var typeArg = attr.ConstructorArguments[0];
 5277            if (typeArg.Kind != TypedConstantKind.Type || typeArg.Value is not INamedTypeSymbol targetTypeSymbol)
 278                continue;
 279
 5280            var targetTypeName = GetFullyQualifiedName(targetTypeSymbol);
 5281            var reason = attr.ConstructorArguments.Length > 1 ? attr.ConstructorArguments[1].Value as string : null;
 282
 5283            entries.Add(new HandoffEntry(initialTypeName, typeSymbol.Name, targetTypeName, reason));
 284        }
 285
 5286        return entries.ToImmutable();
 287    }
 288
 289    public static ImmutableArray<GroupChatEntry> GetGroupChatEntries(
 290        GeneratorAttributeSyntaxContext context,
 291        CancellationToken cancellationToken)
 292    {
 8293        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 8294        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0295            return ImmutableArray<GroupChatEntry>.Empty;
 296
 8297        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 8298        var entries = ImmutableArray.CreateBuilder<GroupChatEntry>();
 299
 32300        foreach (var attr in context.Attributes)
 301        {
 8302            if (attr.ConstructorArguments.Length < 1)
 303                continue;
 304
 8305            var groupName = attr.ConstructorArguments[0].Value as string;
 8306            if (string.IsNullOrWhiteSpace(groupName))
 307                continue;
 308
 8309            var order = 0;
 16310            foreach (var named in attr.NamedArguments)
 311            {
 0312                if (named.Key == "Order" && named.Value.Value is int orderValue)
 313                {
 0314                    order = orderValue;
 0315                    break;
 316                }
 317            }
 318
 8319            entries.Add(new GroupChatEntry(agentTypeName, groupName!, order));
 320        }
 321
 8322        return entries.ToImmutable();
 323    }
 324
 325    public static ImmutableArray<SequenceEntry> GetSequenceEntries(
 326        GeneratorAttributeSyntaxContext context,
 327        CancellationToken cancellationToken)
 328    {
 24329        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 24330        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0331            return ImmutableArray<SequenceEntry>.Empty;
 332
 24333        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 24334        var entries = ImmutableArray.CreateBuilder<SequenceEntry>();
 335
 96336        foreach (var attr in context.Attributes)
 337        {
 24338            if (attr.ConstructorArguments.Length < 2)
 339                continue;
 340
 24341            var pipelineName = attr.ConstructorArguments[0].Value as string;
 24342            if (string.IsNullOrWhiteSpace(pipelineName))
 343                continue;
 344
 24345            if (attr.ConstructorArguments[1].Value is not int order)
 346                continue;
 347
 24348            entries.Add(new SequenceEntry(agentTypeName, pipelineName!, order));
 349        }
 350
 24351        return entries.ToImmutable();
 352    }
 353
 354    public static ImmutableArray<TerminationConditionEntry> GetTerminationConditionEntries(
 355        GeneratorAttributeSyntaxContext context,
 356        CancellationToken cancellationToken)
 357    {
 8358        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 8359        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0360            return ImmutableArray<TerminationConditionEntry>.Empty;
 361
 8362        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 8363        var entries = ImmutableArray.CreateBuilder<TerminationConditionEntry>();
 364
 32365        foreach (var attr in context.Attributes)
 366        {
 8367            if (attr.ConstructorArguments.Length < 1)
 368                continue;
 369
 8370            var typeArg = attr.ConstructorArguments[0];
 8371            if (typeArg.Kind != TypedConstantKind.Type || typeArg.Value is not INamedTypeSymbol condTypeSymbol)
 372                continue;
 373
 8374            var condTypeFQN = GetFullyQualifiedName(condTypeSymbol);
 8375            var ctorArgLiterals = ImmutableArray<string>.Empty;
 376
 8377            if (attr.ConstructorArguments.Length > 1)
 378            {
 8379                var paramsArg = attr.ConstructorArguments[1];
 8380                if (paramsArg.Kind == TypedConstantKind.Array)
 381                {
 8382                    ctorArgLiterals = paramsArg.Values
 8383                        .Select(SerializeTypedConstant)
 8384                        .Where(s => s is not null)
 8385                        .Select(s => s!)
 8386                        .ToImmutableArray();
 387                }
 388            }
 389
 8390            entries.Add(new TerminationConditionEntry(agentTypeName, condTypeFQN, ctorArgLiterals));
 391        }
 392
 8393        return entries.ToImmutable();
 394    }
 395
 396    public static string? SerializeTypedConstant(TypedConstant constant)
 397    {
 8398        return constant.Kind switch
 8399        {
 8400            TypedConstantKind.Primitive when constant.Value is string s =>
 8401                "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"",
 0402            TypedConstantKind.Primitive when constant.Value is int i => i.ToString(),
 0403            TypedConstantKind.Primitive when constant.Value is long l => l.ToString() + "L",
 0404            TypedConstantKind.Primitive when constant.Value is bool b => b ? "true" : "false",
 0405            TypedConstantKind.Primitive when constant.Value is null => "null",
 0406            TypedConstantKind.Type when constant.Value is ITypeSymbol ts =>
 0407                $"typeof({GetFullyQualifiedName(ts)})",
 0408            _ => null,
 8409        };
 410    }
 411
 412    public static ImmutableArray<ProgressSinksEntry> GetProgressSinksEntries(
 413        GeneratorAttributeSyntaxContext context,
 414        CancellationToken cancellationToken)
 415    {
 4416        var typeSymbol = context.TargetSymbol as INamedTypeSymbol;
 4417        if (typeSymbol is null || !IsAccessibleFromGeneratedCode(typeSymbol))
 0418            return ImmutableArray<ProgressSinksEntry>.Empty;
 419
 4420        var agentTypeName = GetFullyQualifiedName(typeSymbol);
 421
 12422        foreach (var attr in context.Attributes)
 423        {
 424            // params Type[] is a single constructor arg of TypedConstantKind.Array
 4425            if (attr.ConstructorArguments.Length < 1)
 426                continue;
 427
 4428            var firstArg = attr.ConstructorArguments[0];
 4429            if (firstArg.Kind != TypedConstantKind.Array)
 430                continue;
 431
 4432            var sinkFQNs = ImmutableArray.CreateBuilder<string>();
 16433            foreach (var element in firstArg.Values)
 434            {
 4435                if (element.Kind == TypedConstantKind.Type && element.Value is INamedTypeSymbol sinkType)
 436                {
 4437                    sinkFQNs.Add(GetFullyQualifiedName(sinkType));
 438                }
 439            }
 440
 4441            if (sinkFQNs.Count > 0)
 442            {
 4443                return ImmutableArray.Create(
 4444                    new ProgressSinksEntry(agentTypeName, typeSymbol.Name, sinkFQNs.ToImmutable()));
 445            }
 446        }
 447
 0448        return ImmutableArray<ProgressSinksEntry>.Empty;
 449    }
 450
 451    public static string? GetDescriptionFromAttributes(ImmutableArray<AttributeData> attributes)
 452    {
 123453        foreach (var attr in attributes)
 454        {
 29455            if (attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ==
 29456                "global::System.ComponentModel.DescriptionAttribute" &&
 29457                attr.ConstructorArguments.Length == 1 &&
 29458                attr.ConstructorArguments[0].Value is string desc)
 9459                return desc;
 460        }
 28461        return null;
 462    }
 463
 464    public static string GetJsonSchemaType(ITypeSymbol type, out string? itemType)
 465    {
 52466        itemType = null;
 52467        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 0468            type = nullable.TypeArguments[0];
 469
 52470        switch (type.SpecialType)
 471        {
 33472            case SpecialType.System_String: return "string";
 2473            case SpecialType.System_Boolean: return "boolean";
 474            case SpecialType.System_Byte:
 475            case SpecialType.System_SByte:
 476            case SpecialType.System_Int16:
 477            case SpecialType.System_UInt16:
 478            case SpecialType.System_Int32:
 479            case SpecialType.System_UInt32:
 480            case SpecialType.System_Int64:
 8481            case SpecialType.System_UInt64: return "integer";
 482            case SpecialType.System_Single:
 483            case SpecialType.System_Double:
 0484            case SpecialType.System_Decimal: return "number";
 485        }
 486
 9487        if (type is IArrayTypeSymbol arrayType)
 488        {
 4489            itemType = GetJsonSchemaType(arrayType.ElementType, out _);
 4490            if (string.IsNullOrEmpty(itemType))
 0491                itemType = "object";
 4492            return "array";
 493        }
 494
 5495        if (type is INamedTypeSymbol named && named.IsGenericType && named.TypeArguments.Length == 1)
 496        {
 0497            var baseName = named.ConstructedFrom.ToDisplayString();
 0498            if (baseName == "System.Collections.Generic.IEnumerable<T>" ||
 0499                baseName == "System.Collections.Generic.List<T>" ||
 0500                baseName == "System.Collections.Generic.IReadOnlyList<T>" ||
 0501                baseName == "System.Collections.Generic.ICollection<T>" ||
 0502                baseName == "System.Collections.Generic.IList<T>")
 503            {
 0504                itemType = GetJsonSchemaType(named.TypeArguments[0], out _);
 0505                if (string.IsNullOrEmpty(itemType))
 0506                    itemType = "object";
 0507                return "array";
 508            }
 509        }
 510
 511        // Enum types map to string (LLMs pass enum values as strings)
 5512        if (type.TypeKind == TypeKind.Enum)
 0513            return "string";
 514
 515        // Complex types (classes, records, structs) â†’ "object"
 5516        if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct)
 5517            return "object";
 518
 0519        return "";
 520    }
 521
 522    /// <summary>
 523    /// Builds a JSON schema string for a complex object type's properties.
 524    /// Returns <see langword="null"/> if the type has no public properties or is not a complex type.
 525    /// </summary>
 526    public static string? BuildObjectSchemaJson(ITypeSymbol type)
 527    {
 4528        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 0529            type = nullable.TypeArguments[0];
 530
 531        // Get public instance properties (including inherited)
 4532        var properties = type.GetMembers()
 4533            .OfType<IPropertySymbol>()
 9534            .Where(p => p.DeclaredAccessibility == Accessibility.Public &&
 9535                        !p.IsStatic && !p.IsIndexer &&
 9536                        p.GetMethod != null)
 4537            .ToList();
 538
 4539        if (properties.Count == 0)
 0540            return null;
 541
 4542        var sb = new StringBuilder();
 4543        sb.Append("{\"type\":\"object\",\"properties\":{");
 544
 4545        var required = new List<string>();
 4546        var first = true;
 26547        foreach (var prop in properties)
 548        {
 14549            if (!first) sb.Append(",");
 9550            first = false;
 551
 552            // Use camelCase for JSON property names (standard convention)
 9553            var propName = char.ToLowerInvariant(prop.Name[0]) + prop.Name.Substring(1);
 9554            var propSchemaType = GetJsonSchemaType(prop.Type, out _);
 9555            if (string.IsNullOrEmpty(propSchemaType))
 0556                propSchemaType = "string"; // fallback
 557
 558            // Check for [Description] attribute on the property
 9559            string? propDesc = null;
 24560            foreach (var attr in prop.GetAttributes())
 561            {
 6562                if (attr.AttributeClass?.Name == "DescriptionAttribute")
 563                {
 6564                    propDesc = attr.ConstructorArguments.FirstOrDefault().Value?.ToString();
 6565                    break;
 566                }
 567            }
 568
 9569            sb.Append($"\"{propName}\":{{\"type\":\"{propSchemaType}\"");
 9570            if (!string.IsNullOrEmpty(propDesc))
 571            {
 6572                var escapedDesc = propDesc!.Replace("\\", "\\\\").Replace("\"", "\\\"");
 6573                sb.Append($",\"description\":\"{escapedDesc}\"");
 574            }
 9575            sb.Append("}");
 576
 577            // Non-nullable value types and non-nullable reference types are required
 9578            if (!prop.Type.IsValueType && prop.NullableAnnotation != NullableAnnotation.Annotated)
 7579                required.Add(propName);
 2580            else if (prop.Type.IsValueType && prop.NullableAnnotation != NullableAnnotation.Annotated
 2581                     && prop.Type is INamedTypeSymbol nt && nt.ConstructedFrom.SpecialType != SpecialType.System_Nullabl
 2582                required.Add(propName);
 583        }
 584
 4585        sb.Append("}");
 4586        if (required.Count > 0)
 587        {
 4588            sb.Append(",\"required\":[");
 13589            sb.Append(string.Join(",", required.Select(r => "\"" + r + "\"")));
 4590            sb.Append("]");
 591        }
 4592        sb.Append("}");
 593
 4594        return sb.ToString();
 595    }
 596
 597    /// <summary>
 598    /// Builds a list of <see cref="ObjectPropertyInfo"/> for manual property extraction
 599    /// from JsonElement. Used in generated code for AOT-safe deserialization of
 600    /// complex array element types.
 601    /// </summary>
 602    public static IReadOnlyList<ObjectPropertyInfo>? BuildObjectPropertyInfos(ITypeSymbol type)
 603    {
 3604        if (type is INamedTypeSymbol nullable && nullable.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
 0605            type = nullable.TypeArguments[0];
 606
 3607        var properties = type.GetMembers()
 3608            .OfType<IPropertySymbol>()
 6609            .Where(p => p.DeclaredAccessibility == Accessibility.Public &&
 6610                        !p.IsStatic && !p.IsIndexer &&
 6611                        p.GetMethod != null && p.SetMethod != null)
 3612            .ToList();
 613
 3614        if (properties.Count == 0)
 0615            return null;
 616
 3617        var result = new List<ObjectPropertyInfo>();
 18618        foreach (var prop in properties)
 619        {
 6620            var jsonName = char.ToLowerInvariant(prop.Name[0]) + prop.Name.Substring(1);
 6621            var schemaType = GetJsonSchemaType(prop.Type, out _);
 6622            if (string.IsNullOrEmpty(schemaType))
 0623                schemaType = "string";
 6624            var isNullable = prop.NullableAnnotation == NullableAnnotation.Annotated ||
 6625                (prop.Type is INamedTypeSymbol pnt && pnt.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T);
 6626            result.Add(new ObjectPropertyInfo(prop.Name, jsonName, schemaType, isNullable));
 627        }
 628
 3629        return result;
 630    }
 631
 632    public static bool IsAccessibleFromGeneratedCode(INamedTypeSymbol typeSymbol)
 633    {
 4133634        if (typeSymbol.DeclaredAccessibility == Accessibility.Private ||
 4133635            typeSymbol.DeclaredAccessibility == Accessibility.Protected)
 0636            return false;
 637
 4133638        var current = typeSymbol.ContainingType;
 4133639        while (current != null)
 640        {
 0641            if (current.DeclaredAccessibility == Accessibility.Private)
 0642                return false;
 0643            current = current.ContainingType;
 644        }
 645
 4133646        return true;
 647    }
 648
 649    public static string GetFullyQualifiedName(ITypeSymbol typeSymbol) =>
 309650        "global::" + typeSymbol.ToDisplayString(
 309651            SymbolDisplayFormat.FullyQualifiedFormat
 309652                .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
 309653                .WithMiscellaneousOptions(
 309654                    SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions
 309655                        & ~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
 656
 657    public static string SanitizeIdentifier(string name)
 658    {
 101659        if (string.IsNullOrEmpty(name))
 0660            return "Generated";
 661
 101662        var sb = new StringBuilder(name.Length);
 2626663        foreach (var c in name)
 664        {
 1212665            if (char.IsLetterOrDigit(c) || c == '_')
 1212666                sb.Append(c);
 0667            else if (c == '.' || c == '-' || c == ' ')
 0668                sb.Append(c == '.' ? '.' : '_');
 669        }
 670
 101671        var result = sb.ToString();
 101672        var segments = result.Split('.');
 404673        for (int i = 0; i < segments.Length; i++)
 674        {
 101675            if (segments[i].Length > 0 && char.IsDigit(segments[i][0]))
 0676                segments[i] = "_" + segments[i];
 677        }
 678
 202679        return string.Join(".", segments.Where(s => s.Length > 0));
 680    }
 681
 682    public static string GetShortName(string fqn)
 683    {
 93684        var stripped = fqn.StartsWith("global::", System.StringComparison.Ordinal) ? fqn.Substring(8) : fqn;
 93685        var lastDot = stripped.LastIndexOf('.');
 93686        return lastDot >= 0 ? stripped.Substring(lastDot + 1) : stripped;
 687    }
 688
 689    public static string StripAgentSuffix(string className)
 690    {
 691        const string suffix = "Agent";
 6692        return className.EndsWith(suffix, System.StringComparison.Ordinal) && className.Length > suffix.Length
 6693            ? className.Substring(0, className.Length - suffix.Length)
 6694            : className;
 695    }
 696
 697    public static string GroupNameToPascalCase(string groupName)
 698    {
 98699        var sb = new StringBuilder();
 98700        var capitalizeNext = true;
 1884701        foreach (var c in groupName)
 702        {
 844703            if (c == '-' || c == '_' || c == ' ')
 704            {
 44705                capitalizeNext = true;
 706            }
 800707            else if (char.IsLetterOrDigit(c))
 708            {
 800709                sb.Append(capitalizeNext ? char.ToUpperInvariant(c) : c);
 800710                capitalizeNext = false;
 711            }
 712        }
 98713        return sb.ToString();
 714    }
 715}

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&)
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)