Skip to content
Open
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,9 @@ packages/database/generated
# Documentation
.contentlayer
.content-collections
.source
.source

# Eve agent (apps/agent)
.eve
.output
.workflow-data
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ next-forge comes with batteries included:
- **Docs** — Documentation site powered by Mintlify
- **Email** — Email templates with React Email
- **Storybook** — Component development environment
- **Agent** — Optional durable backend AI agent powered by [Eve](https://eve.dev)

### Packages

Expand Down
10 changes: 10 additions & 0 deletions apps/agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Server

# Shared application database (same value as the other apps). The agent reads it
# through @repo/database; count-pages reports itself disabled when it's unset.
DATABASE_URL=""

# Optional — enables live model responses through the Vercel AI Gateway.
# Without it, the agent runs in a deterministic offline fallback so the monorepo
# boots with zero API keys. Browse model ids at https://vercel.com/ai-gateway/models
AI_GATEWAY_API_KEY=""
74 changes: 74 additions & 0 deletions apps/agent/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { defineAgent } from "eve";
import { mockModel } from "eve/evals";

// Model selection follows next-forge's graceful-degradation philosophy.
//
// - With an AI Gateway credential — `AI_GATEWAY_API_KEY` locally, or the Vercel
// OIDC token that Vercel injects on a deployment — the agent talks to a real
// model through the Vercel AI Gateway. Swap the id for any model in the
// catalog: https://vercel.com/ai-gateway/models
// - Without one, the agent stays fully functional but answers from a
// deterministic offline fixture. That keeps `bun run dev`, `bun run build`,
// and `eve eval` green with zero API keys — the first thing a fresh
// next-forge user experiences — and makes CI provider-free.
//
// `||` (not `??`) so an empty `AI_GATEWAY_API_KEY=""` from `.env.example` falls
// through to the OIDC token instead of masking it.
const gatewayReady = Boolean(
process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN
);

// Warn once, at request time (not build time — the OIDC token only exists at
// runtime on Vercel), so degraded mode is visible in server logs.
let warnedOffline = false;
const warnOfflineOnce = () => {
if (warnedOffline) {
return;
}
warnedOffline = true;
console.warn(
"[agent] No AI Gateway credential — serving from the deterministic offline " +
"fallback model. Set AI_GATEWAY_API_KEY (or deploy on Vercel with the AI " +
"Gateway enabled) for live responses."
);
};

export default defineAgent(
gatewayReady
? {
// Live path: eve resolves the context window from the gateway catalog,
// so compaction works with no extra config.
model: "anthropic/claude-sonnet-5",
}
: {
model: mockModel({
modelId: "offline-fallback",
provider: "next-forge",
respond: ({ toolResults }) => {
warnOfflineOnce();
// First turn: exercise the monorepo-native tool so the offline path
// still proves the wiring works end to end.
if (toolResults.length === 0) {
return { toolCalls: [{ name: "count-pages", input: {} }] };
}
const result = toolResults[0]?.output as
| { ok?: boolean; count?: number }
| undefined;
const data = result?.ok
? `The database reports ${result.count} page(s).`
: "The database tool is currently disabled.";
return {
text:
"I'm running in offline fallback mode because no AI Gateway " +
`credential is configured. ${data} Set AI_GATEWAY_API_KEY for ` +
"live model responses, and DATABASE_URL in apps/agent/.env for " +
"live data.",
};
},
}),
// The offline fixture has no AI Gateway catalog entry; hand eve the
// context-window size directly (documented escape hatch) so compaction
// still compiles provider-free.
modelContextWindowTokens: 200_000,
}
);
15 changes: 15 additions & 0 deletions apps/agent/agent/channels/eve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { localDev, placeholderAuth, vercelOidc } from "eve/channels/auth";
import { eveChannel } from "eve/channels/eve";

export default eveChannel({
auth: [
// Lets the eve TUI and your Vercel deployments reach the deployed agent.
vercelOidc(),
// Open on localhost for `eve dev` and the REPL; ignored in production.
localDev(),
// This placeholder will not allow browser requests in production.
// Replace it with your app's auth provider, like Auth.js or Clerk,
// or use none() for a public demo.
placeholderAuth(),
],
});
19 changes: 19 additions & 0 deletions apps/agent/agent/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Identity

You are the operations agent for this application — a SaaS built on next-forge
(Turborepo monorepo: `apps/app` main product, `apps/web` marketing site,
`apps/api` webhooks + cron).

# Capabilities

You have typed tools that operate on the monorepo's own packages
(`@repo/database`, and others as they are added). Integrations follow
next-forge's graceful-degradation philosophy: when an integration's environment
keys are missing, its tool reports itself disabled instead of erroring.

# Behavior

- Answer questions about the application's data using your tools.
- When a tool reports an integration is disabled, say so plainly and tell the
user which environment variable enables it.
- Be concise. No filler.
49 changes: 49 additions & 0 deletions apps/agent/agent/tools/count-pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { neonConfig } from "@neondatabase/serverless";
import { PrismaNeon } from "@prisma/adapter-neon";
// NOTE: imports the generated client + keys directly. @repo/database's index.ts
// carries a `server-only` guard, which only the Next.js runtime satisfies —
// eve agents run on Nitro. Proposed upstream: runtime-neutral entrypoints.
import { PrismaClient } from "@repo/database/generated/client";
import { keys } from "@repo/database/keys";
import { defineTool } from "eve/tools";
import ws from "ws";
import { z } from "zod";

let database: PrismaClient | undefined;

const getDatabase = () => {
if (!database) {
neonConfig.webSocketConstructor = ws;
const adapter = new PrismaNeon({ connectionString: keys().DATABASE_URL });
database = new PrismaClient({ adapter });
}
return database;
};

/**
* Demonstrates the monorepo advantage: the agent's tools reuse the same
* schema, generated client, and env keys as the apps. Degrades gracefully
* when the database is unreachable — mirroring next-forge's
* optional-integration philosophy.
*/
export default defineTool({
description:
"Count the Page records in the application database (via @repo/database).",
inputSchema: z.object({}),
execute: async () => {
try {
const count = await getDatabase().page.count();
return { ok: true, count };
} catch (error) {
// Log the full error server-side; return only a generic reason so raw
// connection details never reach the model or the caller.
console.error("[count-pages] database unreachable:", error);
return {
ok: false,
disabled: true,
reason:
"Database is not reachable. Set DATABASE_URL in apps/agent/.env to enable this tool.",
};
}
},
});
6 changes: 6 additions & 0 deletions apps/agent/evals/evals.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineEvalConfig } from "eve/evals";

// No judge model is configured: the smoke eval is fully deterministic, so
// `eve eval` runs provider-free in CI. Add `judge: { model: "..." }` here when
// you introduce LLM-as-judge assertions.
export default defineEvalConfig({});
13 changes: 13 additions & 0 deletions apps/agent/evals/smoke.eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineEval } from "eve/evals";
import { includes } from "eve/evals/expect";

export default defineEval({
description:
"Agent calls the count-pages tool and reports the integration status",
async test(t) {
await t.send("How many pages are in the database right now?");
t.succeeded();
t.calledTool("count-pages");
t.check(t.reply, includes("DATABASE_URL"));
},
});
35 changes: 35 additions & 0 deletions apps/agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "agent",
"version": "0.0.0",
"private": true,
"type": "module",
"imports": {
"#*": "./agent/*",
"#evals/*": "./evals/*"
},
"scripts": {
"build": "eve build",
"dev": "eve dev --no-ui",
"start": "eve start",
"eval": "eve eval --strict",
"clean": "git clean -xdf .cache .turbo .eve .workflow-data node_modules",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
"@prisma/adapter-neon": "7.4.2",
"@repo/database": "workspace:*",
"ai": "^7.0.0",
"eve": "0.20.0",
"ws": "^8.19.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "25.3.5",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=24"
}
}
13 changes: 13 additions & 0 deletions apps/agent/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["agent/**/*.ts", "evals/**/*.ts", ".eve/**/*.d.ts"]
}
Loading