Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions .agents/skills/neon-ai-gateway/SKILL.md

Large diffs are not rendered by default.

619 changes: 619 additions & 0 deletions .agents/skills/neon-functions/SKILL.md

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions .agents/skills/neon-functions/references/ai-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# AI SDK agents on Neon Functions

A Neon Function is a long-lived Node.js 24 process, which makes it a natural host for a [Vercel AI SDK](https://ai-sdk.dev) agent: the handler keeps streaming for the life of the request (15-minute budget, see [Timeouts](../SKILL.md#timeouts-and-runtime-limits)), so multi-step tool loops and image/video generation don't get cut off the way they do on lambda-style serverless. Point the model at the **Neon AI Gateway** (see the `neon-ai-gateway` skill) and there are no extra provider keys to manage — one Neon credential reaches the whole catalog.

The AI SDK is the **recommended** way to build agents on Functions from TypeScript: one set of primitives (`streamText`, `generateText`, tool calling, structured output) over every catalog model. For a memory- and workflow-heavy agent with built-in tracing, use Mastra instead (see [references/mastra-studio.md](mastra-studio.md)); both point at the same gateway.

The pattern below is a complete agent: it streams chat and, when asked, generates an image, uploads it to Object Storage, and indexes it in Postgres.

## 1. Declare the gateway and the function

The agent needs the AI Gateway (and, for the image example, an Object Storage bucket). Declare both in `neon.ts` alongside the function — `neon deploy` provisions them and injects the credentials at runtime (see the `neon-ai-gateway` and `neon-object-storage` skills):

```typescript
// neon.ts
import { defineConfig } from "@neon/config/v1";

export default defineConfig({
preview: {
aiGateway: true,
buckets: { images: {} },
functions: {
agent: { name: "ai agent", source: "src/index.ts" },
},
},
});
```

## 2. The handler: stream a tool-calling agent

The function's default export is a web-standard `{ fetch }` handler. The `@neon/ai-sdk-provider` reads the injected gateway credentials automatically, so `neon("<model>")` is all the model config you need — it routes each model to the right dialect (Anthropic → Messages, OpenAI/Codex → Responses, everything else → MLflow). Return `result.toUIMessageStreamResponse()` so the AI SDK's `useChat` hooks can consume the stream:

```typescript
// src/index.ts
import { neon } from "@neon/ai-sdk-provider";
import { attachDatabasePool } from "@neon/functions";
import { streamText, tool, stepCountIs, type ModelMessage } from "ai";
import { z } from "zod";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { todos } from "./db/schema";

const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
attachDatabasePool(pool);
const db = drizzle(pool);

export default {
async fetch(request: Request) {
if (request.method !== "POST") {
return new Response("POST chat messages here", { status: 405 });
}
const { messages } = (await request.json()) as { messages: ModelMessage[] };

const result = streamText({
model: neon("claude-sonnet-4-6"), // swap to gpt-5-mini, gemini-3-flash, …
system: "You are a concise assistant with access to the user's todos.",
messages,
tools: {
countOpenTodos: tool({
description: "Count the user's open todos.",
inputSchema: z.object({}),
execute: async () => ({ open: await db.$count(todos) }),
}),
},
// Let the model call tools and then summarize, instead of stopping after
// the first tool call. The loop runs in-process — no host timeout.
stopWhen: stepCountIs(5),
onError({ error }) {
console.error("[streamText] error:", error);
},
});

return result.toUIMessageStreamResponse({
onError: (error) =>
error instanceof Error ? error.message : String(error),
});
},
};
```

`tool({ inputSchema, execute })` is the AI SDK v5+ shape (the parameter is `inputSchema`, not the old `parameters`). The tool's `execute` runs **inside the function**, right next to Postgres — no extra network hop.

## 3. Generate images and persist them

The gateway exposes the OpenAI Responses **`image_generation`** built-in tool (GPT-5 models only; the image comes back inline as base64). Persist generated assets to Object Storage and index them in Postgres so they branch together — the **recommended** storage client is the Files SDK `neon` adapter (see the `neon-object-storage` skill):

```typescript
import { neon } from "@neon/ai-sdk-provider";
import { streamText } from "ai";
import { Files } from "files-sdk";
import { neon as neonFiles } from "files-sdk/neon";
import { randomUUID } from "node:crypto";

const files = new Files({ adapter: neonFiles({ bucket: "images" }) });

const result = streamText({
model: neon("gpt-5-mini"),
system:
"Use image_generation when the user asks for a picture, then describe it.",
messages,
tools: {
image_generation: neon.tools.imageGeneration({
outputFormat: "jpeg",
quality: "low", // the gateway caps a response near 640 KB — keep images small
size: "1024x1024",
}),
},
async onStepFinish({ toolResults }) {
for (const tr of toolResults) {
if (tr.toolName !== "image_generation") continue;
const base64 = imageResultBase64(tr.output);
if (!base64) continue;
const key = `generated/${randomUUID()}.jpg`;
await files.upload(key, Buffer.from(base64, "base64"), {
contentType: "image/jpeg",
});
// …insert a row keyed by `key` into Postgres; serve later via files.url(key)
}
},
});
```

Keep generated images small: the gateway caps a single response near 640 KB and has an upstream timeout, so request a compressed JPEG rather than a full-size PNG.

## 4. Call it directly from the client (don't proxy the stream)

So the long stream isn't cut off by your web host's serverless limits, have the **browser call the function directly** and authenticate at the top of the handler — see [Functions as an agent backend](../SKILL.md#functions-as-an-agent-backend-nextjs-and-similar-frameworks) for the JWT-verify + CORS pattern and the AI SDK `DefaultChatTransport` wiring.

## 5. Run and deploy

```bash
neon dev # injects DATABASE_URL + the gateway/storage creds; hot reload
neon deploy # provisions the gateway + bucket and deploys the function
```

```bash
curl -N -X POST "$(neon functions get agent -o json | jq -r .invocation_url)" \
-H "content-type: application/json" \
-d '{"messages":[{"role":"user","content":"How many open todos do I have?"}]}'
```

## Further reading

- Neon AI Gateway dialects, models, and the `@neon/ai-sdk-provider`: the `neon-ai-gateway` skill
- Storing generated assets that branch with the database: the `neon-object-storage` skill
- AI SDK agents/tools: https://ai-sdk.dev/docs/foundations/agents
127 changes: 127 additions & 0 deletions .agents/skills/neon-functions/references/mastra-studio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Mastra agents with Mastra Studio observability

A Neon Function is a long-lived Node.js 24 process, which makes it a natural host for a [Mastra](https://mastra.ai) agent: the agent keeps running for the life of the request, and you point its model at the Neon AI Gateway so there are no extra provider keys. You can keep **running the agent on Neon Functions** while shipping its traces to a **Mastra Studio (Mastra Cloud) project** for observability — the agent runs on Neon, the traces are viewable in Mastra.

The shape mirrors any other Node integration (see [sentry.md](sentry.md)): instantiate at module load, gate on env vars so local dev and unconfigured branches stay a no-op, and pass secrets at deploy time via `neon.ts`. `@mastra/core` and `@mastra/observability` bundle cleanly through `neon deploy`'s esbuild with no extra config.

## 1. Define the agent against the Neon AI Gateway

With `@mastra/core` 1.47+, use a `neon/<model>` magic string — Mastra reads `NEON_AI_GATEWAY_BASE_URL` and `NEON_AI_GATEWAY_TOKEN` from the environment (injected by `neon deploy` / `neon env pull` when `preview.aiGateway` is enabled in `neon.ts`). No manual `url`/`apiKey` or MLflow dialect swap is needed; Mastra routes each model to the correct gateway endpoint.

```typescript
// src/mastra/agents/pricing.ts
import { Agent } from "@mastra/core/agent";

export const pricingAgent = new Agent({
id: "pricing-analyst",
name: "pricing-analyst",
instructions: "You are a meticulous pricing analyst. …",
model: "neon/gpt-5-mini",
});
```

## 2. Wire observability to Mastra Studio

The `MastraPlatformExporter` (from `@mastra/observability`) sends traces to a Mastra Studio project. It reads `MASTRA_PLATFORM_ACCESS_TOKEN` and `MASTRA_PROJECT_ID` from the environment.

Gotcha: `Observability` requires **at least one exporter** — passing an empty `exporters` array throws `OBSERVABILITY_INVALID_INSTANCE_CONFIG`. So omit the `observability` option entirely until the platform creds are present, keeping the app runnable before the Mastra project exists (and in local dev).

```typescript
// src/mastra/index.ts
import { Mastra } from "@mastra/core/mastra";
import { Observability, MastraPlatformExporter } from "@mastra/observability";
import { pricingAgent } from "./agents/pricing";

const platformReady = Boolean(
process.env.MASTRA_PLATFORM_ACCESS_TOKEN && process.env.MASTRA_PROJECT_ID,
);

const observability = platformReady
? new Observability({
configs: {
default: { serviceName: "my-app", exporters: [new MastraPlatformExporter()] },
},
})
: undefined;

export const mastra = new Mastra({
agents: { pricingAgent },
...(observability ? { observability } : {}),
});
```

Agents must be **registered on the `Mastra` instance** (the `agents` map) for their `.generate()` / `.stream()` calls to be traced. Call them via `mastra.getAgent("pricingAgent")`.

## 3. Structured output through the gateway

The gateway does not enforce **native** structured output, so a bare `structuredOutput: { schema }` can come back missing fields (e.g. a nested `meta` object), failing Zod validation. Set `jsonPromptInjection: true` so Mastra injects the schema into the prompt and the model returns the full shape:

```typescript
const agent = mastra.getAgent("pricingAgent");
const result = await agent.generate(prompt, {
structuredOutput: { schema: myZodSchema, jsonPromptInjection: true },
abortSignal: AbortSignal.timeout(70_000), // bound each attempt; the gateway has an upstream timeout
});
const data = result.object; // validated against myZodSchema
```

For resilience, register a second agent on a different model (e.g. `neon/claude-haiku-4-5`) and fall back to it if the primary attempt throws — both models are reachable on the gateway via the same env vars.

## 4. Create the Mastra project + token with the CLI

Install the Mastra CLI (`npm i -g mastra`) and authenticate. Project/token creation needs a **live login session**:

```bash
mastra auth login # opens a browser; required before the steps below
mastra auth whoami # shows your user + org id (org_…)
```

- **Access token (non-interactive):** `mastra auth tokens create <name>` prints a one-time secret (`sk_…`). This is your `MASTRA_PLATFORM_ACCESS_TOKEN`.
- **Project:** the interactive `mastra studio projects create` TUI is hard to script. Instead, register the project as part of a Studio deploy, which is non-interactive with `-y` and writes the project id to `.mastra-project.json`:

```bash
mastra studio deploy --org org_xxx --project my-app -y
# → .mastra-project.json: { "projectId": "…", "projectName": "my-app", "organizationId": "org_…" }
```

Use that `projectId` as `MASTRA_PROJECT_ID`.

Two gotchas:

- **Don't set `MASTRA_API_TOKEN` in the env for project/deploy commands** — it makes the CLI report `No organizations found`. Rely on the interactive login session instead.
- If you keep multiple env files (e.g. `.env.deploy` and `.env.local`), `studio deploy` errors with `Multiple env files found`; pass `--env-file <file>` to disambiguate.

## 5. Pass the creds via `neon.ts` (third-party env)

Neon-injected vars (`DATABASE_URL`, AI Gateway `NEON_AI_GATEWAY_*`) are automatic. Declare only third-party vars under the function's `env`, resolved from `process.env` at deploy time:

```typescript
// neon.ts
functions: {
myapp: {
name: "my app",
source: "src/index.ts",
env: {
MASTRA_PROJECT_ID: process.env.MASTRA_PROJECT_ID!,
MASTRA_PLATFORM_ACCESS_TOKEN: process.env.MASTRA_PLATFORM_ACCESS_TOKEN!,
},
},
}
```

Load the values from a git-ignored file at deploy time:

```bash
neon deploy --env .env.deploy
```

## 6. Verify

Send a request that exercises the agent, then open the Mastra Studio project's **Observability / Traces** view — you'll see the agent run (model calls, latency, token usage) under the `serviceName` you configured. Only `SPAN_ENDED` events are exported, buffered and flushed periodically, so a trace appears a few seconds after the agent run completes.

## Further reading

- https://mastra.ai/docs/observability/tracing/exporters/cloud
- https://mastra.ai/reference/observability/tracing/exporters/mastra-platform-exporter
- https://mastra.ai/docs/agents/structured-output
- Neon AI Gateway dialects: the `neon-ai-gateway` skill
Loading
Loading