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
ToolResultand are best for small state reads or narrow actions - artifact providers return metadata plus an
IArtifactPayloadthat the SDK streams to the host - artifact requests use the
Readpolicy, 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 id | Policy | Purpose |
|---|---|---|
artifacts.query | Read | Discover registered providers and their currently available artifact definitions. |
artifacts.request | Read | Ask 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:
| Member | Meaning |
|---|---|
Descriptor | Stable 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.
| Artifact | Useful contents | Why use an artifact |
|---|---|---|
network-request.json | Method, 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.json | Camera 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.json | Player 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.json | Current 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.json | Navigation stack, modal stack, selected tab, route parameters, pending deep link, last failed navigation. | Makes a replayable UI-flow checkpoint. |
sync-state.zip | Queue 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:
| Factory | Use 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
- A paired host calls
artifacts.query, optionally filtering byproviderId,category,kind, ortag. - The SDK calls each matching provider’s
QueryAsync(...)and returns provider metadata plus artifact definitions. - The host calls
artifacts.requestwithproviderId,artifactId, optionaldownloadId, optionalchunkBytes, and provider-specific arguments. - The provider creates an
ArtifactResult. - The SDK queues a WebSocket binary transfer and returns artifact metadata, transfer id, delivery mode, wire protocol, chunk size, and queued status.
- 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
| API | Purpose |
|---|---|
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.