<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value=".actorcore
node_modules
public
">
<input type="hidden" name="project[files][README.md]" value="# AI Agent

Example project demonstrating queue-driven Rivet Actor AI agents with streaming Vercel AI SDK responses.

## Getting Started

```sh
git clone https://github.com/rivet-dev/rivet.git
cd rivet/examples/ai-agent
npm install
npm run dev
```


## Features

- Actor-per-agent pattern with a coordinating manager Rivet Actor
- Queue-based intake using `for await` over `c.queue.iter(...)` inside the run loop
- Streaming AI responses sent to the UI as they arrive
- Persistent history stored in Rivet Actor state
- Live status updates via events and polling

## Prerequisites

- OpenAI API key set as `OPENAI_API_KEY`

## Implementation

The AgentManager creates and tracks agent actors, while each AI agent Rivet Actor consumes queue messages in `run` and streams responses with the Vercel AI SDK.

- **Actor definitions and queues** ([`src/actors.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/actors.ts))
- **Frontend orchestration** ([`frontend/App.tsx`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/frontend/App.tsx))
- **Server entry point** ([`src/server.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent/src/server.ts))

## Resources

Read more about [queues](https://rivet.dev/docs/actors/queues), [run handlers](https://rivet.dev/docs/actors/run), [state](https://rivet.dev/docs/actors/state), and [events](https://rivet.dev/docs/actors/events).

## License

MIT
">
<input type="hidden" name="project[files][index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
	&lt;meta charset=&quot;UTF-8&quot; /&gt;
	&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
	&lt;title&gt;AI Agent Example&lt;/title&gt;
	&lt;style&gt;
		@import url(&quot;https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&amp;family=Space+Grotesk:wght@400;600;700&amp;display=swap&quot;);

		:root {
			color-scheme: light;
			--ink: #1a1d22;
			--muted: #5b6069;
			--surface: #ffffff;
			--surface-muted: #f4f6f8;
			--accent: #1f7a75;
			--accent-strong: #0c5f5a;
			--highlight: #f2b93b;
			--border: rgba(26, 29, 34, 0.12);
			--shadow: rgba(20, 24, 31, 0.08);
		}

		* {
			box-sizing: border-box;
		}

		body {
			margin: 0;
			font-family: &quot;Space Grotesk&quot;, &quot;Helvetica Neue&quot;, sans-serif;
			color: var(--ink);
			background:
				radial-gradient(circle at top left, #fef1d8 0%, transparent 45%),
				radial-gradient(circle at 20% 30%, #e0f2f1 0%, transparent 50%),
				linear-gradient(120deg, #f7f5ef 0%, #f2f7f7 100%);
			min-height: 100vh;
		}

		h1,
		h2,
		h3,
		p {
			margin: 0;
		}

		button,
		input,
		textarea {
			font-family: inherit;
		}

		.layout {
			max-width: 1200px;
			margin: 0 auto;
			padding: 32px 20px 48px;
			display: flex;
			flex-direction: column;
			gap: 28px;
		}

		.hero {
			display: flex;
			flex-wrap: wrap;
			gap: 24px;
			align-items: center;
			justify-content: space-between;
			background: var(--surface);
			border: 1px solid var(--border);
			border-radius: 20px;
			padding: 28px;
			box-shadow: 0 18px 40px var(--shadow);
		}

		.hero__eyebrow {
			text-transform: uppercase;
			letter-spacing: 0.2em;
			font-size: 0.75rem;
			color: var(--accent-strong);
			margin-bottom: 12px;
		}

		.hero h1 {
			font-size: clamp(2.2rem, 3vw, 3.2rem);
			line-height: 1.1;
			max-width: 520px;
		}

		.hero__subtitle {
			margin-top: 12px;
			max-width: 520px;
			color: var(--muted);
		}

		.hero__controls {
			flex: 1;
			min-width: 260px;
			background: var(--surface-muted);
			border-radius: 16px;
			padding: 20px;
			display: flex;
			flex-direction: column;
			gap: 12px;
		}

		.control {
			display: flex;
			flex-direction: column;
			gap: 6px;
			font-size: 0.9rem;
			color: var(--muted);
		}

		.control input {
			padding: 10px 12px;
			border-radius: 10px;
			border: 1px solid var(--border);
			font-size: 1rem;
		}

		.hero__controls button {
			padding: 12px 16px;
			border: none;
			border-radius: 12px;
			background: var(--accent);
			color: white;
			font-weight: 600;
			cursor: pointer;
			transition: transform 0.2s ease, box-shadow 0.2s ease;
		}

		.hero__controls button:hover:not(:disabled) {
			transform: translateY(-1px);
			box-shadow: 0 10px 18px rgba(31, 122, 117, 0.25);
		}

		.hero__controls button:disabled {
			opacity: 0.6;
			cursor: not-allowed;
		}

		.agents {
			display: flex;
			flex-direction: column;
			gap: 16px;
		}

		.agents__header {
			display: flex;
			align-items: center;
			gap: 12px;
		}

		.agents__header h2 {
			font-size: 1.4rem;
		}

		.agents__count {
			background: var(--highlight);
			color: #3f2a00;
			padding: 4px 10px;
			border-radius: 999px;
			font-weight: 600;
			font-size: 0.9rem;
		}

		.agents__grid {
			display: grid;
			grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
			gap: 18px;
		}

		.agents__empty {
			padding: 20px;
			background: var(--surface-muted);
			border-radius: 14px;
			color: var(--muted);
			border: 1px dashed var(--border);
		}

		.agent-card {
			background: var(--surface);
			border: 1px solid var(--border);
			border-radius: 16px;
			padding: 18px;
			display: flex;
			flex-direction: column;
			gap: 14px;
			min-height: 420px;
			box-shadow: 0 12px 24px var(--shadow);
		}

		.agent-card__header {
			display: flex;
			justify-content: space-between;
			align-items: center;
			gap: 12px;
		}

		.agent-card__title {
			font-weight: 600;
			font-size: 1.1rem;
		}

		.agent-card__meta {
			font-size: 0.75rem;
			color: var(--muted);
			font-family: &quot;IBM Plex Mono&quot;, monospace;
		}

		.agent-card__status {
			display: inline-flex;
			align-items: center;
			gap: 6px;
			font-size: 0.85rem;
			color: var(--muted);
		}

		.status-dot {
			width: 10px;
			height: 10px;
			border-radius: 50%;
			background: #9aa1ab;
		}

		.status-dot--thinking {
			background: var(--highlight);
		}

		.status-dot--error {
			background: #d95040;
		}

		.status-dot--idle {
			background: #5bc184;
		}

		.agent-card__timeline {
			background: var(--surface-muted);
			border-radius: 12px;
			padding: 12px;
			flex: 1;
			overflow-y: auto;
			display: flex;
			flex-direction: column;
			gap: 12px;
		}

		.agent-card__empty {
			color: var(--muted);
			font-size: 0.95rem;
		}

		.message {
			background: var(--surface);
			border-radius: 12px;
			padding: 10px 12px;
			box-shadow: inset 0 0 0 1px var(--border);
			display: flex;
			flex-direction: column;
			gap: 6px;
		}

		.message--user {
			border-left: 4px solid var(--accent);
		}

		.message--assistant {
			border-left: 4px solid #f0a33b;
		}

		.message__header {
			display: flex;
			justify-content: space-between;
			font-size: 0.75rem;
			color: var(--muted);
			text-transform: uppercase;
			letter-spacing: 0.08em;
		}

		.message__content {
			margin: 0;
			white-space: pre-wrap;
			line-height: 1.4;
			font-size: 0.95rem;
		}

		.agent-card__footer {
			display: flex;
			gap: 10px;
			align-items: center;
		}

		.agent-card__footer textarea {
			flex: 1;
			resize: vertical;
			border-radius: 12px;
			border: 1px solid var(--border);
			padding: 10px 12px;
			background: white;
		}

		.agent-card__footer button {
			padding: 10px 16px;
			border-radius: 12px;
			border: none;
			background: var(--accent-strong);
			color: white;
			font-weight: 600;
			cursor: pointer;
		}

		.agent-card__footer button:disabled {
			opacity: 0.6;
			cursor: not-allowed;
		}

		.agent-card__error {
			font-size: 0.85rem;
			color: #b04032;
			background: #fdebe8;
			padding: 8px 10px;
			border-radius: 10px;
			border: 1px solid #f5c3bb;
		}

		@media (max-width: 720px) {
			.hero {
				padding: 20px;
			}

			.hero__controls {
				width: 100%;
			}
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
	&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
	&lt;script type=&quot;module&quot; src=&quot;/frontend/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
">
<input type="hidden" name="project[files][package.json]" value="{
  &quot;name&quot;: &quot;ai-agent&quot;,
  &quot;version&quot;: &quot;2.0.21&quot;,
  &quot;private&quot;: true,
  &quot;type&quot;: &quot;module&quot;,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;concurrently -n server,vite \&quot;tsx --watch src/index.ts\&quot; \&quot;vite\&quot;&quot;,
    &quot;dev:server&quot;: &quot;tsx --watch src/index.ts&quot;,
    &quot;check-types&quot;: &quot;tsc --noEmit&quot;,
    &quot;test&quot;: &quot;vitest run&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;start&quot;: &quot;tsx src/index.ts&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@types/node&quot;: &quot;^22.13.9&quot;,
    &quot;@types/react&quot;: &quot;^18.2.0&quot;,
    &quot;@types/react-dom&quot;: &quot;^18.2.0&quot;,
    &quot;@vitejs/plugin-react&quot;: &quot;^4.2.0&quot;,
    &quot;concurrently&quot;: &quot;^9.1.2&quot;,
    &quot;tsx&quot;: &quot;^3.12.7&quot;,
    &quot;typescript&quot;: &quot;^5.5.2&quot;,
    &quot;vite&quot;: &quot;^5.0.0&quot;,
    &quot;vitest&quot;: &quot;^3.1.1&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@ai-sdk/openai&quot;: &quot;^0.0.66&quot;,
    &quot;@rivetkit/react&quot;: &quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@49011521cbd31b9a30d471fe187629aa05b36cc1&quot;,
    &quot;ai&quot;: &quot;^4.0.38&quot;,
    &quot;react&quot;: &quot;^18.2.0&quot;,
    &quot;react-dom&quot;: &quot;^18.2.0&quot;,
    &quot;rivetkit&quot;: &quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@49011521cbd31b9a30d471fe187629aa05b36cc1&quot;
  },
  &quot;stableVersion&quot;: &quot;0.8.0&quot;,
  &quot;license&quot;: &quot;MIT&quot;
}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		&quot;target&quot;: &quot;esnext&quot;,
		&quot;lib&quot;: [&quot;esnext&quot;, &quot;dom&quot;],
		&quot;jsx&quot;: &quot;react-jsx&quot;,
		&quot;module&quot;: &quot;esnext&quot;,
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		&quot;types&quot;: [&quot;node&quot;, &quot;vite/client&quot;],
		&quot;noEmit&quot;: true,
		&quot;strict&quot;: true,
		&quot;skipLibCheck&quot;: true,
		&quot;allowImportingTsExtensions&quot;: true,
		&quot;rewriteRelativeImportExtensions&quot;: true
	},
	&quot;include&quot;: [&quot;src/**/*&quot;, &quot;frontend/**/*&quot;, &quot;tests/**/*&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
	&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
	&quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import react from &quot;@vitejs/plugin-react&quot;;
import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
	plugins: [react()],
	publicDir: false,
	build: {
		outDir: &quot;public&quot;,
		emptyOutDir: true,
	},
	server: {
		clearScreen: false,
		proxy: {
			&quot;/actors&quot;: { target: &quot;http://localhost:6420&quot;, ws: true },
			&quot;/metadata&quot;: { target: &quot;http://localhost:6420&quot; },
			&quot;/health&quot;: { target: &quot;http://localhost:6420&quot; },
		},
	},
});
">
<input type="hidden" name="project[files][frontend/App.tsx]" value="import { createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useState } from &quot;react&quot;;
import type {
	AgentInfo,
	AgentMessage,
	AgentStatus,
	registry,
} from &quot;../src/index.ts&quot;;

const { useActor } = createRivetKit&lt;typeof registry&gt;(&quot;http://localhost:6420&quot;);

type ResponseEvent = {
	messageId: string;
	delta: string;
	content: string;
	done: boolean;
	error?: string;
};

function formatTime(timestamp: number) {
	return new Date(timestamp).toLocaleTimeString();
}

function AgentPanel({ info }: { info: AgentInfo }) {
	const agent = useActor({
		name: &quot;agent&quot;,
		key: [info.id],
	});
	const [messages, setMessages] = useState&lt;AgentMessage[]&gt;([]);
	const [status, setStatus] = useState&lt;AgentStatus | null&gt;(null);
	const [input, setInput] = useState(&quot;&quot;);

	useEffect(() =&gt; {
		if (!agent.connection) {
			return;
		}

		agent.connection.getHistory().then(setMessages);
		agent.connection.getStatus().then(setStatus);
	}, [agent.connection]);

	agent.useEvent(&quot;messageAdded&quot;, (message: AgentMessage) =&gt; {
		setMessages((prev) =&gt; {
			const existingIndex = prev.findIndex((item) =&gt; item.id === message.id);
			if (existingIndex !== -1) {
				const next = [...prev];
				next[existingIndex] = message;
				return next;
			}
			return [...prev, message].sort(
				(a, b) =&gt; a.createdAt - b.createdAt,
			);
		});
	});

	agent.useEvent(&quot;response&quot;, (payload: ResponseEvent) =&gt; {
		setMessages((prev) =&gt;
			prev.map((message) =&gt;
				message.id === payload.messageId
					? { ...message, content: payload.content }
					: message,
			),
		);
	});

	agent.useEvent(&quot;status&quot;, (nextStatus: AgentStatus) =&gt; {
		setStatus(nextStatus);
	});

	const sendMessage = async () =&gt; {
		if (!agent.connection) {
			return;
		}

		const trimmed = input.trim();
		if (!trimmed) {
			return;
		}

		await agent.connection.send(&quot;message&quot;, {
			text: trimmed,
			sender: &quot;Operator&quot;,
		});
		setInput(&quot;&quot;);
	};

	const handleKeyDown = (event: React.KeyboardEvent&lt;HTMLTextAreaElement&gt;) =&gt; {
		if (event.key === &quot;Enter&quot; &amp;&amp; !event.shiftKey) {
			event.preventDefault();
			sendMessage();
		}
	};

	return (
		&lt;section className=&quot;agent-card&quot;&gt;
			&lt;header className=&quot;agent-card__header&quot;&gt;
				&lt;div&gt;
					&lt;p className=&quot;agent-card__title&quot;&gt;{info.name}&lt;/p&gt;
					&lt;p className=&quot;agent-card__meta&quot;&gt;{info.id}&lt;/p&gt;
				&lt;/div&gt;
				&lt;div className=&quot;agent-card__status&quot;&gt;
					&lt;span
						className={`status-dot status-dot--${status?.state ?? &quot;idle&quot;}`}
					/&gt;
					&lt;span className=&quot;status-label&quot;&gt;
						{status?.state ?? &quot;idle&quot;}
					&lt;/span&gt;
				&lt;/div&gt;
			&lt;/header&gt;

			&lt;div className=&quot;agent-card__timeline&quot;&gt;
				{messages.length === 0 ? (
					&lt;p className=&quot;agent-card__empty&quot;&gt;
						Send a message to wake this Rivet Actor.
					&lt;/p&gt;
				) : (
					messages.map((message) =&gt; (
						&lt;article
							key={message.id}
							className={`message message--${message.role}`}
						&gt;
							&lt;div className=&quot;message__header&quot;&gt;
								&lt;span className=&quot;message__sender&quot;&gt;{message.sender}&lt;/span&gt;
								&lt;span className=&quot;message__time&quot;&gt;
									{formatTime(message.createdAt)}
								&lt;/span&gt;
							&lt;/div&gt;
							&lt;p className=&quot;message__content&quot;&gt;{message.content}&lt;/p&gt;
						&lt;/article&gt;
					))
				)}
			&lt;/div&gt;

			&lt;div className=&quot;agent-card__footer&quot;&gt;
				&lt;textarea
					value={input}
					onChange={(event) =&gt; setInput(event.target.value)}
					onKeyDown={handleKeyDown}
					placeholder=&quot;Send a message to this agent&quot;
					rows={2}
					disabled={!agent.connection}
				/&gt;
				&lt;button
					onClick={sendMessage}
					disabled={!agent.connection || !input.trim()}
				&gt;
					Send
				&lt;/button&gt;
			&lt;/div&gt;

			{status?.state === &quot;error&quot; &amp;&amp; status.error ? (
				&lt;p className=&quot;agent-card__error&quot;&gt;{status.error}&lt;/p&gt;
			) : null}
		&lt;/section&gt;
	);
}

export function App() {
	const manager = useActor({
		name: &quot;agentManager&quot;,
		key: [&quot;primary&quot;],
	});
	const [agents, setAgents] = useState&lt;AgentInfo[]&gt;([]);
	const [agentName, setAgentName] = useState(&quot;&quot;);

	useEffect(() =&gt; {
		if (!manager.connection) {
			return;
		}

		manager.connection.listAgents().then(setAgents);
	}, [manager.connection]);

	const createAgent = async () =&gt; {
		if (!manager.connection) {
			return;
		}

		const info = await manager.connection.createAgent(agentName);
		setAgents((prev) =&gt; [...prev, info]);
		setAgentName(&quot;&quot;);
	};

	const handleCreateKeyDown = (
		event: React.KeyboardEvent&lt;HTMLInputElement&gt;,
	) =&gt; {
		if (event.key === &quot;Enter&quot;) {
			createAgent();
		}
	};

	return (
		&lt;div className=&quot;layout&quot;&gt;
			&lt;header className=&quot;hero&quot;&gt;
				&lt;div&gt;
					&lt;p className=&quot;hero__eyebrow&quot;&gt;AI Agent&lt;/p&gt;
					&lt;h1&gt;Spin up AI agents and message them in parallel.&lt;/h1&gt;
					&lt;p className=&quot;hero__subtitle&quot;&gt;
						Each Rivet Actor keeps its own memory and streams responses
						as queue messages arrive.
					&lt;/p&gt;
				&lt;/div&gt;
				&lt;div className=&quot;hero__controls&quot;&gt;
					&lt;label className=&quot;control&quot;&gt;
						&lt;span&gt;Agent name&lt;/span&gt;
						&lt;input
							value={agentName}
							onChange={(event) =&gt; setAgentName(event.target.value)}
							onKeyDown={handleCreateKeyDown}
							placeholder=&quot;Ops Analyst&quot;
							disabled={!manager.connection}
						/&gt;
					&lt;/label&gt;
					&lt;button
						onClick={createAgent}
						disabled={!manager.connection}
					&gt;
						Create agent
					&lt;/button&gt;
				&lt;/div&gt;
			&lt;/header&gt;

			&lt;section className=&quot;agents&quot;&gt;
				&lt;div className=&quot;agents__header&quot;&gt;
					&lt;h2&gt;Active agents&lt;/h2&gt;
					&lt;span className=&quot;agents__count&quot;&gt;{agents.length}&lt;/span&gt;
				&lt;/div&gt;
				&lt;div className=&quot;agents__grid&quot;&gt;
					{agents.length === 0 ? (
						&lt;p className=&quot;agents__empty&quot;&gt;
							No agents yet. Create one to start chatting.
						&lt;/p&gt;
					) : (
						agents.map((info) =&gt; (
							&lt;AgentPanel key={info.id} info={info} /&gt;
						))
					)}
				&lt;/div&gt;
			&lt;/section&gt;
		&lt;/div&gt;
	);
}
">
<input type="hidden" name="project[files][frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App.tsx&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) {
	throw new Error(&quot;Root element not found&quot;);
}

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;,
);
">
<input type="hidden" name="project[files][src/index.ts]" value="import { openai } from &quot;@ai-sdk/openai&quot;;
import { streamText, type CoreMessage } from &quot;ai&quot;;
import { actor, event, queue, setup } from &quot;rivetkit&quot;;

export type AgentMessage = {
	id: string;
	role: &quot;user&quot; | &quot;assistant&quot;;
	sender: string;
	content: string;
	createdAt: number;
};

export type AgentStatus = {
	state: &quot;idle&quot; | &quot;thinking&quot; | &quot;error&quot;;
	updatedAt: number;
	error?: string;
};

export type AgentInfo = {
	id: string;
	name: string;
	createdAt: number;
};

export type AgentQueueMessage = {
	text: string;
	sender?: string;
};

export type AgentResponseEvent = {
	messageId: string;
	delta: string;
	content: string;
	done: boolean;
	error?: string;
};

const SYSTEM_PROMPT =
	&quot;You are a focused AI assistant. Keep responses concise, actionable, and ready for handoff.&quot;;

const buildId = (prefix: string) =&gt;
	`${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;

const buildPromptMessages = (messages: AgentMessage[]): CoreMessage[] =&gt; {
	return [
		{ role: &quot;system&quot;, content: SYSTEM_PROMPT },
		...messages.map((message) =&gt; ({
			role: message.role,
			content: message.content,
		})),
	];
};

export const agent = actor({
	// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
	state: {
		messages: [] as AgentMessage[],
		status: {
			state: &quot;idle&quot;,
			updatedAt: Date.now(),
		} as AgentStatus,
	},
	queues: {
		message: queue&lt;AgentQueueMessage&gt;(),
	},
	events: {
		messageAdded: event&lt;AgentMessage&gt;(),
		status: event&lt;AgentStatus&gt;(),
		response: event&lt;AgentResponseEvent&gt;(),
	},

	// The run hook keeps the agent listening for queued messages.
	run: async (c) =&gt; {
		for await (const queued of c.queue.iter()) {
			const { body } = queued;
			if (!body?.text || typeof body.text !== &quot;string&quot;) {
				continue;
			}

			const sender = body.sender?.trim() || &quot;Operator&quot;;
			const userMessage: AgentMessage = {
				id: buildId(&quot;user&quot;),
				role: &quot;user&quot;,
				sender,
				content: body.text.trim(),
				createdAt: Date.now(),
			};

			c.state.messages.push(userMessage);
			c.broadcast(&quot;messageAdded&quot;, userMessage);

			const promptMessages = buildPromptMessages(c.state.messages);

			const assistantMessage: AgentMessage = {
				id: buildId(&quot;assistant&quot;),
				role: &quot;assistant&quot;,
				sender: &quot;Agent&quot;,
				content: &quot;&quot;,
				createdAt: Date.now(),
			};

			c.state.messages.push(assistantMessage);
			c.broadcast(&quot;messageAdded&quot;, assistantMessage);

			c.state.status = {
				state: &quot;thinking&quot;,
				updatedAt: Date.now(),
			};
			c.broadcast(&quot;status&quot;, c.state.status);

			try {
				const result = await streamText({
					model: openai(&quot;gpt-4o-mini&quot;),
					messages: promptMessages,
				});

				let content = &quot;&quot;;
				for await (const delta of result.textStream) {
					if (c.aborted) {
						break;
					}

					content += delta;
					assistantMessage.content = content;
					c.broadcast(&quot;response&quot;, {
						messageId: assistantMessage.id,
						delta,
						content,
						done: false,
					});
				}

				assistantMessage.content = content || assistantMessage.content;
				c.broadcast(&quot;response&quot;, {
					messageId: assistantMessage.id,
					delta: &quot;&quot;,
					content: assistantMessage.content,
					done: true,
				});

				c.state.status = {
					state: &quot;idle&quot;,
					updatedAt: Date.now(),
				};
				c.broadcast(&quot;status&quot;, c.state.status);
			} catch (error) {
				const errorMessage =
					error instanceof Error ? error.message : &quot;Unknown error&quot;;

				assistantMessage.content =
					assistantMessage.content ||
					&quot;I hit a snag while responding. Please try again.&quot;;

				c.state.status = {
					state: &quot;error&quot;,
					updatedAt: Date.now(),
					error: errorMessage,
				};

				c.broadcast(&quot;response&quot;, {
					messageId: assistantMessage.id,
					delta: &quot;&quot;,
					content: assistantMessage.content,
					done: true,
					error: errorMessage,
				});
				c.broadcast(&quot;status&quot;, c.state.status);
			}
		}
	},

	actions: {
		// Callable functions from clients: https://rivet.dev/docs/actors/actions
		getHistory: (c) =&gt; c.state.messages,
		getStatus: (c) =&gt; c.state.status,
	},
});

export const agentManager = actor({
	// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
	state: {
		agents: [] as AgentInfo[],
	},

	actions: {
		// Callable functions from clients: https://rivet.dev/docs/actors/actions
		createAgent: async (c, name?: string) =&gt; {
			const trimmedName = name?.trim();
			const agentName =
				trimmedName || `Agent ${c.state.agents.length + 1}`;
			const info: AgentInfo = {
				id: buildId(&quot;agent&quot;),
				name: agentName,
				createdAt: Date.now(),
			};

			c.state.agents.push(info);

			const client = c.client&lt;typeof registry&gt;();
			const handle = client.agent.getOrCreate([info.id]);
			await handle.getStatus();

			return info;
		},

		listAgents: (c) =&gt; c.state.agents,
	},
});

// Register actors for use: https://rivet.dev/docs/setup
export const registry = setup({
	use: { agent, agentManager },
});

// Start the server on port 6420
registry.start();
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="ai-agent">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>