![[MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始](http://pic.xiahunao.cn/yaotu/[MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始)
1. 基础类型与Agent相关的基础类型主要有AgentRunContext、AgentRunOptions、AgentSession和ChatMessage等。这些类型在Agent的运行过程中扮演着重要的角色承载了运行时的上下文信息、运行选项、会话信息以及消息内容等。理解这些基础类型对于我们深入理解MAF中的Agent设计与实现是非常有帮助的。1.1. AgentRunContextAgentRunContext是一个类包含了当前Agent的运行上下文信息如下所示public sealed class AgentRunContext { public AIAgent Agent { get; } public AgentSession? Session { get; } public IReadOnlyCollectionChatMessage RequestMessages { get; } public AgentRunOptions? RunOptions { get; } }属性成员说明如下Agent当前正在执行的Agent对象Session当前Agent的会话对象可以通过它来实现会话保持RequestMessages当前Agent的输入消息列表RunOptions当前Agent的运行选项可以通过它来设置一些运行时的参数比如是否启用后台响应指定输出格式等1.2. AgentRunOptionsAgentRunOptions包含了用于控制Agent的运行行为的配置选项:public class AgentRunOptions { public ResponseContinuationToken? ContinuationToken { get; set; } public bool? AllowBackgroundResponses { get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } public ChatResponseFormat? ResponseFormat { get; set; } } public class ResponseContinuationToken { public static ResponseContinuationToken FromBytes(ReadOnlyMemorybyte bytes); public virtual ReadOnlyMemorybyte ToBytes(); }属性成员说明如下ContinuationToken恢复/轮询令牌,用于处理长时间运行的任务或被中断的任务流式中断恢复如果在流式输出过程中连接中断可以将上一次更新中获取的ContinuationToken重新传入让Agent从中断点继续生成而不是从头开始;非阻塞轮询对于耗时较长的任务Agent可能先返回一个令牌。调用者后续通过该令牌多次调用RunAsync来检查任务是否完成并获取最终结果AllowBackgroundResponses是否允许后台响应如果设置为true则Agent在执行过程中可以在后台生成一些响应而不必等到整个推理过程结束后才返回结果AdditionalProperties一个字典用于存储一些额外的属性信息ResponseFormat定义Agent输出内容的结构。通常用于强制Agent返回特定格式如JSON对象这在需要将Agent的输出直接对接自动化工作流或结构化数据库时非常关键;在MAF中ContinuationToken是一个十分常见的类型。正如它的名字所体现的那样如果服务端返回的内容包括这样一个ContinuationToken以为着还有后续Continuation对于一个流来说这个ContinuationToken是对这个流的某个中间位置的描述客户端得到这个ContinuationToken后不仅不知道流尚未结束还能利用它从正确的位置继续获取数据。对于一个非流式的后台任务来说这个ContinuationToken用于确定任务尚未结束还需等待。1.ChatResponseFormatResponseFormat属性类型ChatResponseFormat定义如下。如果我们指定ChatResponseFormat.Text或者ChatResponseFormat.JsonAgent会强制要求LLM返回纯文本Markdown或者JSON格式的内容。如果我们调用ChatResponseFormat.ForJsonSchema方法创建一个ChatResponseFormatJson对象作为AgentRunOptions的ResponseFormat属性Agent会强制要求模型返回符合该JSON Schema的JSON。结构化输出在很多场景下都是非常有用的比如当我们需要将Agent的输出直接对接到自动化工作流或者结构化数据库时结构化的输出可以让我们更方便地进行后续的处理和分析。public class ChatResponseFormat { public static ChatResponseFormatText Text { get; } new ChatResponseFormatText(); public static ChatResponseFormatJson Json { get; } new ChatResponseFormatJson(null); public static ChatResponseFormatJson ForJsonSchema( JsonElement schema, string? schemaName null, string? schemaDescription null) ; public static ChatResponseFormatJson ForJsonSchemaT( JsonSerializerOptions? serializerOptions null, string? schemaName null, string? schemaDescription null) ; public static ChatResponseFormatJson ForJsonSchema( Type schemaType, JsonSerializerOptions? serializerOptions null, string? schemaName null, string? schemaDescription null); }如下所示的ChatResponseFormatText和ChatResponseFormatJson分别是ChatResponseFormat的两个子类分别用于表示纯文本格式和JSON格式的响应格式。只有ChatResponseFormatJson与结构化输出有关ChatResponseFormatText仅仅并不定义文本的结构单纯地要求采用自然语言输出。public sealed class ChatResponseFormatText : ChatResponseFormat { public override bool Equals(object? obj) public override int GetHashCode() } public sealed class ChatResponseFormatJson : ChatResponseFormat { public JsonElement? Schema { get; } public string? SchemaName { get; } public string? SchemaDescription { get; } public ChatResponseFormatJson(JsonElement? schema, string? schemaName null, string? schemaDescription null) }1.3. AgentSessionAgentSession是一个抽象类代表了一个Agent的会话对象。如果希望多次针对Agent的调用在同一个Session中进行只需要传递相同的AgentSession即可。其核心成员是AgentSessionStateBag类型的属性StateBag。我们将在很多类型中看到GetService和GetServiceTService看到这两个方法它来源于宿主程序作为Dependecy Injection容器的IServiceProvider对象。我们会在很多类型中看到这两个方法的身影之后我们就不再赘述了。public abstract class AgentSession { public AgentSessionStateBag StateBag { get; protected set; } private string DebuggerDisplay $StateBag Count {StateBag.Count}; public virtual object? GetService(Type serviceType, object? serviceKey null) ; public TService? GetServiceTService(object? serviceKey null) ; }AgentSessionStateBag代表了一个Agent会话状态的容器它提供了TryGetValue、GetValue、SetValue和TryRemoveValue方法来操作状态数据。它还提供了Serialize和Deserialize方法来实现状态的序列化和反序列化以便于在分布式环境中进行状态的持久化和恢复。public class AgentSessionStateBag { public int Count {get;} public bool TryGetValueT( string key, out T? value, JsonSerializerOptions? jsonSerializerOptions null) where T : class public T? GetValueT( string key, JsonSerializerOptions? jsonSerializerOptions null) where T : class public void SetValueT( string key, T? value, JsonSerializerOptions? jsonSerializerOptions null) where T : class public bool TryRemoveValue(string key) ; public JsonElement Serialize(); public static AgentSessionStateBag Deserialize(JsonElement jsonElement); }1.4. ChatMessage所有的基于对话的Agent都采用消息进行交互消息不仅承载了多模态的内容还绑定了一个在作为消息发送发的角色。MAF中的消息通过如下这个ChatMessage类来表示。public class ChatMessage { public string? AuthorName{ get; set; } public DateTimeOffset? CreatedAt { get; set; } public ChatRole Role { get; set; } public string Text Contents.ConcatText(); public IListAIContent Contents{ get; set; } public string? MessageId { get; set; } public object? RawRepresentation { get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } }ChatMessage的属性成员说明如下AuthorName消息发送者的名称可以是用户、模型或者工具等CreatedAt消息创建的时间戳Role消息发送者的角色通常是一个枚举类型如用户、模型、工具等Text消息的文本内容实际上是对Contents中所有内容的文本进行拼接后的结果Contents消息的内容列表每个内容都是一个AIContent对象AIContent是一个抽象类代表了消息内容的基类可以有不同类型的内容如文本、图片、文件等MessageId消息的唯一标识符可以用于消息的追踪和管理RawRepresentation消息的原始表示可以用于存储一些与消息相关的原始数据如模型返回的原始响应等AdditionalProperties一个字典用于存储一些额外的属性信息可以在Agent的运行过程中使用这些属性来进行一些自定义的逻辑处理1.4.1 ChatRole出于可扩展的考虑Role并不是一个简单的枚举类型而是一个具有更丰富功能的结构体。它预定义了System、Assistant、User和Tool四个角色并且允许用户自定义角色。ChatMessage中的Role属性就是利用这个ChatRole结构体来表示消息发送者的角色。public readonly struct ChatRole : IEquatableChatRole { public static ChatRole System { get; } new ChatRole(system); public static ChatRole Assistant { get; } new ChatRole(assistant); public static ChatRole User { get; } new ChatRole(user); public static ChatRole Tool { get; } new ChatRole(tool); public string Value { get; } }1.4.2 AIContentAIContent是MAF 框架中定义一切交互内容的原子基类。它采用高度多态的设计将AI与用户之间的对话拆解为多种专业化的内容块。在传统的AI开发中消息通常只有Text。而AIContent将对话模型化为一个多模态、多状态的流。通过JsonDerivedTypeAttribute声明它支持了19种且在增加子类型涵盖了现代Agent交互的方方面面。[JsonPolymorphic(TypeDiscriminatorPropertyName $type)] [JsonDerivedType(typeof(DataContent), typeDiscriminator: data)] [JsonDerivedType(typeof(ErrorContent), typeDiscriminator: error)] [JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: functionCall)] [JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: functionResult)] [JsonDerivedType(typeof(HostedFileContent), typeDiscriminator: hostedFile)] [JsonDerivedType(typeof(HostedVectorStoreContent), typeDiscriminator: hostedVectorStore)] [JsonDerivedType(typeof(TextContent), typeDiscriminator: text)] [JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: reasoning)] [JsonDerivedType(typeof(UriContent), typeDiscriminator: uri)] [JsonDerivedType(typeof(UsageContent), typeDiscriminator: usage)] [JsonDerivedType(typeof(ToolCallContent), typeDiscriminator: toolCall)] [JsonDerivedType(typeof(ToolResultContent), typeDiscriminator: toolResult)] [JsonDerivedType(typeof(InputRequestContent), typeDiscriminator: inputRequest)] [JsonDerivedType(typeof(InputResponseContent), typeDiscriminator: inputResponse)] [JsonDerivedType(typeof(ToolApprovalRequestContent), typeDiscriminator: toolApprovalRequest)] [JsonDerivedType(typeof(ToolApprovalResponseContent), typeDiscriminator: toolApprovalResponse)] [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: mcpServerToolCall)] [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: mcpServerToolResult)] public class AIContent { public IListAIAnnotation? Annotations { get; set; } [JsonIgnore] public object? RawRepresentation { get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } }1.4.3 AIAnnotationAIContent的Annotations返回一个AIAnnotation的列表AIAnnotation是一个注解类用于为内容提供一些额外的信息或者标记。AIAnnotation也是一个多态类型通过JsonDerivedTypeAttribute声明了不同的子类型如CitationAnnotation等。[JsonPolymorphic(TypeDiscriminatorPropertyName $type)] [JsonDerivedType(typeof(CitationAnnotation), typeDiscriminator: citation)] public class AIAnnotation { public IListAnnotatedRegion? AnnotatedRegions { get; set; } [JsonIgnore] public object? RawRepresentation { get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } } public class CitationAnnotation : AIAnnotation { public string? Title { get; set; } public Uri? Url { get; set; } public string? FileId { get; set; } public string? ToolName { get; set; } public string? Snippet { get; set; } }CitationAnnotation是AIAnnotation的一个子类用于表示引用注解它具有如下的属性成员Title引用的标题可以是文章标题、网页标题等Url引用的URL地址可以是文章链接、网页链接等FileId引用的文件ID如果引用的是一个文件可以通过这个ID来获取文件的相关信息ToolName引用的工具名称如果引用的是一个工具可以通过这个名称来获取工具的相关信息Snippet引用的内容片段可以是引用内容的摘要、引用内容的一部分等1.4.4 AnnotatedRegionAnnotatedRegion是AIAnnotation中的一个属性成员它代表了被注解的内容区域可以是文本区域、图像区域等。AnnotatedRegion同样是一个多态类型通过JsonDerivedTypeAttribute声明了不同的子类型如TextSpanAnnotatedRegion等。[JsonPolymorphic(TypeDiscriminatorPropertyName $type)] [JsonDerivedType(typeof(TextSpanAnnotatedRegion), textSpan)] public class AnnotatedRegion { } public sealed class TextSpanAnnotatedRegion : AnnotatedRegion { [JsonPropertyName(start)] public int? StartIndex { get; set; } [JsonPropertyName(end)] public int? EndIndex { get; set; } }1.5. ChatResponseAgent具有两种执行方式一种是阻塞式调用它会等到整个执行完全结束后才会得到结果另一种是流式调用它会在执行过程中不断地返回结果更新。两者的响应类型分别是AgentResponse和AgentResponseUpdate。如下所示的是AgentResponse的定义它封装了AI的回答、消耗统计、任务状态以及上下文标识等。public class ChatResponse { public IListChatMessage Messages{ get; set; } public string Text { get; } public string? ResponseId { get; set; } public string? ConversationId { get; set; } public string? ModelId { get; set; } public DateTimeOffset? CreatedAt { get; set; } public ChatFinishReason? FinishReason { get; set; } public UsageDetails? Usage { get; set; } public ResponseContinuationToken? ContinuationToken { get; set; } public object? RawRepresentation { get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } public ChatResponseUpdate[] ToChatResponseUpdates(); }属性成员说明如下Messages当前响应包含的消息列表通常是一个ChatMessage的列表Text当前响应的文本内容实际上是对Messages中所有消息的文本内容进行拼接后的结果ResponseId当前响应的唯一标识符可以用于响应的追踪和管理ConversationId当前响应所属的对话ID可以用于将多个响应关联到同一个对话中ModelId当前响应使用的模型ID可以用于追踪模型的使用情况CreatedAt当前响应的创建时间可以用于记录响应的时间戳FinishReason当前响应的结束原因可以用于了解响应的完成状态Usage当前响应的使用情况可以用于统计和分析模型的使用情况ContinuationToken当Agent执行非常耗时的任务如复杂的推理或生成超长文档时如果启用了AllowBackgroundResponsesChatResponse可能会先返回。此时ContinuationToken不为空。你需要利用这个令牌传进行轮询直到任务真正完成并拿到最终结果RawRepresentation当前响应的原始表示可以用于存储一些与响应相关的原始数据如模型返回的原始响应等AdditionalProperties一个字典用于存储一些额外的属性信息可以在Agent的运行过程中使用这些属性来进行一些自定义的逻辑处理ChatResponse是开发者直接打交道最多的类。它不仅告诉你AI说了什么Messages/Text还告诉你怎么继续聊 (ConversationId)任务完没完 (ContinuationToken)花了多少钱 (Usage)模型跑得怎么样 (FinishReason)。1.5.1 ChatFinishReason和ChatRole一样表示响应的结束原因的ChatFinishReason也是一个具有更丰富功能的结构体而不是一个简单的枚举类型。它预定义了Stop、Length、ToolCalls和ContentFilter四个结束原因并且允许用户自定义结束原因。public readonly struct ChatFinishReason(string value) : IEquatableChatFinishReason { public string Value {get;} ; public static ChatFinishReason Stop { get; } new ChatFinishReason(stop); public static ChatFinishReason Length { get; } new ChatFinishReason(length); public static ChatFinishReason ToolCalls { get; } new ChatFinishReason(tool_calls); public static ChatFinishReason ContentFilter { get; } new ChatFinishReason(content_filter); }1.5.2 UsageDetailsUsage属性返回的UsageDetails相当于一本账单详细记录了模型调用的各种花费。我们知道Token就是AI领域的货币所以这本账单使用Token数量来记账它记录了模型调用过程中的各种Token数量包括输入输出的Token数量、是否命中缓存、推理过程中使用的Token数量等。这些信息对于我们了解模型的使用情况、优化提示词和控制成本都非常有帮助。比如如果我们发现输入Token数量过多可能需要优化提示词来减少不必要的上下文如果输出Token数量过多可能需要调整模型的生成策略来控制输出的长度如果命中缓存的Token数量较多说明模型在重复使用之前的结果可能需要调整提示词来引导模型生成更多新的内容如果推理过程中使用的Token数量较多可能需要优化模型的推理过程来减少不必要的计算。总之UsageDetails提供了一个全面的视角来审视模型的使用情况帮助我们更好地管理和优化AI的使用。public class UsageDetails { public long? InputTokenCount { get; set; } public long? OutputTokenCount { get; set; } public long? TotalTokenCount { get; set; } public long? CachedInputTokenCount { get; set; } public long? ReasoningTokenCount { get; set; } public AdditionalPropertiesDictionarylong? AdditionalCounts { get; set; } public void Add(UsageDetails usage) }1.6. AgentResponseUpdate针对流式调用的每次迭代响应的内容都会封装成一个ChatResponseUpdate对象ChatResponse的ToChatResponseUpdates属性返回这些对象的数组。ChatResponseUpdate与ChatResponse的属性基本相同唯一的区别是它多了一个IsFinal属性来表示当前响应是否是最终的响应。当IsFinal为true时意味着当前响应是整个Agent执行过程中的最后一个响应此时ChatResponseUpdate中的内容就是最终的结果当IsFinal为false时意味着当前响应只是整个Agent执行过程中的一个中间更新此时ChatResponseUpdate中的内容可能只是部分结果或者一些临时的状态信息。public class ChatResponseUpdate { public string? AuthorName{ get; set; } public ChatRole? Role { get; set; } public string Text{ get;} public IListAIContent Contents{ get; set; } public object? RawRepresentation { get; set; } public string? ResponseId { get; set; } public string? MessageId { get; set; } public string? ConversationId { get; set; } public DateTimeOffset? CreatedAt { get; set; } public ChatFinishReason? FinishReason { get; set; } public string? ModelId { get; set; } public ResponseContinuationToken? ContinuationToken{ get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } }很多LLM都支持结构化输出。我们在调用LLM的通过提供描述输出的JSON SchemaLLM会按照这个JSON Schema来生成符合要求的JSON格式的输出。为了让开发者更方便地处理这种结构化的输出MAF提供了ChatResponseT这个泛型类它继承自ChatResponse并添加了一个Result属性来表示解析后的结果对象。public class ChatResponseT : ChatResponse { public T Result {get;} public ChatResponse(ChatResponse response, JsonSerializerOptions serializerOptions); public bool TryGetResult([NotNullWhen(true)] out T? result); }2. AIAgentAIAgent是MAF中所有Agent的基类定义了所有Agent共享的基本属性成员和操作方式。它提供了Agent运行的核心方法如RunAsync和RunStreamingAsync等以及与AgentSession相关的方法如CreateSessionAsync、SerializeSessionAsync和DeserializeSessionAsync等。通过继承AIAgent我们可以创建不同类型的Agent来满足不同的需求。2.1. 属性成员AIAgent是MAF中所有Agent的基类定义了所有Agent共享的基本属性成员和操作方式。public abstract class AIAgent { public string Id { get; } protected virtual string? IdCore null; public virtual string? Name { get; } public virtual string? Description { get; } public static AgentRunContext? CurrentRunContext { get; } }属性成员说明如下IdAgent的唯一标识符通常由IdCore属性生成IdCore生成Id的核心字符串通常由子类重写来提供NameAgent的名称通常由子类重写来提供DescriptionAgent的描述信息通常由子类重写来提供CurrentRunContext当前正在执行的Agent的运行上下文信息可以通过它来获取当前Agent的输入、输出、工具调用等信息2.2. AgentSession的创建和序列化AIAgent定义了三个与AgentSession相关的方法分别是CreateSessionAsync、SerializeSessionAsync和DeserializeSessionAsync方法。分别实现了针对AgentSession的创建、序列化和反序列化功能。它们分别调用了对应的CreateSessionCoreAsync、SerializeSessionCoreAsync和DeserializeSessionCoreAsync方法来实现具体的功能这些方法是抽象方法需要在子类中进行实现。public abstract class AIAgent { public ValueTaskAgentSession CreateSessionAsync(CancellationToken cancellationToken default); public ValueTaskJsonElement SerializeSessionAsync( AgentSession session, JsonSerializerOptions? jsonSerializerOptions null, CancellationToken cancellationToken default); public ValueTaskAgentSession DeserializeSessionAsync( JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions null, CancellationToken cancellationToken default); protected abstract ValueTaskAgentSession CreateSessionCoreAsync(CancellationToken cancellationToken default); protected abstract ValueTaskJsonElement SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions null, CancellationToken cancellationToken default); protected abstract ValueTaskAgentSession DeserializeSessionCoreAsync( JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions null, CancellationToken cancellationToken default); }2.3. 阻塞式调用针对AIAgent的调用分两种一种是阻塞式调用另一种是异步流式调用。阻塞式调用通过如下这些重载的RunCoreAsync方法来完成返回的AgentResponse的作为响应内容。我们可以在调用它们时指定绑定的AgentSession将当前调用纳入一个会话中。还可以指定AgentRunOptions来设置一些运行时的参数如是否启用流式输出、是否启用工具调用等如果没有指定将会采用默认的运行选项。四个重载体现了不同的输入形式分别是不指定输入以及将字符串、ChatMessage和ChatMessage的集合。这四个重载的RunAsync方法最终都会调用同一个RunCoreAsync方法来完成具体的执行逻辑而这个RunCoreAsync方法是一个抽象方法需要在子类中进行实现。public abstract class AIAgent { public TaskAgentResponse RunAsync( AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default) RunAsync([], session, options, cancellationToken); public TaskAgentResponse RunAsync( string message, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default) RunAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken); public TaskAgentResponse RunAsync( ChatMessage message, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default) RunAsync([message], session, options, cancellationToken); public TaskAgentResponse RunAsync( IEnumerableChatMessage messages, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default) { CurrentRunContext new(this, session, messages as IReadOnlyCollectionChatMessage ?? messages.ToList(), options); return this.RunCoreAsync(messages, session, options, cancellationToken); } protected abstract TaskAgentResponse RunCoreAsync( IEnumerableChatMessage messages, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default); }2.4 流式响应AIAgent提供的RunStreamingAsync方法来进行流式调用。与阻塞式调用不同流式调用会在执行过程中不断地产生结果而不是等到整个执行完全结束后才返回结果。RunStreamingAsync方法返回一个IAsyncEnumerableAgentResponseUpdate对象AgentResponseUpdate代表了Agent在执行过程中的一个更新可以是一个新的响应、一个工具调用的结果或者一个状态更新等。与RunAsync方法类似RunStreamingAsync方法也提供了四个重载分别是不指定输入以及将字符串、ChatMessage和ChatMessage的集合。这些重载的RunStreamingAsync方法最终都会调用同一个RunCoreStreamingAsync方法来完成具体的执行逻辑而这个RunCoreStreamingAsync方法是一个抽象方法需要在子类中进行实现。public abstract class AIAgent { public IAsyncEnumerableAgentResponseUpdate RunStreamingAsync( AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default) ; public IAsyncEnumerableAgentResponseUpdate RunStreamingAsync( string message, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default); public IAsyncEnumerableAgentResponseUpdate RunStreamingAsync( ChatMessage message, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default); public async IAsyncEnumerableAgentResponseUpdate RunStreamingAsync( IEnumerableChatMessage messages, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default); protected abstract IAsyncEnumerableAgentResponseUpdate RunCoreStreamingAsync( IEnumerableChatMessage messages, AgentSession? session null, AgentRunOptions? options null, CancellationToken cancellationToken default); }