Skip to content

Commit 1f94cb4

Browse files
NathanFlurryrivet-docs-sync[bot]
andauthored
docs(dynamic-apps): sync from rivet-dev/dynamic-apps@cc779c1 (#34)
Co-authored-by: rivet-docs-sync[bot] <docs-sync@rivet.dev>
1 parent ef4093a commit 1f94cb4

12 files changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "generated-rivetkit-app",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"scripts": {
8+
"build": "tsc",
9+
"check-types": "tsc --noEmit"
10+
},
11+
"devDependencies": {
12+
"typescript": "5.7.3"
13+
}
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
fetch(request: Request) {
3+
return Response.json({
4+
message: "Replace this seed with the generated application.",
5+
path: new URL(request.url).pathname,
6+
});
7+
},
8+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"lib": ["ES2022", "DOM"],
5+
"module": "NodeNext",
6+
"moduleResolution": "NodeNext",
7+
"strict": true,
8+
"outDir": "dist",
9+
"skipLibCheck": true
10+
},
11+
"include": ["src"]
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@rivet-dev/dynamic-apps-example-ai-builder",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"start": "node --no-node-snapshot --import tsx src/server.ts",
8+
"check-types": "tsc --noEmit"
9+
},
10+
"dependencies": {
11+
"@ai-sdk/anthropic": "^4.0.19",
12+
"@hono/node-server": "^2.0.11",
13+
"@rivet-dev/dynamic-apps": "workspace:*",
14+
"ai": "^7.0.37",
15+
"hono": "^4.12.9"
16+
},
17+
"devDependencies": {
18+
"@types/node": "^22.19.15",
19+
"tsx": "^4.20.6",
20+
"typescript": "^5.7.3"
21+
}
22+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { readFile } from "node:fs/promises";
2+
import { anthropic } from "@ai-sdk/anthropic";
3+
import { serve } from "@hono/node-server";
4+
import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
5+
import { generateText } from "ai";
6+
import { Hono } from "hono";
7+
8+
const editablePaths = [
9+
"package.json",
10+
"tsconfig.json",
11+
"src/index.ts",
12+
] as const;
13+
const maxRepairs = 3;
14+
const maxFileBytes = 64 * 1024;
15+
16+
async function loadSeed(): Promise<Record<string, string>> {
17+
const files: Record<string, string> = {};
18+
for (const path of editablePaths) {
19+
files[path] = await readFile(
20+
new URL(`../fixtures/app/${path}`, import.meta.url),
21+
"utf8",
22+
);
23+
}
24+
return files;
25+
}
26+
27+
function parseFiles(text: string): Record<string, string> {
28+
const json = text.match(/```json\s*([\s\S]*?)```/)?.[1] ?? text;
29+
const value = JSON.parse(json) as { files?: Record<string, unknown> };
30+
if (!value.files || typeof value.files !== "object") {
31+
throw new TypeError("model response must contain a files object");
32+
}
33+
const files: Record<string, string> = {};
34+
for (const path of editablePaths) {
35+
const content = value.files[path];
36+
if (typeof content !== "string") {
37+
throw new TypeError(`model response is missing ${path}`);
38+
}
39+
if (Buffer.byteLength(content) > maxFileBytes) {
40+
throw new RangeError(`${path} exceeds ${maxFileBytes} bytes`);
41+
}
42+
files[path] = content;
43+
}
44+
return files;
45+
}
46+
47+
async function revise(
48+
prompt: string,
49+
files: Record<string, string>,
50+
diagnostics?: string,
51+
): Promise<Record<string, string>> {
52+
const result = await generateText({
53+
model: anthropic(process.env.AI_MODEL ?? "claude-sonnet-4-5"),
54+
maxOutputTokens: 8_000,
55+
prompt: [
56+
'Return JSON only as {"files":{"path":"content"}}.',
57+
`You may edit only: ${editablePaths.join(", ")}.`,
58+
"The app must export a default object with fetch(request) returning a Web Response.",
59+
`User request: ${prompt}`,
60+
diagnostics ? `Previous build diagnostics:\n${diagnostics}` : "",
61+
`Current files:\n${JSON.stringify(files)}`,
62+
]
63+
.filter(Boolean)
64+
.join("\n\n"),
65+
});
66+
return parseFiles(result.text);
67+
}
68+
69+
async function generateApp(appId: string, prompt: string) {
70+
let files = await revise(prompt, await loadSeed());
71+
for (let attempt = 0; attempt <= maxRepairs; attempt += 1) {
72+
try {
73+
return await deployApp({
74+
appId,
75+
files,
76+
});
77+
} catch (error) {
78+
const appsError =
79+
typeof error === "object" &&
80+
error !== null &&
81+
"code" in error &&
82+
typeof error.code === "string" &&
83+
error.code.startsWith("agentos_apps_");
84+
if (!appsError || attempt === maxRepairs) {
85+
throw error;
86+
}
87+
const details = error as {
88+
code: string;
89+
message?: string;
90+
metadata?: unknown;
91+
};
92+
const diagnostics = JSON.stringify({
93+
code: details.code,
94+
message: details.message ?? String(error),
95+
metadata: details.metadata,
96+
}).slice(0, 16 * 1024);
97+
files = await revise(prompt, files, diagnostics);
98+
}
99+
}
100+
throw new Error("unreachable");
101+
}
102+
103+
const server = new Hono();
104+
const dispatchRegistry = (request: Request) => {
105+
const headers = new Headers(request.headers);
106+
headers.set("x-agentos-app-registry-dispatch", "1");
107+
return appsRouter.fetch(new Request(request, { headers }));
108+
};
109+
server.all("/api/rivet", (c) => dispatchRegistry(c.req.raw));
110+
server.all("/api/rivet/*", (c) => dispatchRegistry(c.req.raw));
111+
112+
// An agent or any other part of the system can call this route. A generic
113+
// deployment endpoint could accept multipart files; this example generates the
114+
// files from a prompt instead.
115+
server.post("/deploy/:name", async (context) => {
116+
const body = await context.req.json<{ prompt?: unknown }>();
117+
if (typeof body.prompt !== "string" || body.prompt.length > 4_000) {
118+
return context.json(
119+
{ error: "prompt must be at most 4,000 characters" },
120+
400,
121+
);
122+
}
123+
return context.json(
124+
await generateApp(context.req.param("name"), body.prompt),
125+
);
126+
});
127+
server.route("/apps", appsRouter);
128+
129+
serve({
130+
fetch: server.fetch,
131+
port: 3000,
132+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"lib": ["ES2022", "DOM"],
5+
"module": "NodeNext",
6+
"moduleResolution": "NodeNext",
7+
"strict": true,
8+
"noEmit": true,
9+
"skipLibCheck": true
10+
},
11+
"include": ["src"]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "hello-world-app",
3+
"version": "0.0.0",
4+
"private": true,
5+
"main": "src/index.ts",
6+
"scripts": {
7+
"check-types": "node --check src/index.mjs"
8+
},
9+
"dependencies": {
10+
"hono": "^4.12.9"
11+
}
12+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Hono } from "hono";
2+
3+
const app = new Hono();
4+
5+
// Serve the application's frontend.
6+
app.get("/", (c) => {
7+
return c.html(`<!doctype html>
8+
<html lang="en">
9+
<head>
10+
<meta charset="utf-8">
11+
<meta name="viewport" content="width=device-width, initial-scale=1">
12+
<title>Hello from Dynamic Apps</title>
13+
</head>
14+
<body>
15+
<main>
16+
<h1>Hello from Dynamic Apps</h1>
17+
<p>This HTML is served by an HTTP app running inside a V8 isolate.</p>
18+
<p><a href="./api/hello">Call the JSON API</a></p>
19+
</main>
20+
</body>
21+
</html>`);
22+
});
23+
24+
// Serve a REST API request from the same application.
25+
app.get("/api/hello", (c) => {
26+
return c.json({ message: "Hello from Dynamic Apps" });
27+
});
28+
29+
export default app;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@rivet-dev/dynamic-apps-example-hello-world",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"start": "node --no-node-snapshot --import tsx src/server.ts",
8+
"deploy": "node --import tsx src/deploy.ts",
9+
"check-types": "tsc --noEmit"
10+
},
11+
"dependencies": {
12+
"@hono/node-server": "^2.0.11",
13+
"@rivet-dev/dynamic-apps": "workspace:*",
14+
"hono": "^4.12.9"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^22.19.15",
18+
"tsx": "^4.20.6",
19+
"typescript": "^5.7.3"
20+
}
21+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { deployApp } from "@rivet-dev/dynamic-apps";
2+
3+
// An agent, upload endpoint, or any other part of the system can call
4+
// deployApp() with the files it generated.
5+
await deployApp({
6+
appId: "hello-world",
7+
files: {
8+
"package.json": JSON.stringify({
9+
name: "hello-world-app",
10+
version: "0.0.0",
11+
private: true,
12+
type: "module",
13+
main: "src/index.ts",
14+
dependencies: {
15+
hono: "^4.12.9",
16+
},
17+
}),
18+
"src/index.ts": `
19+
import { Hono } from "hono";
20+
21+
const app = new Hono();
22+
23+
// Serve the application's frontend.
24+
app.get("/", (c) => c.html("<h1>Hello from Dynamic Apps</h1>"));
25+
26+
// Serve a REST API request from the same application.
27+
app.get("/api/hello", (c) => c.json({ message: "Hello from Dynamic Apps" }));
28+
29+
export default app;
30+
`,
31+
},
32+
});

0 commit comments

Comments
 (0)