Docs
Asset Interceptor

Asset Interceptor

How successful tool outputs are automatically persisted to the assets table.

The Problem It Solves

Without the Asset Interceptor, every tool that produces a persistent output (an image URL, a formatted post, a legal analysis) would need to write its own database insertion code. That scatters persistence logic across 14+ tools and makes it easy to miss.

Instead: persistence happens in one placesaveAgentResponse() in lib/ai/utils.ts — called once in every agent's onFinish. Tools never touch the database.

How It Works

When an agent finishes, saveAgentResponse() orchestrates these steps in order:

  1. normalizeToolResults(messages) — extracts every tool-call + matching tool-result pair from the response
  2. buildMessageParts(messages) — converts SDK messages to the UI parts shape (handles tool-approval-request parts)
  3. Insert the assistant message into the messages table
  4. extractAndSaveAssets(toolResults, context) — iterates results, looks each toolName up in ASSET_REGISTRY, calls the builder, bulk-inserts to assets
  5. resolveApprovalPendingParts() — for any tool that was previously approval-gated, updates the DB message from approval-requestedoutput-available and saves its asset

ASSET_REGISTRY

The registry is a map of tool names to asset builder functions in lib/ai/utils.ts:

const ASSET_REGISTRY: Partial<
  Record<string, (result: NormalizedToolResult) => AssetRow | null>
> = {
  format_post: ({ input, output }) => ({
    type: "social_post",
    title: String(input.topic ?? "").slice(0, 100),
    content: output.value.content + "\n\n" + output.value.hashtags.join(" "),
    metadata: { platform: input.platform },
  }),
  generate_image: ({ input, output }) => ({
    type: "image",
    title: "Generated image",
    content: String(output.value.imageUrl),
    metadata: { prompt: input.visualPrompt },
  }),
  // ...
};

Tool-to-asset-type mapping:

ToolAsset Type
format_postsocial_post
generate_outlinepost_outline
generate_imageimage
format_reelreel_script
format_scriptscript
generate_thumbnail_conceptthumbnail
extract_receipt_datareceipt
analyze_legal_documentlegal_analysis

Registering a New Tool

To have a new tool's output saved as an asset, add one entry to ASSET_REGISTRY in lib/ai/utils.ts:

my_new_tool: ({ input, output }) => {
  const value = (output as any)?.value ?? output;
  if (!value?.result) return null; // return null to skip saving
  return {
    type: "my_asset_type",
    title: String(input.topic ?? "Output").slice(0, 100),
    content: String(value.result),
    metadata: { someField: input.someField },
  };
},

The AssetRow type:

type AssetRow = {
  type: string;      // asset type identifier
  title: string;     // display title, max 100 chars
  content: string;   // always a string: URL for images, Markdown for everything else
  metadata: Record<string, unknown>; // arbitrary JSON metadata
};

The Approval Path

When a tool has needsApproval: true (e.g., format_post), the flow is slightly different:

  1. First call: the agent pauses, the message is saved with state: "approval-requested"
  2. User approves → the tool runs → another onFinish fires
  3. resolveApprovalPendingParts() finds the saved message, updates state to "output-available", and returns the enriched tool results
  4. extractAndSaveAssets() is called a second time with these enriched results to save the asset

This ensures the asset is always saved, even for approval-gated tools.