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 place — saveAgentResponse() 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:
normalizeToolResults(messages)— extracts everytool-call+ matchingtool-resultpair from the responsebuildMessageParts(messages)— converts SDK messages to the UI parts shape (handlestool-approval-requestparts)- Insert the assistant message into the
messagestable extractAndSaveAssets(toolResults, context)— iterates results, looks eachtoolNameup inASSET_REGISTRY, calls the builder, bulk-inserts toassetsresolveApprovalPendingParts()— for any tool that was previously approval-gated, updates the DB message fromapproval-requested→output-availableand 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:
| Tool | Asset Type |
|---|---|
format_post | social_post |
generate_outline | post_outline |
generate_image | image |
format_reel | reel_script |
format_script | script |
generate_thumbnail_concept | thumbnail |
extract_receipt_data | receipt |
analyze_legal_document | legal_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:
- First call: the agent pauses, the message is saved with
state: "approval-requested" - User approves → the tool runs → another
onFinishfires resolveApprovalPendingParts()finds the saved message, updatesstateto"output-available", and returns the enriched tool resultsextractAndSaveAssets()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.