Skip to content

In production ​

Every run records OpenTelemetry spans and metrics with no Typeshift setting to turn on: you only add the Typeshift source and meter to your OpenTelemetry setup. Your app can publish with Native AOT once the types its tools take and return have JSON metadata.

Observability ​

Typeshift traces and measures every run with System.Diagnostics, following OpenTelemetry's semantic conventions for generative AI. Each run, model call and tool call shows up as a span.

Listening ​

Listen to the Typeshift source and meter:

cs
services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource("Typeshift"))
    .WithMetrics(metrics => metrics.AddMeter("Typeshift"));

snippet source | anchor

Spans ​

SpanFor
invoke_agent {agent}A run, with the agent, the requested model, the conversation and the token usage.
chat {model}Each model call, including the calls that write a compaction summary.
execute_tool {tool}Each tool call.

The invoke_agent span carries these tags:

TagHolds
gen_ai.operation.nameinvoke_agent
gen_ai.agent.name, gen_ai.agent.descriptionThe agent's name and description
gen_ai.request.modelThe model the run asks for
gen_ai.conversation.idThe conversation's id, when the run has one
gen_ai.usage.input_tokens, gen_ai.usage.output_tokensThe run's token usage
error.typeThe exception's type, when the run fails; the span's status is then an error

A failed save does not fail the run

When the store fails to save an entry, the run goes on. Typeshift logs the failure as an error and records the exception on the run's span, so watch your logs or spans for it.

Metrics ​

The metrics are Microsoft.Extensions.AI's gen_ai.client.operation.duration and gen_ai.client.token.usage, tagged with the model.

Message content ​

Spans leave out messages and tool arguments unless the environment variable OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT is true:

bash
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true

Captured content is your customers' data

With the variable set, your traces hold every run's messages and every tool call's arguments, including what your customers wrote. Turn it on only where your tracing backend may keep that data.

Native AOT ​

The generator writes the tool bindings and the settings serialization, so generated code never uses reflection over your types. What you add is JSON metadata for the types your tools take and return, and the analyzer reports each type that lacks it at compile time.

The project ​

To publish your app with Native AOT, set PublishAot in its project. Keep GenerateDocumentationFile on, because tools take their descriptions from XML docs (TSH011):

csproj
<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net10.0</TargetFramework>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

snippet source | anchor

Tool parameter and result types ​

Under Native AOT, the types your tools take and return need source-generated metadata: a JsonSerializerContext in the toolset's assembly that lists them. Outside Native AOT, serialization falls back to reflection, so an app that does not publish with Native AOT needs no context. Set IsAotCompatible in a toolset library that such apps use, so the analyzer checks it.

cs
[JsonSerializable(typeof(IReadOnlyList<Company>))]
[JsonSerializable(typeof(CompanyCount))]
internal partial class AppJsonContext : JsonSerializerContext;

snippet source | anchor

You don't register the context yourself: AddTypeshift() does. Each assembly's generated module adds that assembly's contexts, internal ones included, so a toolset library ships its own contexts and your app registers nothing for them.

Strings, char, bool, short, int, long and their unsigned forms, float, double, decimal, DateTime, DateTimeOffset, TimeSpan, IEnumerable<string> and the JSON element, document and node types need no context. In a project that publishes with Native AOT or sets IsAotCompatible, the analyzer reports TSH027 for every other tool type that no context lists.

Everything else ​

The generated Settings and overrides records serialize member by member, conversations go through Microsoft.Extensions.AI's own contracts. None of these needs a context from you. RunAsync<T> does need one: list T in a context too. In the sample, AppJsonContext above lists CompanyCount because the app calls RunAsync<CompanyCount>.

Released under the MIT License.