agent-artifacts is a small TypeScript library for storing and resolving structured outputs created by AI agents.
It is useful when an agent creates something larger or more durable than a chat token stream: a SQL-backed report, a CSV import, a chart spec, a generated document, a search result set, a media job, or any other object you want to reference later without pasting the whole payload into the model transcript.
The package is UI-agnostic. The core API is server-side and has no React dependency. React bindings are available from agent-artifacts/react for apps that want to render artifact tags inside chat messages.
Agent tools often return too much data.
If a tool returns 5,000 rows, a model does not need all 5,000 rows in context. The application usually needs a reusable handle:
- Store a small recipe, such as
{ sql: "select ..." },{ s3Key: "..." }, or{ reportId: "..." }. - Return a short description to the model.
- Persist a lightweight artifact ref next to the message.
- Resolve the artifact later, with pagination or fresh application dependencies.
agent-artifacts gives you that lifecycle without owning your database, HTTP routes, agent SDK, or UI components.
npm install agent-artifacts zodReact users also need React:
npm install reactAn artifact has two representations:
Stored recipe
Small data that can be persisted safely in your app:
{ "region": "north-america", "minOrderValue": 50000 }Resolved data
The real output, produced on demand:
{
columns: ["customer", "revenue"],
rows: [{ customer: "Mercury Bank", revenue: 211300 }],
totalRows: 42,
offset: 0,
limit: 25
}The model sees a short description. Your app keeps the artifact id.
import {
AgentArtifacts,
createMemoryStore,
defineArtifact,
} from "agent-artifacts";
import { z } from "zod";
const RevenueReport = defineArtifact("revenue-report", {
schema: z.object({
region: z.string(),
minOrderValue: z.number().default(0),
}),
describe: ({ region }) => `Revenue report for ${region}`,
paginated: true,
resolve: async ({ region, minOrderValue }, { offset, limit, db }) => {
return db.queryRevenuePage({ region, minOrderValue, offset, limit });
},
});
const artifacts = new AgentArtifacts(createMemoryStore(), [RevenueReport]);
const turn = artifacts.turn("thread_123");
// Inside an agent tool execute() function:
const { describe, ref } = await turn.create(RevenueReport, {
region: "north-america",
minOrderValue: 50000,
});
// Return this to the model:
return describe;
// Persist these with your assistant message:
await saveMessage({
content: turn.content.toString(),
artifactRefs: turn.refs,
});
// Later, in an API route, worker, or follow-up agent step:
const page = await artifacts.getPage(ref.id, 0, 25, { db });agent-artifacts does not wrap an agent runtime. Use it inside the tool system your agent SDK already provides.
Runnable examples live in examples/:
examples/openai-agents.tsuses@openai/agents.examples/vercel-ai-sdk.tsuses Vercel AI SDK (ai+@ai-sdk/openai).examples/claude-agent-sdk.tsuses Anthropic's Claude Agent SDK with an in-process MCP tool.examples/react-nextjs/renders stored artifact tags withagent-artifacts/react.
Run them from this repository:
bun run build
cd examples
bun install
OPENAI_API_KEY=... bun run openai
OPENAI_API_KEY=... bun run vercel
ANTHROPIC_API_KEY=... bun run claudeReact bindings are optional and imported separately:
import { ContentRenderer, useArtifactPage } from "agent-artifacts/react";
function RevenueTable({ id }) {
const { data, loading } = useArtifactPage(id, {
offset: 0,
limit: 25,
fetch: (artifactId, offset, limit) =>
fetch(`/api/artifacts/${artifactId}/page?offset=${offset}&limit=${limit}`).then((r) =>
r.json(),
),
});
if (loading || !data) return <p>Loading...</p>;
return (
<table>
<tbody>
{data.rows.map((row) => (
<tr key={String(row.id)}>
<td>{String(row.customer)}</td>
<td>{String(row.revenue)}</td>
</tr>
))}
</tbody>
</table>
);
}
<ContentRenderer
content={message.content}
artifacts={refsById}
components={{ "revenue-report": RevenueTable }}
/>;The renderer only dispatches artifact refs to your components. Your components own fetching, caching, loading states, and visual design.
Defines an artifact kind.
Paginated artifacts return ArtifactPage from resolve():
const Dataset = defineArtifact("dataset", {
schema,
describe: (recipe) => "...",
paginated: true,
resolve: async (recipe, ctx) => page,
});Data artifacts return a complete value from resolve():
const Chart = defineArtifact("chart", {
schema,
describe: (recipe) => "...",
paginated: false,
resolve: async (recipe, ctx) => chartSpec,
});Creates the runtime registry.
Methods:
turn(groupId)creates anArtifactTurnfor one agent run.getPage(id, offset, limit, ctx?)resolves a paginated artifact.getData(id, ctx?)resolves a non-paginated artifact.get(id)returns the storedArtifactRecordwithout resolving it.list(groupId)returns lightweight refs for a thread/session/group.delete(id)deletes a stored artifact.
Validates data, stores the recipe, appends an artifact tag to turn.content, pushes a ref into turn.refs, and returns:
{
describe: string;
ref: ArtifactRef;
}Use describe as the tool result returned to the model. Persist turn.content.toString() and turn.refs with your assistant message.
Options:
describeoverrides the model-facing description.snapshotstores a page-zeroArtifactPagefor fast first render.metastores lightweight UI or routing metadata on the ref.
Builds and parses stored message content.
const content = new ContentBuilder()
.text("Here is the report:")
.artifact("artifact_123")
.toString();
const blocks = ContentBuilder.parse(content);
const ids = ContentBuilder.artifactIds(content);Serialized content uses XML-like tags:
Here is the report:
<artifact id="artifact_123"/>This format is intentionally simple: it can be stored as normal message text and parsed by servers, workers, agents, or UIs.
type ArtifactRef = {
id: string;
kind: string;
meta: Record<string, unknown> | null;
};
type ArtifactRecord<TData = unknown> = {
id: string;
kind: string;
data: TData;
meta: Record<string, unknown> | null;
snapshot: ArtifactPage | null;
groupId: string | null;
createdAt: number;
};
type ArtifactPage = {
columns: string[];
rows: Record<string, unknown>[];
totalRows: number;
offset: number;
limit: number;
};
type ResolveContext = {
offset: number;
limit: number;
[key: string]: unknown;
};
type FetchPageFn = (
id: string,
offset: number,
limit: number,
) => Promise<ArtifactPage>;Storage adapter:
interface ArtifactStore {
create(input: CreateArtifactInput): Promise<ArtifactRef>;
get(id: string): Promise<ArtifactRecord | null>;
list(groupId: string): Promise<ArtifactRef[]>;
delete(id: string): Promise<void>;
}createMemoryStore() is included for tests, demos, and single-process prototypes. Production apps should provide a durable ArtifactStore.
Use artifacts for outputs that are:
- too large to put in model context,
- useful after the current turn,
- better resolved with application credentials or database connections,
- paginated, cached, audited, or rendered by specialized components.
Do not use artifacts for ordinary short text. Return short text directly to the model and reserve artifacts for durable structured outputs.
Keep artifact recipes small. Store identifiers, query parameters, file keys, or compact specs. Resolve large data from the source system when needed.
bun install
bun test
bun run typecheck
bun run buildThe build uses Bun for JavaScript output and tsc for declaration files.