Explain bugs with app state

Preserve internal app state alongside screenshots, logs, and telemetry so you can investigate what your .NET app knew when a bug happened.

Screenshots show the symptom; app state can help explain it. Artifacts let you attach snapshots of view models, navigation, sync queues, or other internal state to the same session evidence.

Your .NET app exposes these diagnostic exports through artifact providers. A paired host can discover and request them as named files or binary/text payloads.

Why Artifacts Matter

A view model’s loading flag, a map’s camera position, or a sync queue can explain behavior that pixels alone cannot. Capture a small, explicit snapshot and compare it with the surrounding session evidence. Your provider controls the exported fields and redaction, without requiring broad reflection access to live objects.

Start with one snapshot that answers a question about your app. The runtime state examples below cover request context, maps, 3D scenes, navigation, and support bundles.

Artifacts are different from custom tools:

  • custom tools return a JSON ToolResult and are best for small state reads or narrow actions
  • artifact providers return metadata plus an IArtifactPayload that the SDK streams to the host
  • artifact requests use the Read policy, but they can export sensitive app data

Register Providers

Artifact support lives in Ansight.Core under the Ansight.Artifacts namespace. In an all-in-one setup, register one or more providers from the setup callback:

using Ansight;
using Ansight.Artifacts;

var options = Options.CreateBuilder()
    .WithAnsightSdk(ansight =>
    {
#if MYAPP_ANSIGHT_ARTIFACTS
        ansight.AddArtifactProvider(new CurrentReportArtifactProvider(reports));
#endif
        ansight.WithReadOnlyToolAccess();
    })
    .Build();

Runtime.InitializeAndActivate(options);

Core-only apps can call the same provider APIs directly on Options.CreateBuilder():

using Ansight;
using Ansight.Artifacts;

var options = Options.CreateBuilder()
#if MYAPP_ANSIGHT_ARTIFACTS
    .AddArtifactProvider(new CurrentReportArtifactProvider(reports))
#endif
    .WithReadOnlyToolAccess()
    .Build();

Runtime.InitializeAndActivate(options);

When the builder has at least one artifact provider, the SDK automatically registers the core artifact tools:

Tool idPolicyPurpose
artifacts.queryReadDiscover registered providers and their currently available artifact definitions.
artifacts.requestReadAsk a provider to create an artifact snapshot and stream its payload to the host.

Because both tools use ToolPolicy.Read, WithReadOnlyToolAccess() is enough to allow discovery and requests. Each artifact definition also declares the policy appropriate to its content. Treat requestable artifacts as explicit exports: only register providers in local development builds, keep provider ids stable, and expose the narrowest payloads that support the workflow.

Provider Model

An artifact provider implements IArtifactProvider:

MemberMeaning
DescriptorStable provider metadata, including provider id, name, description, category, tags, and string metadata.
QueryAsync(...)Returns the artifact definitions currently available from that provider.
CreateAsync(...)Creates the requested artifact and returns metadata plus a payload source.

Artifact definitions describe what can be requested. Artifact results describe the concrete snapshot that was created for one request.

using Ansight.Artifacts;
using Ansight.Tools;

internal sealed class CurrentReportArtifactProvider(IReportService reports) : IArtifactProvider
{
    public ArtifactProviderDescriptor Descriptor { get; } = new(
        "app.reports",
        "Reports",
        "Exports app report artifacts.",
        "reports")
    {
        Tags = ["reports", "support"]
    };

    public Task<IReadOnlyList<ArtifactDefinition>> QueryAsync(
        ArtifactQueryContext context,
        CancellationToken cancellationToken = default)
    {
        IReadOnlyList<ArtifactDefinition> artifacts =
        [
            new ArtifactDefinition(
                "current-report",
                "Current Report",
                "Exports the current report as CSV.",
                "report",
                "reports",
                new ArtifactContentDescriptor(["text/csv"])
                {
                    DefaultMimeType = "text/csv",
                    SuggestedFileName = "current-report.csv",
                    SupportsText = true
                },
                ToolSchema.Object(),
                ToolPolicy.Critical)
            {
                Tags = ["csv", "current"]
            }
        ];

        return Task.FromResult(artifacts);
    }

    public async Task<ArtifactResult> CreateAsync(
        ArtifactRequest request,
        CancellationToken cancellationToken = default)
    {
        if (!string.Equals(request.ArtifactId, "current-report", StringComparison.Ordinal))
        {
            throw new InvalidOperationException($"Unknown artifact '{request.ArtifactId}'.");
        }

        var csv = await reports.ExportCurrentCsvAsync(cancellationToken);
        var payload = ArtifactPayload.FromText(csv);

        return new ArtifactResult(
            new ArtifactMetadata(
                request.ArtifactId,
                request.ProviderId,
                "Current Report",
                "report",
                "text/csv",
                "current-report.csv")
            {
                Description = "Current report exported from the running app.",
                SizeBytes = payload.SizeBytes,
                Tags = ["csv", "current"]
            },
            payload);
    }
}

CreateAsync(...) can use request.Arguments for provider-specific filters or export options. The SDK supplies request.Context.ToolRequestId, request.Context.SessionId, and request.Context.RequestedAtUtc so generated output can be correlated with the live tool call and session.

Runtime State Examples

Artifacts are useful when an agent needs a stable snapshot of live app memory rather than a one-line answer. The provider decides how to flatten private runtime state into a safe export shape.

ArtifactUseful contentsWhy use an artifact
network-request.jsonMethod, route, sanitized URL, status code, timing, retry count, cache policy, request/response body snippets, correlation ids, redacted headers.Preserves the exact request context behind a failed screen, without exposing a broad network inspector.
mapbox-state.jsonCamera center/zoom/bearing/pitch, viewport size, style URI, loaded source ids, visible layer ids, selected feature ids, annotation ids, tile/cache counters, last map error.Lets an agent compare what the map thinks is visible with screenshots, logs, and touch input.
player-state.jsonPlayer id, position/rotation/velocity, camera pose, active scene/level, loaded entities, selected object id, animation state, physics/debug flags.Captures 3D runtime state that is hard to infer from a screenshot.
view-model-state.jsonCurrent page model, selected item ids, validation errors, command busy flags, paging cursor, dirty fields.Gives a durable object-state snapshot without opening broad reflection access.
navigation-state.jsonNavigation stack, modal stack, selected tab, route parameters, pending deep link, last failed navigation.Makes a replayable UI-flow checkpoint.
sync-state.zipQueue database export, conflict summaries, retry metadata, endpoint config, recent sync logs.Packages related evidence that should stay together.

For example, one provider can expose multiple JSON runtime snapshots:

using System.IO;
using System.Text.Json;
using Ansight.Artifacts;
using Ansight.Tools;

internal sealed class RuntimeStateArtifactProvider(
    IMapDiagnostics map,
    IPlayerDiagnostics player) : IArtifactProvider
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true
    };

    public ArtifactProviderDescriptor Descriptor { get; } = new(
        "app.runtime-state",
        "Runtime State",
        "Exports selected runtime memory and object-state snapshots.",
        "runtime")
    {
        Tags = ["state", "memory", "diagnostics"]
    };

    public Task<IReadOnlyList<ArtifactDefinition>> QueryAsync(
        ArtifactQueryContext context,
        CancellationToken cancellationToken = default)
    {
        IReadOnlyList<ArtifactDefinition> artifacts =
        [
            CreateJsonDefinition(
                "mapbox-state",
                "Mapbox State",
                "Exports camera, style, source, layer, selection, and tile/cache state.",
                "map",
                "mapbox-state.json",
                ["mapbox", "map", "runtime"]),

            CreateJsonDefinition(
                "player-state",
                "3D Player State",
                "Exports player, camera, scene, animation, physics, and selected-entity state.",
                "scene",
                "player-state.json",
                ["3d", "player", "scene", "runtime"])
        ];

        return Task.FromResult(artifacts);
    }

    public Task<ArtifactResult> CreateAsync(
        ArtifactRequest request,
        CancellationToken cancellationToken = default)
    {
        var snapshot = request.ArtifactId switch
        {
            "mapbox-state" => map.CaptureState(),
            "player-state" => player.CaptureState(),
            _ => throw new InvalidOperationException($"Unknown artifact '{request.ArtifactId}'.")
        };

        var fileName = request.ArtifactId switch
        {
            "mapbox-state" => "mapbox-state.json",
            "player-state" => "player-state.json",
            _ => $"{request.ArtifactId}.json"
        };

        var json = JsonSerializer.Serialize(snapshot, JsonOptions);
        var payload = ArtifactPayload.FromText(json);

        return Task.FromResult(new ArtifactResult(
            new ArtifactMetadata(
                request.ArtifactId,
                request.ProviderId,
                Path.GetFileNameWithoutExtension(fileName),
                "runtime-state",
                "application/json",
                fileName)
            {
                Description = "Runtime state captured from the running app.",
                SizeBytes = payload.SizeBytes,
                Tags = ["runtime", "state", "json"]
            },
            payload));
    }

    private static ArtifactDefinition CreateJsonDefinition(
        string id,
        string name,
        string description,
        string category,
        string fileName,
        IReadOnlyList<string> tags) =>
        new(
            id,
            name,
            description,
            "runtime-state",
            category,
            new ArtifactContentDescriptor(["application/json"])
            {
                DefaultMimeType = "application/json",
                SuggestedFileName = fileName,
                SupportsText = true
            },
            ToolSchema.Object(),
            ToolPolicy.Critical)
        {
            Tags = tags
        };
}

The capture services should project runtime objects into DTOs instead of serializing framework objects directly. For a Mapbox-style artifact, include the data that explains the rendered map state: camera, style, layer/source ids, selections, annotations, and relevant counters. For a 3D/player artifact, include transforms, scene ids, selected entity/component summaries, animation names, and debug flags, but avoid full texture buffers, meshes, tokens, or user secrets.

Payload Sources

Use ArtifactPayload factory methods for common payload types:

FactoryUse when
ArtifactPayload.FromText(...)The provider can generate text in memory.
ArtifactPayload.FromBytes(...)The provider already has a byte array.
ArtifactPayload.FromStream(...)The provider can open a fresh readable stream on demand.
ArtifactPayload.FromFile(...)The artifact already exists as an app-local file that the SDK can read.

Each request must return metadata whose ProviderId and ArtifactId match the request. The metadata also needs a non-empty Name, Kind, MimeType, and FileName.

Discovery and Request Flow

  1. A paired host calls artifacts.query, optionally filtering by providerId, category, kind, or tag.
  2. The SDK calls each matching provider’s QueryAsync(...) and returns provider metadata plus artifact definitions.
  3. The host calls artifacts.request with providerId, artifactId, optional downloadId, optional chunkBytes, and provider-specific arguments.
  4. The provider creates an ArtifactResult.
  5. The SDK queues a WebSocket binary transfer and returns artifact metadata, transfer id, delivery mode, wire protocol, chunk size, and queued status.
  6. The resident host stores the received payload as a session artifact snapshot so it can be reviewed alongside logs, screenshots, visual trees, telemetry, touches, and annotations.

Artifact requests require an initialized runtime and an active pairing session. Without a live transfer channel, artifacts.request returns an artifact_transfer_unavailable failure.

Builder APIs

APIPurpose
WithArtifactProviders(IEnumerable<IArtifactProvider>)Replace all registered artifact providers.
AddArtifactProvider(IArtifactProvider)Add one provider.
AddArtifactProviders(IEnumerable<IArtifactProvider>)Add several providers.
ContainsArtifactProvider(string)Check whether a provider id is already registered.

Provider ids are case-insensitive and must be unique. Duplicate ids fail validation when the registry is created.

Choosing Artifacts

Use artifacts for outputs that should leave the app as files or durable session evidence:

  • export runtime memory snapshots such as Mapbox state, 3D player state, view-model state, navigation state, or in-memory store state
  • attach sanitized network request and response evidence to a failed flow
  • export the current report, trace, local cache summary, or sync diagnostics
  • package a group of related files into one archive
  • expose a domain-specific snapshot too large for a ToolResult
  • preserve a support bundle in the captured session timeline

Use Custom Tools instead when the operation returns small JSON, changes live state, or does not need host-side file materialization. Use ansight session artifacts <session-id> --json to inspect artifacts after the host has captured them into a session.