Skip to content

ethndotsh/agent-artifacts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agent-artifacts

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.

Why This Exists

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:

  1. Store a small recipe, such as { sql: "select ..." }, { s3Key: "..." }, or { reportId: "..." }.
  2. Return a short description to the model.
  3. Persist a lightweight artifact ref next to the message.
  4. 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.

Install

npm install agent-artifacts zod

React users also need React:

npm install react

The Model

An 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.

Quick Start

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 });

Using It With Agent SDKs

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.ts uses @openai/agents.
  • examples/vercel-ai-sdk.ts uses Vercel AI SDK (ai + @ai-sdk/openai).
  • examples/claude-agent-sdk.ts uses Anthropic's Claude Agent SDK with an in-process MCP tool.
  • examples/react-nextjs/ renders stored artifact tags with agent-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 claude

React Rendering

React 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.

API Reference

defineArtifact(kind, options)

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,
});

new AgentArtifacts(store, definitions)

Creates the runtime registry.

Methods:

  • turn(groupId) creates an ArtifactTurn for one agent run.
  • getPage(id, offset, limit, ctx?) resolves a paginated artifact.
  • getData(id, ctx?) resolves a non-paginated artifact.
  • get(id) returns the stored ArtifactRecord without resolving it.
  • list(groupId) returns lightweight refs for a thread/session/group.
  • delete(id) deletes a stored artifact.

turn.create(definition, data, options?)

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:

  • describe overrides the model-facing description.
  • snapshot stores a page-zero ArtifactPage for fast first render.
  • meta stores lightweight UI or routing metadata on the ref.

ContentBuilder

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 Reference

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.

Recommended Usage

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.

Development

bun install
bun test
bun run typecheck
bun run build

The build uses Bun for JavaScript output and tsc for declaration files.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors