WolverineFx.AI ships with <IsAotCompatible>false</IsAotCompatible>. This is the follow-up to make it trim- and AOT-clean.
Why it isn't today
A callout names its response type as a string on the message (LlmCallout.ResponseType) rather than as a type parameter — see decision 7 on #4227 for why. LlmCalloutExecutor then does two reflective things with the Type it resolves from that string, neither of which the trimmer can follow from any static call site:
AIJsonUtilities.CreateJsonSchema(responseType, ...) — walks the response type's properties to build the schema handed to the model as ChatResponseFormat.ForJsonSchema.
JsonSerializer.Deserialize(text, responseType, options) — turns the model's answer back into the response type.
LlmCalloutExecutor carries [RequiresUnreferencedCode] / [RequiresDynamicCode] rather than a suppression, deliberately: a suppression here would claim a guarantee the design cannot make. Under a trimmed publish the response type's properties can be trimmed away, and the schema and the deserialization both silently degrade rather than failing loudly.
Note that this is not the same problem the non-generic decision solved. That one was about resolving a name to a Type at all on a cold start, and it is fixed. This one is about what the trimmer can prove about that type once resolved, and it is unchanged by the message's shape — a generic LlmCallout<T> would need exactly the same work here.
What it would take
- Source-generated JSON schemas. Discover the response types an application actually asks for and emit their schemas at compile time, so
CreateJsonSchema never runs at all in an AOT app. The natural trigger is the Ask<TResponse>(...) call sites, which are statically visible in the common handler and HTTP-endpoint cases; a [LlmResponse]-style attribute would be the escape hatch for the cases a call-site scan cannot see (a callout constructed inside a projection's RaiseSideEffects, say).
- A generated
JsonSerializerContext over the same set, fed to LlmCalloutOptions.JsonSerializerOptions so the deserialization is source-generated too.
- A runtime lookup keyed by the same identifier the message already carries, so the executor consults the generated table first and falls back to reflection only on the non-AOT path.
- Flip
IsAotCompatible to true, drop the [Requires*] annotations, and cover it in the AOT publishing guide.
Worth doing alongside, or after, whatever shape the tier 2 agents work (#4226) needs — tool schemas have the same generation problem, and it would be a shame to build the generator twice.
WolverineFx.AIships with<IsAotCompatible>false</IsAotCompatible>. This is the follow-up to make it trim- and AOT-clean.Why it isn't today
A callout names its response type as a string on the message (
LlmCallout.ResponseType) rather than as a type parameter — see decision 7 on #4227 for why.LlmCalloutExecutorthen does two reflective things with theTypeit resolves from that string, neither of which the trimmer can follow from any static call site:AIJsonUtilities.CreateJsonSchema(responseType, ...)— walks the response type's properties to build the schema handed to the model asChatResponseFormat.ForJsonSchema.JsonSerializer.Deserialize(text, responseType, options)— turns the model's answer back into the response type.LlmCalloutExecutorcarries[RequiresUnreferencedCode]/[RequiresDynamicCode]rather than a suppression, deliberately: a suppression here would claim a guarantee the design cannot make. Under a trimmed publish the response type's properties can be trimmed away, and the schema and the deserialization both silently degrade rather than failing loudly.Note that this is not the same problem the non-generic decision solved. That one was about resolving a name to a
Typeat all on a cold start, and it is fixed. This one is about what the trimmer can prove about that type once resolved, and it is unchanged by the message's shape — a genericLlmCallout<T>would need exactly the same work here.What it would take
CreateJsonSchemanever runs at all in an AOT app. The natural trigger is theAsk<TResponse>(...)call sites, which are statically visible in the common handler and HTTP-endpoint cases; a[LlmResponse]-style attribute would be the escape hatch for the cases a call-site scan cannot see (a callout constructed inside a projection'sRaiseSideEffects, say).JsonSerializerContextover the same set, fed toLlmCalloutOptions.JsonSerializerOptionsso the deserialization is source-generated too.IsAotCompatibleto true, drop the[Requires*]annotations, and cover it in the AOT publishing guide.Worth doing alongside, or after, whatever shape the tier 2 agents work (#4226) needs — tool schemas have the same generation problem, and it would be a shame to build the generator twice.