AI Function Parameter Contracts
An [AIFunction] method is a compiled protocol between the model and the tool implementation. HPD's source generator emits the model-facing JSON Schema, a strict argument binder, the typed argument carrier, and the invocation adapter from the same C# signature.
Use this page when deciding how to represent tool parameters. For harness registration and construction, see Author A Tool Harness. For generation and Native AOT behavior, see Source Generation, AOT, And Trimming.
Contract Lifecycle
C# method signature
-> generated closed JSON Schema
-> model function call
-> raw or runtime-canonical JSON
-> generated strict binder
-> typed arguments
-> function bodyThe generated schema and binder are emitted together. A property advertised to the model is bound by the same generated contract; runtime-only parameters are injected separately.
Generated contracts are closed. Unknown properties, incorrect JSON kinds, alternate property casing, invalid enum names, and fields from the wrong union branch fail before the function body runs.
Scalar Parameters
Use ordinary scalar parameters directly:
public sealed class SearchTools
{
[AIFunction(Description = "Searches indexed documents.")]
public string Search(
[AIDescription("Text to search for.")] string query,
[AIDescription("Maximum number of results.")] int limit = 20,
[AIDescription("Whether matching is case-sensitive.")] bool caseSensitive = false)
{
// ...
}
}Required non-nullable parameters appear in the schema's required array. Parameters with C# defaults are optional and use the declared default when omitted.
Use nullable types when JSON null is meaningful:
public string FindOwner(string repository, string? team = null)Nullability and optionality are different:
string valuewithout a default is required and cannot benull.string? valuewithout a default is required but may benull.string? value = nullmay be omitted or supplied asnull.
Enums
Enums produce exact string values:
public enum SearchMode
{
Files,
Content,
Count
}
[AIFunction]
public string Search(string query, SearchMode mode = SearchMode.Content)The model must use the generated name exactly:
{
"query": "TODO",
"mode": "Content"
}"content", numeric enum values, and undeclared aliases are rejected. Choose stable enum member names because they are part of the model-facing protocol.
Nested Objects And Records
Use a public record or class when several values form one concept:
using System.ComponentModel;
public sealed record DateRange(
[property: Description("Inclusive start date.")] DateOnly Start,
[property: Description("Inclusive end date.")] DateOnly End);
public sealed class ReportTools
{
[AIFunction]
public string CreateReport(string project, DateRange range)
{
// ...
}
}The model sees a nested object:
{
"project": "HPD Agent",
"range": {
"start": "2026-07-01",
"end": "2026-07-31"
}
}Prefer immutable positional records for model-facing value objects. They make required values and construction semantics explicit. Nested generated objects are also closed, so an unexpected property inside range is rejected.
Collections
Use supported arrays or collection interfaces when the binder has deterministic construction semantics:
public sealed record Replacement(string OldText, string NewText);
[AIFunction]
public string ReplaceMany(
string path,
IReadOnlyList<Replacement> replacements,
IReadOnlyList<string>? includeGlobs = null)The generated schema describes both the array and its item contract. Every nested item is validated before construction.
Use a dictionary for model-selected string keys:
[AIFunction]
public string RunJob(
string name,
IReadOnlyDictionary<string, string>? environment = null)This produces an object whose additional property values must be strings:
{
"name": "build",
"environment": {
"CONFIGURATION": "Release",
"CI": "true"
}
}Do not use a dictionary as a substitute for a known object shape. A record gives the model stronger guidance and lets HPD reject misspelled fields.
Closed Operation Unions
Use a discriminated union when one function performs several mutually exclusive operations. Do not model the operations as one flat list of loosely related optional parameters.
using System.Text.Json.Serialization;
[JsonPolymorphic(TypeDiscriminatorPropertyName = "action")]
[JsonDerivedType(typeof(StartJob), "start")]
[JsonDerivedType(typeof(ReadJob), "read")]
[JsonDerivedType(typeof(StopJob), "stop")]
public abstract record JobOperation;
public sealed record StartJob(
string Command,
IReadOnlyDictionary<string, string>? Environment = null)
: JobOperation;
public sealed record ReadJob(string HandleId, int TailLines = 200)
: JobOperation;
public sealed record StopJob(string HandleId)
: JobOperation;
public sealed class JobTools
{
[AIFunction]
public Task<string> ManageJob(
JobOperation request,
CancellationToken cancellationToken = default)
=> request switch
{
StartJob start => StartAsync(start, cancellationToken),
ReadJob read => ReadAsync(read, cancellationToken),
StopJob stop => StopAsync(stop, cancellationToken),
_ => throw new ArgumentOutOfRangeException(nameof(request))
};
}The generated schema contains one closed branch per discriminator:
{
"request": {
"action": "read",
"handleId": "job_123",
"tailLines": 100
}
}The discriminator is ordinal and case-sensitive. A read request containing command, or a start request containing handleId, is rejected before invocation.
Use a union when:
- each operation has different required fields;
- fields are valid only for one operation;
- one flat signature would contain many optional parameters;
- permission or policy analysis depends on the selected operation.
Runtime-Only Parameters
HPD supplies runtime parameters. They are excluded from the model schema and generated argument carrier:
[AIFunction]
public async Task<string> Inspect(
string path,
FunctionExecutionContext context,
CancellationToken cancellationToken = default)
{
// The model supplies path. HPD supplies context and cancellationToken.
}Supported runtime parameters include:
FunctionExecutionContextCancellationTokenAIFunctionArgumentsIServiceProvider
Use FunctionExecutionContext for call metadata, events, services, content storage, background work, and bidirectional requests. Unsupported runtime-like parameters produce generator diagnostic HPD020.
Framework Invocation Mode Is Separate
AgentInvocationModePolicy.ModelChoice adds a framework control named invocationMode. It decides whether HPD waits for the function result or registers runtime-owned background work.
It is not a normal method parameter, and it is not the same as a domain parameter such as a command's executionMode.
[AIFunction(InvocationModePolicy = AgentInvocationModePolicy.ModelChoice)]
public Task<string> BuildIndex(string path, CancellationToken cancellationToken)The model may call it with:
{
"path": "src",
"invocationMode": "background"
}HPD removes the framework control before generated argument binding.
Strict Binding Rules
Generated binding intentionally rejects ambiguous input:
- unknown properties;
- duplicate properties when original JSON remains available;
- alternate property casing;
- missing required values;
nullfor non-nullable values;- incorrect JSON kinds;
- numeric or incorrectly cased enum values;
- unknown, missing, blank, or duplicate union discriminators;
- properties belonging to another union branch;
- invalid nested collection items.
The validation result identifies the precise property path, such as request.command or replacements[2].newText.
Raw And Canonical JSON Ingress
Generated binders consume JSON rather than rebuilding a contract from arbitrary runtime objects.
When a provider integration exposes the original argument document, HPD preserves it. Microsoft.Extensions.AI.FunctionCallContent otherwise guarantees parsed arguments, so the agent runtime creates a canonical JSON document with rooted, source-generated serializer metadata before invoking the binder.
Direct programmatic invocation of a generated HPDAIFunction must attach JSON through HPD's argument APIs. This strict boundary prevents a reflection-based dictionary fallback from becoming the production contract.
Unsupported Or Discouraged Shapes
Avoid:
- open-ended
objectparameters; dynamic;- dictionaries used for known records;
- recursive model-facing object graphs;
- collection interfaces without deterministic construction semantics;
- types that require runtime reflection or unrooted serializer metadata;
- flat action methods with branch-specific optional fields;
- runtime services presented as model parameters.
If the generator cannot produce a deterministic schema and binder, change the public contract instead of adding a runtime compatibility path.
Design Checklist
Before publishing a function:
- Give every parameter and nested property one stable meaning.
- Use defaults only when omission has an unambiguous behavior.
- Use nullable types only when
nullis meaningful. - Prefer enums over free-form strings for closed choices.
- Prefer records over dictionaries for known objects.
- Prefer a discriminated union over branch-specific optional fields.
- Keep runtime services in sanctioned runtime parameters.
- Add descriptions where names alone do not guide the model.
- Inspect the generated schema in a contract test.
- Test rejection of unknown fields, incorrect enum casing, and wrong union branches.