Add AgentOS daemon control plane and maintenance tooling
This commit is contained in:
1185
.agents/skills/ai-agent-workspace/BASE_SKILL.md
Normal file
1185
.agents/skills/ai-agent-workspace/BASE_SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
343
.agents/skills/ai-agent-workspace/SKILL.md
Normal file
343
.agents/skills/ai-agent-workspace/SKILL.md
Normal file
@@ -0,0 +1,343 @@
|
||||
---
|
||||
name: "ai-agent-workspace"
|
||||
description: "Give every AI agent its own computer: a persistent workspace with a filesystem, processes, shells, networking, and agent sessions on a lightweight in-process OS."
|
||||
---
|
||||
|
||||
# AI Agent Workspaces
|
||||
|
||||
**IMPORTANT: Before doing anything, you MUST read `BASE_SKILL.md` in this skill's directory. It contains essential guidance on debugging, error handling, state management, deployment, and project setup. Those rules and patterns apply to all RivetKit work. Everything below assumes you have already read and understood it.**
|
||||
|
||||
## Working Examples
|
||||
|
||||
If you need a reference implementation, read the raw working example code in these templates:
|
||||
|
||||
- [agent-os](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os)
|
||||
|
||||
|
||||
Patterns for giving every AI agent its own computer with [agentOS](/docs/agent-os): one Rivet Actor per agent that owns a portable, lightweight in-process OS running on Wasm and V8. Use it for code interpreters that keep state between runs, agents that ship artifacts behind shareable preview URLs, per-user dev environments, and scheduled maintenance agents. agentOS is in preview and the API is subject to change.
|
||||
|
||||
This entry is about giving an agent a workspace. For conversation memory, message queues, and streaming chat patterns, see [AI Agent](/cookbook/ai-agent/).
|
||||
|
||||
## Starter Code
|
||||
|
||||
The [agent-os](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os) collection is reference code, one sub-example per capability; treat it as patterns to copy into your project rather than a turnkey app. The [agent-os-e2e](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os-e2e) example is the complete end-to-end walkthrough.
|
||||
|
||||
| Example | Starter Code | Use When |
|
||||
| --- | --- | --- |
|
||||
| Hello World | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/hello-world) | You want the minimal loop: boot a VM lazily on the first action, write a file, read it back. |
|
||||
| Filesystem | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/filesystem) | The agent needs the full file surface: recursive listing, stat, move, delete, and custom mounts. |
|
||||
| Git | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/git) | The agent works with real git repos inside the workspace: init, commit, branch, and clone via `exec`. |
|
||||
| Processes | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/processes) | The agent runs shell commands with pipes and long-lived spawned programs. |
|
||||
| Network | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/network) | The agent serves HTTP inside the VM and you need `vmFetch` or signed preview URLs. |
|
||||
| Cron | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/cron) | The workspace runs scheduled commands or recurring agent work. |
|
||||
| Tools | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/tools) | You want your backend functions exposed as CLI commands inside the workspace. |
|
||||
| Agent Session | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/agent-session) | You drive a Pi coding agent session inside the workspace. Requires `ANTHROPIC_API_KEY`. |
|
||||
| Sandbox Mounting | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/sandbox) | The agent needs native binaries or a real OS, mounted into the VM from a Docker-backed sandbox. Requires Docker. |
|
||||
| End-to-End Walkthrough | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os-e2e) | You want one runnable script covering files, processes, preview URLs, and a streaming Pi agent session. |
|
||||
|
||||
## Setup
|
||||
|
||||
The whole backend is one registry with one `agentOs()` actor:
|
||||
|
||||
```typescript
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
See the [Quickstart](/docs/agent-os/quickstart) for the client side and project layout.
|
||||
|
||||
## Workspace Model
|
||||
|
||||
- **One actor per workspace, key as identity.** `client.vm.getOrCreate(["my-agent"])` gives each agent its own workspace; key by user id for per-user dev environments. Each workspace has its own filesystem, processes, and networking with no shared state and no cross-contamination (see the [overview](/docs/agent-os)).
|
||||
- **Software packages choose what is installed.** agentOS starts with no commands installed. The `software` option installs packages such as `@rivet-dev/agent-os-common` (a meta-package of Wasm command-line tools: coreutils, sed, grep, gawk, findutils, diffutils, tar, and gzip), `@rivet-dev/agent-os-git` (git), and `@rivet-dev/agent-os-pi` (the Pi coding agent). See [Software](/docs/agent-os/software).
|
||||
- **The VM boots lazily and sleeps when idle.** The first action boots the VM (clients see a `vmBooted` event); when nothing is active, the actor sleeps and broadcasts `vmShutdown`, then wakes on the next action.
|
||||
|
||||
What survives a sleep/wake cycle (see [Persistence](/docs/agent-os/persistence)):
|
||||
|
||||
| Data | Across sleep/wake |
|
||||
| --- | --- |
|
||||
| Session transcripts and event history | Persist in actor SQLite as events stream. `listPersistedSessions` and `getSessionEvents` read them back without booting the VM, and `resumeSession` picks a session back up in a rebooted VM. |
|
||||
| Signed preview URL tokens | Persist in actor SQLite. Requests are validated against the stored token and the VM reboots lazily to serve them, so preview URLs keep working after sleep. |
|
||||
| Files | Persist when the mount is backed by a persistent driver (database-backed, S3, or a sandbox mount). In-memory mounts come back empty on wake. |
|
||||
| Processes, shells, and cron jobs | Do not persist. Restart long-running processes and reschedule cron jobs on wake (recommended extension). |
|
||||
|
||||
The actor holds itself awake while sessions, processes, shells, or hooks are active, then sleeps after a grace period.
|
||||
|
||||
## Capability Tour
|
||||
|
||||
| Area | Use It For | Key Actions | Docs | Example |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Filesystem | Give the agent a file tree to read and write | `readFile`, `writeFile`, `mkdir`, `readdir`, `move` | [Filesystem](/docs/agent-os/filesystem) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/filesystem) |
|
||||
| Processes | Run commands and long-lived programs | `exec`, `spawn`, `waitProcess`, `killProcess` | [Processes](/docs/agent-os/processes) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/processes) |
|
||||
| Shells | Interactive terminals with streamed output | `openShell`, `writeShell`, `resizeShell`, `closeShell` | [Processes](/docs/agent-os/processes) | No standalone example |
|
||||
| Networking and preview URLs | Reach services inside the VM and share them externally | `vmFetch`, `createSignedPreviewUrl`, `expireSignedPreviewUrl` | [Networking](/docs/agent-os/networking) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/network) |
|
||||
| Cron | Scheduled commands and recurring agent sessions | `scheduleCron`, `listCronJobs`, `cancelCronJob` | [Cron](/docs/agent-os/cron) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/cron) |
|
||||
| Agent sessions | Drive a coding agent inside the workspace | `createSession`, `sendPrompt`, `resumeSession`, `closeSession` | [Sessions](/docs/agent-os/sessions) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/agent-session) |
|
||||
|
||||
Two details worth knowing up front:
|
||||
|
||||
- `createSignedPreviewUrl` returns a relative path plus the token and expiry. Build the full URL with the client handle's `getGatewayUrl()` method; it is a client method, not an actor action.
|
||||
- Schedule cron jobs through the actor with the `exec` and `session` action types only. Callback cron actions are defined in server code and do not serialize through `listCronJobs`.
|
||||
|
||||
## Driving a Coding Agent Session
|
||||
|
||||
Only the Pi agent (`@rivet-dev/agent-os-pi`) is currently supported as a session agent; Amp, Claude Code, Codex, and OpenCode are coming soon. See [Sessions](/docs/agent-os/sessions).
|
||||
|
||||
1. `createSession("pi", { env: { ANTHROPIC_API_KEY } })` returns a `sessionId`. The VM does not inherit the host `process.env`, so API keys are passed explicitly per session or kept server-side through the [LLM gateway](/docs/agent-os/llm-gateway).
|
||||
2. Open a realtime connection and subscribe to `sessionEvent` to stream the agent's output, such as message chunks, as it works.
|
||||
3. `sendPrompt(sessionId, ...)` starts a turn; `cancelPrompt` stops one in flight.
|
||||
4. When the agent asks to use a tool, clients receive a `permissionRequest` event and answer with `respondPermission`, or the server auto-approves with the `onPermissionRequest` config hook (see [Permissions](/docs/agent-os/permissions)).
|
||||
5. Transcripts are persisted automatically in the universal transcript format (Agent Communication Protocol, ACP). After sleep, `resumeSession` continues a session in the rebooted VM, and `listPersistedSessions` plus `getSessionEvents` read history without booting the VM at all.
|
||||
|
||||
## Host Tools
|
||||
|
||||
Expose your backend functions to the agent as CLI commands inside the workspace. Define a toolkit with `toolKit()` and `hostTool()` (Zod-schema'd JavaScript functions on the host), pass it via `agentOs({ options: { toolKits: [...] } })`, and it is installed as a command such as `agentos-weather forecast --city Paris --days 3` and injected into the agent's system prompt. The agent calls your backend with no HTTP endpoints or MCP servers to stand up, and CLI-shaped tools are code mode compatible for large token savings. See [Tools](/docs/agent-os/tools) and the [tools example](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/tools).
|
||||
|
||||
## When to Mount a Full Sandbox
|
||||
|
||||
agentOS is not a replacement for sandboxes; it is designed to work alongside them. When a workspace needs native binaries, browsers, compilation, or desktop automation, use sandbox mounting: start a Docker-backed sandbox with `SandboxAgent.start({ sandbox: docker() })`, project its filesystem into the VM as a native directory (for example `/sandbox`) with `createSandboxFs`, and expose sandbox process control as host tools with `createSandboxToolkit`. Filesystem actions like `writeFile` and `readFile` project transparently through the mount while heavy workloads run in the container.
|
||||
|
||||
See [Sandbox Mounting](/docs/agent-os/sandbox) for the hybrid model and [agentOS vs Sandboxes](/docs/agent-os/versus-sandbox) for when each side wins: the lightweight VM has a near-zero cold start (~6 ms) and installs with `npm install`, while sandboxes are full Linux environments billed per second of uptime.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Topology | One `vm[workspaceId]` actor per agent or per user; the actor key is the workspace identity. |
|
||||
| Ingress | Actor actions for files, processes, networking, cron, and sessions; a realtime connection for streamed events. |
|
||||
| Streaming | `sessionEvent` per agent event, `processOutput` and `processExit` for spawned processes, `shellData` for interactive shells. |
|
||||
| Persistence | Session transcripts, event history, and preview tokens in actor SQLite; files persist through persistent mounts. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `vm[workspaceId]`, for example `client.vm.getOrCreate(["my-agent"])`
|
||||
- **Responsibility**: Owns one workspace. Boots the VM lazily on the first action, serves all capability actions, proxies signed preview URL requests into the VM's virtual network, and persists sessions and tokens to actor SQLite.
|
||||
- **Actions** (grouped; the most load-bearing of each area)
|
||||
- Filesystem: `readFile`, `writeFile`, `mkdir`, `readdir`, `readdirRecursive`, `stat`, `exists`, `move`, `deleteFile`
|
||||
- Processes: `exec`, `spawn`, `writeProcessStdin`, `waitProcess`, `listProcesses`, `killProcess`
|
||||
- Shells: `openShell`, `writeShell`, `resizeShell`, `closeShell`
|
||||
- Network: `vmFetch`, `createSignedPreviewUrl`, `expireSignedPreviewUrl`
|
||||
- Cron: `scheduleCron`, `listCronJobs`, `cancelCronJob`
|
||||
- Sessions: `createSession`, `sendPrompt`, `cancelPrompt`, `respondPermission`, `resumeSession`, `closeSession`, `destroySession`, `listPersistedSessions`, `getSessionEvents`
|
||||
- **Queues**
|
||||
- None
|
||||
- **Events**
|
||||
- `vmBooted`
|
||||
- `vmShutdown`
|
||||
- `sessionEvent`
|
||||
- `permissionRequest`
|
||||
- `processOutput`
|
||||
- `processExit`
|
||||
- `shellData`
|
||||
- `cronEvent`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `agent_os_sessions` and `agent_os_session_events` (session metadata plus seq-ordered transcript events)
|
||||
- `agent_os_preview_tokens` (signed preview URL tokens with expiry)
|
||||
- `agent_os_fs_entries` (file content for database-backed mounts)
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant A as vm actor
|
||||
participant V as agentOS VM
|
||||
participant P as Pi session
|
||||
|
||||
C->>A: getOrCreate(["my-agent"])
|
||||
C->>A: writeFile("/tmp/hello.txt", ...)
|
||||
Note over A,V: first action boots the VM
|
||||
A-->>C: vmBooted
|
||||
C->>A: exec("echo hello | tr a-z A-Z")
|
||||
A->>V: run command
|
||||
V-->>A: {exitCode: 0, stdout}
|
||||
C->>A: spawn("node", ["/tmp/server.mjs"])
|
||||
C->>A: createSignedPreviewUrl(8080, 60)
|
||||
A-->>C: {path, token, expiresAt}
|
||||
C->>A: fetch(gatewayUrl + path)
|
||||
Note over A: token checked in SQLite, request proxied into the VM network
|
||||
C->>A: createSession("pi", {env})
|
||||
A->>P: start session
|
||||
C->>A: sendPrompt(sessionId, ...)
|
||||
loop streamed agent output
|
||||
P-->>A: agent event
|
||||
A-->>C: sessionEvent
|
||||
end
|
||||
Note over A: idle, sleeps after grace period (vmShutdown)
|
||||
C->>A: resumeSession(sessionId)
|
||||
Note over A,V: wake reboots the VM, restoring transcripts, preview tokens, and persistent mounts
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- **Authenticate connections**: Add the `onBeforeConnect` hook in the `agentOs()` config so only authorized callers reach a workspace. Signed preview URL requests deliberately skip it because the token is the credential; browsers navigating a preview URL cannot supply actor connection params.
|
||||
- **Gate agent tool use with permissions**: Session permission requests broadcast as `permissionRequest` events for human-in-the-loop approval via `respondPermission`, or run a server-side `onPermissionRequest` policy for automated pipelines. See [Permissions](/docs/agent-os/permissions).
|
||||
- **Treat preview URLs as bearer credentials**: Tokens are randomly generated 32-character values with a default expiry of 1 hour and a maximum of 24; revoke early with `expireSignedPreviewUrl`. Preview responses carry permissive CORS headers, so do not serve private data on a preview port without app-level auth.
|
||||
- **Keep LLM credentials off the browser**: Create sessions from trusted server code with the key in `createSession` env, or keep keys entirely server-side with the [LLM gateway](/docs/agent-os/llm-gateway). Session keys are injected into the session environment inside the VM and are never stored in actor config or SQLite.
|
||||
- **Treat mounted sandboxes as their own trust boundary**: A mounted sandbox is a full Linux environment outside the workspace's Wasm and V8 isolate. Scope what its network and filesystem can reach before projecting it into an agent's VM.
|
||||
- **Set resource and cost limits**: Cap per-workspace memory and CPU (`maxMemoryMb`, `maxCpuPercent`, see [Security](/docs/agent-os/security)). Active sessions, processes, and shells hold the actor awake, so add per-workspace session caps and token budgets as a recommended extension.
|
||||
|
||||
## Reference Map
|
||||
|
||||
### Actors
|
||||
|
||||
- [Access Control](reference/actors/access-control.md)
|
||||
- [Actions](reference/actors/actions.md)
|
||||
- [Actor Keys](reference/actors/keys.md)
|
||||
- [Actor Scheduling](reference/actors/schedule.md)
|
||||
- [Actor Statuses](reference/actors/statuses.md)
|
||||
- [Authentication](reference/actors/authentication.md)
|
||||
- [Cloudflare Workers Quickstart](reference/actors/quickstart/cloudflare.md)
|
||||
- [Communicating Between Actors](reference/actors/communicating-between-actors.md)
|
||||
- [Connections](reference/actors/connections.md)
|
||||
- [Custom Inspector Tabs](reference/actors/inspector-tabs.md)
|
||||
- [Debugging](reference/actors/debugging.md)
|
||||
- [Design Patterns](reference/actors/design-patterns.md)
|
||||
- [Destroying Actors](reference/actors/destroy.md)
|
||||
- [Effect.ts Quickstart (Beta)](reference/actors/quickstart/effect.md)
|
||||
- [Errors](reference/actors/errors.md)
|
||||
- [Fetch and WebSocket Handler](reference/actors/fetch-and-websocket-handler.md)
|
||||
- [Helper Types](reference/actors/helper-types.md)
|
||||
- [Icons & Names](reference/actors/appearance.md)
|
||||
- [In-Memory State](reference/actors/state.md)
|
||||
- [Input Parameters](reference/actors/input.md)
|
||||
- [Lifecycle](reference/actors/lifecycle.md)
|
||||
- [Limits](reference/actors/limits.md)
|
||||
- [Low-Level HTTP Request Handler](reference/actors/request-handler.md)
|
||||
- [Low-Level KV Storage](reference/actors/kv.md)
|
||||
- [Low-Level WebSocket Handler](reference/actors/websocket-handler.md)
|
||||
- [Metadata](reference/actors/metadata.md)
|
||||
- [Next.js Quickstart](reference/actors/quickstart/next-js.md)
|
||||
- [Node.js & Bun Quickstart](reference/actors/quickstart/backend.md)
|
||||
- [Queues & Run Loops](reference/actors/queues.md)
|
||||
- [React Quickstart](reference/actors/quickstart/react.md)
|
||||
- [Realtime](reference/actors/events.md)
|
||||
- [Rust Quickstart (Beta)](reference/actors/quickstart/rust.md)
|
||||
- [Scaling & Concurrency](reference/actors/scaling.md)
|
||||
- [Sharing and Joining State](reference/actors/sharing-and-joining-state.md)
|
||||
- [SQLite](reference/actors/sqlite.md)
|
||||
- [SQLite + Drizzle](reference/actors/sqlite-drizzle.md)
|
||||
- [Supabase Functions Quickstart](reference/actors/quickstart/supabase.md)
|
||||
- [Testing](reference/actors/testing.md)
|
||||
- [Troubleshooting](reference/actors/troubleshooting.md)
|
||||
- [Types](reference/actors/types.md)
|
||||
- [Vanilla HTTP API](reference/actors/http-api.md)
|
||||
- [Versions & Upgrades](reference/actors/versions.md)
|
||||
- [Workflows](reference/actors/workflows.md)
|
||||
|
||||
### Agent Os
|
||||
|
||||
- [Agent-to-Agent Communication](reference/agent-os/agent-to-agent.md)
|
||||
- [agentOS vs Sandbox](reference/agent-os/versus-sandbox.md)
|
||||
- [Authentication](reference/agent-os/authentication.md)
|
||||
- [Benchmarks](reference/agent-os/benchmarks.md)
|
||||
- [Configuration](reference/agent-os/configuration.md)
|
||||
- [Core Package](reference/agent-os/core.md)
|
||||
- [Crash Course](reference/agent-os/crash-course.md)
|
||||
- [Cron Jobs](reference/agent-os/cron.md)
|
||||
- [Deployment](reference/agent-os/deployment.md)
|
||||
- [Embedded LLM Gateway](reference/agent-os/llm-gateway.md)
|
||||
- [Events](reference/agent-os/events.md)
|
||||
- [Filesystem](reference/agent-os/filesystem.md)
|
||||
- [Limitations](reference/agent-os/limitations.md)
|
||||
- [LLM Credentials](reference/agent-os/llm-credentials.md)
|
||||
- [Multiplayer](reference/agent-os/multiplayer.md)
|
||||
- [Networking & Previews](reference/agent-os/networking.md)
|
||||
- [Permissions](reference/agent-os/permissions.md)
|
||||
- [Persistence & Sleep](reference/agent-os/persistence.md)
|
||||
- [Pi](reference/agent-os/agents/pi.md)
|
||||
- [Processes & Shell](reference/agent-os/processes.md)
|
||||
- [Queues](reference/agent-os/queues.md)
|
||||
- [Quickstart](reference/agent-os/quickstart.md)
|
||||
- [Sandbox Mounting](reference/agent-os/sandbox.md)
|
||||
- [Security & Auth](reference/agent-os/security.md)
|
||||
- [Security Model](reference/agent-os/security-model.md)
|
||||
- [Sessions](reference/agent-os/sessions.md)
|
||||
- [Software](reference/agent-os/software.md)
|
||||
- [SQLite](reference/agent-os/sqlite.md)
|
||||
- [System Prompt](reference/agent-os/system-prompt.md)
|
||||
- [Tools](reference/agent-os/tools.md)
|
||||
- [Webhooks](reference/agent-os/webhooks.md)
|
||||
- [Workflow Automation](reference/agent-os/workflows.md)
|
||||
|
||||
### Cli
|
||||
|
||||
- [CLI](reference/cli.md)
|
||||
|
||||
### Clients
|
||||
|
||||
- [Node.js & Bun](reference/clients/javascript.md)
|
||||
- [React](reference/clients/react.md)
|
||||
- [Swift](reference/clients/swift.md)
|
||||
- [SwiftUI](reference/clients/swiftui.md)
|
||||
|
||||
### Cookbook
|
||||
|
||||
- [AI Agent](reference/cookbook/ai-agent.md)
|
||||
- [AI Agent Workspaces](reference/cookbook/ai-agent-workspace.md)
|
||||
- [Chat Room](reference/cookbook/chat-room.md)
|
||||
- [Collaborative Text Editor](reference/cookbook/collaborative-text-editor.md)
|
||||
- [Cron Jobs and Scheduled Tasks](reference/cookbook/cron-jobs.md)
|
||||
- [Database per Tenant](reference/cookbook/per-tenant-database.md)
|
||||
- [Deploying Rivet in a VPC or Air-Gapped Network](reference/cookbook/vpc-air-gapped.md)
|
||||
- [Live Cursors and Presence](reference/cookbook/live-cursors.md)
|
||||
- [Multiplayer Game](reference/cookbook/multiplayer-game.md)
|
||||
|
||||
### Deploy
|
||||
|
||||
- [Deploy To Amazon Web Services Lambda](reference/deploy/aws-lambda.md)
|
||||
- [Deploying to AWS ECS](reference/deploy/aws-ecs.md)
|
||||
- [Deploying to Cloudflare Workers](reference/deploy/cloudflare.md)
|
||||
- [Deploying to Freestyle](reference/deploy/freestyle.md)
|
||||
- [Deploying to Google Cloud Run](reference/deploy/gcp-cloud-run.md)
|
||||
- [Deploying to Hetzner](reference/deploy/hetzner.md)
|
||||
- [Deploying to Kubernetes](reference/deploy/kubernetes.md)
|
||||
- [Deploying to Railway](reference/deploy/railway.md)
|
||||
- [Deploying to Rivet Compute](reference/deploy/rivet-compute.md)
|
||||
- [Deploying to Supabase Functions](reference/deploy/supabase.md)
|
||||
- [Deploying to Vercel](reference/deploy/vercel.md)
|
||||
- [Deploying to VMs & Bare Metal](reference/deploy/vm-and-bare-metal.md)
|
||||
|
||||
### General
|
||||
|
||||
- [Actor Configuration](reference/general/actor-configuration.md)
|
||||
- [Architecture](reference/general/architecture.md)
|
||||
- [Cross-Origin Resource Sharing](reference/general/cors.md)
|
||||
- [Documentation for LLMs & AI](reference/general/docs-for-llms.md)
|
||||
- [Edge Networking](reference/general/edge.md)
|
||||
- [Endpoints](reference/general/endpoints.md)
|
||||
- [Environment Variables](reference/general/environment-variables.md)
|
||||
- [HTTP Server](reference/general/http-server.md)
|
||||
- [Logging](reference/general/logging.md)
|
||||
- [Pool Configuration](reference/general/pool-configuration.md)
|
||||
- [Production Checklist](reference/general/production-checklist.md)
|
||||
- [Registry Configuration](reference/general/registry-configuration.md)
|
||||
- [Runtime Modes](reference/general/runtime-modes.md)
|
||||
- [WASM vs Native SDK](reference/general/wasm-vs-native-sdk.md)
|
||||
|
||||
### Self Hosting
|
||||
|
||||
- [Configuration](reference/self-hosting/configuration.md)
|
||||
- [Docker Compose](reference/self-hosting/docker-compose.md)
|
||||
- [Docker Container](reference/self-hosting/docker-container.md)
|
||||
- [File System](reference/self-hosting/filesystem.md)
|
||||
- [FoundationDB (Enterprise)](reference/self-hosting/foundationdb.md)
|
||||
- [Installing Rivet Engine](reference/self-hosting/install.md)
|
||||
- [Kubernetes](reference/self-hosting/kubernetes.md)
|
||||
- [Multi-Region](reference/self-hosting/multi-region.md)
|
||||
- [PostgreSQL](reference/self-hosting/postgres.md)
|
||||
- [Production Checklist](reference/self-hosting/production-checklist.md)
|
||||
- [Railway Deployment](reference/self-hosting/railway.md)
|
||||
- [Render Deployment](reference/self-hosting/render.md)
|
||||
- [TLS & Certificates](reference/self-hosting/tls.md)
|
||||
|
||||
904
.agents/skills/ai-agent-workspace/index.json
Normal file
904
.agents/skills/ai-agent-workspace/index.json
Normal file
@@ -0,0 +1,904 @@
|
||||
{
|
||||
"name": "ai-agent-workspace",
|
||||
"description": "Give every AI agent its own computer: a persistent workspace with a filesystem, processes, shells, networking, and agent sessions on a lightweight in-process OS.",
|
||||
"skill_url": "/metadata/skills/ai-agent-workspace/SKILL.md",
|
||||
"generated_at": "2026-06-15T20:24:02.496Z",
|
||||
"references": [
|
||||
{
|
||||
"name": "actors/access-control",
|
||||
"title": "Access Control",
|
||||
"description": "Authorize actions, queue publishes, and event subscriptions with explicit hooks.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/access-control",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/access-control.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/actions",
|
||||
"title": "Actions",
|
||||
"description": "Actions are how your backend, frontend, or other actors can communicate with actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/actions",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/actions.md"
|
||||
},
|
||||
{
|
||||
"name": "general/actor-configuration",
|
||||
"title": "Actor Configuration",
|
||||
"description": "This page documents the configuration options available when defining a RivetKit actor. The actor configuration is passed to the `actor()` function.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/actor-configuration",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/actor-configuration.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/keys",
|
||||
"title": "Actor Keys",
|
||||
"description": "Actor keys uniquely identify actor instances within each actor type. Keys are used for addressing which specific actor to communicate with.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/keys",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/keys.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/schedule",
|
||||
"title": "Actor Scheduling",
|
||||
"description": "Schedule actor actions in the future with persistent timers that survive restarts and upgrades.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/schedule",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/schedule.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/statuses",
|
||||
"title": "Actor Statuses",
|
||||
"description": "Understand the lifecycle statuses of Rivet Actors, what they mean, how they appear in the API, and how to troubleshoot common issues.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/statuses",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/statuses.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/agent-to-agent",
|
||||
"title": "Agent-to-Agent Communication",
|
||||
"description": "Use host tools to let agents communicate with each other.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/agent-to-agent",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/agent-to-agent.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/versus-sandbox",
|
||||
"title": "agentOS vs Sandbox",
|
||||
"description": "When to use the lightweight agentOS VM, a full sandbox, or both together.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/versus-sandbox",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/versus-sandbox.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/ai-agent",
|
||||
"title": "AI Agent",
|
||||
"description": "Build an AI agent backend with persistent memory: one Rivet Actor per conversation, queued message handling, and streaming LLM responses as realtime events.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/ai-agent",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/ai-agent.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/ai-agent-workspace",
|
||||
"title": "AI Agent Workspaces",
|
||||
"description": "Give every AI agent its own computer: a persistent workspace with a filesystem, processes, shells, networking, and agent sessions on a lightweight in-process OS.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/ai-agent-workspace",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/ai-agent-workspace.md"
|
||||
},
|
||||
{
|
||||
"name": "general/architecture",
|
||||
"title": "Architecture",
|
||||
"description": "- rivetkit is the typescript library used for both local development & to connect your application to rivet - a rivetkit instance is called a \"runner.\" you can run multiple runners to scale rivetkit horiziotnally. read omre about runners below.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/architecture",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/architecture.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/authentication",
|
||||
"title": "Authentication",
|
||||
"description": "Secure your actors with authentication and authorization.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/authentication",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/authentication.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/authentication",
|
||||
"title": "Authentication",
|
||||
"description": "Authenticate connections to agentOS actors using hooks.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/authentication",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/authentication.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/benchmarks",
|
||||
"title": "Benchmarks",
|
||||
"description": "Performance benchmarks comparing agentOS to traditional sandbox providers.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/benchmarks",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/benchmarks.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/chat-room",
|
||||
"title": "Chat Room",
|
||||
"description": "Build a realtime chat room backend with Rivet Actors: one actor per room, SQLite-backed message history, and WebSocket broadcast to every connected client.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/chat-room",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/chat-room.md"
|
||||
},
|
||||
{
|
||||
"name": "cli",
|
||||
"title": "CLI",
|
||||
"description": "Reference for the optional rivet CLI: deploy to Rivet Compute and run local dev for serverless platforms.",
|
||||
"canonical_url": "https://rivet.dev/docs/cli",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cli.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/cloudflare",
|
||||
"title": "Cloudflare Workers Quickstart",
|
||||
"description": "Set up a Rivet project locally targeting Cloudflare Workers.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/cloudflare",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/cloudflare.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/collaborative-text-editor",
|
||||
"title": "Collaborative Text Editor",
|
||||
"description": "Build a collaborative text editor backend with Yjs CRDTs and Rivet Actors: per-document actors relay sync and awareness updates and persist snapshots.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/collaborative-text-editor",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/collaborative-text-editor.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/communicating-between-actors",
|
||||
"title": "Communicating Between Actors",
|
||||
"description": "Learn how actors can call other actors and share data",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/communicating-between-actors",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/communicating-between-actors.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/configuration",
|
||||
"title": "Configuration",
|
||||
"description": "Configure the agentOS VM options, preview settings, and lifecycle hooks.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/configuration",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/configuration.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/configuration",
|
||||
"title": "Configuration",
|
||||
"description": "Rivet Engine can be configured through environment variables or configuration files.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/configuration",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/configuration.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/connections",
|
||||
"title": "Connections",
|
||||
"description": "Connections represent client connections to your actor. They provide a way to handle client authentication, manage connection-specific data, and control the connection lifecycle.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/connections",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/connections.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/core",
|
||||
"title": "Core Package",
|
||||
"description": "Use @rivet-dev/agent-os-core standalone for direct VM control without the Rivet Actor runtime.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/core",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/core.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/crash-course",
|
||||
"title": "Crash Course",
|
||||
"description": "Run coding agents inside isolated VMs with full filesystem, process, and network control.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/crash-course",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/crash-course.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/cron",
|
||||
"title": "Cron Jobs",
|
||||
"description": "Schedule recurring commands and agent sessions in agentOS VMs.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/cron",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/cron.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/cron-jobs",
|
||||
"title": "Cron Jobs and Scheduled Tasks",
|
||||
"description": "Durable cron jobs with Rivet Actors: schedule.after and schedule.at timers survive restarts and crashes, plus re-arming recurring jobs and idempotent handlers.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/cron-jobs",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/cron-jobs.md"
|
||||
},
|
||||
{
|
||||
"name": "general/cors",
|
||||
"title": "Cross-Origin Resource Sharing",
|
||||
"description": "Cross-Origin Resource Sharing (CORS) controls which origins (domains) can access your actors. When actors are exposed to the public internet, proper origin validation is critical to prevent security breaches and denial of service attacks.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/cors",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/cors.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/inspector-tabs",
|
||||
"title": "Custom Inspector Tabs",
|
||||
"description": "Ship your own UI tabs alongside a Rivet Actor — embedded directly in the dashboard inspector.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/inspector-tabs",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/inspector-tabs.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/per-tenant-database",
|
||||
"title": "Database per Tenant",
|
||||
"description": "Multi-tenant data isolation with one Rivet Actor per tenant: the actor key is the tenant id, so each tenant gets its own isolated dataset and migrations.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/per-tenant-database",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/per-tenant-database.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/debugging",
|
||||
"title": "Debugging",
|
||||
"description": "Inspect and debug running Rivet Actors, runners, and provider configs using management, runner, and actor inspector HTTP APIs.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/debugging",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/debugging.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/aws-lambda",
|
||||
"title": "Deploy To Amazon Web Services Lambda",
|
||||
"description": "_AWS Lambda is coming soon_",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/aws-lambda",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/aws-lambda.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/vpc-air-gapped",
|
||||
"title": "Deploying Rivet in a VPC or Air-Gapped Network",
|
||||
"description": "Run Rivet entirely inside your own perimeter: single-binary or Docker Compose install, file system storage with no database infrastructure, and no outbound telemetry by default.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/vpc-air-gapped",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/vpc-air-gapped.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/aws-ecs",
|
||||
"title": "Deploying to AWS ECS",
|
||||
"description": "Run your backend on Amazon ECS with Fargate.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/aws-ecs",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/aws-ecs.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/cloudflare",
|
||||
"title": "Deploying to Cloudflare Workers",
|
||||
"description": "Deploy an existing Rivet project to Cloudflare Workers.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/cloudflare",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/cloudflare.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/freestyle",
|
||||
"title": "Deploying to Freestyle",
|
||||
"description": "Deploy RivetKit app to Freestyle.sh, a cloud platform for running AI-generated code with built-in security and scalability.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/freestyle",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/freestyle.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/gcp-cloud-run",
|
||||
"title": "Deploying to Google Cloud Run",
|
||||
"description": "Deploy your RivetKit app to Google Cloud Run.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/gcp-cloud-run",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/gcp-cloud-run.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/hetzner",
|
||||
"title": "Deploying to Hetzner",
|
||||
"description": "Please see the VM & Bare Metal guide.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/hetzner",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/hetzner.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/kubernetes",
|
||||
"title": "Deploying to Kubernetes",
|
||||
"description": "Deploy your RivetKit app to any Kubernetes cluster.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/kubernetes",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/kubernetes.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/railway",
|
||||
"title": "Deploying to Railway",
|
||||
"description": "Deploy your RivetKit app to Railway.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/railway",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/railway.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/rivet-compute",
|
||||
"title": "Deploying to Rivet Compute",
|
||||
"description": "Run your backend on Rivet Compute.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/rivet-compute",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/rivet-compute.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/supabase",
|
||||
"title": "Deploying to Supabase Functions",
|
||||
"description": "Deploy an existing Rivet project to Supabase Edge Functions.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/supabase",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/supabase.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/vercel",
|
||||
"title": "Deploying to Vercel",
|
||||
"description": "Deploy your Next.js Rivet app to Vercel.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/vercel",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/vercel.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/vm-and-bare-metal",
|
||||
"title": "Deploying to VMs & Bare Metal",
|
||||
"description": "Deploy your RivetKit app to any Linux VM or bare metal host.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/vm-and-bare-metal",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/deploy/vm-and-bare-metal.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/deployment",
|
||||
"title": "Deployment",
|
||||
"description": "Choose the right deployment option for agentOS.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/deployment",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/deployment.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/design-patterns",
|
||||
"title": "Design Patterns",
|
||||
"description": "Common patterns and anti-patterns for building scalable actor systems.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/design-patterns",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/design-patterns.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/destroy",
|
||||
"title": "Destroying Actors",
|
||||
"description": "Actors can be permanently destroyed. Common use cases include:",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/destroy",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/destroy.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/docker-compose",
|
||||
"title": "Docker Compose",
|
||||
"description": "Deploy Rivet Engine with docker-compose for multi-container setups.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/docker-compose",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/docker-compose.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/docker-container",
|
||||
"title": "Docker Container",
|
||||
"description": "Run Rivet Engine in a single Docker container.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/docker-container",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/docker-container.md"
|
||||
},
|
||||
{
|
||||
"name": "general/docs-for-llms",
|
||||
"title": "Documentation for LLMs & AI",
|
||||
"description": "Rivet provides optimized documentation formats specifically designed for Large Language Models (LLMs) and AI integration tools.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/docs-for-llms",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/docs-for-llms.md"
|
||||
},
|
||||
{
|
||||
"name": "general/edge",
|
||||
"title": "Edge Networking",
|
||||
"description": "Actors automatically run near your users on your provider's global network.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/edge",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/edge.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/effect",
|
||||
"title": "Effect.ts Quickstart (Beta)",
|
||||
"description": "Build a Rivet Actor with the Effect SDK",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/effect",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/effect.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/llm-gateway",
|
||||
"title": "Embedded LLM Gateway",
|
||||
"description": "Route, meter, and manage LLM API calls from agents.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/llm-gateway",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/llm-gateway.md"
|
||||
},
|
||||
{
|
||||
"name": "general/endpoints",
|
||||
"title": "Endpoints",
|
||||
"description": "Configure how your backend connects to Rivet and how clients reach your actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/endpoints",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/endpoints.md"
|
||||
},
|
||||
{
|
||||
"name": "general/environment-variables",
|
||||
"title": "Environment Variables",
|
||||
"description": "This page documents all environment variables that configure RivetKit behavior.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/environment-variables",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/environment-variables.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/errors",
|
||||
"title": "Errors",
|
||||
"description": "Rivet provides robust error handling with security built in by default. Errors are handled differently based on whether they should be exposed to clients or kept private.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/errors",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/errors.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/events",
|
||||
"title": "Events",
|
||||
"description": "Full event catalog with payload shapes for agentOS.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/events",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/events.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/fetch-and-websocket-handler",
|
||||
"title": "Fetch and WebSocket Handler",
|
||||
"description": "These docs have moved to [Low-Level WebSocket Handler](/docs/actors/websocket-handler) and [Low-Level Request Handler](/docs/actors/request-handler).",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/fetch-and-websocket-handler",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/fetch-and-websocket-handler.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/filesystem",
|
||||
"title": "File System",
|
||||
"description": "The file system backend stores all data on the local disk. This is suitable for single-node deployments, development, and testing.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/filesystem",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/filesystem.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/filesystem",
|
||||
"title": "Filesystem",
|
||||
"description": "Read, write, mount, and manage files inside agentOS.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/filesystem",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/filesystem.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/foundationdb",
|
||||
"title": "FoundationDB (Enterprise)",
|
||||
"description": "FoundationDB is the recommended storage backend for scalable production Rivet deployments.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/foundationdb",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/foundationdb.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/helper-types",
|
||||
"title": "Helper Types",
|
||||
"description": "This page has moved to [Types](/docs/actors/types).",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/helper-types",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/helper-types.md"
|
||||
},
|
||||
{
|
||||
"name": "general/http-server",
|
||||
"title": "HTTP Server",
|
||||
"description": "Different ways to run your RivetKit HTTP server.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/http-server",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/http-server.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/appearance",
|
||||
"title": "Icons & Names",
|
||||
"description": "Customize actors with display names and icons for the Rivet inspector and dashboard.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/appearance",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/appearance.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/state",
|
||||
"title": "In-Memory State",
|
||||
"description": "Actors store state in memory for instant reads and writes. State can be persisted automatically or kept ephemeral.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/state",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/state.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/input",
|
||||
"title": "Input Parameters",
|
||||
"description": "Pass initialization data to actors when creating instances",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/input",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/input.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/install",
|
||||
"title": "Installing Rivet Engine",
|
||||
"description": "Install Rivet Engine using Docker, binaries, or a source build.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/install",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/install.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/kubernetes",
|
||||
"title": "Kubernetes",
|
||||
"description": "Deploy production-ready Rivet Engine to Kubernetes with PostgreSQL storage.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/kubernetes",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/kubernetes.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/lifecycle",
|
||||
"title": "Lifecycle",
|
||||
"description": "Learn about actor lifecycle hooks for initialization, state management, and cleanup.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/lifecycle",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/lifecycle.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/limitations",
|
||||
"title": "Limitations",
|
||||
"description": "What the agentOS VM does not support, and how to work around it.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/limitations",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/limitations.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/limits",
|
||||
"title": "Limits",
|
||||
"description": "Limits and constraints for Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/limits",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/limits.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/live-cursors",
|
||||
"title": "Live Cursors and Presence",
|
||||
"description": "Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/live-cursors",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/live-cursors.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/llm-credentials",
|
||||
"title": "LLM Credentials",
|
||||
"description": "Pass LLM API keys to agent sessions securely.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/llm-credentials",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/llm-credentials.md"
|
||||
},
|
||||
{
|
||||
"name": "general/logging",
|
||||
"title": "Logging",
|
||||
"description": "Actors provide a built-in way to log complex data to the console.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/logging",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/logging.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/request-handler",
|
||||
"title": "Low-Level HTTP Request Handler",
|
||||
"description": "Actors can handle HTTP requests through the `onRequest` handler.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/request-handler",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/request-handler.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/kv",
|
||||
"title": "Low-Level KV Storage",
|
||||
"description": "Use the built-in key-value store on ActorContext for durable string and binary data alongside actor state.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/kv",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/kv.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/websocket-handler",
|
||||
"title": "Low-Level WebSocket Handler",
|
||||
"description": "Actors can handle WebSocket connections through the `onWebSocket` handler.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/websocket-handler",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/websocket-handler.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/metadata",
|
||||
"title": "Metadata",
|
||||
"description": "Metadata provides information about the currently running actor.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/metadata",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/metadata.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/multi-region",
|
||||
"title": "Multi-Region",
|
||||
"description": "Rivet Engine supports scaling transparently across multiple regions.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/multi-region",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/multi-region.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/multiplayer",
|
||||
"title": "Multiplayer",
|
||||
"description": "Connect multiple clients to the same agentOS actor for collaborative agent workflows.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/multiplayer",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/multiplayer.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/multiplayer-game",
|
||||
"title": "Multiplayer Game",
|
||||
"description": "Pragmatic patterns for building multiplayer games: matchmaking, tick loops, realtime state, interest management, and validation.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/multiplayer-game",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/cookbook/multiplayer-game.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/networking",
|
||||
"title": "Networking & Previews",
|
||||
"description": "Proxy HTTP requests into agentOS VMs and create shareable preview URLs.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/networking",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/networking.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/next-js",
|
||||
"title": "Next.js Quickstart",
|
||||
"description": "Get started with Rivet Actors in Next.js",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/next-js",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/next-js.md"
|
||||
},
|
||||
{
|
||||
"name": "clients/javascript",
|
||||
"title": "Node.js & Bun",
|
||||
"description": "Connect JavaScript apps to Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/clients/javascript",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/clients/javascript.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/backend",
|
||||
"title": "Node.js & Bun Quickstart",
|
||||
"description": "Get started with Rivet Actors in Node.js and Bun",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/backend",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/backend.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/permissions",
|
||||
"title": "Permissions",
|
||||
"description": "Approve or deny agent tool use with human-in-the-loop or auto-approve patterns.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/permissions",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/permissions.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/persistence",
|
||||
"title": "Persistence & Sleep",
|
||||
"description": "How agentOS persists data and manages sleep/wake cycles.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/persistence",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/persistence.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/agents/pi",
|
||||
"title": "Pi",
|
||||
"description": "Run the Pi coding agent inside a VM with extensions and custom configuration.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/agents/pi",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/agents/pi.md"
|
||||
},
|
||||
{
|
||||
"name": "general/pool-configuration",
|
||||
"title": "Pool Configuration",
|
||||
"description": "Reference for runner pool configuration, including drain behavior, actor eviction rate limiting, and serverless-specific options.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/pool-configuration",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/pool-configuration.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/postgres",
|
||||
"title": "PostgreSQL",
|
||||
"description": "Configure PostgreSQL for self-hosted Rivet deployments.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/postgres",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/postgres.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/processes",
|
||||
"title": "Processes & Shell",
|
||||
"description": "Execute commands, spawn long-running processes, and open interactive shells in agentOS VMs.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/processes",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/processes.md"
|
||||
},
|
||||
{
|
||||
"name": "general/production-checklist",
|
||||
"title": "Production Checklist",
|
||||
"description": "Checklist for deploying Rivet Actors to production.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/production-checklist",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/production-checklist.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/production-checklist",
|
||||
"title": "Production Checklist",
|
||||
"description": "Checklist for deploying a self-hosted Rivet Engine to production.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/production-checklist",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/production-checklist.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/queues",
|
||||
"title": "Queues",
|
||||
"description": "Serialize agent work with durable queues for backpressure and rate limiting.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/queues",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/queues.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/queues",
|
||||
"title": "Queues & Run Loops",
|
||||
"description": "Use actor-local durable queues for serial run loops and request/response workflows.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/queues",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/queues.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/quickstart",
|
||||
"title": "Quickstart",
|
||||
"description": "Set up an agentOS actor, create a session, and run your first coding agent.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/quickstart",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/quickstart.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/railway",
|
||||
"title": "Railway Deployment",
|
||||
"description": "Railway provides a simple platform for deploying Rivet Engine with automatic scaling and managed infrastructure.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/railway",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/railway.md"
|
||||
},
|
||||
{
|
||||
"name": "clients/react",
|
||||
"title": "React",
|
||||
"description": "Connect React apps to Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/clients/react",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/clients/react.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/react",
|
||||
"title": "React Quickstart",
|
||||
"description": "Build realtime React applications with Rivet Actors",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/react",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/react.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/events",
|
||||
"title": "Realtime",
|
||||
"description": "Events enable realtime communication from actors to clients. While clients use actions to send data to actors, events allow actors to push updates to connected clients instantly.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/events",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/events.md"
|
||||
},
|
||||
{
|
||||
"name": "general/registry-configuration",
|
||||
"title": "Registry Configuration",
|
||||
"description": "This page documents the configuration options available when setting up a RivetKit registry. The registry configuration is passed to the `setup()` function.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/registry-configuration",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/registry-configuration.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/render",
|
||||
"title": "Render Deployment",
|
||||
"description": "Deploy Rivet Engine to Render with managed PostgreSQL and automatic HTTPS, using the experimental PostgreSQL backend.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/render",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/render.md"
|
||||
},
|
||||
{
|
||||
"name": "general/runtime-modes",
|
||||
"title": "Runtime Modes",
|
||||
"description": "RivetKit supports two runtime modes for running your actors:",
|
||||
"canonical_url": "https://rivet.dev/docs/general/runtime-modes",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/runtime-modes.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/rust",
|
||||
"title": "Rust Quickstart (Beta)",
|
||||
"description": "Build a Rivet Actor in Rust",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/rust",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/rust.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/sandbox",
|
||||
"title": "Sandbox Mounting",
|
||||
"description": "Extend agentOS with full sandboxes for heavy workloads like browsers, desktop automation, and compilation.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/sandbox",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/sandbox.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/scaling",
|
||||
"title": "Scaling & Concurrency",
|
||||
"description": "This page has moved to [design patterns](/docs/actors/design-patterns).",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/scaling",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/scaling.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/security",
|
||||
"title": "Security & Auth",
|
||||
"description": "Configure resource limits, network control, authentication, and filesystem isolation for agentOS.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/security",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/security.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/security-model",
|
||||
"title": "Security Model",
|
||||
"description": "Trust boundaries, isolation guarantees, and the agentOS threat model.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/security-model",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/security-model.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/sessions",
|
||||
"title": "Sessions",
|
||||
"description": "Create agent sessions, send prompts, stream responses, and replay event history.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/sessions",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/sessions.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/sharing-and-joining-state",
|
||||
"title": "Sharing and Joining State",
|
||||
"description": "This page has moved to [design patterns](/docs/actors/design-patterns).",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/sharing-and-joining-state",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/sharing-and-joining-state.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/software",
|
||||
"title": "Software",
|
||||
"description": "Install software packages and configure the commands available inside agentOS.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/software",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/software.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/sqlite",
|
||||
"title": "SQLite",
|
||||
"description": "Use embedded SQLite in Rivet Actors with raw SQL queries.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/sqlite",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/sqlite.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/sqlite",
|
||||
"title": "SQLite",
|
||||
"description": "Give agents access to a persistent SQLite database via host tools.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/sqlite",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/sqlite.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/sqlite-drizzle",
|
||||
"title": "SQLite + Drizzle",
|
||||
"description": "Use Drizzle ORM with embedded SQLite in Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/sqlite-drizzle",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/sqlite-drizzle.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/quickstart/supabase",
|
||||
"title": "Supabase Functions Quickstart",
|
||||
"description": "Set up a Rivet project locally targeting Supabase Edge Functions.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/quickstart/supabase",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/quickstart/supabase.md"
|
||||
},
|
||||
{
|
||||
"name": "clients/swift",
|
||||
"title": "Swift",
|
||||
"description": "Connect Swift apps to Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/clients/swift",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/clients/swift.md"
|
||||
},
|
||||
{
|
||||
"name": "clients/swiftui",
|
||||
"title": "SwiftUI",
|
||||
"description": "Build SwiftUI apps with Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/clients/swiftui",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/clients/swiftui.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/system-prompt",
|
||||
"title": "System Prompt",
|
||||
"description": "How agentOS injects context into agent sessions.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/system-prompt",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/system-prompt.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/testing",
|
||||
"title": "Testing",
|
||||
"description": "Rivet provides a straightforward testing framework to build reliable and maintainable applications. This guide covers how to write effective tests for your actor-based services.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/testing",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/testing.md"
|
||||
},
|
||||
{
|
||||
"name": "self-hosting/tls",
|
||||
"title": "TLS & Certificates",
|
||||
"description": "How Rivet validates TLS root certificates.",
|
||||
"canonical_url": "https://rivet.dev/docs/self-hosting/tls",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/self-hosting/tls.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/tools",
|
||||
"title": "Tools",
|
||||
"description": "Expose custom tools to agents as CLI commands inside the VM.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/tools",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/tools.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/troubleshooting",
|
||||
"title": "Troubleshooting",
|
||||
"description": "Common issues with Rivet Actors and how to resolve them.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/troubleshooting",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/troubleshooting.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/types",
|
||||
"title": "Types",
|
||||
"description": "TypeScript types for working with Rivet Actors. This page covers context types used in lifecycle hooks and actions, as well as helper types for extracting types from actor definitions.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/types",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/types.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/http-api",
|
||||
"title": "Vanilla HTTP API",
|
||||
"description": "Use the low-level HTTP handler to send and receive requests from actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/http-api",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/http-api.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/versions",
|
||||
"title": "Versions & Upgrades",
|
||||
"description": "When you deploy new code, Rivet ensures actors are upgraded seamlessly without downtime.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/versions",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/versions.md"
|
||||
},
|
||||
{
|
||||
"name": "general/wasm-vs-native-sdk",
|
||||
"title": "WASM vs Native SDK",
|
||||
"description": "RivetKit runs your actors on a native or a WebAssembly runtime depending on your platform.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/wasm-vs-native-sdk",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/general/wasm-vs-native-sdk.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/webhooks",
|
||||
"title": "Webhooks",
|
||||
"description": "Trigger agent workflows from external webhooks using Hono and queues.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/webhooks",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/webhooks.md"
|
||||
},
|
||||
{
|
||||
"name": "agent-os/workflows",
|
||||
"title": "Workflow Automation",
|
||||
"description": "Orchestrate multi-step agent tasks with durable workflows.",
|
||||
"canonical_url": "https://rivet.dev/docs/agent-os/workflows",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/agent-os/workflows.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/workflows",
|
||||
"title": "Workflows",
|
||||
"description": "Build durable, replayable run loops in Rivet Actors with steps, queue waits, timers, and rollback.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/workflows",
|
||||
"reference_url": "/metadata/skills/ai-agent-workspace/reference/actors/workflows.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
1311
.agents/skills/ai-agent-workspace/openapi.json
Normal file
1311
.agents/skills/ai-agent-workspace/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
# Access Control
|
||||
|
||||
> Source: `src/content/docs/actors/access-control.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/access-control
|
||||
> Description: Authorize actions, queue publishes, and event subscriptions with explicit hooks.
|
||||
|
||||
---
|
||||
Use access control to decide what authenticated clients are allowed to do.
|
||||
|
||||
This is authorization, not authentication:
|
||||
|
||||
- Use [authentication](/docs/actors/authentication) to identify who is calling.
|
||||
- Use access-control rules to decide what they can do after connecting.
|
||||
|
||||
## Permission Surfaces
|
||||
|
||||
RivetKit authorization is explicit per surface:
|
||||
|
||||
- `onBeforeConnect` rejects unauthenticated or malformed connections.
|
||||
- Action handlers (`actions.*`) enforce action permissions.
|
||||
- `queues.<name>.canPublish` allows or denies inbound queue publishes.
|
||||
- `events.<name>.canSubscribe` allows or denies event subscriptions.
|
||||
|
||||
## Fail By Default
|
||||
|
||||
Use deny-by-default rules everywhere:
|
||||
|
||||
1. Keep `onBeforeConnect` strict and reject invalid credentials.
|
||||
2. In each action, explicitly allow expected roles and throw `forbidden` otherwise.
|
||||
3. In `canPublish` and `canSubscribe`, return `true` only for allowed roles and end with `return false`.
|
||||
|
||||
```ts
|
||||
import { actor, event, queue, UserError } from "rivetkit";
|
||||
|
||||
type ConnParams = {
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
type ConnState = {
|
||||
userId: string;
|
||||
role: "member" | "admin";
|
||||
};
|
||||
|
||||
async function authenticate(
|
||||
authToken: string,
|
||||
): Promise<ConnState | null> {
|
||||
if (authToken === "admin-token") {
|
||||
return { userId: "admin-1", role: "admin" };
|
||||
}
|
||||
if (authToken === "member-token") {
|
||||
return { userId: "member-1", role: "member" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const chatRoom = actor({
|
||||
state: { messages: [] as Array<{ userId: string; text: string }> },
|
||||
|
||||
onBeforeConnect: async (_c, params: ConnParams) => {
|
||||
if (!params.authToken) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
|
||||
const session = await authenticate(params.authToken);
|
||||
if (!session) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
},
|
||||
|
||||
createConnState: async (_c, params: ConnParams): Promise<ConnState> => {
|
||||
const session = await authenticate(params.authToken);
|
||||
if (!session) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
return session;
|
||||
},
|
||||
|
||||
events: {
|
||||
messages: event<{ userId: string; text: string }>(),
|
||||
moderationLog: event<{ entry: string }>({
|
||||
canSubscribe: (c) => {
|
||||
if (c.conn?.state.role === "admin") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
queues: {
|
||||
moderationJobs: queue<{ action: "ban"; userId: string }>({
|
||||
canPublish: (c) => {
|
||||
if (c.conn?.state.role === "admin") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
actions: {
|
||||
sendMessage: (c, text: string) => {
|
||||
const role = c.conn?.state.role;
|
||||
const userId = c.conn?.state.userId;
|
||||
|
||||
if (!userId || (role !== "member" && role !== "admin")) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
|
||||
const message = { userId, text };
|
||||
c.state.messages.push(message);
|
||||
c.broadcast("messages", message);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Return Value Contract
|
||||
|
||||
`canPublish` and `canSubscribe` must return a boolean:
|
||||
|
||||
- `true`: allow
|
||||
- `false`: deny with `forbidden`
|
||||
|
||||
Returning `undefined`, `null`, or any non-boolean throws an internal error.
|
||||
|
||||
## Notes
|
||||
|
||||
- `canPublish` only applies to queue names defined in `queues`.
|
||||
- Incoming queue messages for undefined queues are ignored and the publish succeeds as completed.
|
||||
- `canSubscribe` only applies to event names defined in `events`.
|
||||
- Broadcasting an event not defined in `events` still publishes to subscribers.
|
||||
|
||||
_Source doc path: /docs/actors/access-control_
|
||||
384
.agents/skills/ai-agent-workspace/reference/actors/actions.md
Normal file
384
.agents/skills/ai-agent-workspace/reference/actors/actions.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# Actions
|
||||
|
||||
> Source: `src/content/docs/actors/actions.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/actions
|
||||
> Description: Actions are how your backend, frontend, or other actors can communicate with actors.
|
||||
|
||||
---
|
||||
Actions are very lightweight. They can be called thousands of times per second safely. Actions are executed via HTTP requests or via WebSockets if [using `.connect()`](/docs/actors/connections).
|
||||
|
||||
For advanced use cases that require direct access to HTTP requests or WebSocket connections, see [raw HTTP and WebSocket handling](/docs/actors/fetch-and-websocket-handler).
|
||||
|
||||
By default, actions run in parallel. If you need advanced control over concurrency, use [queues](/docs/actors/queues).
|
||||
|
||||
## Writing Actions
|
||||
|
||||
Actions are defined in the `actions` object when creating an actor:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const mathUtils = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
// This is an action
|
||||
multiplyByTwo: (c, x: number) => {
|
||||
return x * 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Each action receives a context object (commonly named `c`) as its first parameter, which provides access to state, connections, and other utilities. Additional parameters follow after that.
|
||||
|
||||
## Calling Actions
|
||||
|
||||
Actions can be called in different ways depending on your use case:
|
||||
|
||||
### Frontend (createClient)
|
||||
|
||||
```typescript frontend.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Define actor
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount: number) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create registry
|
||||
const registry = setup({ use: { counter } });
|
||||
|
||||
// Create client
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const counterActor = await client.counter.getOrCreate();
|
||||
const result = await counterActor.increment(42);
|
||||
console.log(result); // The value returned by the action
|
||||
```
|
||||
|
||||
Learn more about [communicating with actors from the frontend](/docs/actors/communicating-between-actors).
|
||||
|
||||
### Backend (registry.handler)
|
||||
|
||||
```typescript server.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { Hono } from "hono";
|
||||
|
||||
// Define actor
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount: number) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create registry
|
||||
const registry = setup({ use: { counter } });
|
||||
|
||||
// Create client
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Mount Rivet handler
|
||||
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
|
||||
|
||||
// Use the client to call actions on a request
|
||||
app.get("/foo", async (c) => {
|
||||
const counterActor = client.counter.getOrCreate();
|
||||
const result = await counterActor.increment(42);
|
||||
return c.text(String(result));
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
Learn more about [communicating with actors from the backend](/docs/actors/communicating-between-actors).
|
||||
|
||||
### Actor-to-Actor (c.client())
|
||||
|
||||
```typescript actor.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Define counter actor
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount: number) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Define actorA that calls counter
|
||||
const actorA = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
callOtherActor: async (c) => {
|
||||
const client = c.client();
|
||||
const counterActor = await client.counter.getOrCreate();
|
||||
return await counterActor.increment(10);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create registry
|
||||
export const registry = setup({ use: { counter, actorA } });
|
||||
```
|
||||
|
||||
Learn more about [communicating between actors](/docs/actors/communicating-between-actors).
|
||||
|
||||
Calling actions from the client are async and require an `await`, even if the action itself is not async.
|
||||
|
||||
### Type Safety
|
||||
|
||||
The actor client includes type safety out of the box. When you use `createClient<typeof registry>()`, TypeScript automatically infers action parameter and return types:
|
||||
|
||||
```typescript index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Create simple counter
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, count: number) => {
|
||||
c.state.count += count;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create and export the registry
|
||||
export const registry = setup({
|
||||
use: { counter }
|
||||
});
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
// Define the actor inline for type inference
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, count: number) => {
|
||||
c.state.count += count;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { counter } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Type-safe client usage
|
||||
const counterActor = await client.counter.get();
|
||||
await counterActor.increment(123); // OK
|
||||
// await counterActor.increment("non-number type"); // TypeScript error
|
||||
// await counterActor.nonexistentMethod(123); // TypeScript error
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Actors provide robust error handling out of the box for actions.
|
||||
|
||||
### User Errors
|
||||
|
||||
`UserError` can be used to return rich error data to the client. You can provide:
|
||||
|
||||
- A human-readable message
|
||||
- A machine-readable code that's useful for matching errors in a try-catch (optional)
|
||||
- A metadata object for providing richer error context (optional)
|
||||
|
||||
For example:
|
||||
|
||||
```typescript actor.ts
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => {
|
||||
// Validate username
|
||||
if (username.length > 32) {
|
||||
// Throw a simple error with a message
|
||||
throw new UserError("Username is too long", {
|
||||
code: "username_too_long",
|
||||
metadata: {
|
||||
maxLength: 32
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update username
|
||||
c.state.username = username;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { actor, setup, UserError } from "rivetkit";
|
||||
import { ActorError, createClient } from "rivetkit/client";
|
||||
|
||||
// Define the user actor
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => {
|
||||
if (username.length > 32) {
|
||||
throw new UserError("Username is too long", {
|
||||
code: "username_too_long",
|
||||
metadata: { maxLength: 32 }
|
||||
});
|
||||
}
|
||||
c.state.username = username;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { user } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const userActor = await client.user.getOrCreate();
|
||||
|
||||
try {
|
||||
await userActor.updateUsername("extremely_long_username_that_exceeds_limit");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log("Message", error.message); // "Username is too long"
|
||||
console.log("Code", error.code); // "username_too_long"
|
||||
console.log("Metadata", error.metadata); // { maxLength: 32 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Internal Errors
|
||||
|
||||
All other errors will return an error with the code `internal_error` to the client. This helps keep your application secure, as errors can sometimes expose sensitive information.
|
||||
|
||||
## Schema Validation
|
||||
|
||||
If passing data to an actor from the frontend, use a library like [Zod](https://zod.dev/) to validate input data.
|
||||
|
||||
For example, to validate action parameters:
|
||||
|
||||
```typescript actor.ts
|
||||
import { actor, UserError } from "rivetkit";
|
||||
import { z } from "zod";
|
||||
|
||||
// Define schema for action parameters
|
||||
const IncrementSchema = z.object({
|
||||
count: z.number().int().positive()
|
||||
});
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, params: unknown) => {
|
||||
// Validate parameters
|
||||
const result = IncrementSchema.safeParse(params);
|
||||
if (!result.success) {
|
||||
throw new UserError("Invalid parameters", {
|
||||
code: "invalid_params",
|
||||
metadata: { errors: result.error.issues }
|
||||
});
|
||||
}
|
||||
c.state.count += result.data.count;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Streaming Data
|
||||
|
||||
Actions have a single return value. To stream realtime data in response to an action, use [events](/docs/actors/events).
|
||||
|
||||
## Canceling Long-Running Actions
|
||||
|
||||
For operations that should be cancelable on-demand, create your own `AbortController`. Chain it with `c.abortSignal` so actor shutdown also cancels the operation.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const chatActor = actor({
|
||||
createVars: () => ({ controller: null as AbortController | null }),
|
||||
|
||||
actions: {
|
||||
generate: async (c, prompt: string) => {
|
||||
const controller = new AbortController();
|
||||
c.vars.controller = controller;
|
||||
c.abortSignal.addEventListener("abort", () => controller.abort());
|
||||
|
||||
const response = await fetch("https://api.example.com/generate", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ prompt }),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
cancel: (c) => {
|
||||
c.vars.controller?.abort();
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See [Actor Shutdown Abort Signal](/docs/actors/lifecycle#actor-shutdown-abort-signal) for automatically canceling operations when the actor stops.
|
||||
|
||||
## Using `ActionContext` Externally
|
||||
|
||||
When writing complex logic for actions, you may want to extract parts of your implementation into separate helper functions. When doing this, you'll need a way to properly type the context parameter.
|
||||
|
||||
Rivet provides the `ActionContextOf` utility type for exactly this purpose:
|
||||
|
||||
```typescript
|
||||
import { actor, ActionContextOf } from "rivetkit";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
|
||||
actions: {
|
||||
increment: (c) => {
|
||||
incrementCount(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Simple helper function with typed context
|
||||
function incrementCount(c: ActionContextOf<typeof counter>) {
|
||||
c.state.count += 1;
|
||||
}
|
||||
```
|
||||
|
||||
See [types](/docs/actors/types) for more details on using `ActionContextOf` and other utility types.
|
||||
|
||||
## Debugging
|
||||
|
||||
- `GET /inspector/rpcs` lists all available actions on an actor.
|
||||
- `POST /inspector/action/:name` executes an action with JSON args and returns output.
|
||||
- In non-dev mode, inspector endpoints require authorization.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`Actions`](/typedoc/interfaces/rivetkit.mod.Actions.html) - Interface for defining actions
|
||||
- [`ActionContext`](/typedoc/interfaces/rivetkit.mod.ActionContext.html) - Context available in action handlers
|
||||
- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining actors with actions
|
||||
- [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Handle for calling actions from client
|
||||
- [`ActorActionFunction`](/typedoc/types/rivetkit.client_mod.ActorActionFunction.html) - Type for action functions
|
||||
|
||||
_Source doc path: /docs/actors/actions_
|
||||
183
.agents/skills/ai-agent-workspace/reference/actors/appearance.md
Normal file
183
.agents/skills/ai-agent-workspace/reference/actors/appearance.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Icons & Names
|
||||
|
||||
> Source: `src/content/docs/actors/appearance.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/appearance
|
||||
> Description: Customize actors with display names and icons for the Rivet inspector and dashboard.
|
||||
|
||||
---
|
||||
# Icons & Names
|
||||
|
||||
Actors can be customized with a display name and icon that appear in the Rivet inspector & dashboard. This helps identify actors at a glance when managing your application.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the `name` and `icon` properties in your actor's `options`:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const chatRoom = actor({
|
||||
options: {
|
||||
name: "Chat Room", // Human-friendly display name
|
||||
icon: "comments", // FontAwesome icon name
|
||||
},
|
||||
state: { messages: [] },
|
||||
actions: {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Icon Formats
|
||||
|
||||
The `icon` property accepts two formats:
|
||||
|
||||
### Emoji
|
||||
|
||||
Use any emoji character directly:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const notificationService = actor({
|
||||
options: {
|
||||
name: "Notifications",
|
||||
icon: "🔔",
|
||||
},
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### FontAwesome Icons
|
||||
|
||||
Use [FontAwesome](https://fontawesome.com/search) icon names without the "fa" prefix:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const gameServer = actor({
|
||||
options: {
|
||||
name: "Game Server",
|
||||
icon: "gamepad",
|
||||
},
|
||||
// ...
|
||||
});
|
||||
|
||||
const analyticsWorker = actor({
|
||||
options: {
|
||||
name: "Analytics",
|
||||
icon: "chart-line",
|
||||
},
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Default Behavior
|
||||
|
||||
If no `icon` is specified, actors display the default actor icon. If no `name` is specified, the actor's registry key (e.g., `chatRoom`, `gameServer`) is displayed instead.
|
||||
|
||||
## Examples
|
||||
|
||||
Here are some common patterns:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
// Chat/messaging actors
|
||||
const chatRoom = actor({
|
||||
options: { name: "Chat Room", icon: "comments" },
|
||||
// ...
|
||||
});
|
||||
|
||||
// Game-related actors
|
||||
const matchmaker = actor({
|
||||
options: { name: "Matchmaker", icon: "users" },
|
||||
// ...
|
||||
});
|
||||
|
||||
const gameSession = actor({
|
||||
options: { name: "Game Session", icon: "gamepad" },
|
||||
// ...
|
||||
});
|
||||
|
||||
// Data processing actors
|
||||
const dataProcessor = actor({
|
||||
options: { name: "Data Processor", icon: "microchip" },
|
||||
// ...
|
||||
});
|
||||
|
||||
// Using emojis for quick identification
|
||||
const alertService = actor({
|
||||
options: { name: "Alerts", icon: "🚨" },
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced: Run Handler Metadata
|
||||
|
||||
For library developers creating reusable run handlers, you can bundle icon and name metadata directly with the `run` property. This allows libraries to provide sensible defaults without requiring users to configure them manually.
|
||||
|
||||
Instead of returning a function from your run handler factory, return an object with `name`, `icon`, and `run`:
|
||||
|
||||
```typescript
|
||||
import type { RunConfig } from "rivetkit";
|
||||
|
||||
type MyOptions = {
|
||||
mode?: "safe" | "fast";
|
||||
};
|
||||
|
||||
function myCustomRunHandler(_options: MyOptions): RunConfig {
|
||||
const run: RunConfig["run"] = async (_c) => {
|
||||
// Your run handler logic...
|
||||
};
|
||||
|
||||
return {
|
||||
name: "My Custom Handler",
|
||||
icon: "bolt",
|
||||
run,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Users can then use this directly:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const myCustomRunHandler = (_options: Record<string, unknown>) => ({
|
||||
name: "My Custom Handler",
|
||||
icon: "bolt",
|
||||
run: async () => {},
|
||||
});
|
||||
|
||||
const myActor = actor({
|
||||
run: myCustomRunHandler({ /* options */ }),
|
||||
// Picks up "My Custom Handler" name and "bolt" icon in registry metadata
|
||||
});
|
||||
```
|
||||
|
||||
This run-handler metadata is currently applied through the registry and serverless metadata paths. The native runtime and inspector config read the actor's `options.name` and `options.icon` directly, so set those explicitly if you need the name or icon to appear everywhere.
|
||||
|
||||
Actor-level `options.name` and `options.icon` always take precedence, allowing users to override library defaults:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const myCustomRunHandler = (_options: Record<string, unknown>) => ({
|
||||
name: "My Custom Handler",
|
||||
icon: "bolt",
|
||||
run: async () => {},
|
||||
});
|
||||
|
||||
const myActor = actor({
|
||||
options: {
|
||||
name: "Custom Name", // Overrides "My Custom Handler"
|
||||
icon: "rocket", // Overrides "bolt"
|
||||
},
|
||||
run: myCustomRunHandler({ /* options */ }),
|
||||
});
|
||||
```
|
||||
|
||||
The built-in `workflow()` helper uses this pattern to automatically display the workflow icon for workflow-based actors.
|
||||
|
||||
_Source doc path: /docs/actors/appearance_
|
||||
@@ -0,0 +1,598 @@
|
||||
# Authentication
|
||||
|
||||
> Source: `src/content/docs/actors/authentication.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/authentication
|
||||
> Description: Secure your actors with authentication and authorization.
|
||||
|
||||
---
|
||||
## Do You Need Authentication?
|
||||
|
||||
### Rivet Cloud
|
||||
|
||||
Actors are private by default on Rivet Cloud. Only requests with the publishable token can interact with actors.
|
||||
|
||||
- **Backend-only actors**: If your publishable token is only included in your backend, then authentication is not necessary.
|
||||
- **Frontend-accessible actors**: If your publishable token is included in your frontend, then implementing authentication is recommended.
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
Actors are public by default on self-hosted Rivet. Anyone can access them without a token.
|
||||
|
||||
- **Only accessible within private network**: If Rivet is only accessible within your private network, then authentication is not necessary.
|
||||
- **Rivet exposed to the public internet**: If Rivet is configured to accept traffic from the public internet, then implementing authentication is recommended.
|
||||
|
||||
## Authentication Connections
|
||||
|
||||
Authentication is configured through either:
|
||||
|
||||
- `onBeforeConnect` for simple pass/fail validation
|
||||
- `createConnState` when you need to access user data in your actions via `c.conn.state`
|
||||
|
||||
## Access Control
|
||||
|
||||
After a connection is authenticated, use [Access Control](/docs/actors/access-control) to enforce authorization:
|
||||
|
||||
- Check permissions in action handlers.
|
||||
- Use `queues.<name>.canPublish` to gate inbound queue publishes.
|
||||
- Use `events.<name>.canSubscribe` to gate event subscriptions.
|
||||
|
||||
### `onBeforeConnect`
|
||||
|
||||
The `onBeforeConnect` hook validates credentials before allowing a connection. Throw an error to reject the connection.
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
// Example token validation function
|
||||
async function validateToken(token: string, roomKey: string[]): Promise<boolean> {
|
||||
// In production, verify JWT or call auth service
|
||||
return token.length > 0 && roomKey.length > 0;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as Message[] },
|
||||
|
||||
onBeforeConnect: async (c, params: ConnParams) => {
|
||||
const roomName = c.key;
|
||||
const isValid = await validateToken(params.authToken, roomName);
|
||||
if (!isValid) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
sendMessage: (c, text: string) => {
|
||||
c.state.messages.push({ text, timestamp: Date.now() });
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `createConnState`
|
||||
|
||||
Use `createConnState` to extract user data from credentials and store it in connection state. This data is accessible in actions via `c.conn.state`. Like `onBeforeConnect`, throwing an error will reject the connection. See [connections](/docs/actors/connections) for more details.
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
userId: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// Example token validation function
|
||||
async function validateToken(token: string, roomKey: string[]): Promise<{ sub: string; role: string } | null> {
|
||||
// In production, verify JWT or call auth service
|
||||
if (token.length > 0 && roomKey.length > 0) {
|
||||
return { sub: "user-123", role: "member" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as Message[] },
|
||||
|
||||
createConnState: async (c, params: ConnParams): Promise<ConnState> => {
|
||||
const roomName = c.key;
|
||||
const payload = await validateToken(params.authToken, roomName);
|
||||
if (!payload) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
return {
|
||||
userId: payload.sub,
|
||||
role: payload.role,
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
sendMessage: (c, text: string) => {
|
||||
// Access user data via c.conn.state
|
||||
const { userId, role } = c.conn.state;
|
||||
|
||||
if (role !== "member") {
|
||||
throw new UserError("Insufficient permissions", { code: "insufficient_permissions" });
|
||||
}
|
||||
|
||||
c.state.messages.push({ userId, text, timestamp: Date.now() });
|
||||
c.broadcast("newMessage", { userId, text });
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Available Auth Data
|
||||
|
||||
Authentication hooks have access to several properties:
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `params` | Custom data passed by the client when connecting (see [connection params](/docs/actors/connections#extracting-data-from-connection-params)) |
|
||||
| `c.request` | The underlying HTTP request object |
|
||||
| `c.request.headers` | Request headers for tokens, API keys (does not work for `.connect()`) |
|
||||
| `c.state` | Actor state for authorization decisions (see [state](/docs/actors/state)) |
|
||||
| `c.key` | The actor's key (see [keys](/docs/actors/keys)) |
|
||||
|
||||
It's recommended to use `params` instead of `c.request.headers` whenever possible since it works for both HTTP & WebSocket connections.
|
||||
|
||||
## Client Usage
|
||||
|
||||
### Passing Credentials
|
||||
|
||||
Pass authentication data when connecting. Use `getParams` when you need a fresh JWT for every connection or reconnect:
|
||||
|
||||
```typescript Connection
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
async function getAuthToken(): Promise<string> {
|
||||
return "jwt-token-here";
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const chat = client.chatRoom.getOrCreate(["general"], {
|
||||
getParams: async () => ({
|
||||
authToken: await getAuthToken(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Authentication will happen on connect by reading connection parameters
|
||||
const connection = chat.connect();
|
||||
```
|
||||
|
||||
```typescript Stateless-Action
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const chat = client.chatRoom.getOrCreate(["general"], {
|
||||
params: { authToken: "jwt-token-here" },
|
||||
});
|
||||
|
||||
// Authentication will happen when calling the action by reading input
|
||||
// parameters
|
||||
await chat.sendMessage("Hello, world!");
|
||||
```
|
||||
|
||||
```typescript HTTP-Headers
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
// This only works for stateless actions, not WebSockets
|
||||
const client = createClient({
|
||||
headers: {
|
||||
Authorization: "Bearer my-token",
|
||||
},
|
||||
});
|
||||
|
||||
const chat = client.chatRoom.getOrCreate(["general"]);
|
||||
|
||||
// Authentication will happen when calling the action by reading headers
|
||||
await chat.sendMessage("Hello, world!");
|
||||
```
|
||||
|
||||
### Handling Errors
|
||||
|
||||
Authentication errors use the same system as regular errors. See [errors](/docs/actors/errors) for more details.
|
||||
|
||||
```typescript Connection
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { ActorError, createClient } from "rivetkit/client";
|
||||
|
||||
// Define actor with protected action
|
||||
const myActor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
protectedAction: (c) => ({ success: true })
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { myActor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const actorHandle = await client.myActor.getOrCreate();
|
||||
|
||||
// Helper to show errors
|
||||
function showError(message: string) {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
const conn = actorHandle.connect();
|
||||
conn.onError((error: ActorError) => {
|
||||
if (error.code === "forbidden") {
|
||||
window.location.href = "/login";
|
||||
} else if (error.code === "insufficient_permissions") {
|
||||
showError("You don't have permission for this action");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Stateless-Action
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { ActorError, createClient } from "rivetkit/client";
|
||||
|
||||
// Define actor with protected action
|
||||
const myActor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
protectedAction: (c) => ({ success: true })
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { myActor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const actorHandle = await client.myActor.getOrCreate();
|
||||
|
||||
// Helper to show errors
|
||||
function showError(message: string) {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await actorHandle.protectedAction();
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError && error.code === "forbidden") {
|
||||
window.location.href = "/login";
|
||||
} else if (error instanceof ActorError && error.code === "insufficient_permissions") {
|
||||
showError("You don't have permission for this action");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### JWT
|
||||
|
||||
Validate JSON Web Tokens and extract user claims:
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface JwtPayload {
|
||||
sub: string;
|
||||
role: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
// Example JWT verification function - in production use a JWT library
|
||||
function verifyJwt(token: string, secret: string): JwtPayload {
|
||||
// This is a simplified example - use jsonwebtoken or similar in production
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) throw new Error("Invalid token");
|
||||
const payload = JSON.parse(atob(parts[1])) as JwtPayload;
|
||||
return payload;
|
||||
}
|
||||
|
||||
const jwtActor = actor({
|
||||
state: {},
|
||||
|
||||
createConnState: (c, params: ConnParams): ConnState => {
|
||||
try {
|
||||
const payload = verifyJwt(params.token, process.env.JWT_SECRET || "secret");
|
||||
return {
|
||||
userId: payload.sub,
|
||||
role: payload.role,
|
||||
permissions: payload.permissions || [],
|
||||
};
|
||||
} catch {
|
||||
throw new UserError("Invalid or expired token", { code: "invalid_token" });
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
protectedAction: (c) => {
|
||||
if (!c.conn.state.permissions.includes("write")) {
|
||||
throw new UserError("Write permission required", { code: "forbidden" });
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### External Auth Provider
|
||||
|
||||
Validate credentials against an external authentication service:
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
tier: string;
|
||||
}
|
||||
|
||||
const apiActor = actor({
|
||||
state: {},
|
||||
|
||||
createConnState: async (c, params: ConnParams): Promise<ConnState> => {
|
||||
const response = await fetch(`https://api.my-auth-provider.com/validate`, {
|
||||
method: "POST",
|
||||
headers: { "X-API-Key": params.apiKey },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new UserError("Invalid API key", { code: "invalid_api_key" });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { userId: data.id, tier: data.tier };
|
||||
},
|
||||
|
||||
actions: {
|
||||
premiumAction: (c) => {
|
||||
if (c.conn.state.tier !== "premium") {
|
||||
throw new UserError("Premium subscription required", { code: "forbidden" });
|
||||
}
|
||||
return "Premium content";
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Using `c.state` In Authorization
|
||||
|
||||
Access actor state via `c.state` and the actor's key via `c.key` to make authorization decisions:
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
const userProfile = actor({
|
||||
state: {
|
||||
ownerId: "user-123",
|
||||
isPrivate: true,
|
||||
},
|
||||
|
||||
onBeforeConnect: (c, params: ConnParams) => {
|
||||
// Use actor state to check access permissions
|
||||
if (c.state.isPrivate && params.userId !== c.state.ownerId) {
|
||||
throw new UserError("Access denied to private profile", { code: "forbidden" });
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
getProfile: (c) => ({ ownerId: c.state.ownerId }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
Create helper functions for common authorization patterns:
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
const ROLE_HIERARCHY = { user: 1, moderator: 2, admin: 3 };
|
||||
|
||||
interface ConnState {
|
||||
role: keyof typeof ROLE_HIERARCHY;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
// Example token validation function
|
||||
async function validateToken(token: string): Promise<{ role: keyof typeof ROLE_HIERARCHY; permissions: string[] }> {
|
||||
// In production, verify JWT or call auth service
|
||||
return { role: "user", permissions: ["read", "edit_posts"] };
|
||||
}
|
||||
|
||||
function requireRole(requiredRole: keyof typeof ROLE_HIERARCHY) {
|
||||
return (c: { conn: { state: ConnState } }) => {
|
||||
const userRole = c.conn.state.role;
|
||||
if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[requiredRole]) {
|
||||
throw new UserError(`${requiredRole} role required`, { code: "forbidden" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function requirePermission(permission: string) {
|
||||
return (c: { conn: { state: ConnState } }) => {
|
||||
if (!c.conn.state.permissions?.includes(permission)) {
|
||||
throw new UserError(`Permission '${permission}' required`, { code: "forbidden" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const forumActor = actor({
|
||||
state: {},
|
||||
|
||||
createConnState: async (c, params: { token: string }): Promise<ConnState> => {
|
||||
const user = await validateToken(params.token);
|
||||
return { role: user.role, permissions: user.permissions };
|
||||
},
|
||||
|
||||
actions: {
|
||||
deletePost: (c, postId: string) => {
|
||||
requireRole("moderator")(c);
|
||||
// Delete post...
|
||||
},
|
||||
|
||||
editPost: (c, postId: string, content: string) => {
|
||||
requirePermission("edit_posts")(c);
|
||||
// Edit post...
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Use `c.vars` to track connection attempts and rate limit by user:
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
interface RateLimitEntry {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
}
|
||||
|
||||
// Example token validation function
|
||||
async function validateToken(token: string): Promise<{ userId: string }> {
|
||||
// In production, verify JWT or call auth service
|
||||
return { userId: "user-123" };
|
||||
}
|
||||
|
||||
const rateLimitedActor = actor({
|
||||
state: {},
|
||||
createVars: () => ({ rateLimits: {} as Record<string, RateLimitEntry> }),
|
||||
|
||||
onBeforeConnect: async (c, params: ConnParams) => {
|
||||
// Extract user ID
|
||||
const { userId } = await validateToken(params.authToken);
|
||||
|
||||
// Check rate limit
|
||||
const now = Date.now();
|
||||
const limit = c.vars.rateLimits[userId];
|
||||
|
||||
if (limit && limit.resetAt > now && limit.count >= 10) {
|
||||
throw new UserError("Too many requests, try again later", { code: "rate_limited" });
|
||||
}
|
||||
|
||||
// Update rate limit
|
||||
if (!limit || limit.resetAt <= now) {
|
||||
c.vars.rateLimits[userId] = { count: 1, resetAt: now + 60_000 };
|
||||
} else {
|
||||
limit.count++;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
getData: (c) => ({ success: true }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The limits in this example are [ephemeral](/docs/actors/state#ephemeral-variables). If you wish to persist rate limits, you can optionally replace `vars` with `state`.
|
||||
|
||||
### Caching Tokens
|
||||
|
||||
Cache validated tokens in `c.vars` to avoid redundant validation on repeated connections. See [ephemeral variables](/docs/actors/state#ephemeral-variables) for more details.
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface TokenCache {
|
||||
[token: string]: {
|
||||
userId: string;
|
||||
role: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Example token validation function
|
||||
async function validateToken(token: string): Promise<{ sub: string; role: string } | null> {
|
||||
// In production, verify JWT or call auth service
|
||||
if (token.length > 0) {
|
||||
return { sub: "user-123", role: "member" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedAuthActor = actor({
|
||||
state: {},
|
||||
createVars: () => ({ tokenCache: {} as TokenCache }),
|
||||
|
||||
createConnState: async (c, params: ConnParams): Promise<ConnState> => {
|
||||
const token = params.authToken;
|
||||
|
||||
// Check cache first
|
||||
const cached = c.vars.tokenCache[token];
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return { userId: cached.userId, role: cached.role };
|
||||
}
|
||||
|
||||
// Validate token (expensive operation)
|
||||
const payload = await validateToken(token);
|
||||
if (!payload) {
|
||||
throw new UserError("Invalid token", { code: "invalid_token" });
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
c.vars.tokenCache[token] = {
|
||||
userId: payload.sub,
|
||||
role: payload.role,
|
||||
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
|
||||
};
|
||||
|
||||
return { userId: payload.sub, role: payload.role };
|
||||
},
|
||||
|
||||
actions: {
|
||||
getData: (c) => ({ userId: c.conn.state.userId }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`AuthIntent`](/typedoc/types/rivetkit.mod.AuthIntent.html) - Authentication intent type
|
||||
- [`OnBeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - Context for auth checks
|
||||
- [`OnConnectContext`](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Context after connection
|
||||
|
||||
_Source doc path: /docs/actors/authentication_
|
||||
@@ -0,0 +1,336 @@
|
||||
# Communicating Between Actors
|
||||
|
||||
> Source: `src/content/docs/actors/communicating-between-actors.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/communicating-between-actors
|
||||
> Description: Learn how actors can call other actors and share data
|
||||
|
||||
---
|
||||
Actors can communicate with each other using the server-side actor client, enabling complex workflows and data sharing between different actor instances.
|
||||
|
||||
We recommend reading the [clients documentation](/docs/clients) first. This guide focuses specifically on communication between actors.
|
||||
|
||||
## Using the Server-Side Actor Client
|
||||
|
||||
The server-side actor client allows actors to call other actors within the same registry. Access it via `c.client()` in your actor context:
|
||||
|
||||
If two actors call each other and their return types are inferred from the other actor's response, you may hit circular type errors (`TS2322`, `TS2722`, or `c.state` becoming `unknown`). Fix this by writing explicit return types on those actions.
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
customerId: string;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
interface ProcessedOrder extends Order {
|
||||
status: string;
|
||||
paymentResult: { transactionId: string };
|
||||
}
|
||||
|
||||
const inventory = actor({
|
||||
state: { stock: 100 },
|
||||
actions: {
|
||||
reserveStock: (c, quantity: number) => {
|
||||
c.state.stock -= quantity;
|
||||
return { reserved: quantity };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const payment = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
processPayment: (c, amount: number) => ({ transactionId: "tx-123" })
|
||||
}
|
||||
});
|
||||
|
||||
const orderProcessor = actor({
|
||||
state: { orders: [] as ProcessedOrder[] },
|
||||
|
||||
actions: {
|
||||
processOrder: async (c, order: Order) => {
|
||||
const client = c.client<typeof registry>();
|
||||
|
||||
// Reserve the stock
|
||||
const inventoryHandle = client.inventory.getOrCreate(["main"]);
|
||||
await inventoryHandle.reserveStock(order.quantity);
|
||||
|
||||
// Process payment through payment actor
|
||||
const paymentHandle = client.payment.getOrCreate([order.customerId]);
|
||||
const result = await paymentHandle.processPayment(order.amount);
|
||||
|
||||
// Update order state
|
||||
c.state.orders.push({ ...order, status: "completed", paymentResult: result });
|
||||
|
||||
return { success: true, orderId: order.id };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { inventory, payment, orderProcessor } });
|
||||
```
|
||||
|
||||
## Use Cases and Patterns
|
||||
|
||||
### Actor Orchestration
|
||||
|
||||
Use a coordinator actor to manage complex workflows:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface WorkflowResult {
|
||||
workflowId: string;
|
||||
result: { finalized: boolean };
|
||||
completedAt: number;
|
||||
}
|
||||
|
||||
const dataProcessor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
initialize: (c, workflowId: string) => ({ workflowId, data: "initialized" })
|
||||
}
|
||||
});
|
||||
|
||||
const validator = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
validate: (c, data: { workflowId: string; data: string }) => ({ valid: true, data })
|
||||
}
|
||||
});
|
||||
|
||||
const finalizer = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
finalize: (c, validationResult: { valid: boolean }) => ({ finalized: validationResult.valid })
|
||||
}
|
||||
});
|
||||
|
||||
const workflowActor = actor({
|
||||
state: { workflows: [] as WorkflowResult[] },
|
||||
|
||||
actions: {
|
||||
executeWorkflow: async (c, workflowId: string) => {
|
||||
const client = c.client<typeof registry>();
|
||||
|
||||
// Step 1: Initialize data
|
||||
const dataProcessorHandle = client.dataProcessor.getOrCreate(["main"]);
|
||||
const data = await dataProcessorHandle.initialize(workflowId);
|
||||
|
||||
// Step 2: Process through multiple actors
|
||||
const validatorHandle = client.validator.getOrCreate(["main"]);
|
||||
const validationResult = await validatorHandle.validate(data);
|
||||
|
||||
// Step 3: Finalize
|
||||
const finalizerHandle = client.finalizer.getOrCreate(["main"]);
|
||||
const result = await finalizerHandle.finalize(validationResult);
|
||||
|
||||
c.state.workflows.push({ workflowId, result, completedAt: Date.now() });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { dataProcessor, validator, finalizer, workflowActor } });
|
||||
```
|
||||
|
||||
### Data Aggregation
|
||||
|
||||
Collect data from multiple actors:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface Stats {
|
||||
count: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Report {
|
||||
id: string;
|
||||
type: string;
|
||||
data: { users: Stats; orders: Stats; system: Stats };
|
||||
generatedAt: number;
|
||||
}
|
||||
|
||||
const userMetrics = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getStats: (c): Stats => ({ count: 100, total: 500 })
|
||||
}
|
||||
});
|
||||
|
||||
const orderMetrics = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getStats: (c): Stats => ({ count: 50, total: 10000 })
|
||||
}
|
||||
});
|
||||
|
||||
const systemMetrics = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getStats: (c): Stats => ({ count: 5, total: 99 })
|
||||
}
|
||||
});
|
||||
|
||||
const analyticsActor = actor({
|
||||
state: { reports: [] as Report[] },
|
||||
|
||||
actions: {
|
||||
generateReport: async (c, reportType: string) => {
|
||||
const client = c.client<typeof registry>();
|
||||
|
||||
// Collect data from multiple sources
|
||||
const userMetricsHandle = client.userMetrics.getOrCreate(["main"]);
|
||||
const orderMetricsHandle = client.orderMetrics.getOrCreate(["main"]);
|
||||
const systemMetricsHandle = client.systemMetrics.getOrCreate(["main"]);
|
||||
|
||||
const [users, orders, system] = await Promise.all([
|
||||
userMetricsHandle.getStats(),
|
||||
orderMetricsHandle.getStats(),
|
||||
systemMetricsHandle.getStats()
|
||||
]);
|
||||
|
||||
const report: Report = {
|
||||
id: crypto.randomUUID(),
|
||||
type: reportType,
|
||||
data: { users, orders, system },
|
||||
generatedAt: Date.now()
|
||||
};
|
||||
|
||||
c.state.reports.push(report);
|
||||
return report;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { userMetrics, orderMetrics, systemMetrics, analyticsActor } });
|
||||
```
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
Use connections to listen for events from other actors:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
interface AuditLog {
|
||||
event: string;
|
||||
data: User | Order;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const userActor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
createUser: (c, name: string) => {
|
||||
const user = { id: crypto.randomUUID(), name };
|
||||
c.broadcast("userCreated", user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const orderActor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
completeOrder: (c, amount: number) => {
|
||||
const order = { id: crypto.randomUUID(), amount };
|
||||
c.broadcast("orderCompleted", order);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const auditLogActor = actor({
|
||||
state: { logs: [] as AuditLog[] },
|
||||
|
||||
actions: {
|
||||
startAuditing: async (c) => {
|
||||
const client = c.client<typeof registry>();
|
||||
|
||||
// Connect to multiple actors to listen for events
|
||||
const userActorConn = client.userActor.getOrCreate(["main"]).connect();
|
||||
const orderActorConn = client.orderActor.getOrCreate(["main"]).connect();
|
||||
|
||||
// Listen for user events
|
||||
userActorConn.on("userCreated", (user: User) => {
|
||||
c.state.logs.push({
|
||||
event: "userCreated",
|
||||
data: user,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for order events
|
||||
orderActorConn.on("orderCompleted", (order: Order) => {
|
||||
c.state.logs.push({
|
||||
event: "orderCompleted",
|
||||
data: order,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
return { status: "auditing started" };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { userActor, orderActor, auditLogActor } });
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
Process multiple items in parallel:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface Item {
|
||||
type: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
const processor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
process: (c, item: Item) => ({ processed: true, item })
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { processor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Process items in parallel
|
||||
const items: Item[] = [
|
||||
{ type: "typeA", data: "data1" },
|
||||
{ type: "typeB", data: "data2" }
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map(item => client.processor.getOrCreate([item.type]).process(item))
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Handle for calling other actors
|
||||
- [`Client`](/typedoc/types/rivetkit.mod.Client.html) - Client type for actor communication
|
||||
- [`ActorAccessor`](/typedoc/interfaces/rivetkit.client_mod.ActorAccessor.html) - Accessor for getting actor handles
|
||||
|
||||
_Source doc path: /docs/actors/communicating-between-actors_
|
||||
@@ -0,0 +1,460 @@
|
||||
# Connections
|
||||
|
||||
> Source: `src/content/docs/actors/connections.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/connections
|
||||
> Description: Connections represent client connections to your actor. They provide a way to handle client authentication, manage connection-specific data, and control the connection lifecycle.
|
||||
|
||||
---
|
||||
For documentation on connecting to actors from clients, see the [Clients documentation](/docs/clients). For worked presence and chat patterns, see the cookbook: [Live Cursors and Presence](/cookbook/live-cursors/) and [Chat Room](/cookbook/chat-room/).
|
||||
|
||||
## Parameters
|
||||
|
||||
When clients connect to an actor, they can pass connection parameters that are handled during the connection process. Use `params` for static values, or `getParams` when you need fresh connection data each time a connection opens.
|
||||
|
||||
For example:
|
||||
|
||||
```typescript Client
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface ConnParams {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const gameRoom = actor({
|
||||
state: {},
|
||||
createConnState: (c, params: ConnParams): ConnState => {
|
||||
return { userId: "user-123", role: "player" };
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { gameRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
async function getAuthToken(): Promise<string> {
|
||||
return "supersekure";
|
||||
}
|
||||
|
||||
const gameRoomHandle = client.gameRoom.getOrCreate(["room-123"], {
|
||||
getParams: async () => ({
|
||||
authToken: await getAuthToken(),
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Actor
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ConnParams {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// Example validation functions
|
||||
function validateToken(token: string): boolean {
|
||||
return token.length > 0;
|
||||
}
|
||||
|
||||
function getUserIdFromToken(token: string): string {
|
||||
return "user-" + token.slice(0, 8);
|
||||
}
|
||||
|
||||
const gameRoom = actor({
|
||||
state: {},
|
||||
|
||||
// Handle connection setup
|
||||
createConnState: (c, params: ConnParams): ConnState => {
|
||||
// Validate authentication token
|
||||
const authToken = params.authToken;
|
||||
|
||||
if (!authToken || !validateToken(authToken)) {
|
||||
throw new Error("Invalid auth token");
|
||||
}
|
||||
|
||||
// Create connection state
|
||||
return { userId: getUserIdFromToken(authToken), role: "player" };
|
||||
},
|
||||
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
## Connection State
|
||||
|
||||
There are two ways to define an actor's connection state:
|
||||
|
||||
|
||||
### connState
|
||||
|
||||
Define connection state as a constant value:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] },
|
||||
|
||||
// Define default connection state as a constant
|
||||
connState: {
|
||||
role: "guest",
|
||||
joinedAt: 0
|
||||
},
|
||||
|
||||
onConnect: (c) => {
|
||||
// Update join timestamp when a client connects
|
||||
c.conn.state.joinedAt = Date.now();
|
||||
},
|
||||
|
||||
actions: {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This value will be cloned for every new connection using `structuredClone`.
|
||||
|
||||
|
||||
|
||||
### createConnState
|
||||
|
||||
Create connection state dynamically with a function called for each connection:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
joinedAt: number;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
username: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function generateUserId(): string {
|
||||
return "user-" + Math.random().toString(36).slice(2, 11);
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as Message[] },
|
||||
|
||||
// Create connection state dynamically
|
||||
createConnState: (c): ConnState => {
|
||||
// Return the connection state
|
||||
return {
|
||||
userId: generateUserId(),
|
||||
role: "guest",
|
||||
joinedAt: Date.now()
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
sendMessage: (c, message: string) => {
|
||||
const username = c.conn.state.userId;
|
||||
c.state.messages.push({ username, message });
|
||||
c.broadcast("newMessage", { username, message });
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
Each client connection goes through a series of lifecycle hooks that allow you to validate, initialize, and clean up connection-specific resources.
|
||||
|
||||
**On Connect** (per client)
|
||||
|
||||
- `onBeforeConnect`
|
||||
- `createConnState`
|
||||
- `onConnect`
|
||||
|
||||
Pending connections are not visible in `c.conns` while `onBeforeConnect` or `createConnState` is running. RivetKit adds the connection to `c.conns` after those hooks succeed and before `onConnect` runs.
|
||||
|
||||
**On Disconnect** (per client)
|
||||
|
||||
- `onDisconnect`
|
||||
|
||||
### `createConnState` and `connState`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.CreateConnStateContext.html)
|
||||
|
||||
There are two ways to define the initial state for connections:
|
||||
1. `connState`: Define a constant object that will be used as the initial state for all connections
|
||||
2. `createConnState`: A function that dynamically creates initial connection state based on connection parameters. Can be async.
|
||||
|
||||
Connections are not visible in `c.conns` until `createConnState` completes successfully.
|
||||
|
||||
### `onBeforeConnect`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html)
|
||||
|
||||
The `onBeforeConnect` hook is called whenever a new client connects to the actor. Can be async. Clients can pass parameters when connecting, accessible via `params`. This hook is used for connection validation and can throw errors to reject connections.
|
||||
|
||||
The `onBeforeConnect` hook does NOT return connection state - it's used solely for validation.
|
||||
|
||||
Connections are not visible in `c.conns` while `onBeforeConnect` is running.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface Message {
|
||||
text: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
interface ConnParams {
|
||||
authToken?: string;
|
||||
userId?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
joinTime: number;
|
||||
}
|
||||
|
||||
function validateToken(token: string): boolean {
|
||||
return token.length > 0;
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as Message[] },
|
||||
|
||||
// Dynamically create connection state
|
||||
createConnState: (c, params: ConnParams): ConnState => {
|
||||
return {
|
||||
userId: params.userId || "anonymous",
|
||||
role: params.role || "guest",
|
||||
joinTime: Date.now()
|
||||
};
|
||||
},
|
||||
|
||||
// Validate connections before accepting them
|
||||
onBeforeConnect: (c, params: ConnParams) => {
|
||||
// Validate authentication
|
||||
const authToken = params.authToken;
|
||||
if (!authToken || !validateToken(authToken)) {
|
||||
throw new Error("Invalid authentication");
|
||||
}
|
||||
|
||||
// Authentication is valid, connection will proceed
|
||||
// The actual connection state will come from createConnState
|
||||
},
|
||||
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication, see [Authentication](/docs/actors/authentication) for details.
|
||||
|
||||
### `onConnect`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html)
|
||||
|
||||
Executed after the client has successfully connected. Can be async. Receives the connection object as a second parameter.
|
||||
|
||||
By the time `onConnect` runs, the connection is visible in `c.conns`.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface UserStatus {
|
||||
online: boolean;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { users: {} as Record<string, UserStatus>, messages: [] as string[] },
|
||||
|
||||
createConnState: (): ConnState => ({
|
||||
userId: "user-" + Math.random().toString(36).slice(2, 11)
|
||||
}),
|
||||
|
||||
onConnect: (c, conn) => {
|
||||
// Add user to the room's user list using connection state
|
||||
const userId = conn.state.userId;
|
||||
c.state.users[userId] = {
|
||||
online: true,
|
||||
lastSeen: Date.now()
|
||||
};
|
||||
|
||||
// Broadcast that a user joined
|
||||
c.broadcast("userJoined", { userId, timestamp: Date.now() });
|
||||
|
||||
console.log(`User ${userId} connected`);
|
||||
},
|
||||
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
Messages will not be processed for this actor until this hook succeeds. Errors thrown from this hook will cause the client to disconnect.
|
||||
|
||||
### `onDisconnect`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
Called when a client disconnects from the actor. Can be async. Receives the connection object as a second parameter. Use this to clean up any connection-specific resources.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface UserStatus {
|
||||
online: boolean;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { users: {} as Record<string, UserStatus>, messages: [] as string[] },
|
||||
|
||||
createConnState: (): ConnState => ({
|
||||
userId: "user-" + Math.random().toString(36).slice(2, 11)
|
||||
}),
|
||||
|
||||
onDisconnect: (c, conn) => {
|
||||
// Update user status when they disconnect
|
||||
const userId = conn.state.userId;
|
||||
if (c.state.users[userId]) {
|
||||
c.state.users[userId].online = false;
|
||||
c.state.users[userId].lastSeen = Date.now();
|
||||
}
|
||||
|
||||
// Broadcast that a user left
|
||||
c.broadcast("userLeft", { userId, timestamp: Date.now() });
|
||||
|
||||
console.log(`User ${userId} disconnected`);
|
||||
},
|
||||
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
## Connection List
|
||||
|
||||
All active connections can be accessed through the context object's `conns` property. This is a `Map<string, Conn>` of all current connections, keyed by connection ID.
|
||||
|
||||
This is frequently used with `conn.send(name, event)` to send messages directly to clients. To send an event to all connections at once, use `c.broadcast()` instead. See [Events](/docs/actors/events) for more details on broadcasting.
|
||||
|
||||
For example:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { users: {} as Record<string, { online: boolean }> },
|
||||
|
||||
createConnState: (): ConnState => ({
|
||||
userId: "user-" + Math.random().toString(36).slice(2, 11)
|
||||
}),
|
||||
|
||||
actions: {
|
||||
sendDirectMessage: (c, recipientId: string, message: string) => {
|
||||
// Find the recipient's connection by iterating over the Map
|
||||
let recipientConn = null;
|
||||
for (const conn of c.conns.values()) {
|
||||
if (conn.state.userId === recipientId) {
|
||||
recipientConn = conn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientConn) {
|
||||
// Send a private message to just that client
|
||||
recipientConn.send("directMessage", {
|
||||
from: c.conn.state.userId,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`conn.send()` has no effect on [low-level WebSocket connections](/docs/actors/websocket-handler). For low-level WebSockets, use the WebSocket API directly (e.g., `websocket.send()`).
|
||||
|
||||
## Disconnecting clients
|
||||
|
||||
Connections can be disconnected from within an action:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const secureRoom = actor({
|
||||
state: {},
|
||||
|
||||
createConnState: (): ConnState => ({
|
||||
userId: "user-" + Math.random().toString(36).slice(2, 11)
|
||||
}),
|
||||
|
||||
actions: {
|
||||
kickUser: (c, targetUserId: string, reason?: string) => {
|
||||
// Find the connection to kick by iterating over the Map
|
||||
for (const conn of c.conns.values()) {
|
||||
if (conn.state.userId === targetUserId) {
|
||||
// Disconnect with a reason
|
||||
conn.disconnect(reason || "Kicked by admin");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you need to wait for the disconnection to complete, you can use `await`:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const myActor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
disconnect: async (c) => {
|
||||
await c.conn.disconnect("Too many requests");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This ensures the underlying network connections close cleanly before continuing.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`Conn`](/typedoc/interfaces/rivetkit.mod.Conn.html) - Connection interface
|
||||
- [`ConnInitContext`](/typedoc/interfaces/rivetkit.mod.ConnInitContext.html) - Connection initialization context
|
||||
- [`CreateConnStateContext`](/typedoc/interfaces/rivetkit.mod.CreateConnStateContext.html) - Context for creating connection state
|
||||
- [`OnBeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - Pre-connection lifecycle hook context
|
||||
- [`OnConnectContext`](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Post-connection lifecycle hook context
|
||||
- [`ActorConn`](/typedoc/types/rivetkit.client_mod.ActorConn.html) - Typed connection from client side
|
||||
|
||||
_Source doc path: /docs/actors/connections_
|
||||
644
.agents/skills/ai-agent-workspace/reference/actors/debugging.md
Normal file
644
.agents/skills/ai-agent-workspace/reference/actors/debugging.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# Debugging
|
||||
|
||||
> Source: `src/content/docs/actors/debugging.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/debugging
|
||||
> Description: Inspect and debug running Rivet Actors, runners, and provider configs using management, runner, and actor inspector HTTP APIs.
|
||||
|
||||
---
|
||||
## Connecting to Rivet
|
||||
|
||||
All debugging endpoints in this guide are available both locally and in production. In local development, the base URL is `http://localhost:6420` with no authentication. In production (Rivet Cloud or self-hosted), you connect to your Rivet Engine endpoint with a token.
|
||||
|
||||
### Setup
|
||||
|
||||
All examples in this guide use these shell variables. Extract them from your `RIVET_ENDPOINT` (`https://<namespace>:<token>@<host>`):
|
||||
|
||||
```bash
|
||||
# From RIVET_ENDPOINT=https://my-namespace:sk_abc123@api.rivet.dev
|
||||
export RIVET_API="https://api.rivet.dev"
|
||||
export RIVET_NAMESPACE="my-namespace"
|
||||
export RIVET_TOKEN="sk_abc123"
|
||||
|
||||
# For local development:
|
||||
# export RIVET_API="http://localhost:6420"
|
||||
```
|
||||
|
||||
Rivet Cloud issues two token types: `sk_` (secret key, server-side only) and `pk_` (public key, client-safe). For debugging, always use `sk_`. See [Endpoints](/docs/general/endpoints) for more details.
|
||||
|
||||
## Management API
|
||||
|
||||
The management API runs on the manager base path (default root path) and is used to list, create, and look up actors.
|
||||
|
||||
### Authentication
|
||||
|
||||
| Environment | Authentication |
|
||||
|---|---|
|
||||
| **Local development** | No authentication required. All endpoints are accessible without tokens. |
|
||||
| **Self-hosted engine** | Set `RIVET_TOKEN` to enable authenticated access to restricted endpoints like KV. |
|
||||
| **Rivet Cloud** | Authentication is enforced by your deployment entrypoint. For manager KV access, use the bearer token header below when enabled. |
|
||||
|
||||
Restricted endpoints (like KV reads) require the `Authorization: Bearer` header when `RIVET_TOKEN` is configured:
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/actors/{actor_id}/kv/keys/{base64_key}" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
### List Actors
|
||||
|
||||
```bash
|
||||
# List all actors with a given name
|
||||
curl "$RIVET_API/actors?name=my-actor&namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
|
||||
# Look up one actor by key (name is required when key is provided)
|
||||
curl "$RIVET_API/actors?name=my-actor&key=%5B%22my-key%22%5D&namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
|
||||
# List actors by IDs (comma-separated)
|
||||
curl "$RIVET_API/actors?actor_ids=id1,id2&namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `key` requires `name`.
|
||||
- `actor_ids` cannot be combined with `name` or `key`.
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"actors": [
|
||||
{
|
||||
"actor_id": "abc123",
|
||||
"name": "my-actor",
|
||||
"key": "[\"default\"]",
|
||||
"namespace_id": "default",
|
||||
"create_ts": 1706000000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create Actor
|
||||
|
||||
`POST /actors` creates a new actor.
|
||||
|
||||
```bash
|
||||
curl -X POST "$RIVET_API/actors?namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"name": "my-actor",
|
||||
"runner_name_selector": "default",
|
||||
"crash_policy": "restart"
|
||||
}'
|
||||
```
|
||||
|
||||
### Create or Get Actor
|
||||
|
||||
`PUT /actors` creates an actor if it does not exist, otherwise returns the existing one.
|
||||
|
||||
```bash
|
||||
curl -X PUT "$RIVET_API/actors?namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"name": "my-actor",
|
||||
"key": "[\"default\"]",
|
||||
"runner_name_selector": "default",
|
||||
"crash_policy": "restart"
|
||||
}'
|
||||
```
|
||||
|
||||
Returns the actor object with its `actor_id`.
|
||||
|
||||
### List Actor Names
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/actors/names?namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
Returns all registered actor names and their metadata.
|
||||
|
||||
### Read Actor KV
|
||||
|
||||
Requires authentication (see above).
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/actors/{actor_id}/kv/keys/{base64_key}" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
Returns the value stored at the given key.
|
||||
|
||||
See the [OpenAPI spec](https://github.com/rivet-dev/rivet/tree/main/rivetkit-openapi) for the full schema of all management endpoints.
|
||||
|
||||
## Runner API
|
||||
|
||||
Use the runner endpoints to debug scheduler capacity and provider configuration (for example serverless URL, headers, and limits) through the Rivet API.
|
||||
|
||||
### List Runner Names
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/runners/names?namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
Returns the runner pools available in the namespace:
|
||||
|
||||
```json
|
||||
{
|
||||
"names": ["default", "gpu-workers"],
|
||||
"pagination": { "cursor": null }
|
||||
}
|
||||
```
|
||||
|
||||
### List Runners in a Pool
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/runners?namespace=$RIVET_NAMESPACE&name=default&include_stopped=true&limit=100" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
Useful fields when debugging:
|
||||
|
||||
- `remaining_slots` / `total_slots` for capacity.
|
||||
- `drain_ts` and `stop_ts` for shutdown behavior.
|
||||
- `last_ping_ts` and `last_connected_ts` for connectivity.
|
||||
|
||||
### Inspect Provider Config (Runner Config)
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/runner-configs?namespace=$RIVET_NAMESPACE&runner_name=default" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN"
|
||||
```
|
||||
|
||||
Returns the configured provider settings per datacenter and the latest pool error (if any):
|
||||
|
||||
```json
|
||||
{
|
||||
"runner_configs": {
|
||||
"default": {
|
||||
"datacenters": {
|
||||
"dc-1": {
|
||||
"serverless": {
|
||||
"url": "https://your-deployment.example.com/rivet",
|
||||
"headers": { "Authorization": "Bearer token" },
|
||||
"request_lifespan": 55,
|
||||
"slots_per_runner": 1,
|
||||
"max_runners": 10
|
||||
},
|
||||
"runner_pool_error": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pagination": { "cursor": null }
|
||||
}
|
||||
```
|
||||
|
||||
`runner_pool_error` mirrors actor scheduling errors such as `serverless_http_error`, `serverless_connection_error`, and `serverless_stream_ended_early`.
|
||||
|
||||
### Check Serverless Provider Health
|
||||
|
||||
Use this to test whether Rivet can reach your serverless provider URL and read runner metadata:
|
||||
|
||||
```bash
|
||||
curl -X POST "$RIVET_API/runner-configs/serverless-health-check?namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "https://your-deployment.example.com/rivet",
|
||||
"headers": {
|
||||
"Authorization": "Bearer token"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Possible responses:
|
||||
|
||||
```json
|
||||
{ "success": { "version": "1.2.3" } }
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"failure": {
|
||||
"error": {
|
||||
"message": "non-success status from metadata endpoint",
|
||||
"details": "received status 503"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Provider Metadata
|
||||
|
||||
If you deploy new actor code or routes and metadata has not updated yet, force a refresh:
|
||||
|
||||
```bash
|
||||
curl -X POST "$RIVET_API/runner-configs/default/refresh-metadata?namespace=$RIVET_NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Actor API
|
||||
|
||||
All actor-level endpoints are accessed through the gateway. The gateway routes requests to the correct actor instance using the actor ID in the URL path:
|
||||
|
||||
```
|
||||
{RIVET_API}/gateway/{actor_id}/{path}
|
||||
```
|
||||
|
||||
The gateway only accepts actor IDs, not names. Use `GET /actors?name=...` from the management API to look up actor IDs first.
|
||||
|
||||
### Authentication
|
||||
|
||||
Standard actor endpoints (health, actions, requests) and inspector endpoints have separate authentication requirements.
|
||||
|
||||
#### Standard Endpoints
|
||||
|
||||
| Environment | Authentication |
|
||||
|---|---|
|
||||
| **Local development** | No authentication required. |
|
||||
| **Self-hosted engine** | The Rivet Engine handles authentication at the gateway level. |
|
||||
| **Rivet Cloud** | Authentication is handled by the Rivet Cloud platform at the gateway level. |
|
||||
|
||||
#### Inspector Endpoints
|
||||
|
||||
Each actor generates a unique inspector token on first start and persists it in its internal KV store at key `0x03` (base64 `Aw==`). Pass it as a bearer token in the `Authorization` header.
|
||||
|
||||
Inspector endpoints always require the actor's inspector token, including in local development. There is no local-development bypass.
|
||||
|
||||
| Environment | Authentication |
|
||||
|---|---|
|
||||
| **Local development** | Bearer the actor's inspector token in the `Authorization` header. Fetch it through the management KV endpoint (see below). |
|
||||
| **Self-hosted engine** | Bearer the actor's inspector token in the `Authorization` header. The Rivet dashboard fetches it automatically; for direct API access, fetch it through the management KV endpoint (see below). |
|
||||
| **Rivet Cloud** | Bearer the actor's inspector token in the `Authorization` header. The Rivet dashboard fetches it automatically; for direct API access, fetch it through the management KV endpoint (see below). |
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/gateway/{actor_id}/inspector/summary" \
|
||||
-H 'Authorization: Bearer YOUR_INSPECTOR_TOKEN'
|
||||
```
|
||||
|
||||
#### Retrieving the Inspector Token
|
||||
|
||||
Each actor generates a unique inspector token on first start and persists it in its internal KV store. The Rivet dashboard retrieves this token automatically, but if you need it for direct API access, fetch it from the management KV endpoint. This applies in every environment, including local development.
|
||||
|
||||
The inspector token is stored at internal KV key `0x03` (base64: `Aw==`). The response value is also base64-encoded.
|
||||
|
||||
```bash
|
||||
# Fetch the inspector token for a specific actor
|
||||
ACTOR_ID="your-actor-id"
|
||||
|
||||
RESPONSE=$(curl -s "$RIVET_API/actors/$ACTOR_ID/kv/keys/Aw==" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN")
|
||||
|
||||
# Extract and decode the base64 value
|
||||
INSPECTOR_TOKEN=$(echo "$RESPONSE" | jq -r '.value' | base64 -d)
|
||||
|
||||
# Use it to call inspector endpoints
|
||||
curl "$RIVET_API/gateway/$ACTOR_ID/inspector/summary" \
|
||||
-H "Authorization: Bearer $INSPECTOR_TOKEN"
|
||||
```
|
||||
|
||||
### Standard Actor Endpoints
|
||||
|
||||
These are the built-in actor endpoints available through the gateway:
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl $RIVET_API/gateway/{actor_id}/health
|
||||
|
||||
# Metadata
|
||||
curl $RIVET_API/gateway/{actor_id}/metadata
|
||||
|
||||
# Call an action
|
||||
curl -X POST $RIVET_API/gateway/{actor_id}/action/myAction \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"args": [1, 2, 3]}'
|
||||
|
||||
# Send queue message (queue name in path)
|
||||
curl -X POST $RIVET_API/gateway/{actor_id}/queue/jobs \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"body":{"id":"job-1"}}'
|
||||
|
||||
# Send queue message and wait for completion (optional timeout in ms)
|
||||
curl -X POST $RIVET_API/gateway/{actor_id}/queue/jobs \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"body":{"id":"job-1"},"wait":true,"timeout":5000}'
|
||||
|
||||
# Forward an HTTP request to the actor's onRequest handler
|
||||
curl $RIVET_API/gateway/{actor_id}/request/my/custom/path
|
||||
```
|
||||
|
||||
Queue send responses always include a `status` field:
|
||||
|
||||
```json
|
||||
{ "status": "completed" }
|
||||
```
|
||||
|
||||
The `response` field is only present when the queue handler returns a value:
|
||||
|
||||
```json
|
||||
{ "status": "completed", "response": { "result": "ok" } }
|
||||
```
|
||||
|
||||
If `wait: true` and the timeout is reached, `status` is `"timedOut"`.
|
||||
|
||||
### Inspector Endpoints
|
||||
|
||||
The inspector HTTP API exposes JSON endpoints for querying and modifying actor internals at runtime. These are designed for agent-based debugging and tooling.
|
||||
|
||||
Every inspector endpoint requires the actor's inspector token as a bearer token, including in local development. The examples below omit the `Authorization` header for brevity, but you must add `-H "Authorization: Bearer $INSPECTOR_TOKEN"` to each request. See [Retrieving the Inspector Token](#retrieving-the-inspector-token) above.
|
||||
|
||||
#### Get State
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/state
|
||||
```
|
||||
|
||||
Returns the actor's current persisted state:
|
||||
|
||||
```json
|
||||
{
|
||||
"state": { "count": 42, "users": [] },
|
||||
"isStateEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Set State
|
||||
|
||||
```bash
|
||||
curl -X PATCH $RIVET_API/gateway/{actor_id}/inspector/state \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"state": {"count": 0, "users": []}}'
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
#### Get Connections
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/connections
|
||||
```
|
||||
|
||||
Returns all active connections with their params, state, and metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"connections": [
|
||||
{
|
||||
"type": "websocket",
|
||||
"id": "conn-id",
|
||||
"details": {
|
||||
"type": "websocket",
|
||||
"params": {},
|
||||
"stateEnabled": true,
|
||||
"state": {},
|
||||
"subscriptions": 2,
|
||||
"isHibernatable": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get RPCs
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/rpcs
|
||||
```
|
||||
|
||||
Returns a list of available actions:
|
||||
|
||||
```json
|
||||
{ "rpcs": ["increment", "getCount"] }
|
||||
```
|
||||
|
||||
#### Execute Action
|
||||
|
||||
```bash
|
||||
curl -X POST $RIVET_API/gateway/{actor_id}/inspector/action/increment \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"args": [5]}'
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{ "output": 47 }
|
||||
```
|
||||
|
||||
#### Get Queue Status
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/queue?limit=10
|
||||
```
|
||||
|
||||
Returns queue status with messages:
|
||||
|
||||
```json
|
||||
{
|
||||
"size": 3,
|
||||
"maxSize": 1000,
|
||||
"truncated": false,
|
||||
"messages": [
|
||||
{ "id": 1, "name": "process", "createdAtMs": 1706000000000 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Workflow History
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/workflow-history
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"history": null,
|
||||
"isWorkflowEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Database Schema
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/database/schema
|
||||
```
|
||||
|
||||
Returns discovered SQLite tables and views when the actor has `c.db` enabled:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": {
|
||||
"tables": [
|
||||
{
|
||||
"table": { "schema": "main", "name": "test_data", "type": "table" },
|
||||
"columns": [
|
||||
{ "cid": 0, "name": "id", "type": "", "notnull": 0, "dflt_value": null, "pk": 0 }
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"records": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Database Rows
|
||||
|
||||
```bash
|
||||
curl "$RIVET_API/gateway/{actor_id}/inspector/database/rows?table=test_data&limit=100&offset=0"
|
||||
```
|
||||
|
||||
Returns paged rows for a specific SQLite table or view:
|
||||
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"id": 1,
|
||||
"value": "Alice",
|
||||
"payload": "",
|
||||
"created_at": 1706000000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Execute SQLite
|
||||
|
||||
Run manual SQL against an actor's SQLite database. This supports both read-only queries and mutations.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:6420/gateway/{actor_id}/inspector/database/execute \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"sql": "SELECT id, value FROM test_data WHERE value = ? ORDER BY id DESC",
|
||||
"args": ["alpha"]
|
||||
}'
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
{ "id": 2, "value": "alpha" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can also use named SQLite bindings through a `properties` object:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:6420/gateway/{actor_id}/inspector/database/execute \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"sql": "SELECT id, value FROM test_data WHERE value = :value ORDER BY id DESC",
|
||||
"properties": {
|
||||
"value": "alpha"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
For mutations, use `RETURNING` if you want rows back. Otherwise the statement still runs and `rows` is empty:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:6420/gateway/{actor_id}/inspector/database/execute \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"sql": "INSERT INTO test_data (value, created_at) VALUES (?, ?) RETURNING id, value",
|
||||
"args": ["beta", 1706000000000]
|
||||
}'
|
||||
```
|
||||
|
||||
For workflow-enabled actors, `history` is a JSON object with `nameRegistry`, `entries`, and `entryMetadata`. Step outputs, loop state, and message payloads are decoded from CBOR into normal JSON values.
|
||||
|
||||
#### Replay Workflow From Step
|
||||
|
||||
Reset a workflow to a specific step and restart execution immediately. Omitting `entryId` replays the workflow from the beginning.
|
||||
|
||||
If the workflow is still running when you call replay, the endpoint rejects the request with `409 Conflict` and an `actor/workflow_in_flight` error instead of cancelling the live run for you.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:6420/gateway/{actor_id}/inspector/workflow/replay \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"entryId":"workflow-step-id"}'
|
||||
```
|
||||
|
||||
Returns the same JSON shape as `/inspector/workflow-history`:
|
||||
|
||||
```json
|
||||
{
|
||||
"history": {
|
||||
"nameRegistry": ["step-one", "step-two"],
|
||||
"entries": [],
|
||||
"entryMetadata": {}
|
||||
},
|
||||
"isWorkflowEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
While a workflow is in flight, the response shape is:
|
||||
|
||||
```json
|
||||
{
|
||||
"group": "actor",
|
||||
"code": "workflow_in_flight",
|
||||
"message": "Workflow replay is unavailable while the workflow is currently in flight.",
|
||||
"metadata": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Summary
|
||||
|
||||
Get a full snapshot of the actor in a single request:
|
||||
|
||||
```bash
|
||||
curl $RIVET_API/gateway/{actor_id}/inspector/summary
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"state": { "count": 42 },
|
||||
"connections": [],
|
||||
"rpcs": ["increment", "getCount"],
|
||||
"queueSize": 0,
|
||||
"isStateEnabled": true,
|
||||
"isDatabaseEnabled": false,
|
||||
"isWorkflowEnabled": false,
|
||||
"workflowHistory": null
|
||||
}
|
||||
```
|
||||
|
||||
When workflow history is present in `/inspector/summary`, `workflowHistory` is returned as the same decoded JSON shape as `/inspector/workflow-history`.
|
||||
|
||||
### Polling
|
||||
|
||||
Inspector endpoints are safe to poll. For live monitoring, poll at 1-5 second intervals. The `/inspector/summary` endpoint is useful for periodic snapshots since it returns all data in a single request.
|
||||
|
||||
## OpenAPI Spec
|
||||
|
||||
An OpenAPI specification covering many of the management and actor endpoints is available:
|
||||
|
||||
- In the repository at [`rivetkit-openapi/openapi.json`](https://github.com/rivet-dev/rivet/tree/main/rivetkit-openapi)
|
||||
- Served at `/doc` on the manager when running locally
|
||||
|
||||
The checked-in spec does not yet list every endpoint documented on this page (for example the actor metadata and queue routes and the inspector database routes), so treat this page as the authoritative reference where they differ.
|
||||
|
||||
_Source doc path: /docs/actors/debugging_
|
||||
@@ -0,0 +1,616 @@
|
||||
# Design Patterns
|
||||
|
||||
> Source: `src/content/docs/actors/design-patterns.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/design-patterns
|
||||
> Description: Common patterns and anti-patterns for building scalable actor systems.
|
||||
|
||||
---
|
||||
## How Actors Scale
|
||||
|
||||
Actors are inherently scalable because of how they're designed:
|
||||
|
||||
- **Isolated state:** Each actor manages its own private data. No shared state means no conflicts and no locks, so actors run concurrently without coordination.
|
||||
- **Actor-to-actor communication:** Actors interact through [actions](/docs/actors/actions) and [events](/docs/actors/events), so they don't need to coordinate access to shared data. This makes it easy to distribute them across machines.
|
||||
- **Small, focused units:** Each actor handles a limited scope (a single user, document, or chat room), so load naturally spreads across many actors rather than concentrating in one place.
|
||||
- **Horizontal scaling:** Adding more machines automatically distributes actors across them.
|
||||
|
||||
These properties form the foundation for the patterns described below.
|
||||
|
||||
## Actor Per Entity
|
||||
|
||||
The core pattern is creating one actor per entity in your system. Each actor represents a single user, document, chat room, or other distinct object. This keeps actors small, independent, and easy to scale.
|
||||
|
||||
**Good examples**
|
||||
|
||||
- `User`: Manages user profile, preferences, and authentication
|
||||
- `Document`: Handles document content, metadata, and versioning
|
||||
- `ChatRoom`: Manages participants and message history
|
||||
|
||||
**Bad examples**
|
||||
|
||||
- `Application`: Too broad, handles everything
|
||||
- `DocumentWordCount`: Too granular, should be part of Document actor
|
||||
|
||||
## Coordinator & Data Actors
|
||||
|
||||
Actors scale by splitting state into isolated entities. However, it's common to need to track and coordinate actors in a central place. This is where coordinator actors come in.
|
||||
|
||||
**Data actors** handle the main logic in your application. Examples: chat rooms, user sessions, game lobbies.
|
||||
|
||||
**Coordinator actors** track other actors. Think of them as an index of data actors. Examples: a list of chat rooms, a list of active users, a list of game lobbies.
|
||||
|
||||
**Example: Chat Room Coordinator**
|
||||
|
||||
### Actor
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Data actor: handles messages and connections
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as { sender: string; text: string }[] },
|
||||
actions: {
|
||||
sendMessage: (c, sender: string, text: string) => {
|
||||
const message = { sender, text };
|
||||
c.state.messages.push(message);
|
||||
c.broadcast("newMessage", message);
|
||||
return message;
|
||||
},
|
||||
getHistory: (c) => c.state.messages,
|
||||
},
|
||||
});
|
||||
|
||||
// Coordinator: indexes chat rooms
|
||||
const chatRoomList = actor({
|
||||
state: { chatRoomIds: [] as string[] },
|
||||
actions: {
|
||||
createChatRoom: async (c, name: string) => {
|
||||
const client = c.client<typeof registry>();
|
||||
// Create the chat room actor and get its ID
|
||||
const handle = await client.chatRoom.create([name]);
|
||||
const actorId = await handle.resolve();
|
||||
// Track it in the list
|
||||
c.state.chatRoomIds.push(actorId);
|
||||
return actorId;
|
||||
},
|
||||
listChatRooms: (c) => c.state.chatRoomIds,
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({
|
||||
use: { chatRoom, chatRoomList },
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as { sender: string; text: string }[] },
|
||||
actions: {
|
||||
sendMessage: (c, sender: string, text: string) => {
|
||||
const message = { sender, text };
|
||||
c.state.messages.push(message);
|
||||
return message;
|
||||
},
|
||||
getHistory: (c) => c.state.messages,
|
||||
},
|
||||
});
|
||||
|
||||
const chatRoomList = actor({
|
||||
state: { chatRoomIds: [] as string[] },
|
||||
actions: {
|
||||
createChatRoom: async (c, name: string) => "room-id",
|
||||
listChatRooms: (c) => c.state.chatRoomIds,
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chatRoom, chatRoomList } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Create a new chat room via coordinator
|
||||
const coordinator = client.chatRoomList.getOrCreate(["main"]);
|
||||
const actorId = await coordinator.createChatRoom("general");
|
||||
|
||||
// Get list of all chat rooms
|
||||
const chatRoomIds = await coordinator.listChatRooms();
|
||||
|
||||
// Connect to a chat room using its ID
|
||||
const chatRoomHandle = client.chatRoom.getForId(actorId);
|
||||
await chatRoomHandle.sendMessage("alice", "Hello!");
|
||||
const history = await chatRoomHandle.getHistory();
|
||||
```
|
||||
|
||||
## Sharding
|
||||
|
||||
Sharding splits a single actor's workload across multiple actors based on a key. Use this when one actor can't handle all the load or data for an entity.
|
||||
|
||||
**How it works:**
|
||||
- Partition data using a shard key (user ID, region, time bucket, or random)
|
||||
- Requests are routed to shards based on the key
|
||||
- Shards operate independently without coordination
|
||||
|
||||
**Example: Sharding by Time**
|
||||
|
||||
### Actor
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface Event {
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const hourlyAnalytics = actor({
|
||||
state: { events: [] as Event[] },
|
||||
actions: {
|
||||
trackEvent: (c, event: Event) => {
|
||||
c.state.events.push(event);
|
||||
},
|
||||
getEvents: (c) => c.state.events,
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { hourlyAnalytics },
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface Event {
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const hourlyAnalytics = actor({
|
||||
state: { events: [] as Event[] },
|
||||
actions: {
|
||||
trackEvent: (c, event: Event) => {
|
||||
c.state.events.push(event);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { hourlyAnalytics } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Shard by hour: hourlyAnalytics:2024-01-15T00, hourlyAnalytics:2024-01-15T01
|
||||
const shardKey = new Date().toISOString().slice(0, 13); // "2024-01-15T00"
|
||||
const analytics = client.hourlyAnalytics.getOrCreate([shardKey]);
|
||||
await analytics.trackEvent({ type: "page_view", url: "/home" });
|
||||
```
|
||||
|
||||
**Example: Random Sharding**
|
||||
|
||||
### Actor
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const rateLimiter = actor({
|
||||
state: { requests: {} as Record<string, number> },
|
||||
actions: {
|
||||
checkLimit: (c, userId: string, limit: number) => {
|
||||
const count = c.state.requests[userId] ?? 0;
|
||||
if (count >= limit) return false;
|
||||
c.state.requests[userId] = count + 1;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { rateLimiter },
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const rateLimiter = actor({
|
||||
state: { requests: {} as Record<string, number> },
|
||||
actions: {
|
||||
checkLimit: (c, userId: string, limit: number) => {
|
||||
const count = c.state.requests[userId] ?? 0;
|
||||
if (count >= limit) return false;
|
||||
c.state.requests[userId] = count + 1;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { rateLimiter } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Shard randomly: rateLimiter:shard-0, rateLimiter:shard-1, rateLimiter:shard-2
|
||||
const shardKey = `shard-${Math.floor(Math.random() * 3)}`;
|
||||
const limiter = client.rateLimiter.getOrCreate([shardKey]);
|
||||
const allowed = await limiter.checkLimit("user-123", 100);
|
||||
```
|
||||
|
||||
Choose shard keys that distribute load evenly. Note that cross-shard queries require coordination.
|
||||
|
||||
## Fan-In & Fan-Out
|
||||
|
||||
Fan-in and fan-out are patterns for distributing work and aggregating results.
|
||||
|
||||
**Fan-Out**: One actor spawns work across multiple actors. Use for parallel processing or broadcasting updates.
|
||||
|
||||
**Fan-In**: Multiple actors send results to one aggregator. Use for collecting results or reducing data.
|
||||
|
||||
**Example: Map-Reduce**
|
||||
|
||||
### Actor
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
taskId: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
// Coordinator fans out tasks, then fans in results
|
||||
const coordinator = actor({
|
||||
state: { results: [] as Result[] },
|
||||
actions: {
|
||||
// Fan-out: distribute work in parallel
|
||||
startJob: async (c, tasks: Task[]) => {
|
||||
const client = c.client<typeof registry>();
|
||||
await Promise.all(
|
||||
tasks.map(task => client.worker.getOrCreate(task.id).process(task))
|
||||
);
|
||||
},
|
||||
// Fan-in: collect results
|
||||
reportResult: (c, result: Result) => {
|
||||
c.state.results.push(result);
|
||||
},
|
||||
getResults: (c) => c.state.results,
|
||||
},
|
||||
});
|
||||
|
||||
const worker = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
process: async (c, task: Task) => {
|
||||
const result = { taskId: task.id, output: `Processed ${task.data}` };
|
||||
const client = c.client<typeof registry>();
|
||||
await client.coordinator.getOrCreate("main").reportResult(result);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { coordinator, worker },
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
taskId: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
const coordinator = actor({
|
||||
state: { results: [] as Result[] },
|
||||
actions: {
|
||||
startJob: async (c, tasks: Task[]) => {},
|
||||
reportResult: (c, result: Result) => { c.state.results.push(result); },
|
||||
getResults: (c) => c.state.results,
|
||||
},
|
||||
});
|
||||
|
||||
const worker = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
process: async (c, task: Task) => {},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { coordinator, worker } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const coordinatorHandle = client.coordinator.getOrCreate(["main"]);
|
||||
|
||||
// Start a job with multiple tasks
|
||||
await coordinatorHandle.startJob([
|
||||
{ id: "task-1", data: "..." },
|
||||
{ id: "task-2", data: "..." },
|
||||
{ id: "task-3", data: "..." },
|
||||
]);
|
||||
|
||||
// Results are collected as workers report back
|
||||
const results = await coordinatorHandle.getResults();
|
||||
```
|
||||
|
||||
## Integrating With External Databases & APIs
|
||||
|
||||
Actors can integrate with external resources like databases or external APIs.
|
||||
|
||||
### Loading State
|
||||
|
||||
Load external data during actor initialization using `createVars`. This keeps your actor's persisted state clean while caching expensive lookups.
|
||||
|
||||
Use this when:
|
||||
|
||||
- Fetching user profiles, configs, or permissions from a database
|
||||
- Loading data that changes externally and shouldn't be persisted
|
||||
- Caching expensive API calls or computations
|
||||
|
||||
**Example: Loading User Profile**
|
||||
|
||||
### Actor
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Mock database interface for demonstration
|
||||
const db = {
|
||||
users: {
|
||||
findById: async (id: string): Promise<User> => ({ id, email: "user@example.com", name: "User" }),
|
||||
update: async (id: string, data: Partial<User>) => {},
|
||||
},
|
||||
};
|
||||
|
||||
const userSession = actor({
|
||||
state: { requestCount: 0 },
|
||||
|
||||
// createVars runs on every wake (after restarts, crashes, or sleep), so
|
||||
// external data stays fresh.
|
||||
createVars: async (c): Promise<{ user: User }> => {
|
||||
// Load from database on every wake
|
||||
const user = await db.users.findById(c.key.join("-"));
|
||||
return { user };
|
||||
},
|
||||
|
||||
actions: {
|
||||
getProfile: (c) => {
|
||||
c.state.requestCount++;
|
||||
return c.vars.user;
|
||||
},
|
||||
updateEmail: async (c, email: string) => {
|
||||
c.state.requestCount++;
|
||||
await db.users.update(c.key.join("-"), { email });
|
||||
// Refresh cached data
|
||||
c.vars.user = await db.users.findById(c.key.join("-"));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({
|
||||
use: { userSession },
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const userSession = actor({
|
||||
state: { requestCount: 0 },
|
||||
createVars: () => ({ user: null as User | null }),
|
||||
actions: {
|
||||
getProfile: (c) => c.vars.user,
|
||||
updateEmail: async (c, email: string) => {},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { userSession } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const session = client.userSession.getOrCreate(["user-123"]);
|
||||
|
||||
// Get profile (loaded from database on actor wake)
|
||||
const profile = await session.getProfile();
|
||||
|
||||
// Update email (writes to database and refreshes cache)
|
||||
await session.updateEmail("alice@example.com");
|
||||
```
|
||||
|
||||
### Syncing State Changes
|
||||
|
||||
Use `onStateChange` to automatically sync actor state changes to external resources. This hook runs after state changes are flushed, which is coalesced to once per event loop tick rather than once per individual field mutation.
|
||||
|
||||
Use this when:
|
||||
|
||||
- You need to mirror actor state in an external database
|
||||
- Triggering external side effects when state changes
|
||||
- Keeping external systems in sync with actor state
|
||||
|
||||
**Example: Syncing to Database**
|
||||
|
||||
### Actor
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Mock database interface for demonstration
|
||||
const db = {
|
||||
users: {
|
||||
insert: async (data: { id: string; email: string; createdAt: number }) => {},
|
||||
update: async (id: string, data: { email: string; lastActive: number }) => {},
|
||||
},
|
||||
};
|
||||
|
||||
const userActor = actor({
|
||||
state: {
|
||||
email: "",
|
||||
lastActive: 0,
|
||||
},
|
||||
|
||||
onCreate: async (c, input: { email: string }) => {
|
||||
// Insert into database on actor creation
|
||||
await db.users.insert({
|
||||
id: c.key.join("-"),
|
||||
email: input.email,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
onStateChange: async (c, newState) => {
|
||||
// Sync any state changes to database
|
||||
await db.users.update(c.key.join("-"), {
|
||||
email: newState.email,
|
||||
lastActive: newState.lastActive,
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateEmail: (c, email: string) => {
|
||||
c.state.email = email;
|
||||
c.state.lastActive = Date.now();
|
||||
},
|
||||
getUser: (c) => ({
|
||||
email: c.state.email,
|
||||
lastActive: c.state.lastActive,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({
|
||||
use: { userActor },
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const userActor = actor({
|
||||
state: { email: "", lastActive: 0 },
|
||||
actions: {
|
||||
updateEmail: (c, email: string) => {
|
||||
c.state.email = email;
|
||||
c.state.lastActive = Date.now();
|
||||
},
|
||||
getUser: (c) => ({
|
||||
email: c.state.email,
|
||||
lastActive: c.state.lastActive,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { userActor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const user = await client.userActor.create(["user-123"], {
|
||||
input: { email: "alice@example.com" },
|
||||
});
|
||||
|
||||
// Updates state and triggers onStateChange
|
||||
await user.updateEmail("alice2@example.com");
|
||||
|
||||
const userData = await user.getUser();
|
||||
```
|
||||
|
||||
`onStateChange` is called once per flush with the final coalesced state, ensuring external resources stay in sync. In the `updateEmail` example above, the two synchronous assignments produce a single `onStateChange` call.
|
||||
|
||||
Do not mutate `c.state` inside `onStateChange`; re-entrant state mutation is rejected.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### "God" Actor
|
||||
|
||||
Avoid creating a single actor that handles everything. This defeats the purpose of the actor model and creates a bottleneck.
|
||||
|
||||
**Problem:**
|
||||
```ts
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
// Bad: one actor doing everything
|
||||
const app = actor({
|
||||
state: { users: {}, orders: {}, inventory: {}, analytics: {} },
|
||||
actions: {
|
||||
createUser: (c, user) => { /* ... */ },
|
||||
processOrder: (c, order) => { /* ... */ },
|
||||
updateInventory: (c, item) => { /* ... */ },
|
||||
trackEvent: (c, event) => { /* ... */ },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Solution:** Split into focused actors per entity (User, Order, Inventory, Analytics).
|
||||
|
||||
### Actor-Per-Request
|
||||
|
||||
Actors are designed to maintain state across multiple requests. Creating a new actor for each request wastes resources and loses the benefits of persistent state.
|
||||
|
||||
**Problem:**
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { Hono } from "hono";
|
||||
|
||||
const processor = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
process: (c, body: unknown) => ({ processed: true }),
|
||||
destroySelf: (c) => c.destroy(),
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { processor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const app = new Hono();
|
||||
|
||||
// Bad: creating an actor for each API request
|
||||
app.post("/process", async (c) => {
|
||||
const actorHandle = client.processor.getOrCreate([crypto.randomUUID()]);
|
||||
const result = await actorHandle.process(await c.req.json());
|
||||
await actorHandle.destroySelf();
|
||||
return c.json(result);
|
||||
});
|
||||
```
|
||||
|
||||
**Solution:** Use actors for entities that persist (users, sessions, documents), not for one-off operations. For stateless request handling, use regular functions.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for pattern examples
|
||||
- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - Context usage patterns
|
||||
- [`ActionContext`](/typedoc/interfaces/rivetkit.mod.ActionContext.html) - Action patterns
|
||||
|
||||
_Source doc path: /docs/actors/design-patterns_
|
||||
123
.agents/skills/ai-agent-workspace/reference/actors/destroy.md
Normal file
123
.agents/skills/ai-agent-workspace/reference/actors/destroy.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Destroying Actors
|
||||
|
||||
> Source: `src/content/docs/actors/destroy.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/destroy
|
||||
> Description: Actors can be permanently destroyed. Common use cases include:
|
||||
|
||||
---
|
||||
- User account deletion
|
||||
- Ending a user session
|
||||
- Closing a room or game
|
||||
- Cleaning up temporary resources
|
||||
- GDPR/compliance data removal
|
||||
|
||||
Actors sleep when idle, so destruction is only needed to permanently remove data — not to save compute.
|
||||
|
||||
## Destroying An Actor
|
||||
|
||||
### Destroy via Action
|
||||
|
||||
To destroy an actor, use `c.destroy()` like this:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface UserInput {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const userActor = actor({
|
||||
createState: (c, input: UserInput) => ({
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
}),
|
||||
actions: {
|
||||
deleteAccount: (c) => {
|
||||
c.destroy();
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Destroy via HTTP
|
||||
|
||||
Send a DELETE request to destroy an actor. This requires a token for authentication:
|
||||
|
||||
- **Rivet Cloud**: Use the service key (`sk_*`) from your `RIVET_ENDPOINT` URL. Find this in the dashboard under **Settings > Advanced > Backend Configuration** (e.g. the `sk_...` portion of `https://default:sk_abc123@api.rivet.dev`).
|
||||
- **Self-hosted**: Use an admin token.
|
||||
|
||||
```typescript
|
||||
const actorId = "your-actor-id";
|
||||
const namespace = "default";
|
||||
const token = "your-token";
|
||||
|
||||
await fetch(`https://api.rivet.dev/actors/${actorId}?namespace=${namespace}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X DELETE "https://api.rivet.dev/actors/{actorId}?namespace={namespace}" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
To find the actor ID, you can list actors first:
|
||||
|
||||
```bash
|
||||
curl "https://api.rivet.dev/actors?namespace={namespace}" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
### Destroy via Dashboard
|
||||
|
||||
To destroy an actor via the dashboard, navigate to the actor and press the red "X" in the top right.
|
||||
|
||||
## Lifecycle Hook
|
||||
|
||||
Once destroyed, the `onDestroy` hook will be called. This can be used to clean up resources related to the actor. For example:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface UserState {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Example email service interface
|
||||
const emailService = {
|
||||
send: async (options: { from: string; to: string; subject: string; text: string }) => {},
|
||||
};
|
||||
|
||||
const userActor = actor({
|
||||
state: { email: "", name: "" } as UserState,
|
||||
onDestroy: async (c) => {
|
||||
await emailService.send({
|
||||
from: "noreply@example.com",
|
||||
to: c.state.email,
|
||||
subject: "Account Deleted",
|
||||
text: `Goodbye ${c.state.name}, your account has been deleted.`,
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
deleteAccount: (c) => {
|
||||
c.destroy();
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Accessing Actor After Destroy
|
||||
|
||||
Once an actor is destroyed, any subsequent requests to it will fail with an `actor.not_found` error (`{ group: "actor", code: "not_found" }`). The actor's state is permanently deleted.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Has destroy methods
|
||||
- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - Context during destruction
|
||||
|
||||
_Source doc path: /docs/actors/destroy_
|
||||
434
.agents/skills/ai-agent-workspace/reference/actors/errors.md
Normal file
434
.agents/skills/ai-agent-workspace/reference/actors/errors.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# Errors
|
||||
|
||||
> Source: `src/content/docs/actors/errors.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/errors
|
||||
> Description: Rivet provides robust error handling with security built in by default. Errors are handled differently based on whether they should be exposed to clients or kept private.
|
||||
|
||||
---
|
||||
There are two types of errors:
|
||||
|
||||
- **UserError**: Thrown from actors and safely returned to clients with full details
|
||||
- **Internal errors**: All other errors that are converted to a generic error message for security
|
||||
|
||||
## Throwing and Catching Errors
|
||||
|
||||
`UserError` lets you throw custom errors that will be safely returned to the client.
|
||||
|
||||
Throw a `UserError` with just a message:
|
||||
|
||||
### Actor
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => {
|
||||
// Validate username
|
||||
if (username.length > 32) {
|
||||
throw new UserError("Username is too long");
|
||||
}
|
||||
|
||||
// Update username
|
||||
c.state.username = username;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => {
|
||||
if (username.length > 32) throw new Error("Username is too long");
|
||||
c.state.username = username;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { user } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const conn = client.user.getOrCreate([]).connect();
|
||||
|
||||
try {
|
||||
await conn.updateUsername("extremely_long_username_that_exceeds_the_limit");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.message); // "Username is too long"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => {
|
||||
if (username.length > 32) throw new Error("Username is too long");
|
||||
c.state.username = username;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { user } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const userActor = client.user.getOrCreate([]);
|
||||
|
||||
try {
|
||||
await userActor.updateUsername("extremely_long_username_that_exceeds_the_limit");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.message); // "Username is too long"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
Use error codes for explicit error matching in try-catch blocks:
|
||||
|
||||
### Actor
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => {
|
||||
if (username.length < 3) {
|
||||
throw new UserError("Username is too short", {
|
||||
code: "username_too_short"
|
||||
});
|
||||
}
|
||||
|
||||
if (username.length > 32) {
|
||||
throw new UserError("Username is too long", {
|
||||
code: "username_too_long"
|
||||
});
|
||||
}
|
||||
|
||||
// Update username
|
||||
c.state.username = username;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => { c.state.username = username; }
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { user } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const conn = client.user.getOrCreate([]).connect();
|
||||
|
||||
try {
|
||||
await conn.updateUsername("ab");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
if (error.code === "username_too_short") {
|
||||
console.log("Please choose a longer username");
|
||||
} else if (error.code === "username_too_long") {
|
||||
console.log("Please choose a shorter username");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const user = actor({
|
||||
state: { username: "" },
|
||||
actions: {
|
||||
updateUsername: (c, username: string) => { c.state.username = username; }
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { user } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const userActor = client.user.getOrCreate([]);
|
||||
|
||||
try {
|
||||
await userActor.updateUsername("ab");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
if (error.code === "username_too_short") {
|
||||
console.log("Please choose a longer username");
|
||||
} else if (error.code === "username_too_long") {
|
||||
console.log("Please choose a shorter username");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Errors With Metadata
|
||||
|
||||
Include metadata to provide additional context for rich error handling:
|
||||
|
||||
### Actor
|
||||
|
||||
```typescript
|
||||
import { actor, UserError } from "rivetkit";
|
||||
|
||||
const api = actor({
|
||||
state: { requestCount: 0, lastReset: Date.now() },
|
||||
actions: {
|
||||
makeRequest: (c) => {
|
||||
c.state.requestCount++;
|
||||
|
||||
const limit = 100;
|
||||
if (c.state.requestCount > limit) {
|
||||
const resetAt = c.state.lastReset + 60_000; // Reset after 1 minute
|
||||
|
||||
throw new UserError("Rate limit exceeded", {
|
||||
code: "rate_limited",
|
||||
metadata: {
|
||||
limit: limit,
|
||||
resetAt: resetAt,
|
||||
retryAfter: Math.ceil((resetAt - Date.now()) / 1000)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rest of request logic...
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const api = actor({
|
||||
state: { requestCount: 0 },
|
||||
actions: { makeRequest: (c) => {} }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { api } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const conn = client.api.getOrCreate([]).connect();
|
||||
|
||||
try {
|
||||
await conn.makeRequest();
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.message); // "Rate limit exceeded"
|
||||
console.log(error.code); // "rate_limited"
|
||||
console.log(error.metadata); // { limit: 100, resetAt: 1234567890, retryAfter: 45 }
|
||||
|
||||
if (error.code === "rate_limited") {
|
||||
const metadata = error.metadata as { retryAfter: number };
|
||||
console.log(`Rate limit hit. Try again in ${metadata.retryAfter} seconds`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const api = actor({
|
||||
state: { requestCount: 0 },
|
||||
actions: { makeRequest: (c) => {} }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { api } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const apiActor = client.api.getOrCreate([]);
|
||||
|
||||
try {
|
||||
await apiActor.makeRequest();
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.message); // "Rate limit exceeded"
|
||||
console.log(error.code); // "rate_limited"
|
||||
console.log(error.metadata); // { limit: 100, resetAt: 1234567890, retryAfter: 45 }
|
||||
|
||||
if (error.code === "rate_limited") {
|
||||
const metadata = error.metadata as { retryAfter: number };
|
||||
console.log(`Rate limit hit. Try again in ${metadata.retryAfter} seconds`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Internal Errors
|
||||
|
||||
All errors that are not UserError instances are automatically converted to a generic "internal error" response. This prevents accidentally leaking sensitive information like stack traces, database details, or internal system information.
|
||||
|
||||
### Actor
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const payment = actor({
|
||||
state: { transactions: [] },
|
||||
actions: {
|
||||
processPayment: async (c, amount: number) => {
|
||||
// This will throw a regular Error (not UserError)
|
||||
const result = await fetch("https://payment-api.example.com/charge", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ amount })
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
// This internal error will be hidden from the client
|
||||
throw new Error(`Payment API returned ${result.status}: ${await result.text()}`);
|
||||
}
|
||||
|
||||
// Rest of payment logic...
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
interface Transaction {
|
||||
amount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const payment = actor({
|
||||
state: { transactions: [] as Transaction[] },
|
||||
actions: { processPayment: async (c, amount: number) => {} }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { payment } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const conn = client.payment.getOrCreate([]).connect();
|
||||
|
||||
try {
|
||||
await conn.processPayment(100);
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.code); // "internal_error"
|
||||
console.log(error.message); // "An internal error occurred"
|
||||
|
||||
// Original error details are NOT exposed to the client
|
||||
// Check your server logs to see the actual error message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
interface Transaction {
|
||||
amount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const payment = actor({
|
||||
state: { transactions: [] as Transaction[] },
|
||||
actions: { processPayment: async (c, amount: number) => {} }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { payment } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const paymentActor = client.payment.getOrCreate([]);
|
||||
|
||||
try {
|
||||
await paymentActor.processPayment(100);
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.code); // "internal_error"
|
||||
console.log(error.message); // "An internal error occurred"
|
||||
|
||||
// Original error details are NOT exposed to the client
|
||||
// Check your server logs to see the actual error message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Server-Side Logging
|
||||
|
||||
**All internal errors are logged server-side with full details.** When an internal error occurs, the complete error message, stack trace, and context are written to your server logs. This is where you should look first when debugging internal errors in production.
|
||||
|
||||
The client receives only a generic "Internal error" message for security, but you can find the full error details in your server logs including:
|
||||
|
||||
- Complete error message
|
||||
- Stack trace
|
||||
- Request context (actor ID, action name, connection ID, etc.)
|
||||
- Timestamp
|
||||
|
||||
**Always check your server logs to see the actual error details when debugging internal errors.**
|
||||
|
||||
### Exposing Errors to Clients (Development Only)
|
||||
|
||||
**Warning:** Only enable error exposure in development environments. In production, this will leak sensitive internal details to clients.
|
||||
|
||||
For faster debugging during development, you can expose internal error details to clients by setting `RIVET_EXPOSE_ERRORS=1`.
|
||||
|
||||
With error exposure enabled, clients will see the full error message instead of the generic "Internal error" response:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient, ActorError } from "rivetkit/client";
|
||||
|
||||
const payment = actor({
|
||||
state: {},
|
||||
actions: { processPayment: async (c, amount: number) => {} }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { payment } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const paymentActor = client.payment.getOrCreate([]);
|
||||
|
||||
// With RIVET_EXPOSE_ERRORS=1
|
||||
try {
|
||||
await paymentActor.processPayment(100);
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.message);
|
||||
// "Payment API returned 402: Insufficient funds"
|
||||
// Instead of: "An internal error occurred"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`UserError`](/typedoc/classes/rivetkit.actor_errors.UserError.html) - User-facing error class
|
||||
- [`ActorError`](/typedoc/classes/rivetkit.client_mod.ActorError.html) - Errors received by the client
|
||||
|
||||
_Source doc path: /docs/actors/errors_
|
||||
390
.agents/skills/ai-agent-workspace/reference/actors/events.md
Normal file
390
.agents/skills/ai-agent-workspace/reference/actors/events.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# Realtime
|
||||
|
||||
> Source: `src/content/docs/actors/events.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/events
|
||||
> Description: Events enable realtime communication from actors to clients. While clients use actions to send data to actors, events allow actors to push updates to connected clients instantly.
|
||||
|
||||
---
|
||||
Events can be sent to clients connected using `.connect()`. They have no effect on [low-level WebSocket connections](/docs/actors/websocket-handler).
|
||||
|
||||
For worked realtime patterns, see the cookbook: [Live Cursors and Presence](/cookbook/live-cursors/) and [Chat Room](/cookbook/chat-room/).
|
||||
|
||||
## Publishing Events from Actors
|
||||
|
||||
### Broadcasting to All Clients
|
||||
|
||||
Define event names and payload types with `events` and `event()`, then use `c.broadcast(eventName, ...args)` to send events to all connected clients:
|
||||
|
||||
```typescript
|
||||
import { actor, event } from "rivetkit";
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
userId: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const chatRoom = actor({
|
||||
state: {
|
||||
messages: [] as Message[]
|
||||
},
|
||||
|
||||
events: {
|
||||
messageReceived: event<Message>()
|
||||
},
|
||||
|
||||
actions: {
|
||||
sendMessage: (c, userId: string, text: string) => {
|
||||
const message = {
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
text,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
c.state.messages.push(message);
|
||||
|
||||
// Broadcast to all connected clients
|
||||
c.broadcast('messageReceived', message);
|
||||
|
||||
return message;
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Sending to Specific Connections
|
||||
|
||||
Send events to individual connections using `conn.send(eventName, ...args)`:
|
||||
|
||||
```typescript
|
||||
import { actor, event } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
playerId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const gameRoom = actor({
|
||||
state: {
|
||||
players: {} as Record<string, {health: number, position: {x: number, y: number}}>
|
||||
},
|
||||
|
||||
events: {
|
||||
privateMessage: event<{
|
||||
from?: string;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
}>()
|
||||
},
|
||||
|
||||
createConnState: (c, params: { playerId: string, role?: string }): ConnState => ({
|
||||
playerId: params.playerId,
|
||||
role: params.role || "player"
|
||||
}),
|
||||
|
||||
actions: {
|
||||
sendPrivateMessage: (c, targetPlayerId: string, message: string) => {
|
||||
// Find the target player's connection
|
||||
let targetConn = null;
|
||||
for (const conn of c.conns.values()) {
|
||||
if (conn.state.playerId === targetPlayerId) {
|
||||
targetConn = conn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetConn) {
|
||||
targetConn.send('privateMessage', {
|
||||
from: c.conn?.state.playerId,
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} else {
|
||||
throw new Error("Player not found or not connected");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Send events to all connections except the sender:
|
||||
|
||||
```typescript
|
||||
import { actor, event } from "rivetkit";
|
||||
|
||||
interface ConnState {
|
||||
playerId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const gameRoom = actor({
|
||||
state: {
|
||||
players: {} as Record<string, {health: number, position: {x: number, y: number}}>
|
||||
},
|
||||
|
||||
events: {
|
||||
playerMoved: event<{
|
||||
playerId: string;
|
||||
position: { x: number; y: number };
|
||||
}>()
|
||||
},
|
||||
|
||||
createConnState: (c, params: { playerId: string, role?: string }): ConnState => ({
|
||||
playerId: params.playerId,
|
||||
role: params.role || "player"
|
||||
}),
|
||||
|
||||
actions: {
|
||||
updatePlayerPosition: (c, position: {x: number, y: number}) => {
|
||||
const playerId = c.conn?.state.playerId;
|
||||
if (!playerId) return;
|
||||
|
||||
if (c.state.players[playerId]) {
|
||||
c.state.players[playerId].position = position;
|
||||
|
||||
// Send position update to all OTHER players
|
||||
for (const conn of c.conns.values()) {
|
||||
if (conn.state.playerId !== playerId) {
|
||||
conn.send('playerMoved', { playerId, position });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Subscribing to Events from Clients
|
||||
|
||||
Clients must establish a connection to receive events from actors. Use `.connect()` to create a persistent connection, then listen for events.
|
||||
|
||||
### Basic Event Subscription
|
||||
|
||||
Use `connection.on(eventName, callback)` to listen for events:
|
||||
|
||||
```typescript TypeScript
|
||||
import { actor, event, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
type Message = { id: string; userId: string; text: string };
|
||||
|
||||
// Define the actor
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as Message[] },
|
||||
events: {
|
||||
messageReceived: event<Message>()
|
||||
},
|
||||
actions: {
|
||||
sendMessage: (c, userId: string, text: string) => {
|
||||
const message = { id: crypto.randomUUID(), userId, text };
|
||||
c.state.messages.push(message);
|
||||
c.broadcast('messageReceived', message);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chatRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Helper function for demonstration
|
||||
function displayMessage(message: Message) {
|
||||
console.log("Display:", message);
|
||||
}
|
||||
|
||||
// Get actor handle and establish connection
|
||||
const chatRoomHandle = client.chatRoom.getOrCreate(["general"]);
|
||||
const connection = chatRoomHandle.connect();
|
||||
|
||||
// Listen for events
|
||||
connection.on('messageReceived', (message: Message) => {
|
||||
console.log(`${message.userId}: ${message.text}`);
|
||||
displayMessage(message);
|
||||
});
|
||||
|
||||
// Call actions through the connection
|
||||
await connection.sendMessage("user-123", "Hello everyone!");
|
||||
```
|
||||
|
||||
```tsx React @nocheck
|
||||
import { useState } from "react";
|
||||
import { useActor } from "./rivetkit";
|
||||
|
||||
function ChatRoom() {
|
||||
const [messages, setMessages] = useState<Array<{id: string, userId: string, text: string}>>([]);
|
||||
|
||||
const chatRoom = useActor({
|
||||
name: "chatRoom",
|
||||
key: ["general"]
|
||||
});
|
||||
|
||||
// Listen for events
|
||||
chatRoom.useEvent("messageReceived", (message) => {
|
||||
setMessages(prev => [...prev, message]);
|
||||
});
|
||||
|
||||
// ...rest of component...
|
||||
}
|
||||
```
|
||||
|
||||
### One-time Event Listeners
|
||||
|
||||
Use `connection.once(eventName, callback)` for events that should only trigger once:
|
||||
|
||||
```typescript TypeScript
|
||||
import { actor, event, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const gameRoom = actor({
|
||||
state: { started: false },
|
||||
events: {
|
||||
gameStarted: event<[]>()
|
||||
},
|
||||
actions: {
|
||||
startGame: (c) => {
|
||||
c.state.started = true;
|
||||
c.broadcast('gameStarted');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { gameRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
function showGameInterface() {
|
||||
console.log("Showing game interface");
|
||||
}
|
||||
|
||||
const gameRoomHandle = client.gameRoom.getOrCreate(["room-456"]);
|
||||
const connection = gameRoomHandle.connect();
|
||||
|
||||
// Listen for game start (only once)
|
||||
connection.once('gameStarted', () => {
|
||||
console.log('Game has started!');
|
||||
showGameInterface();
|
||||
});
|
||||
```
|
||||
|
||||
```tsx React @nocheck
|
||||
import { useState, useEffect } from "react";
|
||||
import { useActor } from "./rivetkit";
|
||||
|
||||
function GameLobby() {
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
|
||||
const gameRoom = useActor({
|
||||
name: "gameRoom",
|
||||
key: ["room-456"],
|
||||
params: {
|
||||
playerId: "player-789",
|
||||
role: "player"
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for game start (only once)
|
||||
useEffect(() => {
|
||||
if (!gameRoom.connection) return;
|
||||
|
||||
const handleGameStart = () => {
|
||||
console.log('Game has started!');
|
||||
setGameStarted(true);
|
||||
};
|
||||
|
||||
gameRoom.connection.once('gameStarted', handleGameStart);
|
||||
}, [gameRoom.connection]);
|
||||
|
||||
// ...rest of component...
|
||||
}
|
||||
```
|
||||
|
||||
### Removing Event Listeners
|
||||
|
||||
Use the callback returned from `.on()` to remove event listeners:
|
||||
|
||||
```typescript TypeScript
|
||||
import { actor, event, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
type Message = { text: string };
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
events: {
|
||||
messageReceived: event<Message>()
|
||||
},
|
||||
actions: {
|
||||
sendMessage: (c, text: string) => {
|
||||
c.state.messages.push(text);
|
||||
c.broadcast('messageReceived', { text });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chatRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const connection = client.chatRoom.getOrCreate(["general"]).connect();
|
||||
|
||||
// Add listener
|
||||
const unsubscribe = connection.on('messageReceived', (message) => {
|
||||
console.log("Received:", message);
|
||||
});
|
||||
|
||||
// Remove listener
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
```tsx React @nocheck
|
||||
import { useState, useEffect } from "react";
|
||||
import { useActor } from "./rivetkit";
|
||||
|
||||
function ConditionalListener() {
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [messages, setMessages] = useState<string[]>([]);
|
||||
|
||||
const chatRoom = useActor({
|
||||
name: "chatRoom",
|
||||
key: ["general"]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatRoom.connection || !isListening) return;
|
||||
|
||||
// Add listener
|
||||
const unsubscribe = chatRoom.connection.on('messageReceived', (message) => {
|
||||
setMessages(prev => [...prev, message.text]);
|
||||
});
|
||||
|
||||
// Cleanup - remove listener when component unmounts or listening stops
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [chatRoom.connection, isListening]);
|
||||
|
||||
// ...rest of component...
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
- `GET /inspector/connections` shows active connections and connection metadata.
|
||||
- Use this to confirm clients are connected before expecting broadcasts.
|
||||
- `GET /inspector/summary` provides connections, RPCs, and queue size in one response.
|
||||
- In non-dev mode, inspector endpoints require authorization.
|
||||
|
||||
## More About Connections
|
||||
|
||||
For more details on actor connections, including connection lifecycle, authentication, and advanced connection patterns, see the [Connections documentation](/docs/actors/connections).
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`RivetEvent`](/typedoc/interfaces/rivetkit.mod.RivetEvent.html) - Base event interface
|
||||
- [`RivetMessageEvent`](/typedoc/interfaces/rivetkit.mod.RivetMessageEvent.html) - Message event type
|
||||
- [`RivetCloseEvent`](/typedoc/interfaces/rivetkit.mod.RivetCloseEvent.html) - Close event type
|
||||
- [`UniversalEvent`](/typedoc/interfaces/rivetkit.mod.UniversalEvent.html) - Universal event type
|
||||
- [`UniversalMessageEvent`](/typedoc/interfaces/rivetkit.mod.UniversalMessageEvent.html) - Universal message event
|
||||
- [`UniversalErrorEvent`](/typedoc/interfaces/rivetkit.mod.UniversalErrorEvent.html) - Universal error event
|
||||
- [`EventUnsubscribe`](/typedoc/types/rivetkit.client_mod.EventUnsubscribe.html) - Unsubscribe function type
|
||||
|
||||
_Source doc path: /docs/actors/events_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Fetch and WebSocket Handler
|
||||
|
||||
> Source: `src/content/docs/actors/fetch-and-websocket-handler.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/fetch-and-websocket-handler
|
||||
> Description: These docs have moved to [Low-Level WebSocket Handler](/docs/actors/websocket-handler) and [Low-Level Request Handler](/docs/actors/request-handler).
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Source doc path: /docs/actors/fetch-and-websocket-handler_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Helper Types
|
||||
|
||||
> Source: `src/content/docs/actors/helper-types.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/helper-types
|
||||
> Description: This page has moved to [Types](/docs/actors/types).
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Source doc path: /docs/actors/helper-types_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Vanilla HTTP API
|
||||
|
||||
> Source: `src/content/docs/actors/http-api.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/http-api
|
||||
> Description: Use the low-level HTTP handler to send and receive requests from actors.
|
||||
|
||||
---
|
||||
TODO
|
||||
|
||||
_Source doc path: /docs/actors/http-api_
|
||||
287
.agents/skills/ai-agent-workspace/reference/actors/input.md
Normal file
287
.agents/skills/ai-agent-workspace/reference/actors/input.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Input Parameters
|
||||
|
||||
> Source: `src/content/docs/actors/input.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/input
|
||||
> Description: Pass initialization data to actors when creating instances
|
||||
|
||||
---
|
||||
Actors can receive input parameters when created, allowing for flexible initialization and configuration. Input is passed during actor creation and is available in lifecycle hooks.
|
||||
|
||||
## Passing Input to Actors
|
||||
|
||||
Input is provided when creating actor instances using the `input` property:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface GameInput {
|
||||
gameMode: string;
|
||||
maxPlayers: number;
|
||||
difficulty?: string;
|
||||
}
|
||||
|
||||
const game = actor({
|
||||
createState: (c, input: GameInput) => ({
|
||||
gameMode: input.gameMode,
|
||||
maxPlayers: input.maxPlayers,
|
||||
difficulty: input.difficulty ?? "medium",
|
||||
}),
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { game } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Client side - create with input
|
||||
const gameHandle = await client.game.create(["game-123"], {
|
||||
input: {
|
||||
gameMode: "tournament",
|
||||
maxPlayers: 8,
|
||||
difficulty: "hard",
|
||||
}
|
||||
});
|
||||
|
||||
// getOrCreate can also accept input (used only if creating)
|
||||
const gameHandle2 = client.game.getOrCreate(["game-456"], {
|
||||
createWithInput: {
|
||||
gameMode: "casual",
|
||||
maxPlayers: 4,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Accessing Input in Lifecycle Hooks
|
||||
|
||||
Input is available as the second argument to the `createState` and `onCreate` lifecycle hooks:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface ChatRoomInput {
|
||||
roomName: string;
|
||||
isPrivate: boolean;
|
||||
maxUsers?: number;
|
||||
}
|
||||
|
||||
interface ChatRoomState {
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
maxUsers: number;
|
||||
users: Record<string, boolean>;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
// Mock function for demonstration
|
||||
function setupPrivateRoomLogging(roomName: string) {
|
||||
console.log(`Setting up logging for private room: ${roomName}`);
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
createState: (c, input: ChatRoomInput): ChatRoomState => ({
|
||||
name: input?.roomName ?? "Unnamed Room",
|
||||
isPrivate: input?.isPrivate ?? false,
|
||||
maxUsers: input?.maxUsers ?? 50,
|
||||
users: {},
|
||||
messages: [],
|
||||
}),
|
||||
|
||||
onCreate: (c, input: ChatRoomInput) => {
|
||||
console.log(`Creating room: ${input.roomName}`);
|
||||
|
||||
// Setup external services based on input
|
||||
if (input.isPrivate) {
|
||||
setupPrivateRoomLogging(input.roomName);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Input remains accessible in actions via initial state
|
||||
getRoomInfo: (c) => ({
|
||||
name: c.state.name,
|
||||
isPrivate: c.state.isPrivate,
|
||||
maxUsers: c.state.maxUsers,
|
||||
currentUsers: Object.keys(c.state.users).length,
|
||||
}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
You can validate input parameters in the `createState` or `onCreate` hooks:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
import { z } from "zod";
|
||||
|
||||
const GameInputSchema = z.object({
|
||||
gameMode: z.enum(["casual", "tournament", "ranked"]),
|
||||
maxPlayers: z.number().min(2).max(16),
|
||||
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
||||
});
|
||||
|
||||
type GameInput = z.infer<typeof GameInputSchema>;
|
||||
|
||||
interface GameState {
|
||||
gameMode: string;
|
||||
maxPlayers: number;
|
||||
difficulty: string;
|
||||
players: Record<string, boolean>;
|
||||
gameState: string;
|
||||
}
|
||||
|
||||
const game = actor({
|
||||
createState: (c, inputRaw: GameInput): GameState => {
|
||||
// Validate input
|
||||
const input = GameInputSchema.parse(inputRaw);
|
||||
|
||||
return {
|
||||
gameMode: input.gameMode,
|
||||
maxPlayers: input.maxPlayers,
|
||||
difficulty: input.difficulty ?? "medium",
|
||||
players: {},
|
||||
gameState: "waiting",
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Actions can access the validated input via state
|
||||
getGameInfo: (c) => ({
|
||||
gameMode: c.state.gameMode,
|
||||
maxPlayers: c.state.maxPlayers,
|
||||
difficulty: c.state.difficulty,
|
||||
currentPlayers: Object.keys(c.state.players).length,
|
||||
}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Input vs Connection Parameters
|
||||
|
||||
Input parameters are different from connection parameters:
|
||||
|
||||
- **Input**:
|
||||
- Passed when creating the actor instance
|
||||
- Use for actor-wide configuration
|
||||
- Available in lifecycle hooks
|
||||
- **Connection parameters**:
|
||||
- Passed when connecting to an existing actor
|
||||
- Used for connection-specific configuration
|
||||
- Available in connection hooks
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface RoomInput { roomName: string; isPrivate: boolean; }
|
||||
|
||||
const chatRoom = actor({
|
||||
createState: (c, input: RoomInput) => ({ name: input.roomName, isPrivate: input.isPrivate }),
|
||||
createConnState: (c, params: { userId: string; displayName: string }) => ({
|
||||
userId: params.userId,
|
||||
displayName: params.displayName,
|
||||
}),
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chatRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Actor creation with input
|
||||
const room = await client.chatRoom.create(["room-123"], {
|
||||
input: {
|
||||
roomName: "General Discussion",
|
||||
isPrivate: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Input Best Practices
|
||||
|
||||
### Use Type Safety
|
||||
|
||||
Define input types to ensure type safety:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface GameInput {
|
||||
gameMode: "casual" | "tournament" | "ranked";
|
||||
maxPlayers: number;
|
||||
difficulty?: "easy" | "medium" | "hard";
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
gameMode: string;
|
||||
maxPlayers: number;
|
||||
difficulty: string;
|
||||
}
|
||||
|
||||
const game = actor({
|
||||
createState: (c, input: GameInput): GameState => ({
|
||||
gameMode: input.gameMode,
|
||||
maxPlayers: input.maxPlayers,
|
||||
difficulty: input.difficulty ?? "medium",
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// Actions are now type-safe
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Store Input in State
|
||||
|
||||
Input is only available in `createState` and `onCreate` lifecycle hooks. If you need to access input data later (in actions, timers, or other hooks), store it in the actor's state during creation. This is the recommended pattern because input shapes can evolve over time, and persisting input in state ensures you always have access to the values the actor was created with:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface GameInput {
|
||||
gameMode: string;
|
||||
maxPlayers: number;
|
||||
difficulty?: string;
|
||||
}
|
||||
|
||||
interface GameConfig {
|
||||
gameMode: string;
|
||||
maxPlayers: number;
|
||||
difficulty: string;
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
config: GameConfig;
|
||||
players: Record<string, boolean>;
|
||||
gameState: string;
|
||||
}
|
||||
|
||||
const game = actor({
|
||||
createState: (c, input: GameInput): GameState => ({
|
||||
// Store input configuration in state
|
||||
config: {
|
||||
gameMode: input.gameMode,
|
||||
maxPlayers: input.maxPlayers,
|
||||
difficulty: input?.difficulty ?? "medium",
|
||||
},
|
||||
// Runtime state
|
||||
players: {},
|
||||
gameState: "waiting",
|
||||
}),
|
||||
|
||||
actions: {
|
||||
getConfig: (c) => c.state.config,
|
||||
updateDifficulty: (c, difficulty: string) => {
|
||||
c.state.config.difficulty = difficulty;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Options for creating actors
|
||||
- [`CreateRequest`](/typedoc/types/rivetkit.client_mod.CreateRequest.html) - Request type for creation
|
||||
- [`ActorDefinition`](/typedoc/classes/rivetkit.mod.ActorDefinition.html) - Actor definition returned by `actor()`
|
||||
|
||||
_Source doc path: /docs/actors/input_
|
||||
@@ -0,0 +1,290 @@
|
||||
# Custom Inspector Tabs
|
||||
|
||||
> Source: `src/content/docs/actors/inspector-tabs.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/inspector-tabs
|
||||
> Description: Ship your own UI tabs alongside a Rivet Actor — embedded directly in the dashboard inspector.
|
||||
|
||||
---
|
||||
Custom inspector tabs let you embed your own UI directly in the Rivet
|
||||
dashboard, next to the built-in tabs. Declare a tab on your actor, point
|
||||
it at a folder of static files, and the dashboard picks it up
|
||||
automatically.
|
||||
|
||||
Common uses:
|
||||
|
||||
- Domain-specific debugging panels (queue depth, connection maps, log
|
||||
filters).
|
||||
- Operational tools (admin buttons, drain controls, snapshot uploaders).
|
||||
- Any author-defined view that ships with your actor.
|
||||
|
||||
A runnable example lives at
|
||||
[`examples/inspector-tabs`](https://github.com/rivet-dev/rivet/tree/main/examples/inspector-tabs).
|
||||
|
||||
## Quickstart
|
||||
|
||||
Declare a tab on your actor:
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { value: 0 },
|
||||
actions: {
|
||||
increment: (c, amount: number) => {
|
||||
c.state.value += amount;
|
||||
return c.state.value;
|
||||
},
|
||||
},
|
||||
inspector: {
|
||||
tabs: [
|
||||
{
|
||||
id: "counter",
|
||||
label: "Counter",
|
||||
icon: "tag",
|
||||
source: "./inspector-tabs/counter",
|
||||
},
|
||||
{ id: "queue", hidden: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
Drop an `index.html` in the `source` directory:
|
||||
|
||||
```html
|
||||
<!-- ./inspector-tabs/counter/index.html -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../tab.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Counter: <span id="value">…</span></h1>
|
||||
<script>
|
||||
const SHELL_ORIGIN = (() => {
|
||||
const raw =
|
||||
new URLSearchParams(location.search).get("shellOrigin") ??
|
||||
location.origin;
|
||||
try {
|
||||
return new URL(raw).origin;
|
||||
} catch {
|
||||
return location.origin;
|
||||
}
|
||||
})();
|
||||
let token = null;
|
||||
|
||||
window.addEventListener("message", async (e) => {
|
||||
if (e.origin !== SHELL_ORIGIN) return;
|
||||
if (e.data?.type !== "init" || e.data?.v !== 1) return;
|
||||
token = e.data.authToken;
|
||||
const r = await fetch("../../state", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const { state } = await r.json();
|
||||
document.getElementById("value").textContent = state.value;
|
||||
document.documentElement.classList.toggle(
|
||||
"dark",
|
||||
(e.data.theme ?? "dark") === "dark",
|
||||
);
|
||||
});
|
||||
|
||||
window.parent.postMessage({ type: "ready", v: 1 }, SHELL_ORIGIN);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Open the dashboard and the "Counter" tab appears alongside the built-ins.
|
||||
|
||||
## Configuration
|
||||
|
||||
Each entry in `inspector.tabs[]` is either a **custom tab** or a
|
||||
**hide modifier** for a built-in.
|
||||
|
||||
### Custom tab
|
||||
|
||||
```ts @nocheck
|
||||
{
|
||||
id: string, // URL-safe id: /^[A-Za-z0-9_-]+$/
|
||||
label: string, // Shown in the tab strip
|
||||
source: string, // Directory of static assets
|
||||
icon?: string, // Optional icon id
|
||||
}
|
||||
```
|
||||
|
||||
- **`id`** — used as the URL segment and tab-strip key. Cannot collide
|
||||
with a built-in id (`workflow`, `database`, `state`, `queue`,
|
||||
`connections`, `console`).
|
||||
- **`source`** — directory of static files. The bytes you put there are
|
||||
the bytes the browser sees. Point it at a Vite/webpack `dist/` and
|
||||
any framework works (React, Vue, Svelte, vanilla — all fine).
|
||||
- **`icon`** — one of `workflow`, `database`, `state`, `queue`, `plug`,
|
||||
`terminal`, `tag`, `logs`. Anything else falls back to a neutral icon.
|
||||
|
||||
### Hide a built-in tab
|
||||
|
||||
```ts @nocheck
|
||||
{
|
||||
id: "workflow" | "database" | "state" | "queue" | "connections" | "console",
|
||||
hidden: true,
|
||||
}
|
||||
```
|
||||
|
||||
Use this to clean up the strip when the actor doesn't use a given
|
||||
subsystem — e.g. a counter actor with no queues:
|
||||
|
||||
```ts
|
||||
inspector: { tabs: [{ id: "queue", hidden: true }] }
|
||||
```
|
||||
|
||||
Misconfigurations (missing directory, duplicate id, invalid characters,
|
||||
empty label) throw at registry construction, so problems show up
|
||||
immediately.
|
||||
|
||||
## Talking to the dashboard
|
||||
|
||||
The tab loads in an iframe and communicates with the dashboard via
|
||||
`postMessage`. The contract is small.
|
||||
|
||||
### From the dashboard
|
||||
|
||||
The dashboard sends an `init` message on load and again whenever the
|
||||
inspector token rotates. Always overwrite the cached token when it
|
||||
arrives.
|
||||
|
||||
```ts @nocheck
|
||||
{
|
||||
type: "init",
|
||||
v: 1,
|
||||
actorId: string,
|
||||
authToken: string, // Per-actor inspector bearer token
|
||||
theme?: "light" | "dark",
|
||||
activeTab?: string, // For multi-view tabs
|
||||
}
|
||||
```
|
||||
|
||||
Multi-view tabs can read the optional `activeTab` field on `init` to
|
||||
seed their initial sub-view. The dashboard does not send a separate
|
||||
message when the user switches custom tabs — it navigates the iframe
|
||||
`src` instead, so the tab reloads and receives a fresh `init`.
|
||||
|
||||
### From the tab
|
||||
|
||||
Send `ready` once your message listener is registered:
|
||||
|
||||
```ts @nocheck
|
||||
{ type: "ready", v: 1 }
|
||||
```
|
||||
|
||||
If a fetch returns 401, the token has rotated. Ask the dashboard for a
|
||||
fresh one and wait for the next `init` — don't retry with the stale
|
||||
token:
|
||||
|
||||
```ts @nocheck
|
||||
{ type: "token-refresh-needed", v: 1 }
|
||||
```
|
||||
|
||||
### Security check
|
||||
|
||||
Always reject messages whose `event.origin` doesn't match the
|
||||
`?shellOrigin=` URL parameter. Without this check, any page that frames
|
||||
your tab could forge an `init` and feed you a fake token.
|
||||
|
||||
### TypeScript types
|
||||
|
||||
If you build the tab with TypeScript, the message and response types
|
||||
are exported as types-only:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
V1Init,
|
||||
ShellToTabMessage,
|
||||
TabToShellMessage,
|
||||
InspectorStateResponse,
|
||||
InspectorActionResponse,
|
||||
InspectorRpcsResponse,
|
||||
} from "rivetkit/inspector-tab";
|
||||
```
|
||||
|
||||
## Reading state and calling actions
|
||||
|
||||
The tab can hit any inspector endpoint with the supplied bearer token.
|
||||
Use relative paths so the tab doesn't need to know the engine origin or
|
||||
actor id:
|
||||
|
||||
```js
|
||||
fetch("../../state", { headers: { Authorization: `Bearer ${token}` } });
|
||||
fetch("../../action/increment", { method: "POST", headers: { ... }, body: ... });
|
||||
fetch("../../rpcs", { headers: { Authorization: `Bearer ${token}` } });
|
||||
fetch("../../connections", { headers: { Authorization: `Bearer ${token}` } });
|
||||
fetch("../../queue", { headers: { Authorization: `Bearer ${token}` } });
|
||||
```
|
||||
|
||||
The action body shape is `{ args: [...] }` — the array is passed as
|
||||
positional arguments to the action.
|
||||
|
||||
Full endpoint reference:
|
||||
[Debugging → Inspector Endpoints](/docs/actors/debugging#inspector-endpoints).
|
||||
|
||||
For high-frequency UIs, prefer the inspector WebSocket
|
||||
(`/inspector/connect`) over polling.
|
||||
|
||||
## Styling
|
||||
|
||||
A shared stylesheet matching the dashboard's design tokens is served at
|
||||
`../../tab.css`:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="../../tab.css" />
|
||||
```
|
||||
|
||||
It defines `--rivet-*` tokens for colors, spacing, radius, and
|
||||
typography, plus sensible defaults so a bare tab looks at home without
|
||||
custom CSS:
|
||||
|
||||
```css
|
||||
.my-card {
|
||||
background: var(--rivet-card);
|
||||
color: var(--rivet-foreground);
|
||||
border: 1px solid var(--rivet-border);
|
||||
border-radius: var(--rivet-radius-md);
|
||||
padding: var(--rivet-space-4);
|
||||
}
|
||||
```
|
||||
|
||||
Toggle dark mode by adding the `dark` class to `<html>` — the dashboard
|
||||
sends the active theme in the `init` message.
|
||||
|
||||
Color tokens come in both pre-wrapped (`--rivet-card`) and raw HSL
|
||||
(`--rivet-card-raw`) forms, so you can compose with alpha:
|
||||
|
||||
```css
|
||||
.overlay { background: hsl(var(--rivet-background-raw) / 0.6); }
|
||||
```
|
||||
|
||||
You're free to skip the stylesheet entirely and bring your own.
|
||||
|
||||
## Security
|
||||
|
||||
The tab runs in an iframe at the engine origin and can call any
|
||||
inspector endpoint with the supplied token. Treat the bundle like any
|
||||
author code that ships with your actor:
|
||||
|
||||
- **Don't inline secrets.** The bundle is fetchable by anyone who can
|
||||
reach the actor.
|
||||
- **Always validate `event.origin`.** Reject inbound messages from
|
||||
anywhere other than the dashboard origin.
|
||||
- **Never retry silently on 401.** Post `token-refresh-needed` and wait
|
||||
for a fresh `init`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Debugging](/docs/actors/debugging) — full inspector HTTP API
|
||||
- [Actions](/docs/actors/actions) — actions your tab can invoke
|
||||
- [State](/docs/actors/state) — state your tab can read
|
||||
- [`examples/inspector-tabs`](https://github.com/rivet-dev/rivet/tree/main/examples/inspector-tabs) — runnable example
|
||||
|
||||
_Source doc path: /docs/actors/inspector-tabs_
|
||||
261
.agents/skills/ai-agent-workspace/reference/actors/keys.md
Normal file
261
.agents/skills/ai-agent-workspace/reference/actors/keys.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# Actor Keys
|
||||
|
||||
> Source: `src/content/docs/actors/keys.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/keys
|
||||
> Description: Actor keys uniquely identify actor instances within each actor type. Keys are used for addressing which specific actor to communicate with.
|
||||
|
||||
---
|
||||
## Key Format
|
||||
|
||||
Actor keys can be either a string or an array of strings:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: { increment: (c) => c.state.count++ }
|
||||
});
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { counter, chatRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// String key
|
||||
const counterHandle = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
// Array key (compound key)
|
||||
const chatRoomHandle = client.chatRoom.getOrCreate(["room", "general"]);
|
||||
```
|
||||
|
||||
### Compound Keys & User Data
|
||||
|
||||
Array keys are useful when you need compound keys with user-provided data. Using arrays makes adding user data safe by preventing key injection attacks:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const chatRoom = actor({ state: { messages: [] as string[] }, actions: {} });
|
||||
const gameRoom = actor({ state: { players: [] as string[] }, actions: {} });
|
||||
const workspace = actor({ state: { data: {} }, actions: {} });
|
||||
|
||||
const registry = setup({ use: { chatRoom, gameRoom, workspace } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Example user data
|
||||
const userId = "user-123";
|
||||
const gameId = "game-456";
|
||||
const tenantId = "tenant-789";
|
||||
const workspaceId = "workspace-abc";
|
||||
|
||||
// User-specific chat rooms
|
||||
const userRoomHandle = client.chatRoom.getOrCreate(["user", userId, "private"]);
|
||||
|
||||
// Game rooms by region and difficulty
|
||||
const gameRoomHandle = client.gameRoom.getOrCreate(["us-west", "hard", gameId]);
|
||||
|
||||
// Multi-tenant resources
|
||||
const workspaceHandle = client.workspace.getOrCreate(["tenant", tenantId, workspaceId]);
|
||||
```
|
||||
|
||||
This allows you to create hierarchical addressing schemes and organize actors by multiple dimensions.
|
||||
|
||||
Don't build keys using string interpolation like `"foo:${userId}:bar"` when `userId` contains user data. If a user provides a value containing the delimiter (`:` in this example), it can break your key structure and cause key injection attacks.
|
||||
|
||||
### Omitting Keys
|
||||
|
||||
You can create actors without specifying a key in situations where there is a singleton actor (i.e. only one actor of a given type). For example:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const globalActor = actor({
|
||||
state: { config: {} },
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { globalActor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Get the singleton session
|
||||
const globalActorHandle = client.globalActor.getOrCreate();
|
||||
```
|
||||
|
||||
This pattern should be avoided, since a singleton actor usually means you have a single actor serving all traffic & your application will not scale. See [scaling documentation](/docs/actors/scaling) for more information.
|
||||
|
||||
### Key Uniqueness
|
||||
|
||||
Keys are unique within each actor name. Different actor types can use the same key:
|
||||
|
||||
```typescript
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const chatRoom = actor({ state: { messages: [] as string[] }, actions: {} });
|
||||
const userProfile = actor({ state: { name: "" }, actions: {} });
|
||||
|
||||
const registry = setup({ use: { chatRoom, userProfile } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// These are different actors, same key is fine
|
||||
const userChat = client.chatRoom.getOrCreate(["user-123"]);
|
||||
const userProfileHandle = client.userProfile.getOrCreate(["user-123"]);
|
||||
```
|
||||
|
||||
## Accessing Keys in Metadata
|
||||
|
||||
Access the actor's key within the actor using the [metadata](/docs/actors/metadata) API:
|
||||
|
||||
```typescript index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
actions: {
|
||||
getRoomName: (c) => {
|
||||
// Access the key from metadata
|
||||
const key = c.key;
|
||||
return key[1]; // Get "general" from ["room", "general"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { chatRoom }
|
||||
});
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
actions: { getRoomName: (c) => c.key[1] }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chatRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
async function connectToRoom(roomName: string) {
|
||||
// Connect to a chat room
|
||||
const chatRoomHandle = client.chatRoom.getOrCreate(["room", roomName]);
|
||||
|
||||
// Get the room name from the key
|
||||
const retrievedRoomName = await chatRoomHandle.getRoomName();
|
||||
console.log("Room name:", retrievedRoomName); // e.g., "general"
|
||||
|
||||
return chatRoomHandle;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const generalRoom = await connectToRoom("general");
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Simple Configuration with Keys
|
||||
|
||||
Use keys to provide basic actor configuration:
|
||||
|
||||
```typescript index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface UserSessionState {
|
||||
userId: string;
|
||||
loginTime: number;
|
||||
preferences: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const userSession = actor({
|
||||
createState: (c): UserSessionState => ({
|
||||
userId: c.key[0], // Extract user ID from key
|
||||
loginTime: Date.now(),
|
||||
preferences: {}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
getUserId: (c) => c.state.userId
|
||||
}
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { userSession }
|
||||
});
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const userSession = actor({
|
||||
state: { userId: "", loginTime: 0, preferences: {} },
|
||||
actions: { getUserId: (c) => c.state.userId }
|
||||
});
|
||||
|
||||
const registry = setup({ use: { userSession } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Pass user ID in the key for user-specific actors
|
||||
const userId = "user-123";
|
||||
const userSessionHandle = client.userSession.getOrCreate([userId]);
|
||||
```
|
||||
|
||||
### Complex Configuration with Input
|
||||
|
||||
For more complex configuration, use [input parameters](/docs/actors/input):
|
||||
|
||||
```typescript client.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
interface ChatRoomInput {
|
||||
maxUsers: number;
|
||||
isPrivate: boolean;
|
||||
moderators: string[];
|
||||
settings: { allowImages: boolean; slowMode: boolean };
|
||||
}
|
||||
|
||||
const chatRoom = actor({
|
||||
createState: (c, input: ChatRoomInput) => ({
|
||||
maxUsers: input.maxUsers,
|
||||
isPrivate: input.isPrivate,
|
||||
moderators: input.moderators,
|
||||
settings: input.settings,
|
||||
}),
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chatRoom } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const roomName = "general";
|
||||
|
||||
// Create with both key and input
|
||||
const chatRoomHandle = await client.chatRoom.create(["room", roomName], {
|
||||
input: {
|
||||
maxUsers: 100,
|
||||
isPrivate: false,
|
||||
moderators: ["admin1", "admin2"],
|
||||
settings: {
|
||||
allowImages: true,
|
||||
slowMode: false
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorKey`](/typedoc/types/rivetkit.mod.ActorKey.html) - Key type for actors
|
||||
- [`ActorQuery`](/typedoc/types/rivetkit.mod.ActorQuery.html) - Query type using keys
|
||||
- [`GetOptions`](/typedoc/interfaces/rivetkit.client_mod.GetOptions.html) - Options for getting by key
|
||||
- [`QueryOptions`](/typedoc/interfaces/rivetkit.client_mod.QueryOptions.html) - Options for querying
|
||||
|
||||
_Source doc path: /docs/actors/keys_
|
||||
162
.agents/skills/ai-agent-workspace/reference/actors/kv.md
Normal file
162
.agents/skills/ai-agent-workspace/reference/actors/kv.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Low-Level KV Storage
|
||||
|
||||
> Source: `src/content/docs/actors/kv.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/kv
|
||||
> Description: Use the built-in key-value store on ActorContext for durable string and binary data alongside actor state.
|
||||
|
||||
---
|
||||
KV is deprecated. It is a low-level escape hatch kept for backward compatibility. For new actors, prefer [in-memory state](/docs/actors/state) for small serializable values or [SQLite](/docs/actors/sqlite) for larger or queryable data.
|
||||
|
||||
Every Rivet Actor includes a lightweight key-value store on `c.kv`. It is useful for dynamic keys, blobs, or data that does not fit well in structured state.
|
||||
|
||||
If your data has a known schema, prefer [state](/docs/actors/state). KV is best for flexible or user-defined keys.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Keys and values default to `text`, so you can use strings without extra options.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const greetings = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
setGreeting: async (c, userId: string, message: string) => {
|
||||
await c.kv.put(`greeting:${userId}`, message);
|
||||
},
|
||||
getGreeting: async (c, userId: string) => {
|
||||
return await c.kv.get(`greeting:${userId}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Value Types
|
||||
|
||||
You can store binary values by passing `Uint8Array` or `ArrayBuffer`. Use `type` on both reads and writes to get the right value type: `binary` for `Uint8Array` and `arrayBuffer` for `ArrayBuffer`.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const assets = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
putAvatar: async (c, bytes: Uint8Array) => {
|
||||
await c.kv.put("avatar", bytes);
|
||||
},
|
||||
getAvatar: async (c) => {
|
||||
return await c.kv.get("avatar", { type: "binary" });
|
||||
},
|
||||
putSnapshot: async (c, data: ArrayBuffer) => {
|
||||
await c.kv.put("snapshot", data, { type: "arrayBuffer" });
|
||||
},
|
||||
getSnapshot: async (c) => {
|
||||
return await c.kv.get("snapshot", { type: "arrayBuffer" });
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
TypeScript returns a concrete type based on the option you pass in:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
demo: async (c) => {
|
||||
const textValue = await c.kv.get("greeting");
|
||||
// ^? string | null
|
||||
|
||||
const bytes = await c.kv.get("avatar", { type: "binary" });
|
||||
// ^? Uint8Array | null
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Key Types
|
||||
|
||||
Keys accept either `string` or `Uint8Array`. String keys are encoded as UTF-8 by default.
|
||||
|
||||
When listing by prefix, you can control how keys are decoded with `keyType`. Returned keys have the prefix removed.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
listGreetings: async (c) => {
|
||||
const results = await c.kv.list("greeting:", { keyType: "text" });
|
||||
|
||||
for (const [key, value] of results) {
|
||||
console.log(key, value);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you use binary keys, set `keyType: "binary"` so the returned keys stay as `Uint8Array`.
|
||||
|
||||
## Range Operations
|
||||
|
||||
Use `listRange(start, end)` to read an arbitrary half-open range `[start, end)`. Use `deleteRange(start, end)` to clear that same range efficiently.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
pruneAndScan: async (c) => {
|
||||
const active = await c.kv.listRange("job:", "joc:", {
|
||||
keyType: "text",
|
||||
});
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
await c.kv.deleteRange(
|
||||
encoder.encode("job:old:"),
|
||||
encoder.encode("job:old;"),
|
||||
);
|
||||
|
||||
return active.map(([key, value]) => ({ key, value }));
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Batch Operations
|
||||
|
||||
KV supports batch operations for efficiency. `batchPut` and `batchGet` work on raw `Uint8Array` keys and values, so encode strings before passing them in.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
batchOps: async (c) => {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
await c.kv.batchPut([
|
||||
[encoder.encode("alpha"), encoder.encode("1")],
|
||||
[encoder.encode("beta"), encoder.encode("2")],
|
||||
]);
|
||||
|
||||
const values = await c.kv.batchGet([
|
||||
encoder.encode("alpha"),
|
||||
encoder.encode("beta"),
|
||||
]);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - `c.kv` is available on the context
|
||||
|
||||
_Source doc path: /docs/actors/kv_
|
||||
1145
.agents/skills/ai-agent-workspace/reference/actors/lifecycle.md
Normal file
1145
.agents/skills/ai-agent-workspace/reference/actors/lifecycle.md
Normal file
File diff suppressed because it is too large
Load Diff
174
.agents/skills/ai-agent-workspace/reference/actors/limits.md
Normal file
174
.agents/skills/ai-agent-workspace/reference/actors/limits.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Limits
|
||||
|
||||
> Source: `src/content/docs/actors/limits.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/limits
|
||||
> Description: Limits and constraints for Rivet Actors.
|
||||
|
||||
---
|
||||
This page documents the limits for Rivet Actors.
|
||||
|
||||
There are two types of limits:
|
||||
|
||||
- **Soft Limit**: Application-level limit, configurable in RivetKit. These cannot exceed the hard limit.
|
||||
- **Hard Limit**: Infrastructure-level limit that cannot be configured.
|
||||
|
||||
Soft limits are configured in two places. Registry-level WebSocket message-size limits are passed to `setup`:
|
||||
|
||||
```typescript
|
||||
import { setup } from "rivetkit";
|
||||
|
||||
const rivet = setup({
|
||||
use: { /* ... */ },
|
||||
maxIncomingMessageSize: 1_048_576,
|
||||
maxOutgoingMessageSize: 10_485_760,
|
||||
});
|
||||
```
|
||||
|
||||
Per-actor limits such as queue sizes and lifecycle timeouts are passed to `actor(...)` via `options`:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const myActor = actor({
|
||||
options: {
|
||||
maxQueueSize: 1000,
|
||||
actionTimeout: 60_000,
|
||||
stateSaveInterval: 1_000,
|
||||
},
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
### WebSocket
|
||||
|
||||
These limits affect actions that use `.connect()` and [low-level WebSockets](/docs/actors/websocket-handler).
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max incoming message size | 64 KiB | 32 MiB | Maximum size of incoming WebSocket messages. Soft limit configurable via `maxIncomingMessageSize`. |
|
||||
| Max outgoing message size | 1 MiB | 32 MiB | Maximum size of outgoing WebSocket messages. Soft limit configurable via `maxOutgoingMessageSize`. |
|
||||
| WebSocket open timeout | — | 15 seconds | Time allowed for WebSocket connection to be established, including `onBeforeConnect` and `createConnState` hooks. Connection is closed if exceeded. |
|
||||
| Message ack timeout | — | 30 seconds | Time allowed for message acknowledgment before connection is closed. Only relevant in the case of a network issue and does not affect your application. |
|
||||
|
||||
### Hibernating WebSocket
|
||||
|
||||
Hibernating WebSockets allow actors to sleep while keeping client connections alive. All WebSocket limits above also apply to hibernating WebSockets. See [WebSocket Hibernation](/docs/actors/websocket-handler#web-socket-hibernation) for details.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max pending buffer size | — | 128 MiB | Total size of all buffered messages per connection while actor is sleeping. |
|
||||
| Max pending message count | — | 65,535 | Maximum number of buffered messages per connection while actor is sleeping. |
|
||||
| Hibernation timeout | — | 90 seconds | Maximum time an actor has to wake up before the client is disconnected. Only relevant if something is wrong with starting actors. |
|
||||
|
||||
### HTTP
|
||||
|
||||
These limits affect actions that do not use `.connect()` and [low-level HTTP requests](/docs/actors/request-handler).
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max request body size | — | 20 MiB | Maximum size of HTTP request bodies. |
|
||||
| Max response body size | — | 20 MiB | Maximum size of HTTP response bodies. |
|
||||
| Request timeout | — | 5 minutes | Maximum time for a request to complete. |
|
||||
|
||||
### Networking
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Connection ping timeout | — | 30 seconds | Connection is closed if a ping is not acknowledged within this time. Applies to both HTTP and WebSocket. Only relevant in the case of a network issue and does not affect your application. |
|
||||
|
||||
### Queue
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max queue size | 1,000 messages | — | Maximum number of messages in the queue before new messages are rejected. Configurable via `maxQueueSize`. |
|
||||
| Max queue message size | 64 KiB | 128 KiB (effective) | Maximum size of each individual queue message. Configurable via `maxQueueMessageSize`. Actual payload is slightly lower after queue serialization overhead. |
|
||||
|
||||
### Actor KV Storage
|
||||
|
||||
These limits apply to the low-level KV storage interface powering Rivet Actors. They likely do not affect your application, but are documented for completeness.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max key size | — | 2 KiB | Maximum size of a single key. |
|
||||
| Max value size | — | 128 KiB | Maximum size of a single value. |
|
||||
| Max keys per operation | — | 128 | Maximum number of keys in a single batch get/put/delete operation. Does not apply to range operations (`listRange`, `deleteRange`). |
|
||||
| Max batch put payload size | — | 976 KiB | Maximum total size of all key-value pairs in a single batch put operation. |
|
||||
| Max storage size per actor | — | 10 GiB | Maximum total KV storage size for a single actor. |
|
||||
| List default limit | — | 16,384 | Default maximum number of keys returned by a list operation. |
|
||||
|
||||
### Actor SQLite Storage
|
||||
|
||||
These limits apply to the [SQLite database](/docs/actors/state#sqlite-database) available via `this.sql`. SQLite data is stored through the KV layer, so the storage limit is shared with KV storage.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max storage size per actor | — | 10 GiB | Maximum total storage size for a single actor. This limit is shared with KV storage. |
|
||||
| Max dirty data per commit | — | 1,310,720 bytes | Maximum raw SQLite page data that can be written in a single commit. This is 320 dirty pages at 4 KiB per page. |
|
||||
|
||||
SQLite commit deltas are compressed and split into internal chunks before storage, but all chunks for one commit are published atomically. The commit limit is therefore based on raw dirty page bytes before compression, not the compressed delta size.
|
||||
|
||||
### KV Preloading
|
||||
|
||||
When an actor starts, the engine can pre-fetch KV data declared in the actor name metadata and deliver it alongside the start command. This removes round-trips to storage during actor startup. RivetKit emits the preload manifest from its own key layout and exposes per-actor overrides via `options`. Operators can still enforce a global cap in the [engine config](/docs/self-hosting/configuration) with `pegboard.preload_max_total_bytes`. In serverless mode, this data is serialized into the `/api/rivet/start` request body along with actor config and protocol metadata, so the accepted body size must be larger than the preload budget. RivetKit defaults `serverless.maxStartPayloadBytes` to 16 MiB to leave margin for the default 1 MiB preload budget and larger SQLite startup page preloads.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max total preload size | 1 MiB | — | Maximum total size of all preloaded KV data sent with the start command. Configurable via `pegboard.preload_max_total_bytes`. Setting to 0 disables all preloading. |
|
||||
| Max workflow preload size | 128 KiB | — | Default maximum size of preloaded workflow data for RivetKit actors. Configurable per actor via `options.preloadMaxWorkflowBytes`. Setting to 0 disables workflow preloading for that actor. |
|
||||
| Max connections preload size | 64 KiB | — | Default maximum size of preloaded connection data for RivetKit actors. Configurable per actor via `options.preloadMaxConnectionsBytes`. Setting to 0 disables connections preloading for that actor. |
|
||||
|
||||
### Actor Input
|
||||
|
||||
See [Actor Input](/docs/actors/input) for details.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Max actor input size | — | 4 MiB | Maximum size of the input passed when creating an actor. |
|
||||
| Max connection params size | — | 4 KiB | Maximum size of connection parameters passed when connecting to an actor. |
|
||||
| Max actor key component size | — | 128 bytes | Maximum size of each component in an actor key array. |
|
||||
| Max actor key total size | — | 1,024 bytes | Maximum total size of the serialized actor key string. |
|
||||
| Max actor name length | — | 64 characters | Maximum length for actor and project identifiers. |
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Rate limit | — | 1200 requests/minute | Default rate limit per actor per IP address with a 1 minute time bucket. |
|
||||
| Max in-flight requests | — | 32 | Default maximum concurrent requests to an actor per IP address. |
|
||||
|
||||
### Timeouts
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Action timeout | 60 seconds | — | Timeout for RPC actions. Configurable via `actionTimeout`. |
|
||||
| Create vars timeout | 5 seconds | — | Timeout for `createVars` hook. Configurable via `createVarsTimeout`. |
|
||||
| Create conn state timeout | 5 seconds | — | Timeout for `createConnState` hook. Configurable via `createConnStateTimeout`. |
|
||||
| On connect timeout | 5 seconds | — | Timeout for `onConnect` hook. Configurable via `onConnectTimeout`. |
|
||||
| Sleep grace period | 15 seconds | — | Total graceful shutdown budget for both sleep and destroy. Covers `onSleep`/`onDestroy`, run handler shutdown, `waitUntil`, `keepAwake`, async raw WebSocket handlers, and connection cleanup. Configurable via `sleepGracePeriod`. |
|
||||
| Sleep timeout | 30 seconds | — | Time of inactivity before actor hibernates. Configurable via `sleepTimeout`. |
|
||||
| State save interval | 1 second | — | Interval between automatic state saves. Configurable via `stateSaveInterval`. |
|
||||
|
||||
### Serverless Shutdown
|
||||
|
||||
These timeouts control how actors are shut down when a serverless request reaches its lifespan limit. See [Shutdown Sequence](/docs/general/runtime-modes#shutdown-sequence) for details.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Request lifespan | 3600 seconds (60 min) | — | Total lifespan of a serverless request before drain begins. Configurable via `requestLifespan` in [`configurePool`](/docs/general/registry-configuration). |
|
||||
| Runner config drain grace period | — | 30 minutes | Time a serverless runner reserves for actors to stop gracefully. Configurable via `drainGracePeriod` in [`configurePool`](/docs/general/registry-configuration). |
|
||||
| Engine serverless drain fallback | — | 10 seconds | Engine-level fallback used when no per-runner config applies. Configurable via [engine config](/docs/self-hosting/configuration) (`pegboard.serverless_drain_grace_period`). |
|
||||
|
||||
### Actor Lifecycle
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Actor start threshold | — | 30 seconds | Maximum time for an actor to start before it is considered lost and rescheduled. |
|
||||
| Actor stop threshold | — | 30 minutes | Maximum time for an actor to stop before it is considered lost. |
|
||||
|
||||
## Increasing Limits
|
||||
|
||||
These limits are sane defaults designed to protect your application from exploits and accidental runaway bugs. If you have a use case that requires different limits, [contact us](https://rivet.dev/contact) to discuss your requirements.
|
||||
|
||||
_Source doc path: /docs/actors/limits_
|
||||
137
.agents/skills/ai-agent-workspace/reference/actors/metadata.md
Normal file
137
.agents/skills/ai-agent-workspace/reference/actors/metadata.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Metadata
|
||||
|
||||
> Source: `src/content/docs/actors/metadata.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/metadata
|
||||
> Description: Metadata provides information about the currently running actor.
|
||||
|
||||
---
|
||||
## Actor ID
|
||||
|
||||
Get the unique instance ID of the actor:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getId: (c) => {
|
||||
const actorId = c.actorId;
|
||||
return actorId;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Actor Name
|
||||
|
||||
Get the actor type name:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getName: (c) => {
|
||||
const actorName = c.name;
|
||||
return actorName;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This is useful when you need to know which actor type is running, especially if you have generic utility functions that are shared between different actor implementations.
|
||||
|
||||
## Actor Key
|
||||
|
||||
Get the actor key used to identify this actor instance:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getKey: (c) => {
|
||||
const actorKey = c.key;
|
||||
return actorKey;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The key is used to route requests to the correct actor instance and can include parameters passed when creating the actor.
|
||||
|
||||
Learn more about using keys for actor addressing and configuration in the [keys documentation](/docs/actors/keys).
|
||||
|
||||
## Region
|
||||
|
||||
Region can be accessed from the context object via `c.region`.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const example = actor({
|
||||
state: {},
|
||||
actions: {
|
||||
getRegion: (c) => {
|
||||
const region = c.region;
|
||||
return region;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`c.region` is only supported on Rivet at the moment.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```typescript index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const chatRoom = actor({
|
||||
state: {
|
||||
messages: [],
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Get actor metadata
|
||||
getMetadata: (c) => {
|
||||
return {
|
||||
actorId: c.actorId,
|
||||
name: c.name,
|
||||
key: c.key,
|
||||
region: c.region,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { chatRoom },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Connect to a chat room
|
||||
const chatRoomHandle = client.chatRoom.get(["general"]);
|
||||
|
||||
// Get actor metadata
|
||||
const metadata = await chatRoomHandle.getMetadata();
|
||||
console.log("Actor metadata:", metadata);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining metadata
|
||||
- [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Options for creating an actor, including `region` and `input`
|
||||
|
||||
_Source doc path: /docs/actors/metadata_
|
||||
496
.agents/skills/ai-agent-workspace/reference/actors/queues.md
Normal file
496
.agents/skills/ai-agent-workspace/reference/actors/queues.md
Normal file
@@ -0,0 +1,496 @@
|
||||
# Queues & Run Loops
|
||||
|
||||
> Source: `src/content/docs/actors/queues.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/queues
|
||||
> Description: Use actor-local durable queues for serial run loops and request/response workflows.
|
||||
|
||||
---
|
||||
## What are queues?
|
||||
|
||||
- **Realtime**: messages are delivered to a live actor as soon as possible.
|
||||
- **Durable**: messages are persisted and survive actor sleep/restart.
|
||||
- **Request/response**: clients can wait for a queue completion response.
|
||||
- **Scalable**: queues absorb large bursts and handle heavy backpressure safely.
|
||||
- **Local per actor**: each actor instance has its own queue storage (scoped by actor key/id).
|
||||
|
||||
Queues are commonly referred to as "mailboxes" in other actor frameworks.
|
||||
|
||||
For a worked queue-driven pattern, see the cookbook: [AI Agent](/cookbook/ai-agent/).
|
||||
|
||||
## What are queues good for?
|
||||
|
||||
- Great for any task that changes actor state.
|
||||
- Helps avoid race conditions by handling work in order.
|
||||
- Makes complex behavior easier to organize.
|
||||
|
||||
## Basic queue
|
||||
|
||||
This is the default pattern. Define queue names in `queues`, process them in `run`, and publish from the client with `handle.send(...)`.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { value: 0 },
|
||||
queues: {
|
||||
increment: queue<{ amount: number }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter()) {
|
||||
c.state.value += message.body.amount;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.counter.getOrCreate(["main"]);
|
||||
|
||||
await handle.send("increment", { amount: 1 });
|
||||
await handle.send("increment", { amount: 5 });
|
||||
```
|
||||
|
||||
## Completable messages
|
||||
|
||||
Use this when you want explicit completion/ack semantics but do not need to return data.
|
||||
|
||||
- `message.complete()` resolves a sender waiting on `wait: true` (or `enqueueAndWait`). It does not change durability: messages are removed from queue storage when they are received, not when they are completed.
|
||||
- If processing fails before `message.complete()`, the message is not redelivered, and any waiting sender times out instead of receiving a completion.
|
||||
- `status: "timedOut"` means sender timeout elapsed before `message.complete(...)`.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { value: 0 },
|
||||
queues: {
|
||||
increment: queue<{ amount: number }, undefined>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter({ completable: true })) {
|
||||
c.state.value += message.body.amount;
|
||||
await message.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.counter.getOrCreate(["main"]);
|
||||
|
||||
const result = await handle.send(
|
||||
"increment",
|
||||
{ amount: 5 },
|
||||
{ wait: true, timeout: 5_000 },
|
||||
);
|
||||
|
||||
if (result.status === "completed") {
|
||||
console.log("applied");
|
||||
} else if (result.status === "timedOut") {
|
||||
console.log("timed out");
|
||||
}
|
||||
```
|
||||
|
||||
## Request/reply pattern
|
||||
|
||||
Use this when the sender needs data back from queued work.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { value: 0 },
|
||||
queues: {
|
||||
increment: queue<{ amount: number }, { value: number }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter({ completable: true })) {
|
||||
c.state.value += message.body.amount;
|
||||
await message.complete({ value: c.state.value });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.counter.getOrCreate(["main"]);
|
||||
|
||||
const result = await handle.send(
|
||||
"increment",
|
||||
{ amount: 5 },
|
||||
{ wait: true, timeout: 5_000 },
|
||||
);
|
||||
|
||||
if (result.status === "completed") {
|
||||
console.log(result.response); // { value: 5 }
|
||||
} else if (result.status === "timedOut") {
|
||||
console.log("timed out");
|
||||
}
|
||||
```
|
||||
|
||||
## Queue messages from within an actor
|
||||
|
||||
Queueing is useful from inside actor logic too, not just from clients.
|
||||
|
||||
- Use actions as entrypoints, then enqueue into the run loop to keep mutations serialized.
|
||||
- You can also call `c.queue.send(...)` from other parts of `run` when needed.
|
||||
- `c.queue.send(...)` confirms durable enqueue. It does not wait for processing to finish.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { value: 0 },
|
||||
queues: {
|
||||
mutate: queue<{ delta: number }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter()) {
|
||||
c.state.value += message.body.delta;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
increment: async (c, delta: number) => {
|
||||
await c.queue.send("mutate", { delta });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.counter.getOrCreate(["main"]);
|
||||
|
||||
await handle.increment(5);
|
||||
await handle.increment(2);
|
||||
```
|
||||
|
||||
## Defining queue schemas
|
||||
|
||||
You can define queue types with `queue()` or with schema objects. Schema objects support [Standard Schema](https://standardschema.dev/) validators, including [Zod](https://zod.dev/).
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
import { z } from "zod";
|
||||
|
||||
export const worker = actor({
|
||||
state: {},
|
||||
queues: {
|
||||
// Use generic queue typing when you want compile-time typing only.
|
||||
foo: queue<{ id: string }, { ok: true }>(),
|
||||
// Use schema objects when you want runtime validation for message and completion payloads.
|
||||
bar: {
|
||||
message: z.object({ id: z.string() }),
|
||||
complete: z.object({ ok: z.boolean() }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { worker } });
|
||||
```
|
||||
|
||||
## Pull messages with `next` and `nextBatch`
|
||||
|
||||
Use `next` when you want to wait for one queue message.
|
||||
Use `nextBatch` when you want to wait for multiple queue messages.
|
||||
|
||||
- Waits until messages are available unless timeout is hit.
|
||||
- Omit `timeout` to wait indefinitely.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const queueWorker = actor({
|
||||
state: {},
|
||||
queues: {
|
||||
jobs: queue<{ id: string }>(),
|
||||
},
|
||||
actions: {
|
||||
pull: async (c) => {
|
||||
const batch = await c.queue.nextBatch({
|
||||
count: 10,
|
||||
timeout: 1_000,
|
||||
});
|
||||
|
||||
const oneWithoutTimeout = await c.queue.next();
|
||||
|
||||
return {
|
||||
batchCount: batch.length,
|
||||
receivedOneWithoutTimeout: oneWithoutTimeout !== undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { queueWorker } });
|
||||
```
|
||||
|
||||
## Poll messages
|
||||
|
||||
Use `tryNext` when you need one non-blocking read.
|
||||
Use `tryNextBatch` for non-blocking batch reads.
|
||||
|
||||
- Returns immediately and never waits.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const queueWorker = actor({
|
||||
state: {},
|
||||
queues: {
|
||||
jobs: queue<{ id: string }>(),
|
||||
},
|
||||
actions: {
|
||||
poll: async (c) => {
|
||||
const immediate = await c.queue.tryNext();
|
||||
|
||||
const immediateBatch = await c.queue.tryNextBatch({
|
||||
count: 10,
|
||||
});
|
||||
|
||||
return {
|
||||
hasImmediate: immediate !== undefined,
|
||||
immediateBatchCount: immediateBatch.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { queueWorker } });
|
||||
```
|
||||
|
||||
## Abort signals
|
||||
|
||||
Use `signal` when your receive loop needs external cancellation semantics in addition to actor shutdown behavior.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
import { joinSignals } from "rivetkit/utils";
|
||||
|
||||
export const signalWorker = actor({
|
||||
state: {},
|
||||
createVars: () => ({
|
||||
cancelController: new AbortController(),
|
||||
}),
|
||||
queues: {
|
||||
jobs: queue<{ id: string }>(),
|
||||
},
|
||||
actions: {
|
||||
cancelProcessing: async (c) => {
|
||||
c.vars.cancelController.abort();
|
||||
},
|
||||
},
|
||||
run: async (c) => {
|
||||
while (!c.aborted) {
|
||||
const signal = joinSignals(c.abortSignal, c.vars.cancelController.signal);
|
||||
|
||||
try {
|
||||
const message = await c.queue.next({ signal });
|
||||
if (!message) continue;
|
||||
console.log("Processing job", message.body.id);
|
||||
} catch (error) {
|
||||
if (c.vars.cancelController.signal.aborted && !c.aborted) {
|
||||
c.vars.cancelController = new AbortController();
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { signalWorker } });
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.signalWorker.getOrCreate(["main"]);
|
||||
|
||||
await handle.send("jobs", { id: "job-1" });
|
||||
await handle.cancelProcessing();
|
||||
```
|
||||
|
||||
## Multiple queues
|
||||
|
||||
Multiple queues let you separate message flows by purpose. By default, receive calls race across all queues when `names` is not specified. In this pattern, prompt messages run through a streaming loop while stop messages act as control signals on a separate receive path.
|
||||
|
||||
Use `iter({ names: ["prompt"] })` as the main stream and `next({ names: ["stop"] })` as a stop signal.
|
||||
|
||||
```ts index.ts
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { generateText } from "ai";
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
import { joinSignals } from "rivetkit/utils";
|
||||
|
||||
export const agent = actor({
|
||||
state: { running: false, messages: [] as string[] },
|
||||
queues: {
|
||||
prompt: queue<{ prompt: string }, undefined>(),
|
||||
stop: queue<{ reason?: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const promptMessage of c.queue.iter({ names: ["prompt"], completable: true })) {
|
||||
// Create a stop controller for this prompt run.
|
||||
const stopController = new AbortController();
|
||||
const runSignal = joinSignals(c.abortSignal, stopController.signal);
|
||||
|
||||
// Watch for stop messages while generation is running.
|
||||
const stopWatcher = c.queue
|
||||
.next({ names: ["stop"], signal: runSignal })
|
||||
.then((stopMessage) => {
|
||||
if (stopMessage) stopController.abort();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Generate a response for the prompt.
|
||||
c.state.running = true;
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-5"),
|
||||
prompt: promptMessage.body.prompt,
|
||||
abortSignal: runSignal,
|
||||
}).finally(async () => {
|
||||
stopController.abort();
|
||||
c.state.running = false;
|
||||
});
|
||||
|
||||
// Append each model response to actor state and acknowledge the prompt.
|
||||
c.state.messages.push(text);
|
||||
await promptMessage.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { agent } });
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.agent.getOrCreate(["main"]);
|
||||
|
||||
await handle.send("prompt", { prompt: "summarize latest logs" });
|
||||
await handle.send("stop", { reason: "user canceled" });
|
||||
```
|
||||
|
||||
## Sleeping behavior
|
||||
|
||||
If an actor has a `run` handler, it does not sleep while that handler is actively doing work. It only can sleep when the run loop is blocked waiting for queue entries (for example inside `iter(...)` or `next(...)`).
|
||||
|
||||
This means you can run normal code in `run` without worrying about sleep interrupting it mid-call.
|
||||
|
||||
## Debugging
|
||||
|
||||
- `GET /inspector/queue?limit=50` returns queue size and pending message metadata.
|
||||
- `GET /inspector/summary` includes `queueSize` for quick queue health checks.
|
||||
- `POST /queue/:name` with `wait: true` is useful to verify completable/request-response behavior.
|
||||
- In non-dev mode, inspector endpoints require authorization.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Actions are for getting data, queue entries are for mutating data.
|
||||
- Implement connection auth in `onBeforeConnect`. See [Authentication](/docs/actors/authentication).
|
||||
- Route most state changes through one queue loop so ordering stays predictable.
|
||||
- If you need more complex multi-step run loops, consider using workflows.
|
||||
- Use `c.aborted` and `c.abortSignal` for actor shutdown. Use your own `AbortController` for earlier loop cancellation.
|
||||
- Add `timeout` when callers need bounded wait behavior.
|
||||
- Use `wait: true` only when the caller actually needs a response.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
### Avoid `wait: true` between actors
|
||||
|
||||
`wait: true` blocks the sender's run loop until the receiver finishes. Between actors, this adds unnecessary overhead and risks deadlocks, especially if the target actor needs to communicate back. If an actor sends a `wait: true` message to *itself*, it is a guaranteed deadlock because the run loop is already busy processing the current message.
|
||||
|
||||
Reserve `wait: true` for external callers (HTTP handlers, CLI tools, client apps). For actor-to-actor communication, send a queue message to the other actor without `wait: true`, then have that actor send a queue message back when the work is done.
|
||||
|
||||
## Tips
|
||||
|
||||
### Message TTL
|
||||
|
||||
Every queue message includes a `createdAt` timestamp. Use this to skip or discard stale messages in your run loop:
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const worker = actor({
|
||||
state: {},
|
||||
queues: {
|
||||
jobs: queue<{ task: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter()) {
|
||||
const ageMs = Date.now() - message.createdAt;
|
||||
if (ageMs > 60_000) {
|
||||
// Message is older than 60 seconds, skip it.
|
||||
continue;
|
||||
}
|
||||
console.log("Processing", message.body.task);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { worker } });
|
||||
```
|
||||
|
||||
### Delayed delivery
|
||||
|
||||
Use [`c.schedule`](/docs/actors/schedule) to enqueue messages at a future time instead of processing them immediately:
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
export const reminder = actor({
|
||||
state: {},
|
||||
queues: {
|
||||
notify: queue<{ userId: string }>(),
|
||||
},
|
||||
actions: {
|
||||
scheduleReminder: async (c, userId: string) => {
|
||||
// Enqueue a message in 30 seconds.
|
||||
c.schedule.after(30_000, "enqueueReminder", userId);
|
||||
},
|
||||
enqueueReminder: async (c, userId: string) => {
|
||||
await c.queue.send("notify", { userId });
|
||||
},
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter()) {
|
||||
console.log("Sending reminder to", message.body.userId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { reminder } });
|
||||
```
|
||||
|
||||
See [Schedule](/docs/actors/schedule) for the full scheduling API.
|
||||
|
||||
_Source doc path: /docs/actors/queues_
|
||||
@@ -0,0 +1,183 @@
|
||||
# Node.js & Bun Quickstart
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/backend.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/backend
|
||||
> Description: Get started with Rivet Actors in Node.js and Bun
|
||||
|
||||
---
|
||||
Prefer to start from a complete project? See the runnable [`hello-world`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world) example.
|
||||
|
||||
## Steps
|
||||
|
||||
### Add Rivet Skill to Coding Agent (Optional)
|
||||
|
||||
If you're using an AI coding assistant (like Claude Code, Cursor, Windsurf, etc.), add Rivet skills for enhanced development assistance:
|
||||
|
||||
```sh
|
||||
npx skills add rivet-dev/skills
|
||||
```
|
||||
|
||||
### Install Rivet
|
||||
|
||||
```sh
|
||||
npm install rivetkit
|
||||
```
|
||||
|
||||
### Create Actors and Start Server
|
||||
|
||||
Create a file with your actors, set up the registry, and start the server:
|
||||
|
||||
```ts index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### Run Server
|
||||
|
||||
```sh Node.js
|
||||
npx tsx --watch index.ts
|
||||
```
|
||||
|
||||
```sh Bun
|
||||
bun --watch index.ts
|
||||
```
|
||||
|
||||
```sh Deno
|
||||
deno run --allow-net --allow-read --allow-env --watch index.ts
|
||||
```
|
||||
|
||||
Your server is now running on `http://localhost:6420`. Clients connect directly to the Rivet Engine on this port.
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
### Connect To The Rivet Actor
|
||||
|
||||
This code can run either in your frontend or within your backend:
|
||||
|
||||
### TypeScript
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Get or create a counter actor for the key "my-counter"
|
||||
const counter = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
// Call actions
|
||||
const count = await counter.increment(3);
|
||||
console.log("New count:", count);
|
||||
|
||||
// Listen to realtime events
|
||||
const connection = counter.connect();
|
||||
connection.on("newCount", (newCount: number) => {
|
||||
console.log("Count changed:", newCount);
|
||||
});
|
||||
|
||||
// Increment through connection
|
||||
await connection.increment(1);
|
||||
```
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### React
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```tsx Counter.tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import { useState } from "react";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const { useActor } = createRivetKit<typeof registry>("http://localhost:6420");
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Get or create a counter actor for the key "my-counter"
|
||||
const counter = useActor({
|
||||
name: "counter",
|
||||
key: ["my-counter"]
|
||||
});
|
||||
|
||||
// Listen to realtime events
|
||||
counter.useEvent("newCount", (x: number) => setCount(x));
|
||||
|
||||
const increment = async () => {
|
||||
// Call actions
|
||||
await counter.connection?.increment(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See the [React documentation](/docs/clients/react) for more information.
|
||||
|
||||
### Deploy
|
||||
|
||||
## Configuration Options
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/backend_
|
||||
@@ -0,0 +1,263 @@
|
||||
# Cloudflare Workers Quickstart
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/cloudflare.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/cloudflare
|
||||
> Description: Set up a Rivet project locally targeting Cloudflare Workers.
|
||||
|
||||
---
|
||||
Set up a Rivet project locally that runs on Cloudflare Workers. The `@rivetkit/cloudflare-workers` package wires the WebAssembly runtime for you.
|
||||
|
||||
Prefer to start from a complete project? See the runnable [`hello-world-cloudflare-workers`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-cloudflare-workers) example (with [Hono](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-cloudflare-workers-hono) and [raw router](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-cloudflare-workers-raw) variants).
|
||||
|
||||
## Steps
|
||||
|
||||
### Install Packages
|
||||
|
||||
```sh
|
||||
npm install rivetkit @rivetkit/cloudflare-workers
|
||||
npm install --save-dev wrangler
|
||||
```
|
||||
|
||||
### Configure Wrangler
|
||||
|
||||
```toml wrangler.toml
|
||||
name = "rivetkit-cloudflare"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-04-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
```
|
||||
|
||||
The `nodejs_compat` flag is required so the runtime can read its connection config from `process.env`.
|
||||
|
||||
### Create the Worker
|
||||
|
||||
`createHandler` serves the Rivet manager API on `/api/rivet`. Pick how you want to handle your own routes:
|
||||
|
||||
- **Default**: Rivet handles everything.
|
||||
- **Hono**: Mount a [Hono](https://hono.dev) app for your routes (`npm install hono`).
|
||||
- **Raw**: Provide a `fetch` and route requests yourself.
|
||||
|
||||
```ts Default @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { createHandler } from "@rivetkit/cloudflare-workers";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount = 1) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default createHandler({ use: { counter } });
|
||||
```
|
||||
|
||||
```ts Hono @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { createHandler, setup } from "@rivetkit/cloudflare-workers";
|
||||
import { Hono } from "hono";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount = 1) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// `setup` returns a typed registry, so the client below is fully typed.
|
||||
const registry = setup({ use: { counter } });
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/", (c) => c.text("Hello from Hono + Rivet Actors!"));
|
||||
|
||||
app.post("/increment/:name", async (c) => {
|
||||
// `createClient` reads RIVET_ENDPOINT from the environment (passed by `rivet
|
||||
// dev`, or set as a Worker secret in production).
|
||||
const client = createClient<typeof registry>();
|
||||
const count = await client.counter.getOrCreate(c.req.param("name")).increment(1);
|
||||
return c.json({ count });
|
||||
});
|
||||
|
||||
// Rivet keeps `/api/rivet`; Hono handles everything else.
|
||||
export default createHandler(registry, { fetch: app.fetch });
|
||||
```
|
||||
|
||||
```ts Raw @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { createHandler, setup } from "@rivetkit/cloudflare-workers";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount = 1) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// `setup` returns a typed registry, so the client below is fully typed.
|
||||
const registry = setup({ use: { counter } });
|
||||
|
||||
// Rivet keeps `/api/rivet`; your `fetch` handles everything else.
|
||||
export default createHandler(registry, {
|
||||
fetch: async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/") {
|
||||
return new Response("Hello from a raw Rivet Worker router!");
|
||||
}
|
||||
|
||||
const increment = url.pathname.match(/^\/increment\/(.+)$/);
|
||||
if (request.method === "POST" && increment) {
|
||||
// `createClient` reads RIVET_ENDPOINT from the environment (passed by
|
||||
// `rivet dev`, or set as a Worker secret in production).
|
||||
const client = createClient<typeof registry>();
|
||||
const count = await client.counter.getOrCreate(increment[1]).increment(1);
|
||||
return Response.json({ count });
|
||||
}
|
||||
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Run Locally
|
||||
|
||||
Start Rivet. The CLI runs the local engine and spawns `wrangler dev` for you:
|
||||
|
||||
```sh
|
||||
npx @rivetkit/cli dev --provider cloudflare
|
||||
```
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
### Connect To The Rivet Actor
|
||||
|
||||
This code can run either in your frontend or within your backend. It connects directly to the local engine on `http://localhost:6420`:
|
||||
|
||||
### TypeScript
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Get or create a counter actor for the key "my-counter"
|
||||
const counter = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
// Call actions
|
||||
const count = await counter.increment(3);
|
||||
console.log("New count:", count);
|
||||
|
||||
// Listen to realtime events
|
||||
const connection = counter.connect();
|
||||
connection.on("newCount", (newCount: number) => {
|
||||
console.log("Count changed:", newCount);
|
||||
});
|
||||
|
||||
// Increment through connection
|
||||
await connection.increment(1);
|
||||
```
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### React
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```tsx Counter.tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import { useState } from "react";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const { useActor } = createRivetKit<typeof registry>("http://localhost:6420");
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Get or create a counter actor for the key "my-counter"
|
||||
const counter = useActor({
|
||||
name: "counter",
|
||||
key: ["my-counter"]
|
||||
});
|
||||
|
||||
// Listen to realtime events
|
||||
counter.useEvent("newCount", (x: number) => setCount(x));
|
||||
|
||||
const increment = async () => {
|
||||
// Call actions
|
||||
await counter.connection?.increment(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See the [React documentation](/docs/clients/react) for more information.
|
||||
|
||||
### Deploy
|
||||
|
||||
Ready to ship? See [Deploying to Cloudflare Workers](/docs/deploy/cloudflare).
|
||||
|
||||
## Related
|
||||
|
||||
- [Quickstart](/docs/actors/quickstart)
|
||||
- [Deploying to Cloudflare Workers](/docs/deploy/cloudflare)
|
||||
- [SQLite](/docs/actors/sqlite)
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/cloudflare_
|
||||
@@ -0,0 +1,239 @@
|
||||
# Effect.ts Quickstart (Beta)
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/effect.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/effect
|
||||
> Description: Build a Rivet Actor with the Effect SDK
|
||||
|
||||
---
|
||||
Effect support is in beta. The `@rivetkit/effect` API may change between releases. See the [`hello-world-effect`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-effect) and [`chat-room-effect`](https://github.com/rivet-dev/rivet/tree/main/examples/chat-room-effect) examples for complete runnable projects.
|
||||
|
||||
## Steps
|
||||
|
||||
### Install Rivet
|
||||
|
||||
Add `rivetkit`, the Effect SDK, and its Effect peers:
|
||||
|
||||
```sh
|
||||
npm install rivetkit @rivetkit/effect effect @effect/platform-node
|
||||
```
|
||||
|
||||
### Define Your Actor
|
||||
|
||||
Split each actor into a public contract and a server-only implementation so the contract can be imported from client code without leaking server details.
|
||||
|
||||
The contract declares the actor and its actions. Actions are standalone values with explicit [`effect/Schema`](https://effect.website/docs/schema/introduction/) payloads and successes, validated end to end:
|
||||
|
||||
```ts src/actors/counter/api.ts @nocheck
|
||||
import { Action, Actor } from "@rivetkit/effect";
|
||||
import { Schema } from "effect";
|
||||
|
||||
export const Increment = Action.make("Increment", {
|
||||
payload: { amount: Schema.Number },
|
||||
success: Schema.Number,
|
||||
});
|
||||
|
||||
export const GetCount = Action.make("GetCount", {
|
||||
success: Schema.Number,
|
||||
});
|
||||
|
||||
export const Counter = Actor.make("Counter", {
|
||||
actions: [Increment, GetCount],
|
||||
});
|
||||
```
|
||||
|
||||
The implementation registers the actor with `.toLayer`. The wake function runs once when the actor awakes and returns the action handlers. Persisted state is accessed through a `SubscriptionRef`-like `State` API:
|
||||
|
||||
```ts src/actors/counter/live.ts @nocheck
|
||||
import { Actor, State } from "@rivetkit/effect";
|
||||
import { Effect, Schema } from "effect";
|
||||
import { Counter } from "./api.ts";
|
||||
|
||||
export const CounterLive = Counter.toLayer(
|
||||
Effect.fnUntraced(function* ({ rawRivetkitContext, state }) {
|
||||
return Counter.of({
|
||||
Increment: Effect.fnUntraced(function* ({ payload }) {
|
||||
const next = yield* State.updateAndGet(state, (current) => ({
|
||||
count: current.count + payload.amount,
|
||||
})).pipe(Effect.orDie);
|
||||
|
||||
// Broadcast the new value to every connected client.
|
||||
rawRivetkitContext.broadcast("newCount", next.count);
|
||||
|
||||
return next.count;
|
||||
}),
|
||||
GetCount: () =>
|
||||
State.get(state).pipe(
|
||||
Effect.map((current) => current.count),
|
||||
Effect.orDie,
|
||||
),
|
||||
});
|
||||
}),
|
||||
{
|
||||
state: {
|
||||
schema: Schema.Struct({ count: Schema.Number }),
|
||||
initialValue: () => ({ count: 0 }),
|
||||
},
|
||||
name: "Counter",
|
||||
icon: "calculator",
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Serve The Registry
|
||||
|
||||
Compose the actor layers and serve them with `Registry.serve`. `Registry.layer()` reads engine config from the environment, and the actor layer is provided a `Client` so actors can call other actors:
|
||||
|
||||
```ts src/main.ts @nocheck
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { Client, Registry } from "@rivetkit/effect";
|
||||
import { Layer } from "effect";
|
||||
import { CounterLive } from "./actors/counter/live.ts";
|
||||
|
||||
const endpoint = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420";
|
||||
|
||||
const ActorsLayer = CounterLive.pipe(Layer.provide(Client.layer({ endpoint })));
|
||||
|
||||
const MainLayer = Registry.serve(ActorsLayer).pipe(Layer.provide(Registry.layer()));
|
||||
|
||||
// Keeps the layer alive. Tears down on SIGINT/SIGTERM.
|
||||
Layer.launch(MainLayer).pipe(NodeRuntime.runMain);
|
||||
```
|
||||
|
||||
### Run The Server
|
||||
|
||||
Set `RIVET_RUN_ENGINE=1` to spawn a local Rivet Engine alongside the server. The engine binary is downloaded and cached the first time you run, so there is nothing else to install:
|
||||
|
||||
```sh
|
||||
RIVET_RUN_ENGINE=1 npx tsx --watch src/main.ts
|
||||
```
|
||||
|
||||
Your server now connects to the Rivet Engine on `http://localhost:6420`. Clients connect directly to the engine on this port.
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
To point at a remote engine instead, set `RIVET_ENDPOINT=https://...` and omit `RIVET_RUN_ENGINE`.
|
||||
|
||||
### Connect To The Rivet Actor
|
||||
|
||||
This code can run either in your frontend or within your backend:
|
||||
|
||||
### Effect
|
||||
|
||||
The Effect client imports the same actor contract from your registry. `Counter.client` yields a typed accessor backed by the client layer:
|
||||
|
||||
```ts src/client.ts @nocheck
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { Client } from "@rivetkit/effect";
|
||||
import { Effect } from "effect";
|
||||
import { Counter } from "./actors/counter/api.ts";
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const counter = (yield* Counter.client).getOrCreate("my-counter");
|
||||
|
||||
const count = yield* counter.Increment({ amount: 3 });
|
||||
yield* Effect.log(`New count: ${count}`);
|
||||
|
||||
const total = yield* counter.GetCount();
|
||||
yield* Effect.log(`Total: ${total}`);
|
||||
});
|
||||
|
||||
const ClientLayer = Client.layer({ endpoint: "http://localhost:6420" });
|
||||
|
||||
program.pipe(Effect.provide(ClientLayer), NodeRuntime.runMain);
|
||||
```
|
||||
|
||||
With the server still running, start the client in another terminal:
|
||||
|
||||
```sh
|
||||
npx tsx src/client.ts
|
||||
```
|
||||
|
||||
See the [`chat-room-effect`](https://github.com/rivet-dev/rivet/tree/main/examples/chat-room-effect) example for a larger project with typed errors and actor-to-actor calls.
|
||||
|
||||
### TypeScript
|
||||
|
||||
A plain RivetKit client can call your Effect actor by name through the same engine. Actor and action names are resolved at runtime, so the client is untyped here:
|
||||
|
||||
```ts client.ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient("http://localhost:6420");
|
||||
|
||||
const counter = client.Counter.getOrCreate(["my-counter"]);
|
||||
|
||||
const count = await counter.Increment({ amount: 3 });
|
||||
console.log("New count:", count);
|
||||
```
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### Deploy
|
||||
|
||||
## Feature Support
|
||||
|
||||
The Effect SDK wraps the most common actor features with typed, schema-validated APIs. Everything else is still fully usable through the raw RivetKit context (see [Raw Escape Hatch](#raw-escape-hatch) below), so no feature is off limits, it just isn't typed yet.
|
||||
|
||||
| Feature | Effect-native API | Access |
|
||||
| --- | --- | --- |
|
||||
| Actor contract & actions | `Actor.make`, `Action.make` | Typed |
|
||||
| Persisted state | `State.get` / `set` / `update` / `updateAndGet` / `changes` | Typed |
|
||||
| Typed client | `Actor.client`, `Client.layer` | Typed |
|
||||
| Typed errors | `RivetError` | Typed |
|
||||
| Logging | `Logger` | Typed |
|
||||
| Sleep request | `Actor.Sleep` | Typed |
|
||||
| Actor address (`actorId` / `name` / `key`) | `Actor.CurrentAddress` | Typed |
|
||||
| Registry serve / test / web handler | `Registry` | Typed |
|
||||
| [Events & broadcast](/docs/actors/events) | Not yet wrapped | `rawRivetkitContext.broadcast(...)` |
|
||||
| [Schedule](/docs/actors/schedule) | Not yet wrapped | `rawRivetkitContext.schedule.*` |
|
||||
| [Embedded SQLite](/docs/actors/sqlite) | Not yet wrapped | `rawRivetkitContext.db.execute(...)` |
|
||||
| [Destroy](/docs/actors/lifecycle) | Not yet wrapped | `rawRivetkitContext.destroy()` |
|
||||
| Queues, connections, vars, alarms | Not yet wrapped | `rawRivetkitContext.*` |
|
||||
| Lifecycle hooks (`onSleep` / `onDestroy`) | Not yet wrapped | `rawRivetkitContext.*` |
|
||||
| Raw HTTP / WebSocket handlers | Not yet wrapped | `rawRivetkitContext.*` |
|
||||
|
||||
### Raw Escape Hatch
|
||||
|
||||
Every wake function receives `rawRivetkitContext`, the underlying RivetKit [actor context](/docs/actors). Reach for it to use any feature that does not have a typed wrapper yet. The typed `state` argument and the raw context point at the same actor, so you can mix both:
|
||||
|
||||
```ts src/actors/counter/live.ts @nocheck
|
||||
export const CounterLive = Counter.toLayer(
|
||||
Effect.fnUntraced(function* ({ rawRivetkitContext, state }) {
|
||||
return Counter.of({
|
||||
Increment: Effect.fnUntraced(function* ({ payload }) {
|
||||
// Typed state wrapper
|
||||
const next = yield* State.updateAndGet(state, (current) => ({
|
||||
count: current.count + payload.amount,
|
||||
})).pipe(Effect.orDie);
|
||||
|
||||
// Untyped features run through the raw context
|
||||
rawRivetkitContext.broadcast("newCount", next.count);
|
||||
rawRivetkitContext.schedule.after(1_000, "tick", {});
|
||||
|
||||
return next.count;
|
||||
}),
|
||||
});
|
||||
}),
|
||||
{
|
||||
state: {
|
||||
schema: Schema.Struct({ count: Schema.Number }),
|
||||
initialValue: () => ({ count: 0 }),
|
||||
},
|
||||
name: "Counter",
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Calls through `rawRivetkitContext` are not validated by `effect/Schema` and their payloads are typed as they are in the base RivetKit API.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
- [Actions](/docs/actors/actions) — Define the RPC surface clients call on your actor.
|
||||
|
||||
|
||||
- [State](/docs/actors/state) — Persist and load actor state across sleeps and restarts.
|
||||
|
||||
|
||||
- [Events](/docs/actors/events) — Broadcast realtime updates to connected clients.
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/effect_
|
||||
@@ -0,0 +1,136 @@
|
||||
# Next.js Quickstart
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/next-js.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/next-js
|
||||
> Description: Get started with Rivet Actors in Next.js
|
||||
|
||||
---
|
||||
### Add Rivet Skill to Coding Agent (Optional)
|
||||
|
||||
If you're using an AI coding assistant (like Claude Code, Cursor, Windsurf, etc.), add Rivet skills for enhanced development assistance:
|
||||
|
||||
```sh
|
||||
npx skills add rivet-dev/skills
|
||||
```
|
||||
|
||||
### Create a Next.js App
|
||||
|
||||
```sh
|
||||
npx create-next-app@latest my-app
|
||||
cd my-app
|
||||
```
|
||||
|
||||
### Install RivetKit
|
||||
|
||||
### Create an Actor
|
||||
|
||||
Create a file at `src/rivet/registry.ts` with a simple counter actor:
|
||||
|
||||
```ts src/rivet/registry.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
```
|
||||
|
||||
### Setup Rivet API route
|
||||
|
||||
Create a file at `src/app/api/rivet/[...all]/route.ts` to setup the API routes:
|
||||
|
||||
```ts src/app/api/rivet/[...all]/route.ts @nocheck
|
||||
import { toNextHandler } from "@rivetkit/next-js";
|
||||
import { registry } from "@/rivet/registry";
|
||||
|
||||
export const maxDuration = 300;
|
||||
|
||||
export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } = toNextHandler(registry);
|
||||
```
|
||||
|
||||
### Use the Actor in a component
|
||||
|
||||
Create a Counter component and add it to your page:
|
||||
|
||||
```tsx src/components/Counter.tsx @nocheck
|
||||
"use client";
|
||||
|
||||
import { createRivetKit } from "@rivetkit/next-js/client";
|
||||
import type { registry } from "@/rivet/registry";
|
||||
import { useState } from "react";
|
||||
|
||||
export const { useActor } = createRivetKit<typeof registry>({
|
||||
endpoint:
|
||||
process.env.NEXT_PUBLIC_RIVET_ENDPOINT ?? "http://localhost:3000/api/rivet",
|
||||
namespace: process.env.NEXT_PUBLIC_RIVET_NAMESPACE,
|
||||
token: process.env.NEXT_PUBLIC_RIVET_TOKEN,
|
||||
});
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Get or create a counter actor for the key "my-counter"
|
||||
const counter = useActor({
|
||||
name: "counter",
|
||||
key: ["my-counter"]
|
||||
});
|
||||
|
||||
// Listen to realtime events
|
||||
counter.useEvent("newCount", (x: number) => setCount(x));
|
||||
|
||||
const increment = async () => {
|
||||
// Call actions
|
||||
await counter.connection?.increment(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx src/app/page.tsx @nocheck
|
||||
import { Counter } from "@/components/Counter";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<h1>My App</h1>
|
||||
<Counter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For information about the Next.js client API, see the [React Client API Reference](/docs/clients/react).
|
||||
|
||||
### Run Locally
|
||||
|
||||
Start the Next.js dev server. Rivet auto-starts a local engine alongside it:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:3000` in your browser to use the app.
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
### Deploy to Vercel
|
||||
|
||||
See the [Vercel deployment guide](/docs/deploy/vercel) for detailed instructions on deploying your Rivet app to Vercel.
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/next-js_
|
||||
@@ -0,0 +1,157 @@
|
||||
# React Quickstart
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/react.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/react
|
||||
> Description: Build realtime React applications with Rivet Actors
|
||||
|
||||
---
|
||||
## Steps
|
||||
|
||||
### Add Rivet Skill to Coding Agent (Optional)
|
||||
|
||||
If you're using an AI coding assistant (like Claude Code, Cursor, Windsurf, etc.), add Rivet skills for enhanced development assistance:
|
||||
|
||||
```sh
|
||||
npx skills add rivet-dev/skills
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```sh
|
||||
npm install rivetkit @rivetkit/react
|
||||
```
|
||||
|
||||
### Create Backend Actor and Start Server
|
||||
|
||||
Create your actor registry on the backend and start the server:
|
||||
|
||||
```ts backend/index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### Create React Frontend
|
||||
|
||||
Set up your React application:
|
||||
|
||||
```tsx Counter.tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import { useState } from "react";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const { useActor } = createRivetKit<typeof registry>("http://localhost:6420");
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Get or create a counter actor for the key "my-counter"
|
||||
const counter = useActor({
|
||||
name: "counter",
|
||||
key: ["my-counter"]
|
||||
});
|
||||
|
||||
// Listen to realtime events
|
||||
counter.useEvent("newCount", (x: number) => setCount(x));
|
||||
|
||||
const increment = async () => {
|
||||
// Call actions
|
||||
await counter.connection?.increment(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
For detailed information about the React client API, see the [React Client API Reference](/docs/clients/react).
|
||||
|
||||
### Setup Vite Configuration
|
||||
|
||||
Configure Vite for development:
|
||||
|
||||
```ts vite.config.ts @nocheck
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Run Your Application
|
||||
|
||||
Start both the backend and frontend:
|
||||
|
||||
**Terminal 1**: Start the backend
|
||||
|
||||
```sh Node.js
|
||||
npx tsx --watch backend/index.ts
|
||||
```
|
||||
|
||||
```sh Bun
|
||||
bun --watch backend/index.ts
|
||||
```
|
||||
|
||||
```sh Deno
|
||||
deno run --allow-net --allow-read --allow-env --watch backend/index.ts
|
||||
```
|
||||
|
||||
**Terminal 2**: Start the frontend
|
||||
|
||||
```sh Frontend
|
||||
npx vite
|
||||
```
|
||||
|
||||
Open `http://localhost:5173` in your browser. Try opening multiple tabs to see realtime sync in action.
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
### Deploy
|
||||
|
||||
## Configuration Options
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/react_
|
||||
@@ -0,0 +1,249 @@
|
||||
# Rust Quickstart (Beta)
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/rust.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/rust
|
||||
> Description: Build a Rivet Actor in Rust
|
||||
|
||||
---
|
||||
Rust support is in beta. The supported public Rust API is `rivetkit` and `rivetkit-client`; lower-level crates are internal implementation details and do not carry a stability guarantee. See the full API reference on [docs.rs/rivetkit](https://docs.rs/rivetkit), or the runnable [`hello-world-rust`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-rust) example.
|
||||
|
||||
## Steps
|
||||
|
||||
### Add Rivet
|
||||
|
||||
Add the `rivetkit` crate and its companions:
|
||||
|
||||
```sh
|
||||
cargo add rivetkit anyhow async-trait
|
||||
cargo add serde --features derive
|
||||
cargo add tokio --features full
|
||||
```
|
||||
|
||||
### Define Your Actor
|
||||
|
||||
Put the actor in `src/lib.rs` so both your server and your client can share the same types. An actor is a type that implements `Actor`, plus one `Handles` implementation for each action. Persisted state lives in `type State`; ephemeral runtime state is just fields on your actor struct.
|
||||
|
||||
```rust src/lib.rs
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rivetkit::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
type BoxFuture<T> = Pin<Box<dyn Future<Output = Result<T>> + Send>>;
|
||||
|
||||
pub struct Counter;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct CounterState {
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Increment {
|
||||
pub amount: i64,
|
||||
}
|
||||
|
||||
impl Action for Increment {
|
||||
type Output = i64;
|
||||
|
||||
const NAME: &'static str = "increment";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct NewCount {
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
impl Event for NewCount {
|
||||
const NAME: &'static str = "newCount";
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Actor for Counter {
|
||||
type State = CounterState;
|
||||
type Input = ();
|
||||
type Actions = (Increment,);
|
||||
type Events = (NewCount,);
|
||||
type Queue = ();
|
||||
type ConnParams = ();
|
||||
type ConnState = ();
|
||||
type Action = action::Raw;
|
||||
|
||||
async fn create_state(_ctx: &Ctx<Self>, _input: Self::Input) -> Result<Self::State> {
|
||||
Ok(CounterState::default())
|
||||
}
|
||||
|
||||
async fn create(_ctx: &Ctx<Self>) -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Handles<Increment> for Counter {
|
||||
type Future = BoxFuture<i64>;
|
||||
|
||||
fn handle(self: Arc<Self>, ctx: Ctx<Self>, action: Increment) -> Self::Future {
|
||||
Box::pin(async move {
|
||||
let count = {
|
||||
let mut state = ctx.state_mut();
|
||||
state.count += action.amount;
|
||||
state.count
|
||||
};
|
||||
ctx.emit(NewCount { count })?;
|
||||
Ok(count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registry() -> Registry {
|
||||
let mut registry = Registry::new();
|
||||
registry.register_actor::<Counter>("counter");
|
||||
registry
|
||||
}
|
||||
```
|
||||
|
||||
### Serve The Registry
|
||||
|
||||
Your `src/main.rs` just starts the registry from the library:
|
||||
|
||||
```rust src/main.rs
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
counter::registry().start().await
|
||||
}
|
||||
```
|
||||
|
||||
Replace `counter` with your crate name (the package `name` in `Cargo.toml`, with dashes as underscores).
|
||||
|
||||
### Run The Server
|
||||
|
||||
The Rust runtime connects to the Rivet Engine. Setting `RIVETKIT_ENGINE_AUTO_DOWNLOAD=1` lets the runtime download and cache a matching engine binary the first time you run, so there is nothing else to install:
|
||||
|
||||
```sh
|
||||
RIVETKIT_ENGINE_AUTO_DOWNLOAD=1 cargo run
|
||||
```
|
||||
|
||||
Your server now connects to the Rivet Engine on `http://localhost:6420`. Clients connect directly to the engine on this port.
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
Already have an engine binary? Set `RIVET_ENGINE_BINARY_PATH=/path/to/rivet-engine` to point at it instead. If you are working inside the [Rivet monorepo](https://github.com/rivet-dev/rivet), a local `cargo build -p rivet-engine` is discovered automatically from `target/debug`.
|
||||
|
||||
### Connect To The Rivet Actor
|
||||
|
||||
This code can run either in your frontend or within your backend:
|
||||
|
||||
### Rust
|
||||
|
||||
Add a `src/bin/client.rs` that imports the same actor types from your library. There is no need to redefine the actor on the client.
|
||||
|
||||
```rust src/bin/client.rs
|
||||
use counter::{Counter, Increment, NewCount};
|
||||
use rivetkit::{
|
||||
client::{Client, ClientConfig},
|
||||
prelude::*,
|
||||
TypedClientExt,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
||||
|
||||
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
|
||||
let count = counter.send(Increment { amount: 3 }).await?;
|
||||
println!("New count: {count}");
|
||||
|
||||
let connection = counter.connect();
|
||||
connection
|
||||
.on::<NewCount>(|event| println!("Count changed: {}", event.count))
|
||||
.await;
|
||||
connection.send(Increment { amount: 1 }).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
With the server still running, start the client in another terminal:
|
||||
|
||||
```sh
|
||||
cargo run --bin client
|
||||
```
|
||||
|
||||
See the [`hello-world-rust`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-rust) example for a complete runnable counter.
|
||||
|
||||
### TypeScript
|
||||
|
||||
A TypeScript client can call your Rust actor by name through the same engine. Actor and action names are resolved at runtime, so the client is untyped here:
|
||||
|
||||
```ts client.ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient("http://localhost:6420");
|
||||
|
||||
const counter = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
const counterConnection = counter.connect();
|
||||
counterConnection.on("newCount", (event) => {
|
||||
console.log("Event count:", event.count);
|
||||
});
|
||||
|
||||
const count = await counterConnection.increment(3);
|
||||
console.log("New count:", count);
|
||||
|
||||
await counterConnection.increment(1);
|
||||
```
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### React
|
||||
|
||||
```tsx Counter.tsx @nocheck
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import { useState } from "react";
|
||||
|
||||
const { useActor } = createRivetKit("http://localhost:6420");
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const counter = useActor({
|
||||
name: "counter",
|
||||
key: ["my-counter"],
|
||||
});
|
||||
|
||||
const increment = async () => {
|
||||
await counter.connection?.increment(1);
|
||||
};
|
||||
|
||||
counter.useEvent("newCount", (event) => {
|
||||
setCount(event.count);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See the [React documentation](/docs/clients/react) for more information.
|
||||
|
||||
### Deploy
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
- [API Reference](https://docs.rs/rivetkit) — Full `rivetkit` crate documentation on docs.rs.
|
||||
|
||||
|
||||
- [Actions](/docs/actors/actions) — Define the RPC surface clients call on your actor.
|
||||
|
||||
|
||||
- [State](/docs/actors/state) — Persist and load actor state across sleeps and restarts.
|
||||
|
||||
|
||||
- [Events](/docs/actors/events) — Broadcast realtime updates to connected clients.
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/rust_
|
||||
@@ -0,0 +1,96 @@
|
||||
# Supabase Functions Quickstart
|
||||
|
||||
> Source: `src/content/docs/actors/quickstart/supabase.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/quickstart/supabase
|
||||
> Description: Set up a Rivet project locally targeting Supabase Edge Functions.
|
||||
|
||||
---
|
||||
Set up a Rivet project locally that runs on Supabase Edge Functions. The `@rivetkit/supabase` package wires the WebAssembly runtime for you.
|
||||
|
||||
Prefer to start from a complete project? See the runnable [`hello-world-supabase-functions`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-supabase-functions) example.
|
||||
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Supabase CLI](https://supabase.com/docs/guides/cli)
|
||||
- Docker, for Supabase's local Edge Runtime
|
||||
|
||||
The CLI runs the local Rivet engine as a bundled native binary, so Docker is only needed for Supabase itself. A Supabase project is only needed to deploy.
|
||||
|
||||
### Create the Function
|
||||
|
||||
```sh
|
||||
npx supabase functions new rivet
|
||||
```
|
||||
|
||||
Add the packages used by the function:
|
||||
|
||||
```sh
|
||||
npm install rivetkit @rivetkit/supabase
|
||||
```
|
||||
|
||||
### Configure the Function
|
||||
|
||||
Call `serve` from `@rivetkit/supabase`. It loads the WebAssembly runtime and serves the Rivet handler.
|
||||
|
||||
```ts supabase/functions/rivet/index.ts @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { serve, setup } from "@rivetkit/supabase";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, amount = 1) => {
|
||||
c.state.count += amount;
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// `setup` returns a typed registry, so a client can type itself with
|
||||
// `typeof registry`.
|
||||
export const registry = setup({ use: { counter } });
|
||||
|
||||
await serve(registry);
|
||||
```
|
||||
|
||||
### Run Locally
|
||||
|
||||
Start Rivet. The CLI runs the local engine, spawns `supabase functions serve` for you, and populates the connection values:
|
||||
|
||||
```sh
|
||||
npx @rivetkit/cli dev --provider supabase
|
||||
```
|
||||
|
||||
Visit [http://localhost:6420](http://localhost:6420) in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
|
||||
|
||||
### Call the Actor
|
||||
|
||||
Connect to your actor from a client. This connects directly to the local engine on `http://localhost:6420`:
|
||||
|
||||
```ts client.ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./supabase/functions/rivet/index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const counter = client.counter.getOrCreate(["my-counter"]);
|
||||
const count = await counter.increment(3);
|
||||
console.log("New count:", count);
|
||||
```
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### Deploy
|
||||
|
||||
Ready to ship? See [Deploying to Supabase Functions](/docs/deploy/supabase).
|
||||
|
||||
## Related
|
||||
|
||||
- [Quickstart](/docs/actors/quickstart)
|
||||
- [Deploying to Supabase Functions](/docs/deploy/supabase)
|
||||
- [SQLite](/docs/actors/sqlite)
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/supabase_
|
||||
@@ -0,0 +1,256 @@
|
||||
# Low-Level HTTP Request Handler
|
||||
|
||||
> Source: `src/content/docs/actors/request-handler.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/request-handler
|
||||
> Description: Actors can handle HTTP requests through the `onRequest` handler.
|
||||
|
||||
---
|
||||
For most use cases, [actions](/docs/actors/actions) provide high-level API powered by HTTP that's easier to work with than low-level HTTP. However, low-level handlers are required when implementing custom use cases or integrating external libraries that need direct access to the underlying HTTP `Request`/`Response` objects or WebSocket connections.
|
||||
|
||||
## Handling HTTP Requests
|
||||
|
||||
The `onRequest` handler processes HTTP requests sent to your actor. It receives the actor context and a standard `Request` object and returns a `Response` object.
|
||||
|
||||
### Raw HTTP
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
export const counterActor = actor({
|
||||
state: {
|
||||
count: 0,
|
||||
},
|
||||
// WinterTC compliant - accepts standard Request and returns standard Response
|
||||
onRequest: (c, request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method === "GET" && url.pathname === "/count") {
|
||||
return Response.json({ count: c.state.count });
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/increment") {
|
||||
c.state.count++;
|
||||
return Response.json({ count: c.state.count });
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
### Hono
|
||||
|
||||
```typescript
|
||||
import { actor, ActorContextOf } from "rivetkit";
|
||||
import { Hono } from "hono";
|
||||
|
||||
// Define the actor first
|
||||
const counterActor = actor({
|
||||
state: { count: 0 },
|
||||
actions: {}
|
||||
});
|
||||
|
||||
// Build router with typed context
|
||||
function buildRouter(actorCtx: ActorContextOf<typeof counterActor>) {
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/count", (honoCtx) => {
|
||||
return honoCtx.json({ count: actorCtx.state.count });
|
||||
});
|
||||
|
||||
app.post("/increment", (honoCtx) => {
|
||||
actorCtx.state.count++;
|
||||
return honoCtx.json({ count: actorCtx.state.count });
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
// Define the full actor with onRequest
|
||||
export const counterActorWithRouter = actor({
|
||||
state: { count: 0 },
|
||||
vars: { app: null as Hono | null },
|
||||
createVars: () => ({
|
||||
app: null as Hono | null
|
||||
}),
|
||||
onRequest: async (c, request) => {
|
||||
// Build router lazily
|
||||
const app = buildRouter(c as ActorContextOf<typeof counterActor>);
|
||||
return await app.fetch(request);
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
See also the [raw fetch handler example](https://github.com/rivet-dev/rivet/tree/main/examples/raw-fetch-handler).
|
||||
|
||||
## Sending Requests To Actors
|
||||
|
||||
### Via RivetKit Client
|
||||
|
||||
Use the `.fetch()` method on an actor handle to send HTTP requests to the actor's `onRequest` handler. This can be executed from either your frontend or backend.
|
||||
|
||||
```typescript index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
onRequest: (c, request) => {
|
||||
if (request.method === "POST") c.state.count++;
|
||||
return Response.json(c.state);
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const actor = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
// .fetch() is WinterTC compliant, it accepts standard Request and returns standard Response
|
||||
const response = await actor.fetch("/");
|
||||
const data = await response.json();
|
||||
console.log(data); // { count: 0 }
|
||||
```
|
||||
|
||||
### Via getGatewayUrl
|
||||
|
||||
Use `.getGatewayUrl()` to get the raw gateway URL for the actor. This is useful when you need to use the URL with external tools or custom HTTP clients.
|
||||
|
||||
```typescript index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
onRequest: (c, request) => {
|
||||
if (request.method === "POST") c.state.count++;
|
||||
return Response.json(c.state);
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { counter } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```typescript client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const actor = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
// Get the raw gateway URL
|
||||
const gatewayUrl = await actor.getGatewayUrl();
|
||||
// gatewayUrl = "https://...rivet.dev/..."
|
||||
|
||||
// Use with native fetch
|
||||
const response = await fetch(`${gatewayUrl}/request/`);
|
||||
const data = await response.json();
|
||||
console.log(data); // { count: 0 }
|
||||
```
|
||||
|
||||
### Via HTTP API
|
||||
|
||||
This handler can be accessed with raw HTTP using `https://api.rivet.dev/gateway/{actorId}/request/{...path}`.
|
||||
|
||||
For example, to call `POST /increment` on the counter actor above:
|
||||
|
||||
```typescript
|
||||
// Replace with your actor ID and token
|
||||
const actorId = "your-actor-id";
|
||||
const token = "your-token";
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.rivet.dev/gateway/${actorId}/request/increment`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-rivet-token": token,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(data); // { count: 1 }
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.rivet.dev/gateway/{actorId}/request/increment" \
|
||||
-H "x-rivet-token: {token}"
|
||||
```
|
||||
|
||||
The request is routed to the actor's `onRequest` handler where:
|
||||
|
||||
- `request.method` is `"POST"`
|
||||
- `request.url` ends with `/increment` (the path after `/request/`)
|
||||
- Headers, body, and other request properties are passed through unchanged
|
||||
|
||||
See the [HTTP API reference](/docs/actors/http-api) for more information on HTTP routing and authentication.
|
||||
|
||||
### Via Proxying Requests
|
||||
|
||||
You can proxy HTTP requests from your own server to actor handlers using the RivetKit client. This is useful when you need to add custom authentication, rate limiting, or request transformation before forwarding to actors.
|
||||
|
||||
```typescript
|
||||
import { Hono } from "hono";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { serve } from "@hono/node-server";
|
||||
|
||||
const client = createClient();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Proxy requests to actor's onRequest handler
|
||||
app.all("/actors/:id/:path{.*}", async (c) => {
|
||||
const actorId = c.req.param("id");
|
||||
const actorPath = (c.req.param("path") || "");
|
||||
|
||||
// Rewrite the incoming request to the actor-relative path, preserving
|
||||
// method, headers, and body
|
||||
const url = new URL(actorPath, "http://actor");
|
||||
const actorRequest = new Request(url, c.req.raw);
|
||||
|
||||
// Forward the rewritten Request to the actor's onRequest handler
|
||||
const actor = client.counter.get(actorId);
|
||||
return await actor.fetch(actorRequest);
|
||||
});
|
||||
|
||||
serve(app);
|
||||
```
|
||||
|
||||
## Connection & Lifecycle Hooks
|
||||
|
||||
`onRequest` will trigger the `onBeforeConnect`, `onConnect`, and `onDisconnect` hooks. Read more about [lifecycle hooks](/docs/actors/lifecycle).
|
||||
|
||||
Requests in flight will be listed in `c.conns`. Read more about [connections](/docs/actors/connections).
|
||||
|
||||
## WinterTC Compliance
|
||||
|
||||
The `onRequest` handler is WinterTC compliant and will work with existing libraries using the standard `Request` and `Response` types.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Does not support streaming responses & server-sent events at the moment. See the [tracking issue](https://github.com/rivet-dev/rivet/issues/3529).
|
||||
- `OPTIONS` requests currently are handled by Rivet and are not passed to `onRequest`
|
||||
|
||||
## Advanced
|
||||
|
||||
### Skip Ready Wait
|
||||
|
||||
Requests are normally held at the gateway until the actor is ready. Pass `skipReadyWait: true` on `handle.fetch()` to deliver immediately, including while the actor is still starting or in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence). See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) for details.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`RequestContext`](/typedoc/interfaces/rivetkit.mod.RequestContext.html) - Context for HTTP request handlers
|
||||
- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining request handlers
|
||||
|
||||
_Source doc path: /docs/actors/request-handler_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Scaling & Concurrency
|
||||
|
||||
> Source: `src/content/docs/actors/scaling.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/scaling
|
||||
> Description: This page has moved to [design patterns](/docs/actors/design-patterns).
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Source doc path: /docs/actors/scaling_
|
||||
103
.agents/skills/ai-agent-workspace/reference/actors/schedule.md
Normal file
103
.agents/skills/ai-agent-workspace/reference/actors/schedule.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Actor Scheduling
|
||||
|
||||
> Source: `src/content/docs/actors/schedule.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/schedule
|
||||
> Description: Schedule actor actions in the future with persistent timers that survive restarts and upgrades.
|
||||
|
||||
---
|
||||
Scheduling is used to trigger events in the future. The actor scheduler is like `setTimeout`, except the timeout will persist even if the actor restarts, upgrades, or crashes.
|
||||
|
||||
For a pattern guide to durable recurring jobs, see the cookbook: [Cron Jobs and Scheduled Tasks](/cookbook/cron-jobs/).
|
||||
|
||||
## Use Cases
|
||||
|
||||
Scheduling is helpful for long-running timeouts like month-long billing periods or account trials.
|
||||
|
||||
## Scheduling
|
||||
|
||||
### `c.schedule.after(duration, actionName, ...args)`
|
||||
|
||||
Schedules a function to be executed after a specified duration. This function persists across actor restarts, upgrades, or crashes.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `duration` (number): The delay in milliseconds.
|
||||
- `actionName` (string): The name of the action to be executed.
|
||||
- `...args` (unknown[]): Additional arguments to pass to the function.
|
||||
|
||||
### `c.schedule.at(timestamp, actionName, ...args)`
|
||||
|
||||
Schedules a function to be executed at a specific timestamp. This function persists across actor restarts, upgrades, or crashes.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `timestamp` (number): The exact time in milliseconds since the Unix epoch when the function should be executed.
|
||||
- `actionName` (string): The name of the action to be executed.
|
||||
- `...args` (unknown[]): Additional arguments to pass to the function.
|
||||
|
||||
## Full Example
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface Reminder {
|
||||
userId: string;
|
||||
message: string;
|
||||
scheduledFor: number;
|
||||
}
|
||||
|
||||
interface ReminderState {
|
||||
reminders: Record<string, Reminder>;
|
||||
}
|
||||
|
||||
// Mock email function
|
||||
function sendEmail(to: string, message: string) {
|
||||
console.log(`Sending email to ${to}: ${message}`);
|
||||
}
|
||||
|
||||
const reminderService = actor({
|
||||
state: { reminders: {} } as ReminderState,
|
||||
|
||||
actions: {
|
||||
setReminder: async (c, userId: string, message: string, delayMs: number) => {
|
||||
const reminderId = crypto.randomUUID();
|
||||
|
||||
// Store the reminder in state
|
||||
c.state.reminders[reminderId] = {
|
||||
userId,
|
||||
message,
|
||||
scheduledFor: Date.now() + delayMs
|
||||
};
|
||||
|
||||
// Schedule the sendReminder action to run after the delay
|
||||
await c.schedule.after(delayMs, "sendReminder", reminderId);
|
||||
|
||||
return { reminderId };
|
||||
},
|
||||
|
||||
sendReminder: (c, reminderId: string) => {
|
||||
const reminder = c.state.reminders[reminderId];
|
||||
if (!reminder) return;
|
||||
|
||||
// Send reminder notification
|
||||
if (c.conns.size > 0) {
|
||||
// Send the reminder to all connected clients
|
||||
for (const conn of c.conns.values()) {
|
||||
conn.send("reminder", {
|
||||
message: reminder.message,
|
||||
scheduledAt: reminder.scheduledFor
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// User is offline, send an email notification
|
||||
sendEmail(reminder.userId, reminder.message);
|
||||
}
|
||||
|
||||
// Clean up the processed reminder
|
||||
delete c.state.reminders[reminderId];
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
_Source doc path: /docs/actors/schedule_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Sharing and Joining State
|
||||
|
||||
> Source: `src/content/docs/actors/sharing-and-joining-state.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/sharing-and-joining-state
|
||||
> Description: This page has moved to [design patterns](/docs/actors/design-patterns).
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Source doc path: /docs/actors/sharing-and-joining-state_
|
||||
@@ -0,0 +1,260 @@
|
||||
# SQLite + Drizzle
|
||||
|
||||
> Source: `src/content/docs/actors/sqlite-drizzle.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/sqlite-drizzle
|
||||
> Description: Use Drizzle ORM with embedded SQLite in Rivet Actors.
|
||||
|
||||
---
|
||||
Use Drizzle when you want typed schema, typed queries, and generated migrations on top of actor-local SQLite.
|
||||
|
||||
For a high-level overview of where to store actor data, see [State & Storage](/docs/actors/state). For a worked multi-tenant pattern, see the cookbook: [Database per Tenant](/cookbook/per-tenant-database/).
|
||||
|
||||
## What is Drizzle good for?
|
||||
|
||||
- **Typed schema**: define tables in TypeScript and get typed query results.
|
||||
- **Typed query builder**: write SQL-like queries with autocompletion.
|
||||
- **Migration workflow**: generate SQL migration files from schema changes.
|
||||
- **Raw SQL escape hatch**: use `c.db.execute(...)` for direct SQLite when needed.
|
||||
|
||||
## Project structure
|
||||
|
||||
Use one folder per actor database:
|
||||
|
||||
```txt
|
||||
src/
|
||||
actors/
|
||||
todo-list/
|
||||
index.ts
|
||||
schema.ts
|
||||
drizzle.config.ts
|
||||
drizzle/
|
||||
0000_init.sql
|
||||
migrations.js
|
||||
migrations.d.ts
|
||||
meta/
|
||||
_journal.json
|
||||
```
|
||||
|
||||
- `index.ts` is the actor implementation.
|
||||
- `drizzle/` holds the SQL migrations (`*.sql`) and `meta/_journal.json` generated by `drizzle-kit`.
|
||||
- `migrations.js` is a small RivetKit glue file you maintain by hand. It imports the journal and each `*.sql` file and exports a `{ journal, migrations }` object keyed by migration (for example `m0000`). Add a new entry here whenever `db:generate` produces a new migration.
|
||||
- Commit the generated migration files and `migrations.js` to source control.
|
||||
|
||||
## Basic setup
|
||||
|
||||
```json package.json
|
||||
{
|
||||
"scripts": {
|
||||
"db:generate": "find src/actors -name drizzle.config.ts -exec drizzle-kit generate --config {} \\;"
|
||||
},
|
||||
"dependencies": {
|
||||
"rivetkit": "*",
|
||||
"drizzle-orm": "^0.44.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-kit": "^0.31.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts vite.config.ts @nocheck
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
function sqlRawPlugin(): Plugin {
|
||||
return {
|
||||
name: "sql-raw",
|
||||
transform(_code, id) {
|
||||
if (id.endsWith(".sql")) {
|
||||
const content = readFileSync(id, "utf-8");
|
||||
return { code: `export default ${JSON.stringify(content)};` };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sqlRawPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
```ts schema.ts
|
||||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
|
||||
export const todos = sqliteTable("todos", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
title: text("title").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
});
|
||||
|
||||
export const schema = { todos };
|
||||
```
|
||||
|
||||
```ts drizzle.config.ts @nocheck
|
||||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./src/actors/todo-list/schema.ts",
|
||||
out: "./src/actors/todo-list/drizzle",
|
||||
});
|
||||
```
|
||||
|
||||
```sql 0000_init.sql
|
||||
CREATE TABLE `todos` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`created_at` integer NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
```json _journal.json
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1735689600000,
|
||||
"tag": "0000_init",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```ts migrations.js @nocheck
|
||||
import journal from "./meta/_journal.json";
|
||||
import m0000 from "./0000_init.sql";
|
||||
|
||||
export default {
|
||||
journal,
|
||||
migrations: {
|
||||
m0000,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts index.ts @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { db } from "rivetkit/db/drizzle";
|
||||
import migrations from "./drizzle/migrations.js";
|
||||
import { schema, todos } from "./schema.ts";
|
||||
|
||||
export const todoList = actor({
|
||||
db: db({ schema, migrations }),
|
||||
actions: {
|
||||
addTodo: async (c, title: string) => {
|
||||
const rows = await c.db
|
||||
.insert(todos)
|
||||
.values({ title, createdAt: Date.now() })
|
||||
.returning();
|
||||
return rows[0];
|
||||
},
|
||||
getTodos: async (c) => {
|
||||
return await c.db.select().from(todos).orderBy(todos.id);
|
||||
},
|
||||
getTodoCount: async (c) => {
|
||||
const rows = (await c.db.execute(
|
||||
"SELECT COUNT(*) AS count FROM todos",
|
||||
)) as { count: number }[];
|
||||
return rows[0]?.count ?? 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts index.ts @nocheck
|
||||
import { setup } from "rivetkit";
|
||||
import { todoList } from "./todo-list/index.ts";
|
||||
|
||||
export const registry = setup({ use: { todoList } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts client.ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const todoList = client.todoList.getOrCreate(["main"]);
|
||||
|
||||
await todoList.addTodo("Write Drizzle docs");
|
||||
|
||||
const todos = await todoList.getTodos();
|
||||
const count = await todoList.getTodoCount();
|
||||
|
||||
console.log(todos, count);
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
### Query builder
|
||||
|
||||
Use Drizzle's typed query APIs for most reads and writes.
|
||||
|
||||
```ts @nocheck
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
await c.db.insert(todos).values({ title, createdAt: Date.now() });
|
||||
|
||||
const rows = await c.db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.title, title));
|
||||
```
|
||||
|
||||
### Raw SQL
|
||||
|
||||
`rivetkit/db/drizzle` also exposes raw SQLite access through `c.db.execute(...)`.
|
||||
|
||||
```ts @nocheck
|
||||
await c.db.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_todos_created_at ON todos(created_at)",
|
||||
);
|
||||
```
|
||||
|
||||
## Queues
|
||||
|
||||
Use queues for ordered mutations and keep actions read-only. Import `queue` alongside `actor` from `rivetkit`.
|
||||
|
||||
```ts @nocheck
|
||||
import { actor, queue } from "rivetkit";
|
||||
|
||||
// ...
|
||||
|
||||
queues: {
|
||||
addTodo: queue<{ title: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter()) {
|
||||
if (message.name === "addTodo") {
|
||||
await c.db.insert(todos).values({
|
||||
title: message.body.title,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
getTodos: async (c) => await c.db.select().from(todos),
|
||||
},
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Prefer Drizzle query APIs for app code and use raw SQL for advanced SQLite features.
|
||||
- Keep one `drizzle.config.ts` per actor folder.
|
||||
- Re-run `db:generate` after schema changes and commit generated migration files.
|
||||
- Use queues for writes and actions for reads.
|
||||
- Keep related writes in one action or queue message to reduce interleaved query risk.
|
||||
|
||||
## Read more
|
||||
|
||||
- [Drizzle SQLite quickstart](https://orm.drizzle.team/docs/get-started-sqlite)
|
||||
- [Drizzle `drizzle-kit generate`](https://orm.drizzle.team/docs/drizzle-kit-generate)
|
||||
- [Drizzle + Cloudflare D1](https://orm.drizzle.team/docs/deploy-cloudflare-d1)
|
||||
- [Drizzle + Cloudflare Durable Objects](https://orm.drizzle.team/docs/deploy-cloudflare-do)
|
||||
- [Cloudflare Durable Objects SQLite storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/)
|
||||
|
||||
_Source doc path: /docs/actors/sqlite-drizzle_
|
||||
267
.agents/skills/ai-agent-workspace/reference/actors/sqlite.md
Normal file
267
.agents/skills/ai-agent-workspace/reference/actors/sqlite.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# SQLite
|
||||
|
||||
> Source: `src/content/docs/actors/sqlite.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/sqlite
|
||||
> Description: Use embedded SQLite in Rivet Actors with raw SQL queries.
|
||||
|
||||
---
|
||||
For a high-level overview of where to store actor data, including when to use `c.state` versus SQLite, see [State & Storage](/docs/actors/state). For a worked multi-tenant pattern, see the cookbook: [Database per Tenant](/cookbook/per-tenant-database/).
|
||||
|
||||
## What is SQLite?
|
||||
|
||||
- **Database per actor**: each actor instance has its own SQLite database, scoped to that actor.
|
||||
- **High performance**: Rivet Actors keep compute and storage together, so queries avoid network round trips to an external database.
|
||||
- **Larger-than-memory storage**: SQLite stores data on disk, so you can work with datasets that do not fit in actor memory.
|
||||
- **Embedded relational database**: use tables, indexes, and SQL queries directly inside actor logic.
|
||||
|
||||
### SQLite features
|
||||
|
||||
- **Indexes**: speed up lookups on frequently queried fields.
|
||||
- **Search and filtering**: use `WHERE`, `LIKE`, and `ORDER BY` instead of manual in-memory loops.
|
||||
- **Relationships**: use multiple tables and `JOIN` queries for connected data.
|
||||
- **Constraints**: use primary keys, unique constraints, and foreign keys for data integrity.
|
||||
- **Transactions**: apply multiple writes atomically when changes must stay consistent.
|
||||
|
||||
## Raw SQL vs ORM (Drizzle)
|
||||
|
||||
Rivet supports both raw SQL and [Drizzle](https://orm.drizzle.team/) for actor-local SQLite.
|
||||
|
||||
Use **raw SQL** when you want direct query control and minimal abstraction.
|
||||
|
||||
```ts @nocheck
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
const rows = await c.db.execute("SELECT id, title FROM todos ORDER BY id DESC");
|
||||
```
|
||||
|
||||
Use **Drizzle** when you want typed schema and typed query APIs.
|
||||
|
||||
```ts @nocheck
|
||||
await c.vars.drizzle.insert(todos).values({ title });
|
||||
const rows = await c.vars.drizzle.select().from(todos).orderBy(desc(todos.id));
|
||||
```
|
||||
|
||||
You can mix both in the same actor.
|
||||
|
||||
For Drizzle setup, see [SQLite + Drizzle](/docs/actors/sqlite-drizzle).
|
||||
|
||||
## Basic setup
|
||||
|
||||
Define `db: db({ onMigrate })` on your actor, create your schema in `onMigrate`, and execute SQL with `c.db.execute(...)`.
|
||||
|
||||
RivetKit wraps `onMigrate` in a SQLite savepoint, so migration steps are atomic. If `onMigrate` throws, all SQL run by that hook is rolled back before the actor starts.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
export const todoList = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
todo_id INTEGER NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
FOREIGN KEY(todo_id) REFERENCES todos(id)
|
||||
);
|
||||
`);
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
addTodo: async (c, title: string) => {
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
},
|
||||
addComment: async (c, todoId: number, body: string) => {
|
||||
await c.db.execute(
|
||||
"INSERT INTO comments (todo_id, body) VALUES (?, ?)",
|
||||
todoId,
|
||||
body,
|
||||
);
|
||||
},
|
||||
getTodos: async (c) => {
|
||||
return (await c.db.execute(
|
||||
"SELECT id, title FROM todos ORDER BY id DESC",
|
||||
)) as {
|
||||
id: number;
|
||||
title: string;
|
||||
}[];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { todoList } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.todoList.getOrCreate(["main"]);
|
||||
|
||||
await handle.addTodo("Write SQLite docs");
|
||||
await handle.addTodo("Ship docs update");
|
||||
|
||||
const todos = await handle.getTodos();
|
||||
console.log(todos);
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
`c.db.execute(...)` returns an array of row objects for `SELECT` queries.
|
||||
|
||||
```ts @nocheck
|
||||
const rows = await c.db.execute(
|
||||
"SELECT id, title FROM todos WHERE title LIKE ?",
|
||||
`%${query}%`,
|
||||
);
|
||||
```
|
||||
|
||||
### Parameterized queries
|
||||
|
||||
Use `?` placeholders for dynamic values and pass parameters in order after the SQL string.
|
||||
|
||||
```ts @nocheck
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
```
|
||||
|
||||
You can also use named SQLite bindings by passing a single properties object.
|
||||
|
||||
```ts @nocheck
|
||||
const rows = await c.db.execute(
|
||||
"SELECT id, title FROM todos WHERE title = :title",
|
||||
{ title: "Write SQLite docs" },
|
||||
);
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
Use transactions when multiple writes must succeed or fail together.
|
||||
|
||||
- Start with `BEGIN`.
|
||||
- End with `COMMIT` if all queries succeed.
|
||||
- On error, run `ROLLBACK` and rethrow.
|
||||
- A transaction is global for the entire shared `c.db` connection. Be careful with other interleaved queries.
|
||||
|
||||
```ts @nocheck
|
||||
await c.db.execute("BEGIN");
|
||||
|
||||
try {
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
await c.db.execute(
|
||||
"INSERT INTO comments (todo_id, body) VALUES (last_insert_rowid(), ?)",
|
||||
body,
|
||||
);
|
||||
await c.db.execute("COMMIT");
|
||||
} catch (error) {
|
||||
await c.db.execute("ROLLBACK");
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Queues
|
||||
|
||||
It's recommended to use queues for mutations and actions for read-only queries. This is the same code structure as the basic setup, but mutation writes are routed through queues.
|
||||
|
||||
```ts index.ts
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
export const todoList = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
todo_id INTEGER NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
FOREIGN KEY(todo_id) REFERENCES todos(id)
|
||||
);
|
||||
`);
|
||||
},
|
||||
}),
|
||||
queues: {
|
||||
addTodo: queue<{ title: string }>(),
|
||||
addComment: queue<{ todoId: number; body: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.queue.iter()) {
|
||||
if (message.name === "addTodo") {
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", message.body.title);
|
||||
} else if (message.name === "addComment") {
|
||||
await c.db.execute(
|
||||
"INSERT INTO comments (todo_id, body) VALUES (?, ?)",
|
||||
message.body.todoId,
|
||||
message.body.body,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
getTodos: async (c) => {
|
||||
return (await c.db.execute(
|
||||
"SELECT id, title FROM todos ORDER BY id DESC",
|
||||
)) as {
|
||||
id: number;
|
||||
title: string;
|
||||
}[];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { todoList } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.todoList.getOrCreate(["main"]);
|
||||
|
||||
await handle.send("addTodo", { title: "Write SQLite docs" });
|
||||
await handle.send("addTodo", { title: "Ship docs update" });
|
||||
|
||||
const todos = await handle.getTodos();
|
||||
console.log(todos);
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
- `GET /inspector/summary` includes `isDatabaseEnabled` so you can confirm SQLite is configured.
|
||||
- `GET /inspector/database/schema` returns the tables and views discovered in the actor's SQLite database.
|
||||
- `GET /inspector/database/rows?table=...&limit=100&offset=0` returns paged rows for a specific table or view.
|
||||
- `POST /inspector/database/execute` lets you run ad-hoc SQL for debugging and data fixes with positional `args` or named `properties`.
|
||||
- Keep a small read-only action for quick query verification while debugging.
|
||||
- In non-dev mode, inspector endpoints require authorization.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Keep schema creation and migration steps in `onMigrate`; RivetKit runs them atomically inside a SQLite savepoint.
|
||||
- Use `?` placeholders for dynamic values.
|
||||
- Prefer queue-driven writes for ordered or background work.
|
||||
- Use transactions for related multi-step mutations when atomicity matters.
|
||||
|
||||
## Read more
|
||||
|
||||
- [SQLite + Drizzle in Rivet Actors](/docs/actors/sqlite-drizzle)
|
||||
- [SQLite docs](https://sqlite.org/docs.html)
|
||||
- [SQLite SQL language reference](https://sqlite.org/lang.html)
|
||||
|
||||
_Source doc path: /docs/actors/sqlite_
|
||||
437
.agents/skills/ai-agent-workspace/reference/actors/state.md
Normal file
437
.agents/skills/ai-agent-workspace/reference/actors/state.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# In-Memory State
|
||||
|
||||
> Source: `src/content/docs/actors/state.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/state
|
||||
> Description: Actors store state in memory for instant reads and writes. State can be persisted automatically or kept ephemeral.
|
||||
|
||||
---
|
||||
## Types of State
|
||||
|
||||
There are three ways to store data in an actor, depending on what it looks like and whether it needs to survive restarts.
|
||||
|
||||
### Durable
|
||||
|
||||
Simple, serializable data on `c.state` that is automatically persisted and restored across restarts. The default starting point.
|
||||
|
||||
```typescript Basic
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const counter = actor({
|
||||
// Constant initial state
|
||||
state: { count: 0 },
|
||||
|
||||
actions: {
|
||||
get: (c) => c.state.count,
|
||||
|
||||
// Update state, changes are persisted automatically
|
||||
increment: (c) => {
|
||||
c.state.count += 1;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Dynamic init
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface CounterState {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const counter = actor({
|
||||
// Compute the initial state when the actor is created
|
||||
createState: (): CounterState => ({ count: 0 }),
|
||||
|
||||
actions: {
|
||||
get: (c) => c.state.count,
|
||||
|
||||
increment: (c) => {
|
||||
c.state.count += 1;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```typescript With input
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
interface CounterState {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const counter = actor({
|
||||
// Compute the initial state from input passed at creation
|
||||
createState: (c, input: { startingCount: number }): CounterState => ({
|
||||
count: input.startingCount,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
get: (c) => c.state.count,
|
||||
|
||||
increment: (c) => {
|
||||
c.state.count += 1;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Ephemeral
|
||||
|
||||
Live objects on `c.vars` like database connections, API clients, and event emitters, or data loaded from an external source. Never persisted.
|
||||
|
||||
```typescript Basic
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
|
||||
// Constant ephemeral value, reset each time the actor starts
|
||||
vars: { lastAccessedAt: 0 },
|
||||
|
||||
actions: {
|
||||
increment: (c) => {
|
||||
// Read and write the ephemeral var
|
||||
c.vars.lastAccessedAt = Date.now();
|
||||
return ++c.state.count;
|
||||
},
|
||||
|
||||
getLastAccessed: (c) => c.vars.lastAccessedAt
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Dynamic init
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
|
||||
// Build a non-serializable emitter on each start
|
||||
createVars: () => ({ emitter: createEventEmitter() }),
|
||||
|
||||
actions: {
|
||||
broadcast: (c, text: string) => {
|
||||
c.state.messages.push(text);
|
||||
// Use the ephemeral emitter
|
||||
c.vars.emitter.emit("message", text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Mock event emitter for demonstration
|
||||
interface EventEmitter {
|
||||
on: (event: string, callback: (data: unknown) => void) => void;
|
||||
emit: (event: string, data: unknown) => void;
|
||||
}
|
||||
|
||||
function createEventEmitter(): EventEmitter {
|
||||
const listeners: Record<string, ((data: unknown) => void)[]> = {};
|
||||
return {
|
||||
on: (event, callback) => {
|
||||
listeners[event] = listeners[event] || [];
|
||||
listeners[event].push(callback);
|
||||
},
|
||||
emit: (event, data) => {
|
||||
listeners[event]?.forEach(cb => cb(data));
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```typescript @nocheck External database
|
||||
import { actor } from "rivetkit";
|
||||
import { Pool } from "pg";
|
||||
|
||||
// One shared pool for the whole process, created once and reused by every actor
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
const userActor = actor({
|
||||
state: { profile: null as Record<string, unknown> | null },
|
||||
|
||||
// Load this actor's row from the shared pool on each start
|
||||
createVars: async (c) => {
|
||||
const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", [c.key[0]]);
|
||||
return { profile: rows[0] };
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateEmail: async (c, email: string) => {
|
||||
await pool.query("UPDATE users SET email = $1 WHERE id = $2", [email, c.key[0]]);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
Rivet also provides an embedded SQLite database (`c.db`) for when your data needs to be queried, requires safe schema migrations, or grows too large to hold in memory. See [SQLite](/docs/actors/sqlite).
|
||||
|
||||
```typescript @nocheck Basic
|
||||
import { actor } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
const todoList = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
},
|
||||
}),
|
||||
|
||||
actions: {
|
||||
add: async (c, title: string) => {
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
},
|
||||
|
||||
list: async (c) => {
|
||||
return (await c.db.execute(
|
||||
"SELECT id, title FROM todos ORDER BY id DESC",
|
||||
)) as { id: number; title: string }[];
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```typescript @nocheck Load into memory
|
||||
import { actor } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
const counter = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS counter (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
count INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
await db.execute("INSERT OR IGNORE INTO counter (id, count) VALUES (1, 0)");
|
||||
},
|
||||
}),
|
||||
|
||||
// Load the count from SQLite into memory on every start
|
||||
createVars: async (c) => {
|
||||
const rows = (await c.db.execute(
|
||||
"SELECT count FROM counter WHERE id = 1",
|
||||
)) as { count: number }[];
|
||||
return { count: rows[0].count };
|
||||
},
|
||||
|
||||
actions: {
|
||||
get: (c) => c.vars.count,
|
||||
|
||||
increment: async (c) => {
|
||||
// Update the in-memory value and write it back to SQLite
|
||||
c.vars.count += 1;
|
||||
await c.db.execute("UPDATE counter SET count = ? WHERE id = 1", c.vars.count);
|
||||
return c.vars.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## State Isolation
|
||||
|
||||
Each actor's state is fully isolated. Other actors and clients can't touch it directly; all reads and writes go through the actor's own [Actions](/docs/actors/actions). To share state across actors, see [sharing and joining state](/docs/actors/sharing-and-joining-state).
|
||||
|
||||
## Durable State
|
||||
|
||||
`c.state` lives in memory and is persisted automatically, so reads and writes have no added latency while the data still survives sleeps, restarts, upgrades, and crashes. Use it for small, simple values like counters, flags, and small maps.
|
||||
|
||||
`createState` runs once when the actor is first created. On later starts, state is loaded from storage instead of recreated. See [Lifecycle](/docs/actors/lifecycle).
|
||||
|
||||
### When state saves
|
||||
|
||||
Mutating `c.state` schedules a save automatically. Rapid mutations are batched into a single write on a throttle (`stateSaveInterval`, default 1 second). Reads never trigger a save, saves aren't tied to action or handler boundaries, and state is also flushed when the actor sleeps or shuts down.
|
||||
|
||||
To force a save mid-action, call `c.saveState()`:
|
||||
|
||||
- `c.saveState({ immediate: true })` writes immediately and resolves once the write completes.
|
||||
- `c.saveState()` schedules a throttled save and returns right away, without waiting for the write.
|
||||
|
||||
Force an immediate save before a risky side effect so a crash can't lose progress:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const checkout = actor({
|
||||
state: { status: "pending" as "pending" | "charged" | "fulfilled" },
|
||||
|
||||
actions: {
|
||||
fulfill: async (c) => {
|
||||
c.state.status = "charged";
|
||||
// Persist before the side effect so a crash can't undo it
|
||||
await c.saveState({ immediate: true });
|
||||
|
||||
await chargeExternalProvider();
|
||||
|
||||
c.state.status = "fulfilled";
|
||||
return c.state.status;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function chargeExternalProvider() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
```
|
||||
|
||||
### Supported types
|
||||
|
||||
State must be serializable.
|
||||
|
||||
- `null`, `undefined`, `boolean`, `string`, `number`, `BigInt`
|
||||
- `Date`, `RegExp`, `Error`
|
||||
- `ArrayBuffer` and typed arrays (`Uint8Array`, `Int8Array`, `Float32Array`, etc.)
|
||||
- `Map`, `Set`, `Array`
|
||||
- Plain objects
|
||||
|
||||
When data grows large or needs querying, store it in [Embedded SQLite](#embedded-sqlite) instead.
|
||||
|
||||
## Ephemeral State
|
||||
|
||||
`c.vars` holds data that exists only while the actor runs and is never saved. Use it for live objects that can't be serialized (connections, clients, emitters) or for data loaded from an external source. Most actors use both: `state` for durable data, `vars` for live objects.
|
||||
|
||||
`createVars` runs on every actor start, unlike `createState` which runs once. That makes it the place to open connections and load data each time the actor wakes.
|
||||
|
||||
### Runtime objects
|
||||
|
||||
Build non-serializable objects in `createVars` and use them from actions:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const room = actor({
|
||||
state: { messages: [] as string[] },
|
||||
|
||||
// EventTarget can't be serialized, so it lives in vars
|
||||
createVars: () => ({ events: new EventTarget() }),
|
||||
|
||||
actions: {
|
||||
send: (c, text: string) => {
|
||||
c.state.messages.push(text);
|
||||
c.vars.events.dispatchEvent(new CustomEvent("message", { detail: text }));
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Loading from external sources
|
||||
|
||||
Create the connection pool once at module scope and share it across all actors, then use `createVars` (which can be `async`) to load this actor's data from it on each start:
|
||||
|
||||
```typescript @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { Pool } from "pg";
|
||||
|
||||
// One shared pool for the whole process, not one per actor
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
const profile = actor({
|
||||
state: { cachedName: "" },
|
||||
|
||||
createVars: async (c) => {
|
||||
const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", [c.key[0]]);
|
||||
return { user: rows[0] };
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateEmail: async (c, email: string) => {
|
||||
await pool.query("UPDATE users SET email = $1 WHERE id = $2", [email, c.key[0]]);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
When the actor owns its data, prefer [durable state](#durable-state) or [SQLite](#embedded-sqlite), which need no external infrastructure.
|
||||
|
||||
### Cleanup
|
||||
|
||||
`vars` is dropped when the actor stops, but per-actor resources like timers, subscriptions, and dedicated connections aren't cleaned up for you. Release them in `onSleep` and `onDestroy`. A shared pool stays open for the whole process, so don't close it per actor.
|
||||
|
||||
```typescript @nocheck
|
||||
const poller = actor({
|
||||
state: { ticks: 0 },
|
||||
|
||||
// Per-actor timer started on each wake
|
||||
createVars: (c) => ({ timer: setInterval(() => c.state.ticks++, 5000) }),
|
||||
|
||||
// Clear it before the actor sleeps or is destroyed
|
||||
onSleep: (c) => clearInterval(c.vars.timer),
|
||||
onDestroy: (c) => clearInterval(c.vars.timer),
|
||||
|
||||
actions: { /* ... */ }
|
||||
});
|
||||
```
|
||||
|
||||
## Embedded SQLite
|
||||
|
||||
`c.db` is a SQLite database scoped to each actor and stored on disk. Use it for queryable, relational, or larger-than-memory data. Because compute and storage live together, queries run locally with no network round trips.
|
||||
|
||||
A common pattern is to treat SQLite as the source of truth and keep a working copy in `c.vars`: load rows in `createVars`, serve reads from memory, and write changes back to `c.db`.
|
||||
|
||||
```typescript @nocheck
|
||||
import { actor } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
const leaderboard = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS scores (
|
||||
player TEXT PRIMARY KEY,
|
||||
score INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
},
|
||||
}),
|
||||
|
||||
// Load the table into memory once per start
|
||||
createVars: async (c) => {
|
||||
const rows = (await c.db.execute("SELECT player, score FROM scores")) as {
|
||||
player: string;
|
||||
score: number;
|
||||
}[];
|
||||
return { scores: new Map(rows.map((r) => [r.player, r.score])) };
|
||||
},
|
||||
|
||||
actions: {
|
||||
top: (c) => [...c.vars.scores].sort((a, b) => b[1] - a[1]).slice(0, 10),
|
||||
|
||||
record: async (c, player: string, score: number) => {
|
||||
c.vars.scores.set(player, score);
|
||||
// Write through to SQLite
|
||||
await c.db.execute(
|
||||
"INSERT INTO scores (player, score) VALUES (?, ?) ON CONFLICT(player) DO UPDATE SET score = ?",
|
||||
player, score, score,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For the full query API, schema migrations, transactions, and the Drizzle ORM, see:
|
||||
|
||||
- [SQLite](/docs/actors/sqlite): raw SQL against the embedded per-actor database.
|
||||
- [SQLite + Drizzle](/docs/actors/sqlite-drizzle): typed schema and query APIs.
|
||||
|
||||
## Debugging
|
||||
|
||||
- `GET /inspector/state` returns the actor's current state and `isStateEnabled`.
|
||||
- `PATCH /inspector/state` lets you set state directly while debugging.
|
||||
- In non-dev mode, inspector endpoints require authorization.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`CreateContext`](/typedoc/types/rivetkit.mod.CreateContext.html) - Context available during actor state creation
|
||||
- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - Context available throughout actor lifecycle
|
||||
- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining actors with state
|
||||
|
||||
_Source doc path: /docs/actors/state_
|
||||
@@ -0,0 +1,36 @@
|
||||
# Actor Statuses
|
||||
|
||||
> Source: `src/content/docs/actors/statuses.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/statuses
|
||||
> Description: Understand the lifecycle statuses of Rivet Actors, what they mean, how they appear in the API, and how to troubleshoot common issues.
|
||||
|
||||
---
|
||||
## Statuses
|
||||
|
||||
These are the statuses you can see in the dashboard for each actor.
|
||||
|
||||
| Status | Description |
|
||||
|---|---|
|
||||
| **Starting** | The actor has been created but has not yet become connectable. |
|
||||
| **Running** | The actor is live and accepting connections. |
|
||||
| **Destroyed** | The actor has been gracefully destroyed. |
|
||||
| **Crashed** | The actor failed to start or encountered a fatal error. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-crashed) for common failure reasons. |
|
||||
| **Sleeping** | The actor has been put to sleep from inactivity. It will be woken up automatically when a new request arrives. |
|
||||
| **Pending** | The actor is waiting to be allocated to a runner. This happens when no runner is available to handle the actor. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-pending) for common causes. |
|
||||
| **Crash Loop Backoff** | The actor failed to allocate and is waiting to retry with a backoff. This typically means repeated allocation failures. The backoff prevents overloading your infrastructure in the case of a widespread misconfiguration in your backend. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-crashed) for common failure reasons. |
|
||||
|
||||
## API Representation
|
||||
|
||||
The actor object returned by the full engine API (used by the dashboard) includes the following timestamp fields used to derive status:
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `createTs` | When the actor was first created. Always present. |
|
||||
| `connectableTs` | When the actor became connectable. Null if not yet running. |
|
||||
| `destroyTs` | When the actor was destroyed. |
|
||||
| `sleepTs` | When the actor entered a sleeping state. |
|
||||
| `pendingAllocationTs` | When the actor started waiting for an allocation. |
|
||||
| `rescheduleTs` | When the actor will retry allocation after a failure. |
|
||||
| `error` | Error details if the actor failed. |
|
||||
|
||||
_Source doc path: /docs/actors/statuses_
|
||||
236
.agents/skills/ai-agent-workspace/reference/actors/testing.md
Normal file
236
.agents/skills/ai-agent-workspace/reference/actors/testing.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Testing
|
||||
|
||||
> Source: `src/content/docs/actors/testing.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/testing
|
||||
> Description: Rivet provides a straightforward testing framework to build reliable and maintainable applications. This guide covers how to write effective tests for your actor-based services.
|
||||
|
||||
---
|
||||
## Setup
|
||||
|
||||
To set up testing with Rivet:
|
||||
|
||||
```bash
|
||||
# Install Vitest
|
||||
npm install -D vitest
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
## Basic Testing Setup
|
||||
|
||||
Rivet includes a test helper called `setupTest` that starts your registry in test mode and returns a client connected to it. This allows for fast, isolated tests without external dependencies.
|
||||
|
||||
```ts
|
||||
import { test, expect } from "vitest";
|
||||
import { setupTest } from "rivetkit/test";
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Define the actor
|
||||
const myActor = actor({
|
||||
state: { value: "initial" },
|
||||
actions: {
|
||||
someAction: (c) => {
|
||||
c.state.value = "updated";
|
||||
return c.state.value;
|
||||
},
|
||||
getState: (c) => {
|
||||
return c.state.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create the registry
|
||||
const registry = setup({
|
||||
use: { myActor }
|
||||
});
|
||||
|
||||
// Test the actor
|
||||
test("my actor test", async (testCtx) => {
|
||||
const { client } = await setupTest(testCtx, registry);
|
||||
|
||||
// Now you can interact with your actor through the client
|
||||
const myActorHandle = client.myActor.getOrCreate(["test"]);
|
||||
|
||||
// Test your actor's functionality
|
||||
await myActorHandle.someAction();
|
||||
|
||||
// Make assertions
|
||||
const result = await myActorHandle.getState();
|
||||
expect(result).toEqual("updated");
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Actor State
|
||||
|
||||
State persists within each test, allowing you to verify that your actor correctly maintains state between operations.
|
||||
|
||||
```ts
|
||||
import { test, expect } from "vitest";
|
||||
import { setupTest } from "rivetkit/test";
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Define the counter actor
|
||||
const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c) => {
|
||||
c.state.count += 1;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
getCount: (c) => {
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create the registry
|
||||
const registry = setup({
|
||||
use: { counter }
|
||||
});
|
||||
|
||||
// Test state persistence
|
||||
test("actor should persist state", async (testCtx) => {
|
||||
const { client } = await setupTest(testCtx, registry);
|
||||
const counterHandle = client.counter.getOrCreate(["test"]);
|
||||
|
||||
// Initial state
|
||||
expect(await counterHandle.getCount()).toBe(0);
|
||||
|
||||
// Modify state
|
||||
await counterHandle.increment();
|
||||
|
||||
// Verify state was updated
|
||||
expect(await counterHandle.getCount()).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Events
|
||||
|
||||
For actors that emit events, you can verify events are correctly triggered by subscribing to them:
|
||||
|
||||
```ts
|
||||
import { test, expect, vi } from "vitest";
|
||||
import { setupTest } from "rivetkit/test";
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
interface ChatMessage {
|
||||
username: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Define the chat room actor
|
||||
const chatRoom = actor({
|
||||
state: {
|
||||
messages: [] as ChatMessage[]
|
||||
},
|
||||
actions: {
|
||||
sendMessage: (c, username: string, message: string) => {
|
||||
c.state.messages.push({ username, message });
|
||||
c.broadcast("newMessage", username, message);
|
||||
},
|
||||
getHistory: (c) => {
|
||||
return c.state.messages;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create the registry
|
||||
const registry = setup({
|
||||
use: { chatRoom }
|
||||
});
|
||||
|
||||
// Test event emission
|
||||
test("actor should emit events", async (testCtx) => {
|
||||
const { client } = await setupTest(testCtx, registry);
|
||||
const chatRoomHandle = client.chatRoom.getOrCreate(["test"]);
|
||||
|
||||
// Set up event handler with a mock function
|
||||
const mockHandler = vi.fn();
|
||||
const conn = chatRoomHandle.connect();
|
||||
conn.on("newMessage", mockHandler);
|
||||
|
||||
// Trigger the event
|
||||
await conn.sendMessage("testUser", "Hello world");
|
||||
|
||||
// Wait for the event to be emitted
|
||||
await vi.waitFor(() => {
|
||||
expect(mockHandler).toHaveBeenCalledWith("testUser", "Hello world");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Schedules
|
||||
|
||||
Rivet's schedule functionality can be tested by scheduling work and waiting for it to run:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "vitest";
|
||||
import { setupTest } from "rivetkit/test";
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
// Helper to wait for a delay
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Define the scheduler actor
|
||||
const scheduler = actor({
|
||||
state: {
|
||||
tasks: [] as string[],
|
||||
completedTasks: [] as string[]
|
||||
},
|
||||
actions: {
|
||||
scheduleTask: (c, taskName: string, delayMs: number) => {
|
||||
c.state.tasks.push(taskName);
|
||||
// Schedule "completeTask" to run after the specified delay
|
||||
c.schedule.after(delayMs, "completeTask", taskName);
|
||||
return { success: true };
|
||||
},
|
||||
completeTask: (c, taskName: string) => {
|
||||
// This action will be called by the scheduler when the time comes
|
||||
c.state.completedTasks.push(taskName);
|
||||
return { completed: taskName };
|
||||
},
|
||||
getCompletedTasks: (c) => {
|
||||
return c.state.completedTasks;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create the registry
|
||||
const registry = setup({
|
||||
use: { scheduler }
|
||||
});
|
||||
|
||||
// Test scheduled tasks
|
||||
test("scheduled tasks should execute", async (testCtx) => {
|
||||
const { client } = await setupTest(testCtx, registry);
|
||||
const schedulerHandle = client.scheduler.getOrCreate(["test"]);
|
||||
|
||||
// Set up a scheduled task
|
||||
await schedulerHandle.scheduleTask("reminder", 100); // 100ms in the future
|
||||
|
||||
// Wait for the scheduled task to run
|
||||
await wait(150);
|
||||
|
||||
// Verify the scheduled task executed
|
||||
expect(await schedulerHandle.getCompletedTasks()).toContain("reminder");
|
||||
});
|
||||
```
|
||||
|
||||
Use a short real-time delay to wait for scheduled work to run. `setupTest` does not install fake timers, so if you want to use `vi.useFakeTimers()` you must enable it yourself and confirm it works with your selected runtime.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Isolate tests**: Each test should run independently, avoiding shared state.
|
||||
2. **Test edge cases**: Verify how your actor handles invalid inputs, concurrent operations, and error conditions.
|
||||
3. **Test scheduled operations**: Use short real-time delays to wait for scheduled work to run.
|
||||
4. **Use realistic data**: Test with data that resembles production scenarios.
|
||||
|
||||
`setupTest` starts the registry and disposes the returned client when the test finishes, so you can focus on writing effective tests for your business logic.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`setupTest`](/typedoc/functions/rivetkit.test_mod.setupTest.html) - Test setup helper function
|
||||
|
||||
_Source doc path: /docs/actors/testing_
|
||||
@@ -0,0 +1,154 @@
|
||||
# Troubleshooting
|
||||
|
||||
> Source: `src/content/docs/actors/troubleshooting.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/troubleshooting
|
||||
> Description: Common issues with Rivet Actors and how to resolve them.
|
||||
|
||||
---
|
||||
## Common Steps
|
||||
|
||||
Before diving into specific errors, try these general troubleshooting steps:
|
||||
|
||||
- Check your server logs for `level=ERROR` or `level=WARN` messages.
|
||||
- Check if any of your backend processes have crashed or restarted unexpectedly.
|
||||
- If you need more diagnostics, set `RIVET_LOG_LEVEL=DEBUG` for verbose logging. See [Logging](/docs/general/logging) for more options.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you're stuck, reach out on [Discord](https://rivet.dev/discord) or file an issue on [GitHub](https://github.com/rivet-dev/rivet/issues).
|
||||
|
||||
When reporting, please include:
|
||||
|
||||
- **Symptoms**
|
||||
- Whether this is happening in local dev, deployed, or both
|
||||
- The error you're seeing (screenshot or error message)
|
||||
- Relevant source code related to the issue
|
||||
- **What you've tried to solve it**
|
||||
- **Environment**
|
||||
- RivetKit version
|
||||
- Runtime (Node, Bun, etc.) including version
|
||||
- If applicable, provider in use (e.g. Vercel, Railway, Cloudflare)
|
||||
- If applicable, HTTP router in use (e.g. Hono, Express, Elysia)
|
||||
|
||||
## Actor status is crashed
|
||||
|
||||
See [Actor Statuses](/docs/actors/statuses) for more about this status.
|
||||
|
||||
The dashboard will show the specific failure reason. Common errors include:
|
||||
|
||||
### `crashed`
|
||||
|
||||
The actor's `run` handler threw an unhandled exception or exited unexpectedly. Check your actor logs for the error message and stack trace.
|
||||
|
||||
### `no_capacity`
|
||||
|
||||
No server was available to run your actor. The cause depends on your [runtime mode](/docs/general/runtime-modes):
|
||||
|
||||
**Serverless**:
|
||||
|
||||
- Your provider configuration does not have the region enabled that the actor is trying to run in. This is uncommon and usually only happens if an actor was created and then the provider config was updated to remove the region.
|
||||
- There is an issue connecting to your backend. If the engine is hitting `/api/rivet/start` and failing, check your backend logs for errors.
|
||||
|
||||
**Runners**:
|
||||
|
||||
- You don't have enough runners online. Check your runner list in the dashboard to verify runners are visible and connected.
|
||||
- Your runners are full. Each runner has a limited number of actor slots (configured via `RIVET_TOTAL_SLOTS`, default: 100,000). Check the dashboard to see if your runners have available capacity and scale up if needed.
|
||||
|
||||
### `runner_no_response`
|
||||
|
||||
The server running your actor did not respond in time. This can happen if your server is overloaded or experienced a network issue. Try restarting your server or checking its health.
|
||||
|
||||
### `runner_connection_lost`
|
||||
|
||||
The server running your actor lost its connection to Rivet. This is usually caused by a network interruption or your server restarting.
|
||||
|
||||
### `runner_draining_timeout`
|
||||
|
||||
Your server is shutting down and the actor did not finish in time. Consider handling graceful shutdown in your actor or increasing your shutdown timeout.
|
||||
|
||||
### `concurrent_actor_limit_reached`
|
||||
|
||||
The actor could not be allocated because the concurrent actor limit was reached. Reduce the number of concurrently running actors or increase your limit.
|
||||
|
||||
### `no_envoys`
|
||||
|
||||
No server was available to run your actor. This is equivalent to `no_capacity` on the current allocation path. See the `no_capacity` section above for the causes and fixes for your [runtime mode](/docs/general/runtime-modes).
|
||||
|
||||
### `envoy_no_response`
|
||||
|
||||
The server running your actor did not respond in time. This can happen if your server is overloaded or experienced a network issue. Try restarting your server or checking its health.
|
||||
|
||||
### `envoy_connection_lost`
|
||||
|
||||
The server running your actor lost its connection to Rivet. This is usually caused by a network interruption or your server restarting.
|
||||
|
||||
### `serverless_http_error`
|
||||
|
||||
Your serverless endpoint returned an HTTP error. Common causes:
|
||||
|
||||
- Your backend is returning an error before the actor can start. Check your server logs.
|
||||
- Your endpoint is behind authentication or a firewall that is blocking Rivet's requests.
|
||||
- Your serverless function crashed during startup. Check your platform's function logs (e.g. Vercel, Cloudflare).
|
||||
|
||||
### `serverless_connection_error`
|
||||
|
||||
Rivet was unable to connect to your serverless endpoint. Check that:
|
||||
|
||||
- Your backend is deployed and the endpoint URL is correct.
|
||||
- Your server is publicly reachable from the internet.
|
||||
- There are no DNS or firewall issues blocking the connection.
|
||||
|
||||
### `serverless_stream_ended_early`
|
||||
|
||||
The connection to your serverless endpoint was terminated before the actor finished. This usually means your serverless function hit its execution time limit. Ensure that your Rivet provider's request lifespan is configured to match the max duration of your serverless platform.
|
||||
|
||||
### `serverless_invalid_sse_payload`
|
||||
|
||||
Rivet received an unexpected response from your serverless endpoint. This typically means something is intercepting or modifying the request before it reaches your RivetKit handler. Check that:
|
||||
|
||||
- Your server routes requests to `registry.start()`, `registry.serve()`, or `registry.handler()` correctly.
|
||||
- No middleware is modifying the request or response body.
|
||||
|
||||
### `internal_error`
|
||||
|
||||
An unexpected error occurred within Rivet. If this persists, please [contact support](https://rivet.dev/docs).
|
||||
|
||||
## Actors crashing immediately on startup
|
||||
|
||||
If your actors are being created and then immediately destroyed or crashing, there is likely an error being thrown in your `createState` or `onCreate` lifecycle hooks. These hooks run during actor initialization before the actor is marked as ready.
|
||||
|
||||
Check your server logs for the error message and stack trace. Common causes include:
|
||||
|
||||
- An exception thrown in `createState` (e.g. invalid input, failed validation, or a runtime error when computing initial state)
|
||||
- An exception thrown in `onCreate` (e.g. a failing external API call, missing configuration, or invalid setup logic)
|
||||
|
||||
Fix the error in the relevant lifecycle hook and redeploy.
|
||||
|
||||
## Actors not upgrading to new code
|
||||
|
||||
If your actors are still running old code after deploying a new version, your [versioning](/docs/actors/versions) is likely not configured correctly.
|
||||
|
||||
Without versioning, Rivet has no way to distinguish old deployments from new ones. The behavior depends on your [runtime mode](/docs/general/runtime-modes):
|
||||
|
||||
- **Serverless**: Old requests may still be open from the previous deployment, so actors continue running on the old version's connection until those requests close.
|
||||
- **Runners**: The old runner container is still running and will continue accepting new actors. New actors may be scheduled on the old runner instead of the new one.
|
||||
|
||||
To fix this, configure a version number in your [registry configuration](/docs/general/registry-configuration). When a new version is deployed, Rivet will allocate new actors to the latest version and optionally drain old actors to migrate them.
|
||||
|
||||
## Actor status is pending
|
||||
|
||||
See [Actor Statuses](/docs/actors/statuses) for more about this status.
|
||||
|
||||
An actor stays in "pending" status when Rivet is waiting for a server to accept it. The cause depends on your [runtime mode](/docs/general/runtime-modes):
|
||||
|
||||
**Serverless**:
|
||||
|
||||
- A region may have been removed from your provider configuration while an existing actor still lives in that region. The actor has no available server to start on because no provider serves that region anymore. Re-add the region to your provider config or destroy the affected actors.
|
||||
- Check your backend logs for errors on the `/api/rivet/start` endpoint.
|
||||
|
||||
**Runners**:
|
||||
|
||||
- You don't have enough runners online. Check the dashboard to verify your runners are connected.
|
||||
- Your runners may be at capacity. Check the dashboard to see if runners have available actor slots and scale up if needed.
|
||||
|
||||
_Source doc path: /docs/actors/troubleshooting_
|
||||
106
.agents/skills/ai-agent-workspace/reference/actors/types.md
Normal file
106
.agents/skills/ai-agent-workspace/reference/actors/types.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Types
|
||||
|
||||
> Source: `src/content/docs/actors/types.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/types
|
||||
> Description: TypeScript types for working with Rivet Actors. This page covers context types used in lifecycle hooks and actions, as well as helper types for extracting types from actor definitions.
|
||||
|
||||
---
|
||||
## Context Types
|
||||
|
||||
Context types define what properties and methods are available in different parts of the actor lifecycle.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const counter = actor({
|
||||
// CreateContext in createState hook
|
||||
createState: (c, input: { initial: number }): { count: number } => {
|
||||
return { count: input.initial };
|
||||
},
|
||||
|
||||
// ActionContext in actions
|
||||
actions: {
|
||||
increment: (c) => {
|
||||
c.state.count += 1;
|
||||
return c.state.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Extracting Context Types
|
||||
|
||||
When writing helper functions that work with actor contexts, use context extractor types like `CreateContextOf` or `ActionContextOf` to extract the appropriate context type from your actor definition.
|
||||
|
||||
```typescript
|
||||
import { actor, CreateContextOf, ActionContextOf } from "rivetkit";
|
||||
|
||||
const gameRoom = actor({
|
||||
createState: (c, input: { roomId: string }): { players: string[]; score: number } => {
|
||||
initializeRoom(c, input.roomId);
|
||||
return { players: [] as string[], score: 0 };
|
||||
},
|
||||
|
||||
actions: {
|
||||
addPlayer: (c, playerId: string) => {
|
||||
validatePlayer(c, playerId);
|
||||
c.state.players.push(playerId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Extract CreateContext type for createState hook
|
||||
function initializeRoom(
|
||||
context: CreateContextOf<typeof gameRoom>,
|
||||
roomId: string
|
||||
) {
|
||||
console.log(`Initializing room: ${roomId}`);
|
||||
// context.state is not available here (being created)
|
||||
// context.vars is not available here (not created yet)
|
||||
}
|
||||
|
||||
// Extract ActionContext type for actions
|
||||
function validatePlayer(
|
||||
context: ActionContextOf<typeof gameRoom>,
|
||||
playerId: string
|
||||
) {
|
||||
// Full context available in actions
|
||||
if (context.state.players.includes(playerId)) {
|
||||
throw new Error("Player already in room");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### All Context Types
|
||||
|
||||
Each lifecycle hook and handler has a corresponding `*ContextOf` type, exported from `"rivetkit"`. Pass `typeof myActor` as the type parameter.
|
||||
|
||||
| Hook / Handler | Context Type |
|
||||
|---|---|
|
||||
| `createState` | `CreateContextOf` |
|
||||
| `onCreate` | `CreateContextOf` |
|
||||
| `createVars` | `CreateVarsContextOf` |
|
||||
| `createConnState` | `CreateConnStateContextOf` |
|
||||
| `onBeforeConnect` | `BeforeConnectContextOf` |
|
||||
| `onConnect` | `ConnectContextOf` |
|
||||
| `onDisconnect` | `DisconnectContextOf` |
|
||||
| `onDestroy` | `DestroyContextOf` |
|
||||
| `onMigrate` | `MigrateContextOf` |
|
||||
| `onWake` | `WakeContextOf` |
|
||||
| `onSleep` | `SleepContextOf` |
|
||||
| `onStateChange` | `StateChangeContextOf` |
|
||||
| `onBeforeActionResponse` | `BeforeActionResponseContextOf` |
|
||||
| `actions.*` | `ActionContextOf` |
|
||||
| `run` | `RunContextOf` |
|
||||
| `workflow` root context helpers | `WorkflowContextOf` |
|
||||
| `workflow` loop helpers | `WorkflowLoopContextOf` |
|
||||
| `workflow` branch helpers | `WorkflowBranchContextOf` |
|
||||
| `workflow` standalone step helpers | `WorkflowStepContextOf` |
|
||||
| `onRequest` | `RequestContextOf` |
|
||||
| `onWebSocket` | `WebSocketContextOf` |
|
||||
|
||||
`ActorContextOf`, `ConnContextOf`, and `ConnInitContextOf` are general-purpose base context types useful for helper functions that don't correspond to a specific hook.
|
||||
|
||||
Workflow context extractors are exported from both `"rivetkit"` and `"rivetkit/workflow"`.
|
||||
|
||||
_Source doc path: /docs/actors/types_
|
||||
337
.agents/skills/ai-agent-workspace/reference/actors/versions.md
Normal file
337
.agents/skills/ai-agent-workspace/reference/actors/versions.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Versions & Upgrades
|
||||
|
||||
> Source: `src/content/docs/actors/versions.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/versions
|
||||
> Description: When you deploy new code, Rivet ensures actors are upgraded seamlessly without downtime.
|
||||
|
||||
---
|
||||
## How Versions Work
|
||||
|
||||
Each runner has a **version number**. When you deploy new code with a new version, Rivet handles the transition automatically:
|
||||
|
||||
- **New actors go to the newest version**: When allocating actors, Rivet always prefers runners with the highest version number
|
||||
- **Multiple versions can coexist**: Old actors continue running on old versions while new actors are created on the new version
|
||||
- **Drain old actors**: When enabled, a runner connecting with a newer version number will gracefully stop old actors to be rescheduled to the new version
|
||||
|
||||
Versions are not configured by default. See [Registry Configuration](/docs/general/registry-configuration) to learn how to configure the runner version.
|
||||
|
||||
`RIVET_ENVOY_VERSION` is only needed when self-hosting or using a custom runner. Rivet Compute handles versioning automatically.
|
||||
|
||||
### Example Scenario
|
||||
|
||||
### Drain Enabled
|
||||
|
||||
When a new version is deployed, existing actors are gracefully stopped on the old runner and rescheduled onto the new version.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R1 as Runner v1
|
||||
participant R2 as Runner v2
|
||||
|
||||
Note over R1: Currently running
|
||||
Note over R2: Deployed
|
||||
R2->>R1: Drain old actors
|
||||
R1->>R2: Reschedule actors
|
||||
Note over R1: Shut down when all actors stopped
|
||||
```
|
||||
|
||||
### Drain Disabled
|
||||
|
||||
When a new version is deployed, both versions coexist. New actors are created on the new version while existing actors continue running on the old version until.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R1 as Runner v1
|
||||
participant R2 as Runner v2
|
||||
|
||||
Note over R1: Currently running
|
||||
Note over R2: Deployed
|
||||
Note over R1: Actor 1 sleeps from inactivity
|
||||
Note over R2: Actor 1 wakes up when prompted
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Setting the Version
|
||||
|
||||
Configure the runner version using an environment variable or programmatically:
|
||||
|
||||
```bash {{"title": "Environment Variable"}}
|
||||
RIVET_ENVOY_VERSION=2
|
||||
```
|
||||
|
||||
```typescript {{"title": "Programmatic"}}
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const myActor = actor({ state: {}, actions: {} });
|
||||
|
||||
const registry = setup({
|
||||
use: { myActor },
|
||||
envoy: {
|
||||
version: 2,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The version **must** be set at build time, not at runtime. Do not use `Date.now()` or similar runtime values in your registry setup code. This would assign a different version every time the server starts, causing actors to be drained and rescheduled on every restart instead of only on new deployments.
|
||||
|
||||
### Example Configurations
|
||||
|
||||
We recommend injecting a build-time value that increments with every deployment. Here are concrete examples for common setups:
|
||||
|
||||
### Dockerfile
|
||||
|
||||
Generate the version at build time and bake it into the image as an environment variable:
|
||||
|
||||
```bash @nocheck
|
||||
docker build --build-arg RIVET_ENVOY_VERSION=$(date +%s) .
|
||||
```
|
||||
|
||||
```dockerfile @nocheck
|
||||
FROM node:20-slim
|
||||
ARG RIVET_ENVOY_VERSION
|
||||
ENV RIVET_ENVOY_VERSION=$RIVET_ENVOY_VERSION
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm install && npm run build
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
All containers from this image will share the same version.
|
||||
|
||||
### Next.js
|
||||
|
||||
Set the version in `next.config.ts`. Next.js evaluates this file once at build time and inlines the value into the bundle:
|
||||
|
||||
```typescript @nocheck
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
env: {
|
||||
RIVET_ENVOY_VERSION: String(Math.floor(Date.now() / 1000)),
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
### Vite
|
||||
|
||||
Use `define` in your Vite config. This is evaluated once at build time and inlined into the bundle:
|
||||
|
||||
```typescript @nocheck
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
"process.env.RIVET_ENVOY_VERSION": JSON.stringify(
|
||||
String(Math.floor(Date.now() / 1000))
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
|
||||
Set the version from your CI pipeline:
|
||||
|
||||
```yaml @nocheck
|
||||
# GitHub Actions
|
||||
env:
|
||||
RIVET_ENVOY_VERSION: ${{ github.run_number }}
|
||||
```
|
||||
|
||||
```bash @nocheck
|
||||
# Railway / Render / generic CI
|
||||
export RIVET_ENVOY_VERSION=$(date +%s)
|
||||
```
|
||||
|
||||
```bash @nocheck
|
||||
# Git commit count
|
||||
export RIVET_ENVOY_VERSION=$(git rev-list --count HEAD)
|
||||
```
|
||||
|
||||
### Build Script
|
||||
|
||||
Generate a version file during your build step and import it:
|
||||
|
||||
```json @nocheck
|
||||
{
|
||||
"scripts": {
|
||||
"build": "echo \"export const BUILD_VERSION = $(date +%s);\" > src/build-version.ts && tsc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { BUILD_VERSION } from "./build-version";
|
||||
|
||||
const myActor = actor({ state: {}, actions: {} });
|
||||
|
||||
const registry = setup({
|
||||
use: { myActor },
|
||||
envoy: {
|
||||
version: BUILD_VERSION,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Drain on Version Upgrade
|
||||
|
||||
The `drainOnVersionUpgrade` option controls whether old actors are stopped when a new version is deployed. This is configured in the Rivet dashboard under your runner configuration. See [Pool Configuration](/docs/general/pool-configuration) for the full set of pool options, including how to rate-limit actor eviction during the drain.
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `false` | Old actors continue running. New actors go to new version. Versions coexist. |
|
||||
| `true` (default) | Old actors receive stop signal and have 30m to finish gracefully. |
|
||||
|
||||
## Upgrading Actor State
|
||||
|
||||
When you deploy a new version, existing actors may need to handle schema changes in their persisted data.
|
||||
|
||||
### SQLite (recommended for complex schemas)
|
||||
|
||||
**Drizzle (recommended)**
|
||||
|
||||
Use [Drizzle](/docs/actors/sqlite-drizzle) for typed schemas with generated migrations. Drizzle generates versioned `.sql` migration files from your TypeScript schema and applies them in order automatically. This is the recommended approach when your schema evolves frequently.
|
||||
|
||||
**Raw SQL**
|
||||
|
||||
For actors using [raw SQLite](/docs/actors/sqlite), migrations run automatically via the `onMigrate` hook on every actor start. RivetKit wraps the hook in a SQLite savepoint, so the migration is fully atomic. Use SQLite's `user_version` pragma to track which migrations have run:
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
const todoList = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
const [{ user_version }] = (await db.execute(
|
||||
"PRAGMA user_version",
|
||||
)) as { user_version: number }[];
|
||||
|
||||
if (user_version < 1) {
|
||||
await db.execute(`
|
||||
CREATE TABLE todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
if (user_version < 2) {
|
||||
await db.execute(`
|
||||
ALTER TABLE todos ADD COLUMN completed INTEGER NOT NULL DEFAULT 0;
|
||||
`);
|
||||
}
|
||||
|
||||
await db.execute("PRAGMA user_version = 2");
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
addTodo: async (c, title: string) => {
|
||||
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { todoList } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### In-memory state (`c.state`)
|
||||
|
||||
If you use `c.state` for persistence, you are responsible for handling schema changes yourself. If you add, remove, or rename fields between versions, your code must handle the old shape gracefully.
|
||||
|
||||
**Manual defaults in `onWake`**
|
||||
|
||||
Apply defaults for missing fields:
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const myActor = actor({
|
||||
state: { count: 0, label: "" },
|
||||
onWake: (c) => {
|
||||
// Added in v2. Old actors won't have this field.
|
||||
c.state.label ??= "default";
|
||||
},
|
||||
actions: {
|
||||
getLabel: (c) => c.state.label,
|
||||
},
|
||||
});
|
||||
|
||||
const registry = setup({ use: { myActor } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
**Zod schema coercion**
|
||||
|
||||
Use [Zod](https://zod.dev/) to parse persisted state on wake. Zod's `.default()` fills in missing fields automatically, so old actor state is coerced to the current schema:
|
||||
|
||||
```ts
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { z } from "zod";
|
||||
|
||||
const stateSchema = z.object({
|
||||
count: z.number().default(0),
|
||||
label: z.string().default("default"), // Added in v2
|
||||
});
|
||||
|
||||
type State = z.infer<typeof stateSchema>;
|
||||
|
||||
const myActor = actor({
|
||||
state: { count: 0, label: "default" } as State,
|
||||
onWake: (c) => {
|
||||
Object.assign(c.state, stateSchema.parse(c.state));
|
||||
},
|
||||
actions: {
|
||||
getLabel: (c) => c.state.label,
|
||||
},
|
||||
});
|
||||
|
||||
const registry2 = setup({ use: { myActor } });
|
||||
registry2.start();
|
||||
```
|
||||
|
||||
For anything beyond simple defaults, consider moving to [SQLite](/docs/actors/sqlite) where you get proper migration tooling.
|
||||
|
||||
## Advanced
|
||||
|
||||
### How Version Upgrade Detection Works
|
||||
|
||||
When `drainOnVersionUpgrade` is enabled, Rivet uses two mechanisms to detect version changes:
|
||||
|
||||
- **New runner connection**: When a runner connects with a newer version number, the engine immediately drains all older runners with the same name. This is the primary mechanism for [runner mode](/docs/general/runtime-modes) deployments.
|
||||
- **Metadata polling** (serverless only): In [serverless mode](/docs/general/runtime-modes), runners periodically poll the engine to check for newer versions and self-drain if one is found. This ensures old runners drain even if no new requests trigger a runner connection.
|
||||
|
||||
### SIGTERM Handling
|
||||
|
||||
When a runner process receives SIGTERM, it gracefully stops all actors before exiting:
|
||||
|
||||
- Each actor's `onSleep` hook is called, giving it time to save state
|
||||
- Actors are rescheduled to other available runners
|
||||
- The runner waits up to **30 minutes** for all actors to finish stopping
|
||||
- If the process is force-killed before actors finish (e.g. SIGKILL), actors are rescheduled with a crash backoff penalty instead of a clean handoff
|
||||
|
||||
Actors have a maximum of 30 minutes to clean up during shutdown. Ensure your platform's drain grace period is at most 30 minutes.
|
||||
|
||||
### Shutdown Timeouts
|
||||
|
||||
Several timeouts control how long each part of the shutdown process can take:
|
||||
|
||||
| Timeout | Default | Description | Configuration |
|
||||
|---------|---------|-------------|---------------|
|
||||
| `actor_stop_threshold` | 30m | Engine-side limit on how long each actor has to stop before being marked lost | [Engine config](/docs/self-hosting/configuration) (`pegboard.actor_stop_threshold`) |
|
||||
| `sleepGracePeriod` | 15s | Total graceful sleep budget for `onSleep`, `waitUntil`, `keepAwake`, and async raw WebSocket handlers | [Actor options](/docs/actors/lifecycle#options) |
|
||||
| `runner_lost_threshold` | 15s | Fallback detection if the runner dies without graceful shutdown | [Engine config](/docs/self-hosting/configuration) (`pegboard.runner_lost_threshold`) |
|
||||
|
||||
Rivet has a max shutdown grace period of 30 minutes that cannot be configured.
|
||||
|
||||
## Related
|
||||
|
||||
- [Runtime Modes](/docs/general/runtime-modes): Serverless vs runner deployment modes
|
||||
- [Lifecycle](/docs/actors/lifecycle): Actor lifecycle hooks including `onSleep`
|
||||
|
||||
_Source doc path: /docs/actors/versions_
|
||||
@@ -0,0 +1,330 @@
|
||||
# Low-Level WebSocket Handler
|
||||
|
||||
> Source: `src/content/docs/actors/websocket-handler.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/websocket-handler
|
||||
> Description: Actors can handle WebSocket connections through the `onWebSocket` handler.
|
||||
|
||||
---
|
||||
For most use cases, [actions](/docs/actors/actions) and [events](/docs/actors/events) provide high-level connection handling powered by WebSockets that's easier to work with than low-level WebSockets. However, low-level handlers are required when implementing custom use cases.
|
||||
|
||||
## Handling WebSocket Connections
|
||||
|
||||
The `onWebSocket` handler manages low-level WebSocket connections. It receives the actor context and a `WebSocket` object.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
export const chatActor = actor({
|
||||
state: { messages: [] as string[] },
|
||||
onWebSocket: (c, websocket) => {
|
||||
websocket.addEventListener("open", () => {
|
||||
// Send existing messages to new connection
|
||||
websocket.send(JSON.stringify({
|
||||
type: "history",
|
||||
messages: c.state.messages,
|
||||
}));
|
||||
});
|
||||
|
||||
websocket.addEventListener("message", (event) => {
|
||||
// Store message
|
||||
c.state.messages.push(event.data as string);
|
||||
|
||||
// Echo message back
|
||||
websocket.send(event.data as string);
|
||||
|
||||
// Manually save state since WebSocket connections are long-lived
|
||||
c.saveState({ immediate: true });
|
||||
});
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
See also the [raw WebSocket handler example](https://github.com/rivet-dev/rivet/tree/main/examples/raw-websocket-handler).
|
||||
|
||||
## Connecting To Actors
|
||||
|
||||
### Via RivetKit Client
|
||||
|
||||
Use the `.webSocket()` method on an actor handle to open a WebSocket connection to the actor's `onWebSocket` handler. This can be executed from either your frontend or backend.
|
||||
|
||||
```typescript index.ts @hide @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const chat = actor({
|
||||
state: { messages: [] as string[] },
|
||||
onWebSocket: (c, websocket) => {
|
||||
websocket.addEventListener("message", (event) => {
|
||||
c.state.messages.push(event.data as string);
|
||||
});
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { chat } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```typescript client.ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const actor = client.chat.getOrCreate(["my-chat"]);
|
||||
|
||||
// Open WebSocket connection
|
||||
const ws = await actor.webSocket("/");
|
||||
|
||||
// Listen for messages
|
||||
ws.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data as string);
|
||||
console.log("Received:", message);
|
||||
});
|
||||
|
||||
// Send messages
|
||||
ws.send(JSON.stringify({ type: "chat", text: "Hello!" }));
|
||||
```
|
||||
|
||||
The `.webSocket()` method returns a standard WebSocket.
|
||||
|
||||
### Via getGatewayUrl
|
||||
|
||||
Use `.getGatewayUrl()` to get the raw gateway URL for the actor. This is useful when you need to use the URL with external tools or custom WebSocket clients.
|
||||
|
||||
```typescript index.ts @hide @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const chat = actor({
|
||||
state: { messages: [] as string[] },
|
||||
onWebSocket: (c, websocket) => {
|
||||
websocket.addEventListener("message", (event) => {
|
||||
c.state.messages.push(event.data as string);
|
||||
});
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { chat } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```typescript client.ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const actor = client.chat.getOrCreate(["my-chat"]);
|
||||
|
||||
// Get the raw gateway URL
|
||||
const gatewayUrl = await actor.getGatewayUrl();
|
||||
// gatewayUrl = "https://...rivet.dev/..."
|
||||
|
||||
// Convert to WebSocket URL and connect
|
||||
const wsUrl = gatewayUrl.replace("http://", "ws://").replace("https://", "wss://");
|
||||
const ws = new WebSocket(`${wsUrl}/websocket/`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data as string);
|
||||
console.log("Received:", message);
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(JSON.stringify({ type: "chat", text: "Hello!" }));
|
||||
});
|
||||
```
|
||||
|
||||
### Via HTTP API
|
||||
|
||||
This handler can be accessed with raw WebSockets using `wss://api.rivet.dev/gateway/{actorId}@{token}/websocket/{...path}`.
|
||||
|
||||
For example, to connect to the chat actor above:
|
||||
|
||||
```typescript
|
||||
// Replace with your actor ID and token
|
||||
const actorId = "your-actor-id";
|
||||
const token = "your-token";
|
||||
|
||||
const ws = new WebSocket(
|
||||
`wss://api.rivet.dev/gateway/${actorId}@${token}/websocket/`
|
||||
);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data as string);
|
||||
console.log("Received:", message);
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(JSON.stringify({ type: "chat", text: "Hello!" }));
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
wscat -c "wss://api.rivet.dev/gateway/{actorId}@{token}/websocket/"
|
||||
```
|
||||
|
||||
The path after `/websocket/` is passed to your `onWebSocket` handler and can be used to route to different functionality within your actor. For example, to connect with a custom path `/admin`:
|
||||
|
||||
```typescript
|
||||
// Replace with your actor ID and token
|
||||
const actorId = "your-actor-id";
|
||||
const token = "your-token";
|
||||
|
||||
const ws = new WebSocket(
|
||||
`wss://api.rivet.dev/gateway/${actorId}@${token}/websocket/admin`
|
||||
);
|
||||
```
|
||||
|
||||
```bash
|
||||
wscat -c "wss://api.rivet.dev/gateway/{actorId}@{token}/websocket/admin"
|
||||
```
|
||||
|
||||
See the [HTTP API reference](/docs/actors/http-api) for more information on WebSocket routing and authentication.
|
||||
|
||||
### Via Proxying Connections
|
||||
|
||||
You can proxy WebSocket connections from your own server to actor handlers using the RivetKit client. This is useful when you need to add custom authentication or connection management before forwarding to actors.
|
||||
|
||||
```typescript
|
||||
import { Hono } from "hono";
|
||||
import type { WSContext, WSMessageReceive } from "hono/ws";
|
||||
import { upgradeWebSocket } from "hono/bun";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const chatActor = actor({
|
||||
state: { messages: [] as string[] },
|
||||
onWebSocket: (c, websocket) => {
|
||||
websocket.addEventListener("message", (event) => {
|
||||
c.state.messages.push(event.data as string);
|
||||
websocket.send(event.data as string);
|
||||
});
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
const registry = setup({ use: { chat: chatActor } });
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Proxy WebSocket connections to actor's onWebSocket handler
|
||||
app.get("/ws/:id", upgradeWebSocket(async (c) => {
|
||||
const actorId = c.req.param("id");
|
||||
const actorHandle = client.chat.get([actorId]);
|
||||
const actorWs = await actorHandle.webSocket("/");
|
||||
|
||||
return {
|
||||
onOpen: (evt: Event, ws: WSContext) => {
|
||||
actorWs.addEventListener("message", (event: MessageEvent) => {
|
||||
ws.send(event.data);
|
||||
});
|
||||
actorWs.addEventListener("close", () => {
|
||||
ws.close();
|
||||
});
|
||||
},
|
||||
onMessage: (evt: MessageEvent<WSMessageReceive>, ws: WSContext) => {
|
||||
actorWs.send(evt.data as string);
|
||||
},
|
||||
onClose: () => {
|
||||
actorWs.close();
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
See also the [raw WebSocket handler with proxy example](https://github.com/rivet-dev/rivet/tree/main/examples/raw-websocket-handler-proxy).
|
||||
|
||||
## Connection & Lifecycle Hooks
|
||||
|
||||
`onWebSocket` will trigger the `onBeforeConnect`, `onConnect`, and `onDisconnect` hooks. Read more about [lifecycle hooks](/docs/actors/lifecycle).
|
||||
|
||||
Open WebSockets will be listed in `c.conns`. `conn.send` and `c.broadcast` have no effect on low-level WebSocket connections. Read more about [connections](/docs/actors/connections).
|
||||
|
||||
## WinterTC Compliance
|
||||
|
||||
The `onWebSocket` handler uses standard WebSocket APIs and will work with existing libraries expecting WinterTC-compliant WebSocket objects.
|
||||
|
||||
## Advanced
|
||||
|
||||
## WebSocket Hibernation
|
||||
|
||||
WebSocket hibernation allows actors to go to sleep while keeping WebSocket connections alive. Actors automatically wake up when a message is received or the connection closes.
|
||||
|
||||
Enable hibernation by setting `canHibernateWebSocket: true`. You can also pass a function `(request) => boolean` for conditional control.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
export const myActor = actor({
|
||||
state: {},
|
||||
options: {
|
||||
canHibernateWebSocket: true,
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
Since `open` only fires once when the client first connects, use `c.conn.state` to store per-connection data that persists across sleep cycles. See [connections](/docs/actors/connections) for more details.
|
||||
|
||||
### Accessing the Request
|
||||
|
||||
The underlying HTTP request is available via `c.request`. This is useful for accessing the path or query parameters.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const myActor = actor({
|
||||
state: {},
|
||||
onWebSocket: (c, websocket) => {
|
||||
if (c.request) {
|
||||
const url = new URL(c.request.url);
|
||||
console.log(url.pathname); // e.g., "/admin"
|
||||
console.log(url.searchParams.get("foo")); // e.g., "bar"
|
||||
}
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
### Skip Ready Wait
|
||||
|
||||
Connections are normally held at the gateway until the actor is ready. Pass `skipReadyWait: true` on `handle.webSocket()` to connect immediately, including while the actor is still starting or in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence). See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) for details.
|
||||
|
||||
### Async Handlers
|
||||
|
||||
The `onWebSocket` handler can be async, allowing you to perform async code before setting up event listeners:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const myActor = actor({
|
||||
state: {},
|
||||
onWebSocket: async (c, websocket) => {
|
||||
// Perform async operations before the connection is ready
|
||||
const metadata = await fetch("https://api.example.com/metadata").then(r => r.json());
|
||||
|
||||
websocket.addEventListener("open", () => {
|
||||
// Send metadata on connection
|
||||
websocket.send(JSON.stringify({ metadata }));
|
||||
});
|
||||
|
||||
websocket.addEventListener("message", (event) => {
|
||||
// Handle messages
|
||||
});
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`WebSocketContext`](/typedoc/interfaces/rivetkit.mod.WebSocketContext.html) - Context for WebSocket handlers
|
||||
- [`UniversalWebSocket`](/typedoc/interfaces/rivetkit.mod.UniversalWebSocket.html) - Universal WebSocket interface
|
||||
- [`handleRawWebSocketHandler`](/typedoc/functions/rivetkit.mod.handleRawWebSocketHandler.html) - Function to handle raw WebSocket
|
||||
- [`UpgradeWebSocketArgs`](/typedoc/interfaces/rivetkit.mod.UpgradeWebSocketArgs.html) - Arguments for WebSocket upgrade
|
||||
|
||||
_Source doc path: /docs/actors/websocket-handler_
|
||||
2013
.agents/skills/ai-agent-workspace/reference/actors/workflows.md
Normal file
2013
.agents/skills/ai-agent-workspace/reference/actors/workflows.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
# Agent-to-Agent Communication
|
||||
|
||||
> Source: `src/content/docs/agent-os/agent-to-agent.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/agent-to-agent
|
||||
> Description: Use host tools to let agents communicate with each other.
|
||||
|
||||
---
|
||||
Agents communicate through [host tools](/docs/agent-os/tools). You define a toolkit that lets one agent send work to another, and the agent calls it like any other CLI command.
|
||||
|
||||
## Example: code writer + reviewer
|
||||
|
||||
This example creates a writer agent with a `review` tool. When the writer calls the tool, it reads the file from the writer's VM, writes it to a separate reviewer VM, and sends a review prompt.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { toolKit, hostTool } from "@rivet-dev/agent-os-core";
|
||||
import { createClient } from "rivetkit/client";
|
||||
import { z } from "zod";
|
||||
|
||||
// Tool that bridges the writer to the reviewer
|
||||
const reviewToolkit = toolKit({
|
||||
name: "review",
|
||||
description: "Send code to the reviewer agent",
|
||||
tools: {
|
||||
submit: hostTool({
|
||||
description: "Submit a file for code review",
|
||||
inputSchema: z.object({
|
||||
path: z.string().describe("Path to the file to review"),
|
||||
}),
|
||||
execute: async (input) => {
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const writerHandle = client.writer.getOrCreate(["my-project"]);
|
||||
const reviewerHandle = client.reviewer.getOrCreate(["my-project"]);
|
||||
|
||||
// Read file from writer, write to reviewer
|
||||
const content = await writerHandle.readFile(input.path);
|
||||
await reviewerHandle.writeFile(input.path, content);
|
||||
|
||||
// Ask the reviewer to review
|
||||
const session = await reviewerHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
const response = await reviewerHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Review the code at ${input.path} and list any issues.`,
|
||||
);
|
||||
await reviewerHandle.closeSession(session.sessionId);
|
||||
|
||||
return { review: response };
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Writer has the review toolkit, reviewer is plain
|
||||
const writer = agentOs({
|
||||
options: { software: [common, pi], toolKits: [reviewToolkit] },
|
||||
});
|
||||
const reviewer = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { writer, reviewer } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const writerAgent = client.writer.getOrCreate(["my-project"]);
|
||||
|
||||
const session = await writerAgent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
// The writer will call `agentos-review submit --path /home/user/api.ts`
|
||||
// when it's ready for a review
|
||||
await writerAgent.sendPrompt(
|
||||
session.sessionId,
|
||||
"Write a REST API at /home/user/api.ts, then submit it for review.",
|
||||
);
|
||||
```
|
||||
|
||||
The writer agent sees the review tool as a CLI command:
|
||||
|
||||
```bash
|
||||
agentos-review submit --path /home/user/api.ts
|
||||
```
|
||||
|
||||
When the writer calls this, the host tool reads the file from the writer's VM, writes it to the reviewer's VM, and sends a prompt to the reviewer. The review result is returned to the writer as JSON.
|
||||
|
||||
## Why host tools?
|
||||
|
||||
Host tools are the natural communication layer between agents because:
|
||||
|
||||
- **The agent doesn't need to know about other agents.** It just calls a tool. You can swap the implementation without changing the agent's behavior.
|
||||
- **No credentials in the VM.** The host tool executes on the server, so it can access other agents directly without exposing connection details.
|
||||
- **Composable.** Chain any number of agents by adding more tools. Each tool is a self-contained bridge to another agent.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Each agent has its own isolated VM and filesystem. Use `readFile`/`writeFile` in host tools to pass files between them.
|
||||
- Use [Queues](/docs/agent-os/queues) when agents need to process work asynchronously.
|
||||
- Use [Workflows](/docs/agent-os/workflows) to make multi-agent pipelines durable across restarts.
|
||||
|
||||
_Source doc path: /docs/agent-os/agent-to-agent_
|
||||
@@ -0,0 +1,80 @@
|
||||
# Pi
|
||||
|
||||
> Source: `src/content/docs/agent-os/agents/pi.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/agents/pi
|
||||
> Description: Run the Pi coding agent inside a VM with extensions and custom configuration.
|
||||
|
||||
---
|
||||
## Quick start
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
const { text } = await agent.sendPrompt(
|
||||
session.sessionId,
|
||||
"What files are in the current directory?",
|
||||
);
|
||||
console.log(text);
|
||||
```
|
||||
|
||||
Read [Sessions](/docs/agent-os/sessions) first for session options, streaming events, prompts, and lifecycle management.
|
||||
|
||||
## Extensions
|
||||
|
||||
Pi supports [extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) that let you register custom tools, modify the system prompt, and hook into agent lifecycle events. Write a `.js` file into the VM's extensions directory before creating a session and Pi discovers it automatically.
|
||||
|
||||
Pi scans two directories for `.js` extension files:
|
||||
|
||||
| Directory | Scope |
|
||||
|-----------|-------|
|
||||
| `~/.pi/agent/extensions/` | Global — applies to all Pi sessions |
|
||||
| `<cwd>/.pi/extensions/` | Project — applies only when cwd matches |
|
||||
|
||||
```ts @nocheck
|
||||
const extensionCode = `
|
||||
module.exports = function(pi) {
|
||||
// Modify the system prompt before each agent turn
|
||||
pi.on("before_agent_start", async (event) => {
|
||||
return {
|
||||
systemPrompt: event.systemPrompt +
|
||||
"\\n\\nAlways respond in formal English."
|
||||
};
|
||||
});
|
||||
};
|
||||
`;
|
||||
|
||||
// Write the extension before creating the session
|
||||
await vm.mkdir("/home/user/.pi/agent/extensions", { recursive: true });
|
||||
await vm.writeFile("/home/user/.pi/agent/extensions/formal.js", extensionCode);
|
||||
|
||||
// Pi discovers the extension automatically
|
||||
const { sessionId } = await vm.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
|
||||
});
|
||||
```
|
||||
|
||||
See the [Pi extension documentation](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) for the full extension API.
|
||||
|
||||
_Source doc path: /docs/agent-os/agents/pi_
|
||||
@@ -0,0 +1,81 @@
|
||||
# Authentication
|
||||
|
||||
> Source: `src/content/docs/agent-os/authentication.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/authentication
|
||||
> Description: Authenticate connections to agentOS actors using hooks.
|
||||
|
||||
---
|
||||
agentOS uses the same authentication system as Rivet Actors. Validate credentials in `onBeforeConnect` or extract user data with `createConnState`.
|
||||
|
||||
For full documentation including JWT examples, role-based access control, rate limiting, and token caching, see [Actor Authentication](/docs/actors/authentication).
|
||||
|
||||
## `onBeforeConnect`
|
||||
|
||||
Validate credentials before allowing a connection. Throw an error to reject.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup, UserError } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
onBeforeConnect: async (c, params: { authToken: string }) => {
|
||||
const isValid = await validateToken(params.authToken);
|
||||
if (!isValid) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## `createConnState`
|
||||
|
||||
Extract user data from credentials and store it in connection state. Accessible in actions via `c.conn.state`.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup, UserError } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const vm = agentOs({
|
||||
createConnState: async (c, params: { authToken: string }): Promise<ConnState> => {
|
||||
const payload = await validateToken(params.authToken);
|
||||
if (!payload) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
return { userId: payload.sub, role: payload.role };
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Client usage
|
||||
|
||||
Pass credentials when connecting:
|
||||
|
||||
```ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"], {
|
||||
params: { authToken: "my-jwt-token" },
|
||||
});
|
||||
```
|
||||
|
||||
See [Actor Authentication](/docs/actors/authentication) for more patterns including external auth providers, role-based access control, and token caching.
|
||||
|
||||
_Source doc path: /docs/agent-os/authentication_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Benchmarks
|
||||
|
||||
> Source: `src/content/docs/agent-os/benchmarks.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/benchmarks
|
||||
> Description: Performance benchmarks comparing agentOS to traditional sandbox providers.
|
||||
|
||||
---
|
||||
These are the benchmark figures shown on the [agentOS page](/agent-os). All numbers are computed from the same data source used by the marketing page. For independent sandbox comparison data, see the [ComputeSDK benchmarks](https://www.computesdk.com/benchmarks/).
|
||||
|
||||
_Source doc path: /docs/agent-os/benchmarks_
|
||||
@@ -0,0 +1,68 @@
|
||||
# Configuration
|
||||
|
||||
> Source: `src/content/docs/agent-os/configuration.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/configuration
|
||||
> Description: Configure the agentOS VM options, preview settings, and lifecycle hooks.
|
||||
|
||||
---
|
||||
`agentOs()` accepts the following configuration object.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
// Custom filesystem mounts
|
||||
mounts: [],
|
||||
// Software packages to install in the VM (see /docs/agent-os/software)
|
||||
software: [common, pi],
|
||||
// Ports exempt from SSRF checks
|
||||
loopbackExemptPorts: [3000],
|
||||
// Host directory with node_modules
|
||||
moduleAccessCwd: "/path/to/project",
|
||||
// Extra instructions appended to agent system prompts
|
||||
additionalInstructions: "Always write tests first.",
|
||||
},
|
||||
|
||||
// Preview URL token lifetimes
|
||||
preview: {
|
||||
defaultExpiresInSeconds: 3600, // 1 hour (default)
|
||||
maxExpiresInSeconds: 86400, // 24 hours (default)
|
||||
},
|
||||
|
||||
// Called when a client connects. Throw to reject. See /docs/agent-os/authentication
|
||||
onBeforeConnect: async (c, params) => {
|
||||
const user = await verifyToken(params.token);
|
||||
if (!user) throw new Error("Unauthorized");
|
||||
},
|
||||
// Called for every session event, server-side. Runs once per event.
|
||||
onSessionEvent: async (c, sessionId, event) => {
|
||||
console.log("Session event:", sessionId, event.method);
|
||||
},
|
||||
// Called when an agent requests permission. See /docs/agent-os/permissions
|
||||
onPermissionRequest: async (c, sessionId, request) => {
|
||||
await c.respondPermission(sessionId, request.permissionId, "always");
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Session options
|
||||
|
||||
Options passed to `createSession`. See [Sessions](/docs/agent-os/sessions) for full documentation.
|
||||
|
||||
## Timeouts
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Action timeout | 15 minutes | Maximum time for any single action |
|
||||
| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |
|
||||
|
||||
These are set internally by the `agentOs()` factory and cannot be overridden per-call. See [Persistence & Sleep](/docs/agent-os/persistence) for details on the sleep lifecycle.
|
||||
|
||||
_Source doc path: /docs/agent-os/configuration_
|
||||
270
.agents/skills/ai-agent-workspace/reference/agent-os/core.md
Normal file
270
.agents/skills/ai-agent-workspace/reference/agent-os/core.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Core Package
|
||||
|
||||
> Source: `src/content/docs/agent-os/core.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/core
|
||||
> Description: Use @rivet-dev/agent-os-core standalone for direct VM control without the Rivet Actor runtime.
|
||||
|
||||
---
|
||||
## agentOS vs agentOS Core
|
||||
|
||||
The `agentOs()` actor (from `rivetkit/agent-os`) wraps the core package and adds:
|
||||
|
||||
| | Core (`@rivet-dev/agent-os-core`) | Actor (`rivetkit/agent-os`) |
|
||||
|-|---|---|
|
||||
| Persistence | In-memory by default (pluggable via [mounts](#mounts)) | Persistent filesystem and sessions |
|
||||
| Distributed state | Manage yourself | Built-in distributed statefulness |
|
||||
| Stateful sandboxes | Complex to run yourself | Built into Rivet |
|
||||
| Sleep/wake | Manual `dispose()` / `create()` | Automatic |
|
||||
| Events | Direct callbacks | Broadcasted to all connected clients |
|
||||
| Preview URLs | None | Built-in signed URL server |
|
||||
| Multiplayer | N/A | Multiple clients on same actor |
|
||||
| Orchestration | N/A | Workflows, queues, cron |
|
||||
| Agent-to-agent communication | Custom | Built into [Rivet Actors](/docs/agent-os/agent-to-agent) |
|
||||
| Authentication | Set up yourself | [Documentation](/docs/agent-os/authentication) |
|
||||
|
||||
We recommend using [Rivet Actors](/docs/actors) because they provide a portable way to run agentOS on any infrastructure with built-in persistence, networking, and orchestration. Use the core package if you need the most bare-bones implementation possible.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @rivet-dev/agent-os-core
|
||||
```
|
||||
|
||||
## Boot a VM
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({
|
||||
software: [common],
|
||||
});
|
||||
|
||||
// Run a command
|
||||
const result = await vm.exec("echo hello");
|
||||
console.log(result.stdout); // "hello\n"
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Filesystem
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({ software: [common] });
|
||||
|
||||
await vm.writeFile("/home/user/hello.txt", "Hello, world!");
|
||||
const content = await vm.readFile("/home/user/hello.txt");
|
||||
console.log(new TextDecoder().decode(content));
|
||||
|
||||
await vm.mkdir("/home/user/src");
|
||||
await vm.writeFiles([
|
||||
{ path: "/home/user/src/index.ts", content: "console.log('hi');" },
|
||||
{ path: "/home/user/src/utils.ts", content: "export const add = (a: number, b: number) => a + b;" },
|
||||
]);
|
||||
|
||||
const entries = await vm.readdirRecursive("/home/user");
|
||||
for (const entry of entries) {
|
||||
console.log(entry.type, entry.path);
|
||||
}
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Processes
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({ software: [common] });
|
||||
|
||||
// One-shot execution
|
||||
const result = await vm.exec("ls -la /home/user");
|
||||
console.log(result.stdout);
|
||||
|
||||
// Long-running process with streaming output
|
||||
await vm.writeFile("/tmp/server.mjs", 'import http from "http"; http.createServer((req, res) => res.end("ok")).listen(3000); console.log("listening");');
|
||||
const proc = vm.spawn("node", ["/tmp/server.mjs"]);
|
||||
vm.onProcessStdout(proc.pid, (data) => {
|
||||
console.log("stdout:", new TextDecoder().decode(data));
|
||||
});
|
||||
vm.onProcessExit(proc.pid, (code) => {
|
||||
console.log("exited:", code);
|
||||
});
|
||||
|
||||
// Write to stdin
|
||||
vm.writeProcessStdin(proc.pid, "some input\n");
|
||||
|
||||
// Stop or kill
|
||||
vm.stopProcess(proc.pid);
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Agent sessions
|
||||
|
||||
The core package returns a `sessionId` string. All session operations are called on the `vm` instance with the session ID.
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({ software: [common] });
|
||||
|
||||
const { sessionId } = await vm.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
// Stream events
|
||||
vm.onSessionEvent(sessionId, (event) => {
|
||||
console.log(event.method, event.params);
|
||||
});
|
||||
|
||||
// Handle permissions
|
||||
vm.onPermissionRequest(sessionId, (request) => {
|
||||
console.log("Permission:", request.description);
|
||||
vm.respondPermission(sessionId, request.permissionId, "once");
|
||||
});
|
||||
|
||||
// Send a prompt
|
||||
const response = await vm.prompt(sessionId, "Write a hello world script");
|
||||
console.log(response);
|
||||
|
||||
// Configure the session
|
||||
await vm.setSessionModel(sessionId, "claude-sonnet-4-6");
|
||||
await vm.setSessionMode(sessionId, "plan");
|
||||
|
||||
// Event history (returns SequencedEvent[] with .sequenceNumber and .notification)
|
||||
const events = vm.getSessionEvents(sessionId);
|
||||
for (const event of events) {
|
||||
console.log(event.sequenceNumber, event.notification.method);
|
||||
}
|
||||
|
||||
vm.closeSession(sessionId);
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Interactive shell
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({ software: [common] });
|
||||
|
||||
const { shellId } = vm.openShell();
|
||||
|
||||
vm.onShellData(shellId, (data) => {
|
||||
process.stdout.write(new TextDecoder().decode(data));
|
||||
});
|
||||
|
||||
vm.writeShell(shellId, "echo hello from shell\n");
|
||||
|
||||
// Resize terminal
|
||||
vm.resizeShell(shellId, 120, 40);
|
||||
|
||||
vm.closeShell(shellId);
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Networking
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({ software: [common] });
|
||||
|
||||
// Start a server inside the VM
|
||||
await vm.writeFile("/tmp/app.mjs", 'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);');
|
||||
vm.spawn("node", ["/tmp/app.mjs"]);
|
||||
|
||||
// Fetch from it
|
||||
const response = await vm.fetch(3000, new Request("http://localhost/"));
|
||||
console.log(await response.text());
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Cron jobs
|
||||
|
||||
The core package supports a `"callback"` action type in addition to `"exec"` and `"session"`.
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({ software: [common] });
|
||||
|
||||
const job = vm.scheduleCron({
|
||||
id: "cleanup",
|
||||
schedule: "0 * * * *",
|
||||
action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache"] },
|
||||
});
|
||||
|
||||
// Or use a callback (not available in the actor wrapper)
|
||||
vm.scheduleCron({
|
||||
schedule: "*/5 * * * *",
|
||||
action: {
|
||||
type: "callback",
|
||||
fn: async () => {
|
||||
console.log("Custom logic every 5 minutes");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vm.onCronEvent((event) => {
|
||||
if (event.type === "cron:fire") console.log("Job fired:", event.jobId);
|
||||
if (event.type === "cron:complete") console.log("Job done:", event.jobId, event.durationMs, "ms");
|
||||
if (event.type === "cron:error") console.error("Job error:", event.error);
|
||||
});
|
||||
|
||||
console.log(vm.listCronJobs());
|
||||
job.cancel();
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## Mounts
|
||||
|
||||
Configure filesystem backends at boot time.
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs, createHostDirBackend, createInMemoryFileSystem } from "@rivet-dev/agent-os-core";
|
||||
import { createS3Backend } from "@rivet-dev/agent-os-s3";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({
|
||||
software: [common],
|
||||
mounts: [
|
||||
// Host directory (read-only)
|
||||
{ path: "/mnt/code", driver: createHostDirBackend({ hostPath: "/path/to/repo" }), readOnly: true },
|
||||
// S3 bucket
|
||||
{ path: "/mnt/data", driver: createS3Backend({ bucket: "my-bucket", prefix: "agent/" }) },
|
||||
// In-memory scratch space
|
||||
{ path: "/mnt/scratch", driver: createInMemoryFileSystem() },
|
||||
],
|
||||
});
|
||||
|
||||
const files = await vm.readdir("/mnt/code");
|
||||
console.log(files);
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
## What you give up without the actor
|
||||
|
||||
- **No built-in persistence.** The default filesystem is in-memory and lost on `dispose()`. You can configure your own [mounts](#mounts) (S3, host directories, etc.) for persistence.
|
||||
- **No sleep/wake.** You manage the full VM lifecycle yourself.
|
||||
- **No event broadcasting.** Events are local callbacks, not distributed to remote clients.
|
||||
- **No preview URLs.** No built-in HTTP server for sharing VM services.
|
||||
- **No multiplayer.** Single-process, single-client only.
|
||||
- **No orchestration.** No workflows, queues, or scheduling integration.
|
||||
- **No session persistence.** Session history is lost on dispose.
|
||||
|
||||
If you need any of these, use the [`agentOs()` actor](/docs/agent-os/quickstart) instead.
|
||||
|
||||
_Source doc path: /docs/agent-os/core_
|
||||
@@ -0,0 +1,569 @@
|
||||
# Crash Course
|
||||
|
||||
> Source: `src/content/docs/agent-os/crash-course.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/crash-course
|
||||
> Description: Run coding agents inside isolated VMs with full filesystem, process, and network control.
|
||||
|
||||
---
|
||||
agentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).
|
||||
|
||||
## Features
|
||||
|
||||
- **Isolated VMs**: Each agent gets its own filesystem, processes, and networking. No shared state, no cross-contamination.
|
||||
- **Multi-Agent Support**: Run Amp, Claude Code, Codex, OpenCode, and PI with a unified API. Swap agents without changing your code.
|
||||
- **Host Tools**: Expose your JavaScript functions to agents as CLI commands. Direct binding with near-zero latency and automatic code mode for up to 80% token reduction.
|
||||
- **Persistent State**: Filesystem and transcripts survive sleep/wake cycles automatically. No external database needed.
|
||||
- **Orchestration**: Workflows, queues, cron jobs, and multi-agent coordination built on Rivet Actors.
|
||||
- **Hybrid Sandboxes**: Run agents in the lightweight VM by default. Spin up a full sandbox on demand for browsers, compilation, and desktop automation.
|
||||
|
||||
## When to Use agentOS
|
||||
|
||||
- **Coding agents**: Run any coding agent with full OS access, file editing, shell execution, and tool use.
|
||||
- **Automated pipelines**: CI-like workflows where agents clone repos, fix bugs, run tests, and open PRs.
|
||||
- **Multi-agent systems**: Coordinators dispatching to specialized agents, review pipelines, planning chains.
|
||||
- **Scheduled maintenance**: Cron-based agents that audit code, update dependencies, or generate reports.
|
||||
- **Collaborative workspaces**: Multiple users observing and interacting with the same agent session in realtime.
|
||||
|
||||
## Minimal Project
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Subscribe to streaming events
|
||||
agent.on("sessionEvent", (data) => {
|
||||
console.log(data.event);
|
||||
});
|
||||
|
||||
// Create a session and send a prompt
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
const response = await agent.sendPrompt(
|
||||
session.sessionId,
|
||||
"Write a hello world script to /home/user/hello.js",
|
||||
);
|
||||
console.log(response);
|
||||
|
||||
// Read the file the agent created
|
||||
const content = await agent.readFile("/home/user/hello.js");
|
||||
console.log(new TextDecoder().decode(content));
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
After the quickstart, customize your agent with the [Registry](/agent-os/registry).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Sessions & Transcripts
|
||||
|
||||
Create agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Stream events as they arrive
|
||||
agent.on("sessionEvent", (data) => {
|
||||
console.log(data.event.method, data.event);
|
||||
});
|
||||
|
||||
// Create a session with MCP servers
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
mcpServers: [
|
||||
{
|
||||
type: "local",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"],
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Send a prompt and wait for the response
|
||||
const response = await agent.sendPrompt(
|
||||
session.sessionId,
|
||||
"List all files in the home directory",
|
||||
);
|
||||
console.log(response);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/sessions)
|
||||
|
||||
### Permissions
|
||||
|
||||
Approve or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
// Auto-approve all permissions server-side
|
||||
const vm = agentOs({
|
||||
onPermissionRequest: async (c, sessionId, request) => {
|
||||
await c.respondPermission(sessionId, request.permissionId, "always");
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Or handle permissions client-side for human-in-the-loop
|
||||
agent.on("permissionRequest", async (data) => {
|
||||
console.log("Permission requested:", data.request);
|
||||
// "once" | "always" | "reject"
|
||||
await agent.respondPermission(data.sessionId, data.request.permissionId, "once");
|
||||
});
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/permissions)
|
||||
|
||||
### Tools
|
||||
|
||||
Expose your JavaScript functions to agents as CLI commands inside the VM. Agents call them as shell commands with auto-generated flags from Zod schemas.
|
||||
|
||||
```ts @nocheck
|
||||
import { toolKit, hostTool } from "@rivet-dev/agent-os-core";
|
||||
import { z } from "zod";
|
||||
|
||||
const myTools = toolKit({
|
||||
name: "myapp",
|
||||
description: "Application tools",
|
||||
tools: {
|
||||
createTicket: hostTool({
|
||||
description: "Create a ticket in the issue tracker",
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe("Ticket title"),
|
||||
priority: z.enum(["low", "medium", "high"]).describe("Priority level"),
|
||||
}),
|
||||
execute: async (input) => {
|
||||
const ticket = await db.tickets.create(input);
|
||||
return { id: ticket.id, url: ticket.url };
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Agent calls: agentos-myapp createTicket --title "Fix login" --priority high
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/tools)
|
||||
|
||||
### Filesystem
|
||||
|
||||
Read, write, and manage files inside the VM. The `/home/user` directory is persisted automatically across sleep/wake cycles.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Write a file
|
||||
await agent.writeFile("/home/user/config.json", JSON.stringify({ key: "value" }));
|
||||
|
||||
// Read a file
|
||||
const content = await agent.readFile("/home/user/config.json");
|
||||
console.log(new TextDecoder().decode(content));
|
||||
|
||||
// List directory contents recursively
|
||||
const files = await agent.readdirRecursive("/home/user", { maxDepth: 2 });
|
||||
console.log(files);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/filesystem)
|
||||
|
||||
### Processes & Shell
|
||||
|
||||
Execute commands, spawn long-running processes, and open interactive shells.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// One-shot execution
|
||||
const result = await agent.exec("echo hello && ls /home/user");
|
||||
console.log("stdout:", result.stdout);
|
||||
console.log("exit code:", result.exitCode);
|
||||
|
||||
// Spawn a long-running process
|
||||
agent.on("processOutput", (data) => {
|
||||
console.log(`[${data.processId}]`, data.output);
|
||||
});
|
||||
|
||||
const proc = await agent.spawn("node", ["server.js"]);
|
||||
console.log("Process ID:", proc.processId);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/processes)
|
||||
|
||||
### Networking & Previews
|
||||
|
||||
Proxy HTTP requests into VMs with `vmFetch`. Create preview URLs for port forwarding VM services to shareable public URLs.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Fetch from a service running inside the VM
|
||||
const response = await agent.vmFetch(3000, "/api/health");
|
||||
console.log("Status:", response.status);
|
||||
|
||||
// Create a preview URL (port forwarding to a public URL)
|
||||
const preview = await agent.createSignedPreviewUrl(3000);
|
||||
console.log("Public URL:", preview.path);
|
||||
console.log("Expires at:", new Date(preview.expiresAt));
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/networking)
|
||||
|
||||
### Cron Jobs
|
||||
|
||||
Schedule recurring commands and agent sessions with cron expressions.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Schedule a command every hour
|
||||
await agent.scheduleCron({
|
||||
schedule: "0 * * * *",
|
||||
action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache/*"] },
|
||||
});
|
||||
|
||||
// Schedule an agent session daily at 9 AM
|
||||
await agent.scheduleCron({
|
||||
schedule: "0 9 * * *",
|
||||
action: {
|
||||
type: "session",
|
||||
agent: "pi",
|
||||
prompt: "Review the codebase for security issues and write a report to /home/user/audit.md",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/cron)
|
||||
|
||||
### Sandbox Mounting
|
||||
|
||||
agentOS uses a hybrid model: agents run in a lightweight VM by default and spin up a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi],
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/sandbox)
|
||||
|
||||
### Multiplayer & Realtime
|
||||
|
||||
Connect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
// Client A: creates the session and sends prompts
|
||||
const clientA = createClient<typeof registry>("http://localhost:6420");
|
||||
const agentA = clientA.vm.getOrCreate(["shared-agent"]);
|
||||
agentA.on("sessionEvent", (data) => console.log("[A]", data.event.method));
|
||||
|
||||
const session = await agentA.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentA.sendPrompt(session.sessionId, "Build a REST API");
|
||||
|
||||
// Client B: observes the same session (separate process)
|
||||
const clientB = createClient<typeof registry>("http://localhost:6420");
|
||||
const agentB = clientB.vm.getOrCreate(["shared-agent"]);
|
||||
agentB.on("sessionEvent", (data) => console.log("[B]", data.event.method));
|
||||
// Client B sees the same events as Client A
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/multiplayer)
|
||||
|
||||
### Agent-to-Agent
|
||||
|
||||
Compose specialized agents into pipelines. Each agent gets its own isolated VM and filesystem.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const coder = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
const reviewer = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { coder, reviewer } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Coder writes the feature
|
||||
const coderAgent = client.coder.getOrCreate(["feature-auth"]);
|
||||
const coderSession = await coderAgent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await coderAgent.sendPrompt(coderSession.sessionId, "Implement the login feature");
|
||||
|
||||
// Pass files to the reviewer
|
||||
const src = await coderAgent.readFile("/home/user/src/auth.ts");
|
||||
const reviewerAgent = client.reviewer.getOrCreate(["feature-auth"]);
|
||||
await reviewerAgent.writeFile("/home/user/src/auth.ts", src);
|
||||
|
||||
// Reviewer checks the code
|
||||
const reviewSession = await reviewerAgent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await reviewerAgent.sendPrompt(
|
||||
reviewSession.sessionId,
|
||||
"Review auth.ts for security issues",
|
||||
);
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/agent-to-agent)
|
||||
|
||||
### Workflows
|
||||
|
||||
Orchestrate multi-step agent tasks with durable workflows that survive crashes and restarts.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, setup, workflow } from "rivetkit";
|
||||
|
||||
const automator = actor({
|
||||
workflows: {
|
||||
fixBug: workflow<{ repo: string; issue: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.workflow.iter("fixBug")) {
|
||||
const { repo, issue } = message.body;
|
||||
const agentHandle = c.actors.vm.getOrCreate([`fix-${issue}`]);
|
||||
|
||||
await c.step("clone-repo", async () => {
|
||||
return agentHandle.exec(`git clone ${repo} /home/user/repo`);
|
||||
});
|
||||
|
||||
await c.step("fix-bug", async () => {
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
const response = await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Fix the bug described in issue: ${issue}`,
|
||||
);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
return response;
|
||||
});
|
||||
|
||||
await c.step("run-tests", async () => {
|
||||
return agentHandle.exec("cd /home/user/repo && npm test");
|
||||
});
|
||||
|
||||
await message.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { automator, vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/workflows)
|
||||
|
||||
### SQLite
|
||||
|
||||
Use actor-local SQLite as structured long-term memory that persists across sessions and sleep/wake cycles.
|
||||
|
||||
```ts @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
const memoryAgent = actor({
|
||||
db: db({
|
||||
onMigrate: async (db) => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
store: async (c, sessionId: string, category: string, content: string) => {
|
||||
await c.db.execute(
|
||||
"INSERT INTO memories (session_id, category, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
sessionId, category, content, Date.now(),
|
||||
);
|
||||
},
|
||||
search: async (c, query: string) => {
|
||||
return c.db.execute(
|
||||
"SELECT category, content FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT 20",
|
||||
`%${query}%`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
[Documentation](/docs/agent-os/sqlite)
|
||||
|
||||
_Source doc path: /docs/agent-os/crash-course_
|
||||
223
.agents/skills/ai-agent-workspace/reference/agent-os/cron.md
Normal file
223
.agents/skills/ai-agent-workspace/reference/agent-os/cron.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Cron Jobs
|
||||
|
||||
> Source: `src/content/docs/agent-os/cron.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/cron
|
||||
> Description: Schedule recurring commands and agent sessions in agentOS VMs.
|
||||
|
||||
---
|
||||
- **Cron expressions** for flexible scheduling (e.g. `"0 9 * * *"` for 9 AM daily)
|
||||
- **Two action types**: `exec` for commands, `session` for agent sessions
|
||||
- **Overlap modes**: `allow`, `skip`, or `queue` concurrent executions
|
||||
- **Event streaming** via `cronEvent` for monitoring job execution
|
||||
|
||||
## Schedule a command
|
||||
|
||||
Run a shell command on a recurring schedule.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Schedule a cleanup script every hour
|
||||
const { id } = await agent.scheduleCron({
|
||||
schedule: "0 * * * *",
|
||||
action: {
|
||||
type: "exec",
|
||||
command: "rm",
|
||||
args: ["-rf", "/tmp/cache/*"],
|
||||
},
|
||||
});
|
||||
console.log("Cron job ID:", id);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Schedule an agent session
|
||||
|
||||
Create a recurring agent session that runs a prompt on a schedule.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Run an agent every day at 9 AM to check for issues
|
||||
await agent.scheduleCron({
|
||||
schedule: "0 9 * * *",
|
||||
action: {
|
||||
type: "session",
|
||||
agentType: "pi",
|
||||
prompt: "Review the logs in /home/user/logs/ and summarize any errors",
|
||||
options: { cwd: "/home/user" },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Overlap modes
|
||||
|
||||
Control what happens when a cron job triggers while a previous execution is still running.
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `"skip"` | Skip this trigger if the previous run is still active |
|
||||
| `"allow"` | Allow concurrent executions (default) |
|
||||
| `"queue"` | Queue this trigger and run it after the previous one finishes |
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Queue overlapping executions
|
||||
await agent.scheduleCron({
|
||||
schedule: "*/5 * * * *",
|
||||
overlap: "queue",
|
||||
action: {
|
||||
type: "session",
|
||||
agentType: "pi",
|
||||
prompt: "Process the next batch of tasks",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Monitor cron events
|
||||
|
||||
Subscribe to `cronEvent` to track job execution.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
agent.on("cronEvent", (data) => {
|
||||
console.log("Cron event:", data.event);
|
||||
});
|
||||
|
||||
await agent.scheduleCron({
|
||||
schedule: "*/1 * * * *",
|
||||
action: { type: "exec", command: "echo", args: ["heartbeat"] },
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## List and cancel cron jobs
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// List all cron jobs
|
||||
const jobs = await agent.listCronJobs();
|
||||
for (const job of jobs) {
|
||||
console.log(job.id, job.schedule);
|
||||
}
|
||||
|
||||
// Cancel a specific job
|
||||
await agent.cancelCronJob(jobs[0].id);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Example: Heartbeat pattern
|
||||
|
||||
Schedule a recurring agent session to periodically check on a task. This is the core pattern behind [OpenClaw](https://openclaw.org), where an agent wakes up on a schedule to review progress, take action, and go back to sleep.
|
||||
|
||||
```ts @nocheck
|
||||
await agent.scheduleCron({
|
||||
schedule: "*/30 * * * *",
|
||||
overlap: "skip",
|
||||
action: {
|
||||
type: "session",
|
||||
agentType: "pi",
|
||||
prompt: "Check the status of open issues and take any necessary action",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The agent sleeps between executions and only consumes resources when the cron job fires.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use `"skip"` overlap mode for most jobs. This prevents unbounded concurrency if a job takes longer than the interval. The default is `"allow"`.
|
||||
- Use `"queue"` when every trigger must execute, even if they back up.
|
||||
- Cron jobs keep the actor alive while executing. The actor can sleep between executions.
|
||||
- Provide a custom `id` when scheduling to make it easier to manage and cancel jobs later.
|
||||
|
||||
_Source doc path: /docs/agent-os/cron_
|
||||
@@ -0,0 +1,18 @@
|
||||
# Deployment
|
||||
|
||||
> Source: `src/content/docs/agent-os/deployment.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/deployment
|
||||
> Description: Choose the right deployment option for agentOS.
|
||||
|
||||
---
|
||||
agentOS supports multiple deployment models depending on your needs.
|
||||
|
||||
| Option | Description | Best for |
|
||||
|--------|-------------|----------|
|
||||
| **Local Dev** | Run locally with `npx rivetkit dev`. No infrastructure needed. | Development and testing |
|
||||
| **[Rivet Cloud](/cloud)** | Fully managed hosting on Rivet Compute or bring your own cloud (BYOC). | Teams that want zero-ops deployment |
|
||||
| **[Rivet Self-Hosted](/docs/self-hosting)** | Run the full Rivet platform on your own infrastructure. | Organizations that need full control |
|
||||
| **[Rivet Enterprise](/sales)** | Self-hosted with dedicated support, custom SLAs, and compliance reviews. | Regulated industries and large-scale deployments |
|
||||
| **agentOS Core** | Use `@rivet-dev/agent-os-core` directly without the Rivet platform. Embed the runtime in any Node.js backend. | Custom integrations and existing infrastructure |
|
||||
|
||||
_Source doc path: /docs/agent-os/deployment_
|
||||
195
.agents/skills/ai-agent-workspace/reference/agent-os/events.md
Normal file
195
.agents/skills/ai-agent-workspace/reference/agent-os/events.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Events
|
||||
|
||||
> Source: `src/content/docs/agent-os/events.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/events
|
||||
> Description: Full event catalog with payload shapes for agentOS.
|
||||
|
||||
---
|
||||
## Event types
|
||||
|
||||
### sessionEvent
|
||||
|
||||
Emitted for every agent session event (streaming output, errors, status changes).
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("sessionEvent", (data) => {
|
||||
// data.sessionId: string
|
||||
// data.event: JsonRpcNotification (method, params)
|
||||
console.log(data.sessionId, data.event.method, data.event.params);
|
||||
});
|
||||
```
|
||||
|
||||
Events are also persisted to SQLite for replay via `getSessionEvents`.
|
||||
|
||||
### permissionRequest
|
||||
|
||||
Emitted when an agent requests permission to use a tool.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("permissionRequest", async (data) => {
|
||||
// data.sessionId: string
|
||||
// data.request: PermissionRequest (id, toolName, etc.)
|
||||
console.log("Permission requested:", data.request);
|
||||
|
||||
await agent.respondPermission(data.sessionId, data.request.permissionId, "once");
|
||||
});
|
||||
```
|
||||
|
||||
See [Permissions](/docs/agent-os/permissions) for approval patterns.
|
||||
|
||||
### processOutput
|
||||
|
||||
Emitted when a spawned process writes to stdout or stderr.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("processOutput", (data) => {
|
||||
// data.pid: number
|
||||
// data.stream: "stdout" | "stderr"
|
||||
// data.data: Uint8Array
|
||||
const text = new TextDecoder().decode(data.data);
|
||||
console.log(`[${data.pid}] ${data.stream}: ${text}`);
|
||||
});
|
||||
```
|
||||
|
||||
### processExit
|
||||
|
||||
Emitted when a spawned process exits.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("processExit", (data) => {
|
||||
// data.pid: number
|
||||
// data.exitCode: number
|
||||
console.log(`Process ${data.pid} exited with code ${data.exitCode}`);
|
||||
});
|
||||
```
|
||||
|
||||
### shellData
|
||||
|
||||
Emitted when an interactive shell produces output.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("shellData", (data) => {
|
||||
// data.shellId: string
|
||||
// data.data: Uint8Array
|
||||
const text = new TextDecoder().decode(data.data);
|
||||
process.stdout.write(text);
|
||||
});
|
||||
```
|
||||
|
||||
### cronEvent
|
||||
|
||||
Emitted when a cron job runs.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("cronEvent", (data) => {
|
||||
// data.event: CronEvent
|
||||
console.log("Cron event:", data.event);
|
||||
});
|
||||
```
|
||||
|
||||
### vmBooted
|
||||
|
||||
Emitted when the VM finishes booting. No payload.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("vmBooted", () => {
|
||||
console.log("VM is ready");
|
||||
});
|
||||
```
|
||||
|
||||
### vmShutdown
|
||||
|
||||
Emitted when the VM is shutting down.
|
||||
|
||||
```ts @nocheck
|
||||
agent.on("vmShutdown", (data) => {
|
||||
// data.reason: "sleep" | "destroy" | "error"
|
||||
console.log("VM shutting down:", data.reason);
|
||||
});
|
||||
```
|
||||
|
||||
## Client subscription pattern
|
||||
|
||||
Subscribe to events before triggering actions to avoid missing early events.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Subscribe to all relevant events first
|
||||
agent.on("sessionEvent", (data) => {
|
||||
console.log("Session:", data.event.method);
|
||||
});
|
||||
agent.on("processOutput", (data) => {
|
||||
console.log("Process:", new TextDecoder().decode(data.data));
|
||||
});
|
||||
agent.on("processExit", (data) => {
|
||||
console.log("Exit:", data.pid, data.exitCode);
|
||||
});
|
||||
|
||||
// Then trigger actions
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "Run the test suite");
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Event replay
|
||||
|
||||
There are two ways to replay session events:
|
||||
|
||||
- **`getSequencedEvents`** returns events from the in-memory session. Each event has a `sequenceNumber` and a `notification` (the raw JSON-RPC notification). Use this for live reconnection while the VM is running.
|
||||
- **`getSessionEvents`** returns events from persisted storage (SQLite). Each event has a `seq`, `event`, and `createdAt`. Use this for transcript history, including when the VM is not running.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Replay events from sequence 0
|
||||
const events = await agent.getSequencedEvents("session-id", { since: 0 });
|
||||
for (const e of events) {
|
||||
console.log(e.sequenceNumber, e.notification.method);
|
||||
}
|
||||
|
||||
// Replay from persisted storage (works without running VM)
|
||||
const persisted = await agent.getSessionEvents("session-id");
|
||||
for (const e of persisted) {
|
||||
console.log(e.seq, e.event.method, e.createdAt);
|
||||
}
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
_Source doc path: /docs/agent-os/events_
|
||||
@@ -0,0 +1,243 @@
|
||||
# Filesystem
|
||||
|
||||
> Source: `src/content/docs/agent-os/filesystem.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/filesystem
|
||||
> Description: Read, write, mount, and manage files inside agentOS.
|
||||
|
||||
---
|
||||
Every VM comes with a persistent filesystem out of the box. Files written anywhere in the VM are automatically saved to the Rivet Actor's built-in storage and restored on wake. No configuration needed.
|
||||
|
||||
- **Persistent by default** backed by Rivet Actor storage, up to 10 GB
|
||||
- **Full POSIX filesystem** with read, write, mkdir, stat, move, delete
|
||||
- **Batch operations** for reading and writing multiple files at once
|
||||
- **Mount backends** for additional storage like S3, host directories, and overlays
|
||||
|
||||
## Mounting filesystems
|
||||
|
||||
The default filesystem persists automatically across sleep/wake cycles with no setup required (up to 10 GB). For larger storage or external data, mount additional filesystem drivers via the `options.mounts` config.
|
||||
|
||||
Each mount takes a `path` (where to mount inside the VM), a `driver` (the filesystem implementation), and an optional `readOnly` flag.
|
||||
|
||||
### In-memory
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import { createInMemoryFileSystem } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
software: [common, pi],
|
||||
mounts: [
|
||||
{ path: "/mnt/scratch", driver: createInMemoryFileSystem() },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### Host directory
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import { createHostDirBackend } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
software: [common, pi],
|
||||
mounts: [
|
||||
{ path: "/mnt/code", driver: createHostDirBackend({ hostPath: "/path/to/repo" }), readOnly: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### S3
|
||||
|
||||
Install `@rivet-dev/agent-os-s3` for S3-compatible storage.
|
||||
|
||||
#### Per-agent storage (recommended)
|
||||
|
||||
Use `createS3BackendForAgent` to automatically scope S3 storage by agent key. Each agent instance gets its own prefix (`agents/{key}/`) within the bucket, so you don't need to manage prefixes manually.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import { createS3BackendForAgent } from "@rivet-dev/agent-os-s3";
|
||||
import type { AgentOsContext } from "@rivet-dev/agent-os-s3";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
function createVm(context: AgentOsContext) {
|
||||
return agentOs({
|
||||
options: {
|
||||
software: [common, pi],
|
||||
mounts: [
|
||||
{
|
||||
path: "/mnt/data",
|
||||
driver: createS3BackendForAgent(context, {
|
||||
bucket: "my-bucket",
|
||||
region: "us-east-1",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `AgentOsContext` object contains:
|
||||
|
||||
- `key` — unique identifier for the agent instance (e.g. actor ID, user ID, session ID). Used as the S3 prefix.
|
||||
- `metadata` — optional key-value pairs with additional agent information.
|
||||
|
||||
#### Fixed prefix
|
||||
|
||||
For shared storage or custom prefix schemes, use `createS3Backend` with an explicit prefix.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import { createS3Backend } from "@rivet-dev/agent-os-s3";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
software: [common, pi],
|
||||
mounts: [
|
||||
{
|
||||
path: "/mnt/data",
|
||||
driver: createS3Backend({
|
||||
bucket: "my-bucket",
|
||||
prefix: "agent-data/",
|
||||
region: "us-east-1",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### Google Drive
|
||||
|
||||
Install `@rivet-dev/agent-os-google-drive` for Google Drive storage.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import { GoogleDriveBlockStore } from "@rivet-dev/agent-os-google-drive";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
software: [common, pi],
|
||||
mounts: [
|
||||
{
|
||||
path: "/mnt/drive",
|
||||
driver: new GoogleDriveBlockStore({
|
||||
credentials: {
|
||||
clientEmail: process.env.GOOGLE_DRIVE_CLIENT_EMAIL!,
|
||||
privateKey: process.env.GOOGLE_DRIVE_PRIVATE_KEY!,
|
||||
},
|
||||
folderId: process.env.GOOGLE_DRIVE_FOLDER_ID!,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Filesystem operations
|
||||
|
||||
### Read and write
|
||||
|
||||
```ts @nocheck
|
||||
// Write a file (string or Uint8Array)
|
||||
await agent.writeFile("/home/user/hello.txt", "Hello, world!");
|
||||
|
||||
// Read a file (returns Uint8Array)
|
||||
const content = await agent.readFile("/home/user/hello.txt");
|
||||
console.log(new TextDecoder().decode(content));
|
||||
```
|
||||
|
||||
### Batch read and write
|
||||
|
||||
```ts @nocheck
|
||||
// Batch write (creates parent directories automatically)
|
||||
const writeResults = await agent.writeFiles([
|
||||
{ path: "/home/user/src/index.ts", content: "console.log('hello');" },
|
||||
{ path: "/home/user/src/utils.ts", content: "export function add(a: number, b: number) { return a + b; }" },
|
||||
]);
|
||||
|
||||
// Batch read
|
||||
const readResults = await agent.readFiles([
|
||||
"/home/user/src/index.ts",
|
||||
"/home/user/src/utils.ts",
|
||||
]);
|
||||
for (const result of readResults) {
|
||||
console.log(result.path, new TextDecoder().decode(result.content));
|
||||
}
|
||||
```
|
||||
|
||||
### Directories
|
||||
|
||||
```ts @nocheck
|
||||
// Create a directory
|
||||
await agent.mkdir("/home/user/projects");
|
||||
|
||||
// List directory contents
|
||||
const entries = await agent.readdir("/home/user/projects");
|
||||
|
||||
// Recursive listing with metadata
|
||||
const tree = await agent.readdirRecursive("/home/user", {
|
||||
maxDepth: 3,
|
||||
exclude: ["node_modules"],
|
||||
});
|
||||
for (const entry of tree) {
|
||||
console.log(entry.type, entry.path, entry.size);
|
||||
}
|
||||
```
|
||||
|
||||
### File metadata
|
||||
|
||||
```ts @nocheck
|
||||
// Check if a path exists
|
||||
const fileExists = await agent.exists("/home/user/hello.txt");
|
||||
|
||||
// Get file metadata
|
||||
const info = await agent.stat("/home/user/hello.txt");
|
||||
console.log(info.size, info.isDirectory, info.mtimeMs);
|
||||
```
|
||||
|
||||
### Move and delete
|
||||
|
||||
```ts @nocheck
|
||||
// Move/rename
|
||||
await agent.move("/home/user/old.txt", "/home/user/new.txt");
|
||||
|
||||
// Delete a file
|
||||
await agent.deleteFile("/home/user/new.txt");
|
||||
|
||||
// Delete a directory recursively
|
||||
await agent.deleteFile("/home/user/temp", { recursive: true });
|
||||
```
|
||||
|
||||
_Source doc path: /docs/agent-os/filesystem_
|
||||
@@ -0,0 +1,35 @@
|
||||
# Limitations
|
||||
|
||||
> Source: `src/content/docs/agent-os/limitations.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/limitations
|
||||
> Description: What the agentOS VM does not support, and how to work around it.
|
||||
|
||||
---
|
||||
agentOS is a Linux-like environment with a POSIX-compliant virtual kernel. It handles most agent workloads (coding, scripting, file I/O, networking) with near-zero overhead.
|
||||
|
||||
## Sandbox mounting
|
||||
|
||||
When a workload needs a full Linux OS, agents can escalate to a full sandbox on demand without changing code. The [sandbox mounting](/docs/agent-os/sandbox) extension mounts the sandbox as a filesystem and lets you execute commands on it, like mounting a hard drive on your own machine. Files written in the VM are available in the sandbox and vice versa.
|
||||
|
||||
See [agentOS vs Sandbox](/docs/agent-os/versus-sandbox) for a detailed comparison.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Software registry
|
||||
|
||||
agentOS uses its own [software registry](/agent-os/registry) of popular tools cross-compiled for the runtime. Native language runtimes like Go, Rust, and C++ are supported. Standard Linux package managers (`apt`, `yum`) are not available since agentOS is not a full Linux OS.
|
||||
|
||||
See [Software](/docs/agent-os/software) for how to install and configure available packages.
|
||||
|
||||
### POSIX-compliant, not full Linux
|
||||
|
||||
agentOS provides a POSIX-compliant virtual kernel with full filesystem operations, networking, and process management. It is not a full Linux kernel, so some Linux-specific features are not available:
|
||||
|
||||
- Kernel modules and eBPF
|
||||
- Container runtimes (e.g. Docker)
|
||||
|
||||
### No hardware access
|
||||
|
||||
The VM has no access to GPUs, USB devices, or other hardware.
|
||||
|
||||
_Source doc path: /docs/agent-os/limitations_
|
||||
@@ -0,0 +1,65 @@
|
||||
# LLM Credentials
|
||||
|
||||
> Source: `src/content/docs/agent-os/llm-credentials.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/llm-credentials
|
||||
> Description: Pass LLM API keys to agent sessions securely.
|
||||
|
||||
---
|
||||
- **Keys stay on the server** and are injected at session creation
|
||||
- **Per-tenant isolation** for multi-tenant deployments
|
||||
|
||||
## Passing API keys
|
||||
|
||||
Pass LLM provider keys via the `env` option on `createSession`. The VM does not inherit from the host `process.env`, so keys must be passed explicitly.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
```
|
||||
|
||||
## Per-tenant credentials
|
||||
|
||||
Look up each tenant's API key from your database and pass it at session creation.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup, UserError } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
interface ConnState {
|
||||
userId: string;
|
||||
anthropicApiKey: string;
|
||||
}
|
||||
|
||||
const vm = agentOs({
|
||||
createConnState: async (c, params: { authToken: string }): Promise<ConnState> => {
|
||||
const user = await validateAndLookupUser(params.authToken);
|
||||
if (!user) {
|
||||
throw new UserError("Forbidden", { code: "forbidden" });
|
||||
}
|
||||
return { userId: user.id, anthropicApiKey: user.anthropicApiKey };
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
Then use the connection state when creating sessions:
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: c.conn.state.anthropicApiKey },
|
||||
});
|
||||
```
|
||||
|
||||
See [Sandbox Agent's LLM credentials documentation](https://sandboxagent.dev/docs/llm-credentials) for more details on per-tenant token patterns.
|
||||
|
||||
## Embedded LLM Gateway
|
||||
|
||||
The [Embedded LLM Gateway](/docs/agent-os/llm-gateway) (coming soon) will remove the need to manage API keys manually. It routes all agent LLM requests through a managed proxy built into agentOS, providing per-tenant usage metering, rate limiting, and cost controls without deploying a separate gateway service.
|
||||
|
||||
_Source doc path: /docs/agent-os/llm-credentials_
|
||||
@@ -0,0 +1,17 @@
|
||||
# Embedded LLM Gateway
|
||||
|
||||
> Source: `src/content/docs/agent-os/llm-gateway.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/llm-gateway
|
||||
> Description: Route, meter, and manage LLM API calls from agents.
|
||||
|
||||
---
|
||||
The Embedded LLM Gateway runs as part of the agentOS library, not as an external service. It intercepts and manages all LLM API calls made by agents inside the VM.
|
||||
|
||||
- **Unified routing** for all agent LLM requests
|
||||
- **API keys stay on the server** so they are never exposed to agent code inside the VM
|
||||
- **Usage metering** with per-session and per-agent breakdowns
|
||||
- **Rate limiting** and cost controls
|
||||
|
||||
Check back soon for full documentation.
|
||||
|
||||
_Source doc path: /docs/agent-os/llm-gateway_
|
||||
@@ -0,0 +1,190 @@
|
||||
# Multiplayer
|
||||
|
||||
> Source: `src/content/docs/agent-os/multiplayer.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/multiplayer
|
||||
> Description: Connect multiple clients to the same agentOS actor for collaborative agent workflows.
|
||||
|
||||
---
|
||||
- **Multiple clients** connected to the same agent VM simultaneously
|
||||
- **Broadcast events** so all subscribers see session output, process logs, and shell data
|
||||
- **Collaborative patterns** where one user prompts and others observe
|
||||
- **Handoff** between human and agent control
|
||||
|
||||
## Multiple clients observing a session
|
||||
|
||||
All clients connected to the same actor receive broadcasted events. This enables building collaborative UIs where multiple users watch an agent work.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
// Client A: creates the session and sends prompts
|
||||
const clientA = createClient<typeof registry>("http://localhost:6420");
|
||||
const agentA = clientA.vm.getOrCreate(["shared-agent"]);
|
||||
|
||||
agentA.on("sessionEvent", (data) => {
|
||||
console.log("[A]", data.event.method);
|
||||
});
|
||||
|
||||
const session = await agentA.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentA.sendPrompt(session.sessionId, "Build a REST API");
|
||||
|
||||
// Client B: observes the same session (in a separate process)
|
||||
const clientB = createClient<typeof registry>("http://localhost:6420");
|
||||
const agentB = clientB.vm.getOrCreate(["shared-agent"]);
|
||||
|
||||
agentB.on("sessionEvent", (data) => {
|
||||
console.log("[B]", data.event.method);
|
||||
});
|
||||
|
||||
// Client B sees the same events as Client A
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Shared process output
|
||||
|
||||
All clients receive process output events from the same VM.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["shared-agent"]);
|
||||
|
||||
// All connected clients see process output
|
||||
agent.on("processOutput", (data) => {
|
||||
const text = new TextDecoder().decode(data.data);
|
||||
console.log(`[pid ${data.pid}] ${data.stream}: ${text}`);
|
||||
});
|
||||
|
||||
// All connected clients see shell data
|
||||
agent.on("shellData", (data) => {
|
||||
const text = new TextDecoder().decode(data.data);
|
||||
process.stdout.write(text);
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Collaborative prompt/observe pattern
|
||||
|
||||
One client acts as the driver (sending prompts), while others observe.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
onSessionEvent: async (c, sessionId, event) => {
|
||||
// Server-side hook runs once per event, even with multiple clients
|
||||
console.log("Session event:", sessionId, event.method);
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
// Driver client
|
||||
const driver = createClient<typeof registry>("http://localhost:6420");
|
||||
const driverAgent = driver.vm.getOrCreate(["shared-agent"]);
|
||||
|
||||
const session = await driverAgent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
// Observer client (different user, same actor)
|
||||
const observer = createClient<typeof registry>("http://localhost:6420");
|
||||
const observerAgent = observer.vm.getOrCreate(["shared-agent"]);
|
||||
|
||||
observerAgent.on("sessionEvent", (data) => {
|
||||
console.log("[observer]", data.event.method, data.event.params);
|
||||
});
|
||||
|
||||
// Driver sends a prompt. Observer sees the streaming response.
|
||||
await driverAgent.sendPrompt(session.sessionId, "Refactor the auth module");
|
||||
```
|
||||
|
||||
## Reconnection with event replay
|
||||
|
||||
When a client reconnects, use `getSequencedEvents` to replay missed events and catch up.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["shared-agent"]);
|
||||
|
||||
// On reconnect, replay events from the last known sequence number
|
||||
const lastSeq = 42; // Track this on the client side
|
||||
const missedEvents = await agent.getSequencedEvents("session-id", {
|
||||
since: lastSeq,
|
||||
});
|
||||
for (const event of missedEvents) {
|
||||
console.log("Replaying:", event.sequenceNumber, event.notification.method);
|
||||
}
|
||||
|
||||
// Resume live streaming
|
||||
agent.on("sessionEvent", (data) => {
|
||||
console.log("Live:", data.event.method);
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use the same actor key (e.g. `["shared-agent"]`) for all clients that should share the same VM.
|
||||
- Events are broadcasted to all connected clients automatically. No additional setup needed.
|
||||
- For reconnection, track the last sequence number on the client and use `getSequencedEvents` to replay missed events.
|
||||
- Use the server-side `onSessionEvent` hook for logic that should run once per event regardless of connected clients.
|
||||
|
||||
_Source doc path: /docs/agent-os/multiplayer_
|
||||
@@ -0,0 +1,169 @@
|
||||
# Networking & Previews
|
||||
|
||||
> Source: `src/content/docs/agent-os/networking.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/networking
|
||||
> Description: Proxy HTTP requests into agentOS VMs and create shareable preview URLs.
|
||||
|
||||
---
|
||||
- **`vmFetch`** proxies HTTP requests to services running inside the VM
|
||||
- **Preview URLs** create time-limited, shareable public URLs to VM services
|
||||
- **Token-based access** with configurable expiration and revocation
|
||||
- **CORS enabled** for browser access to preview URLs
|
||||
|
||||
## Fetch from a VM service
|
||||
|
||||
Use `vmFetch` to send HTTP requests to a service running inside the VM.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Start a web server inside the VM
|
||||
await agent.writeFile(
|
||||
"/home/user/server.js",
|
||||
'require("http").createServer((req, res) => res.end("Hello from VM")).listen(3000);',
|
||||
);
|
||||
await agent.spawn("node", ["/home/user/server.js"]);
|
||||
|
||||
// Fetch from the VM service
|
||||
const response = await agent.vmFetch(3000, "/");
|
||||
console.log("Status:", response.status);
|
||||
console.log("Body:", new TextDecoder().decode(response.body));
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## vmFetch with options
|
||||
|
||||
Send requests with custom methods, headers, and body.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const response = await agent.vmFetch(3000, "/api/data", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key: "value" }),
|
||||
});
|
||||
|
||||
console.log("Status:", response.status, response.statusText);
|
||||
console.log("Headers:", response.headers);
|
||||
console.log("Body:", new TextDecoder().decode(response.body));
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Create a preview URL
|
||||
|
||||
Preview URLs are essentially port forwarding for VM services. They create a time-limited, publicly accessible URL that proxies HTTP requests to a specific port inside the VM. Use them to share web app previews with users, embed dev servers in iframes, or give external tools access to services running inside the agent's VM.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
preview: {
|
||||
defaultExpiresInSeconds: 3600, // 1 hour default
|
||||
maxExpiresInSeconds: 86400, // 24 hour maximum
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Start a web app in the VM
|
||||
await agent.spawn("node", ["/home/user/app.js"]);
|
||||
|
||||
// Create a preview URL (default 1 hour expiration)
|
||||
const preview = await agent.createSignedPreviewUrl(3000);
|
||||
console.log("Preview path:", preview.path);
|
||||
console.log("Token:", preview.token);
|
||||
console.log("Expires at:", new Date(preview.expiresAt));
|
||||
|
||||
// Create a preview URL with custom expiration
|
||||
const shortPreview = await agent.createSignedPreviewUrl(3000, 300); // 5 minutes
|
||||
console.log("Short-lived preview:", shortPreview.path);
|
||||
```
|
||||
|
||||
## Revoke a preview URL
|
||||
|
||||
Use `expireSignedPreviewUrl` to immediately revoke a preview token.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const preview = await agent.createSignedPreviewUrl(3000);
|
||||
|
||||
// Revoke the token immediately
|
||||
await agent.expireSignedPreviewUrl(preview.token);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Preview tokens are stored in SQLite and survive sleep/wake cycles. Expired tokens are cleaned up automatically.
|
||||
- Default preview expiration is 1 hour. Configure `preview.maxExpiresInSeconds` to cap the maximum lifetime.
|
||||
- CORS is enabled on preview URLs, allowing browser access from any origin.
|
||||
- Use `vmFetch` for server-to-server access. Use preview URLs for browser or external access.
|
||||
- See [Security](/docs/agent-os/security) for more on preview URL token security.
|
||||
|
||||
_Source doc path: /docs/agent-os/networking_
|
||||
@@ -0,0 +1,156 @@
|
||||
# Permissions
|
||||
|
||||
> Source: `src/content/docs/agent-os/permissions.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/permissions
|
||||
> Description: Approve or deny agent tool use with human-in-the-loop or auto-approve patterns.
|
||||
|
||||
---
|
||||
- **Human-in-the-loop** approval for agent tool use (file writes, command execution, etc.)
|
||||
- **Auto-approve** patterns for trusted workloads
|
||||
- **Server-side hooks** for programmatic permission decisions
|
||||
- **Client-side subscriptions** for building approval UIs
|
||||
|
||||
## Permission request flow
|
||||
|
||||
When an agent wants to use a tool (e.g. write a file, run a command), it emits a `permissionRequest` event. Your code responds with `respondPermission` to approve or deny.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Listen for permission requests
|
||||
agent.on("permissionRequest", async (data) => {
|
||||
console.log("Permission requested:", data.request);
|
||||
|
||||
// Approve this single request
|
||||
await agent.respondPermission(
|
||||
data.sessionId,
|
||||
data.request.permissionId,
|
||||
"once",
|
||||
);
|
||||
});
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "Create a new file at /home/user/output.txt");
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Permission reply options
|
||||
|
||||
| Reply | Behavior |
|
||||
|-------|----------|
|
||||
| `"once"` | Approve this single request |
|
||||
| `"always"` | Approve this and all future requests of the same type |
|
||||
| `"reject"` | Deny the request |
|
||||
|
||||
## Server-side auto-approve
|
||||
|
||||
Use the `onPermissionRequest` hook in the actor config to approve permissions server-side without client involvement. This is useful for fully automated pipelines.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
onPermissionRequest: async (c, sessionId, request) => {
|
||||
// Auto-approve all file operations
|
||||
await c.respondPermission(sessionId, request.permissionId, "always");
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// No need to handle permissions on the client. The server auto-approves.
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "Write files as needed");
|
||||
```
|
||||
|
||||
## Selective approval
|
||||
|
||||
Inspect the permission request to make approval decisions based on the tool or path.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
onPermissionRequest: async (c, sessionId, request) => {
|
||||
// Auto-approve reads, require manual approval for writes
|
||||
const toolName = (request as any).toolName ?? "";
|
||||
if (toolName.includes("read")) {
|
||||
await c.respondPermission(sessionId, request.permissionId, "always");
|
||||
}
|
||||
// Writes are forwarded to the client via the permissionRequest event
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Only write permissions reach the client
|
||||
agent.on("permissionRequest", async (data) => {
|
||||
const approved = confirm(`Allow write: ${JSON.stringify(data.request)}?`);
|
||||
await agent.respondPermission(
|
||||
data.sessionId,
|
||||
data.request.permissionId,
|
||||
approved ? "once" : "reject",
|
||||
);
|
||||
});
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "Read config.json and update it");
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use `"always"` sparingly. It approves all future requests of that type for the session lifetime.
|
||||
- For automated CI/CD pipelines, use the server-side `onPermissionRequest` hook to auto-approve without client round-trips.
|
||||
- For interactive applications, subscribe to `permissionRequest` on the client and build an approval UI.
|
||||
- If neither the server hook nor the client responds, the agent blocks until a response is given or the action times out.
|
||||
|
||||
_Source doc path: /docs/agent-os/permissions_
|
||||
@@ -0,0 +1,185 @@
|
||||
# Persistence & Sleep
|
||||
|
||||
> Source: `src/content/docs/agent-os/persistence.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/persistence
|
||||
> Description: How agentOS persists data and manages sleep/wake cycles.
|
||||
|
||||
---
|
||||
- **Persistent filesystem** backs `/home/user` automatically
|
||||
- **Session transcripts** persisted with sequence numbers for replay
|
||||
- **Configurable sleep** with a 15-minute grace period by default
|
||||
- **Automatic wake** when a client connects or a cron job triggers
|
||||
|
||||
## What persists across sleep
|
||||
|
||||
| Data | Storage | Persists? |
|
||||
|------|---------|-----------|
|
||||
| Files in `/home/user` | Persistent filesystem | Yes |
|
||||
| Session records | SQLite (`agent_os_sessions`) | Yes |
|
||||
| Session event history | SQLite (`agent_os_session_events`) | Yes |
|
||||
| Preview URL tokens | SQLite (`agent_os_preview_tokens`) | Yes |
|
||||
| Cron job definitions | Actor state | Yes |
|
||||
| Running processes | VM kernel | No |
|
||||
| Active shells | VM kernel | No |
|
||||
| In-memory mounts | VM memory | No |
|
||||
| VM kernel state | VM memory | No |
|
||||
|
||||
## What prevents sleep
|
||||
|
||||
The actor stays awake as long as any of these are active:
|
||||
|
||||
- **Active sessions** (created but not closed/destroyed)
|
||||
- **Running processes** (spawned but not exited)
|
||||
- **Active shells** (opened but not closed)
|
||||
- **Pending hooks** (server-side callbacks still executing)
|
||||
|
||||
When all activity stops, the sleep grace period begins.
|
||||
|
||||
## Sleep grace period
|
||||
|
||||
After all activity stops, the actor waits 15 minutes before sleeping. This allows for brief pauses between interactions without restarting the VM.
|
||||
|
||||
```
|
||||
Activity stops ──> 15 min grace period ──> Actor sleeps
|
||||
(VM shutdown, processes killed)
|
||||
|
||||
New client connects ──> Actor wakes ──> VM boots ──> Filesystem restored
|
||||
```
|
||||
|
||||
## Sleep vs destroy
|
||||
|
||||
| | Sleep | Destroy |
|
||||
|-|-------|---------|
|
||||
| Filesystem | Preserved | Deleted |
|
||||
| Session records | Preserved | Deleted |
|
||||
| Event history | Preserved | Deleted |
|
||||
| Preview tokens | Preserved | Deleted |
|
||||
| VM state | Lost | Lost |
|
||||
| Processes | Killed | Killed |
|
||||
|
||||
## VM boot and shutdown events
|
||||
|
||||
Subscribe to `vmBooted` and `vmShutdown` events to track VM lifecycle.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
agent.on("vmBooted", () => {
|
||||
console.log("VM is ready");
|
||||
});
|
||||
|
||||
agent.on("vmShutdown", (data) => {
|
||||
console.log("VM shutdown reason:", data.reason);
|
||||
// reason: "sleep" | "destroy" | "error"
|
||||
});
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Resuming after sleep
|
||||
|
||||
When the actor wakes up:
|
||||
|
||||
1. The VM boots and the filesystem is restored from SQLite
|
||||
2. Session records and event history are available immediately
|
||||
3. Processes and shells from the previous session are gone
|
||||
4. Clients can reconnect and resume sessions using `resumeSession`
|
||||
5. Use `getSessionEvents` to replay missed events
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// List sessions from before sleep
|
||||
const sessions = await agent.listPersistedSessions();
|
||||
console.log("Previous sessions:", sessions.length);
|
||||
|
||||
// Resume the most recent session
|
||||
if (sessions.length > 0) {
|
||||
const last = sessions[0];
|
||||
await agent.resumeSession(last.sessionId);
|
||||
|
||||
// Replay events for transcript
|
||||
const events = await agent.getSessionEvents(last.sessionId);
|
||||
for (const e of events) {
|
||||
console.log(e.seq, e.event.method);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Persisted tables schema
|
||||
|
||||
### `agent_os_fs_entries`
|
||||
|
||||
Stores the virtual filesystem.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `path` | TEXT PRIMARY KEY | File or directory path |
|
||||
| `is_directory` | INTEGER | 1 for directory, 0 for file |
|
||||
| `content` | BLOB | File content |
|
||||
| `mode` | INTEGER | POSIX mode bits |
|
||||
| `size` | INTEGER | File size in bytes |
|
||||
| `atime_ms` | INTEGER | Access time (ms) |
|
||||
| `mtime_ms` | INTEGER | Modification time (ms) |
|
||||
| `ctime_ms` | INTEGER | Change time (ms) |
|
||||
| `birthtime_ms` | INTEGER | Birth time (ms) |
|
||||
|
||||
### `agent_os_sessions`
|
||||
|
||||
Stores session metadata.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `session_id` | TEXT PRIMARY KEY | Unique session identifier |
|
||||
| `agent_type` | TEXT | Agent type (e.g. "pi") |
|
||||
| `capabilities` | TEXT (JSON) | Agent capabilities |
|
||||
| `agent_info` | TEXT (JSON) | Agent metadata |
|
||||
| `created_at` | INTEGER | Creation timestamp (ms) |
|
||||
|
||||
### `agent_os_session_events`
|
||||
|
||||
Stores session event history.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER PRIMARY KEY | Auto-incrementing ID |
|
||||
| `session_id` | TEXT | Session reference |
|
||||
| `seq` | INTEGER | Sequence number within session |
|
||||
| `event` | TEXT (JSON) | JSON-RPC notification |
|
||||
| `created_at` | INTEGER | Timestamp (ms) |
|
||||
|
||||
_Source doc path: /docs/agent-os/persistence_
|
||||
@@ -0,0 +1,253 @@
|
||||
# Processes & Shell
|
||||
|
||||
> Source: `src/content/docs/agent-os/processes.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/processes
|
||||
> Description: Execute commands, spawn long-running processes, and open interactive shells in agentOS VMs.
|
||||
|
||||
---
|
||||
- **One-shot execution** with `exec` for simple commands
|
||||
- **Long-running processes** with `spawn`, stdout/stderr streaming, and stdin writing
|
||||
- **Process lifecycle** management with stop, kill, wait, and inspect
|
||||
- **Interactive shells** with PTY support for terminal I/O
|
||||
- **Process tree** visibility across all VM runtimes
|
||||
|
||||
## One-shot execution
|
||||
|
||||
Use `exec` to run a command and wait for completion. Returns stdout, stderr, and exit code.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const result = await agent.exec("echo hello && ls /home/user");
|
||||
console.log("stdout:", result.stdout);
|
||||
console.log("stderr:", result.stderr);
|
||||
console.log("exit code:", result.exitCode);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Spawn a long-running process
|
||||
|
||||
Use `spawn` for processes that run in the background. Output is streamed via `processOutput` and `processExit` events.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Subscribe to process output
|
||||
agent.on("processOutput", (data) => {
|
||||
const text = new TextDecoder().decode(data.data);
|
||||
console.log(`[pid ${data.pid}] ${data.stream}: ${text}`);
|
||||
});
|
||||
|
||||
agent.on("processExit", (data) => {
|
||||
console.log(`[pid ${data.pid}] exited with code ${data.exitCode}`);
|
||||
});
|
||||
|
||||
// Spawn a dev server
|
||||
const { pid } = await agent.spawn("node", ["/home/user/server.js"]);
|
||||
console.log("Started process:", pid);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Write to stdin
|
||||
|
||||
Send input to a running process.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const { pid } = await agent.spawn("cat", []);
|
||||
|
||||
// Write to stdin
|
||||
await agent.writeProcessStdin(pid, "hello from stdin\n");
|
||||
|
||||
// Close stdin when done
|
||||
await agent.closeProcessStdin(pid);
|
||||
|
||||
// Wait for the process to exit
|
||||
const exitCode = await agent.waitProcess(pid);
|
||||
console.log("exit code:", exitCode);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Process lifecycle
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const { pid } = await agent.spawn("node", ["/home/user/server.js"]);
|
||||
|
||||
// List all spawned processes
|
||||
const processes = await agent.listProcesses();
|
||||
console.log(processes);
|
||||
|
||||
// Get info about a specific process
|
||||
const info = await agent.getProcess(pid);
|
||||
console.log(info.running, info.exitCode);
|
||||
|
||||
// Graceful stop (SIGTERM)
|
||||
await agent.stopProcess(pid);
|
||||
|
||||
// Force kill (SIGKILL)
|
||||
await agent.killProcess(pid);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## System-wide process visibility
|
||||
|
||||
View all processes across all VM runtimes, not just those started via `spawn`.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// All processes
|
||||
const all = await agent.allProcesses();
|
||||
for (const p of all) {
|
||||
console.log(p.pid, p.driver, p.command, p.status);
|
||||
}
|
||||
|
||||
// Process tree (parent-child hierarchy)
|
||||
const tree = await agent.processTree();
|
||||
for (const node of tree) {
|
||||
console.log(node.pid, node.command, "children:", node.children.length);
|
||||
}
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Interactive shells
|
||||
|
||||
Open an interactive shell with PTY support. Shell data is streamed via `shellData` events.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Subscribe to shell output
|
||||
agent.on("shellData", (data) => {
|
||||
const text = new TextDecoder().decode(data.data);
|
||||
process.stdout.write(text);
|
||||
});
|
||||
|
||||
// Open a shell
|
||||
const { shellId } = await agent.openShell();
|
||||
|
||||
// Write commands to the shell
|
||||
await agent.writeShell(shellId, "ls -la /home/user\n");
|
||||
|
||||
// Resize the terminal
|
||||
await agent.resizeShell(shellId, 120, 40);
|
||||
|
||||
// Close the shell when done
|
||||
await agent.closeShell(shellId);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use `exec` for short commands where you need the full output. Use `spawn` for long-running processes where you want streaming output.
|
||||
- Subscribe to `processOutput` and `processExit` **before** calling `spawn` to avoid missing events.
|
||||
- Active processes prevent the actor from sleeping. Stop or kill them when they are no longer needed.
|
||||
- Active shells also prevent sleep. Close shells when the user disconnects.
|
||||
- Use `allProcesses` and `processTree` for debugging. They show everything running in the VM, including agent processes.
|
||||
|
||||
_Source doc path: /docs/agent-os/processes_
|
||||
191
.agents/skills/ai-agent-workspace/reference/agent-os/queues.md
Normal file
191
.agents/skills/ai-agent-workspace/reference/agent-os/queues.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Queues
|
||||
|
||||
> Source: `src/content/docs/agent-os/queues.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/queues
|
||||
> Description: Serialize agent work with durable queues for backpressure and rate limiting.
|
||||
|
||||
---
|
||||
- **Serial execution** ensures agents process one task at a time
|
||||
- **Durable messages** survive sleep and restart
|
||||
- **Completable messages** for request/response patterns with agents
|
||||
- **Backpressure** absorbs bursts and prevents overload
|
||||
|
||||
## Queue agent commands
|
||||
|
||||
Use actor queues to serialize work that an agent processes one task at a time.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
const taskRunner = actor({
|
||||
queues: {
|
||||
tasks: queue<{ prompt: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
const agentHandle = c.actors.vm.getOrCreate(["task-agent"]);
|
||||
|
||||
for await (const message of c.queue.iter()) {
|
||||
// Process one task at a time
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentHandle.sendPrompt(session.sessionId, message.body.prompt);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { taskRunner, vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.taskRunner.getOrCreate(["main"]);
|
||||
|
||||
// Queue up work. Tasks are processed one at a time.
|
||||
await handle.send("tasks", { prompt: "Review PR #123" });
|
||||
await handle.send("tasks", { prompt: "Fix the flaky test in auth.test.ts" });
|
||||
await handle.send("tasks", { prompt: "Update the README" });
|
||||
```
|
||||
|
||||
## Request/response with completable messages
|
||||
|
||||
Use completable messages when the caller needs to wait for the agent to finish.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
const reviewer = actor({
|
||||
queues: {
|
||||
review: queue<{ file: string }, { summary: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
const agentHandle = c.actors.vm.getOrCreate(["reviewer"]);
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
for await (const message of c.queue.iter({ completable: true })) {
|
||||
const content = await agentHandle.readFile(message.body.file);
|
||||
const text = new TextDecoder().decode(content);
|
||||
|
||||
await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Review this code and write a summary to /home/user/review.txt:\n\n${text}`,
|
||||
);
|
||||
|
||||
const review = await agentHandle.readFile("/home/user/review.txt");
|
||||
await message.complete({
|
||||
summary: new TextDecoder().decode(review),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { reviewer, vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.reviewer.getOrCreate(["main"]);
|
||||
|
||||
// Wait for the agent to complete the review
|
||||
const result = await handle.send(
|
||||
"review",
|
||||
{ file: "/home/user/src/auth.ts" },
|
||||
{ wait: true, timeout: 120_000 },
|
||||
);
|
||||
|
||||
if (result.status === "completed") {
|
||||
console.log("Review:", result.response.summary);
|
||||
}
|
||||
```
|
||||
|
||||
## Ingesting from external systems
|
||||
|
||||
Accept tasks from webhooks, APIs, or other services and queue them for agent processing.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
|
||||
const issueWorker = actor({
|
||||
queues: {
|
||||
issues: queue<{ title: string; body: string }>(),
|
||||
},
|
||||
actions: {
|
||||
// HTTP endpoint to receive webhook payloads
|
||||
ingestIssue: async (c, title: string, body: string) => {
|
||||
await c.queue.push("issues", { title, body });
|
||||
},
|
||||
},
|
||||
run: async (c) => {
|
||||
const agentHandle = c.actors.vm.getOrCreate(["issue-worker"]);
|
||||
|
||||
for await (const message of c.queue.iter()) {
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Investigate and fix this issue:\n\nTitle: ${message.body.title}\n\n${message.body.body}`,
|
||||
);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { issueWorker, vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.issueWorker.getOrCreate(["main"]);
|
||||
|
||||
// Ingest from a webhook or external system
|
||||
await handle.ingestIssue(
|
||||
"Login redirect broken",
|
||||
"Users are redirected to /undefined after login on mobile",
|
||||
);
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use queues when you need guaranteed serial execution. Agents process one message at a time, preventing race conditions.
|
||||
- Use completable messages when the caller needs the result. Set a generous timeout since agent work can take minutes.
|
||||
- Queues survive actor sleep. Messages are persisted and processed when the actor wakes up.
|
||||
- See [Queues & Run Loops](/docs/actors/queues) for the full queue API reference.
|
||||
|
||||
_Source doc path: /docs/agent-os/queues_
|
||||
@@ -0,0 +1,91 @@
|
||||
# Quickstart
|
||||
|
||||
> Source: `src/content/docs/agent-os/quickstart.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/quickstart
|
||||
> Description: Set up an agentOS actor, create a session, and run your first coding agent.
|
||||
|
||||
---
|
||||
agentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).
|
||||
|
||||
### Install
|
||||
|
||||
- **rivetkit** — Actor framework with built-in persistence and orchestration
|
||||
- **@rivet-dev/agent-os-common** — Standard VM software (curl, grep, git, and more)
|
||||
- **@rivet-dev/agent-os-pi** — [Pi](https://github.com/mariozechner/pi-coding-agent) coding agent (Claude Code, Amp, and OpenCode coming soon)
|
||||
|
||||
```bash
|
||||
npm install rivetkit @rivet-dev/agent-os-common @rivet-dev/agent-os-pi
|
||||
```
|
||||
|
||||
### Create the Server & Client
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Subscribe to streaming events
|
||||
agent.on("sessionEvent", (data) => {
|
||||
console.log(data.event);
|
||||
});
|
||||
|
||||
// Create a session and send a prompt
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(
|
||||
session.sessionId,
|
||||
"Write a hello world script to /home/user/hello.js",
|
||||
);
|
||||
|
||||
// Read the file the agent created
|
||||
const content = await agent.readFile("/home/user/hello.js");
|
||||
console.log(new TextDecoder().decode(content));
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
Start the server:
|
||||
|
||||
```bash
|
||||
npx tsx server.ts
|
||||
```
|
||||
|
||||
Then in a separate terminal, run the client:
|
||||
|
||||
```bash
|
||||
npx tsx client.ts
|
||||
```
|
||||
|
||||
### Customize
|
||||
|
||||
Now that you have a working agent, customize it to fit your needs:
|
||||
|
||||
- **[Software](/docs/agent-os/software)** — Install software packages inside the VM
|
||||
- **[Tools](/docs/agent-os/tools)** — Expose your JavaScript functions to agents as CLI commands
|
||||
- **[Filesystem](/docs/agent-os/filesystem)** — Read, write, and manage files inside the VM
|
||||
- **[Registry](/docs/registry)** — Browse more agents, tools, and filesystems
|
||||
|
||||
## agentOS Core
|
||||
|
||||
The quickstart above uses `rivetkit/agent-os`, which includes statefulness, multiplayer, and orchestration out of the box. If you only need direct VM control without those features, you can use the core package (`@rivet-dev/agent-os-core`) standalone.
|
||||
|
||||
See [agentOS core documentation](/docs/agent-os/core) for reference.
|
||||
|
||||
_Source doc path: /docs/agent-os/quickstart_
|
||||
108
.agents/skills/ai-agent-workspace/reference/agent-os/sandbox.md
Normal file
108
.agents/skills/ai-agent-workspace/reference/agent-os/sandbox.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Sandbox Mounting
|
||||
|
||||
> Source: `src/content/docs/agent-os/sandbox.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/sandbox
|
||||
> Description: Extend agentOS with full sandboxes for heavy workloads like browsers, desktop automation, and compilation.
|
||||
|
||||
---
|
||||
- **Hybrid architecture** pairs agentOS with full sandboxes on demand
|
||||
- **Pay-per-second billing** so sandboxes only cost money while they are running
|
||||
- **Filesystem mount** projects the sandbox into the VM as a native directory, like mounting a hard drive on your own machine
|
||||
- **Toolkit** exposes sandbox process management as [host tools](/docs/agent-os/tools)
|
||||
- **Provider-agnostic** via [Sandbox Agent](https://sandboxagent.dev) under the hood
|
||||
|
||||
## Why use agentOS with a sandbox?
|
||||
|
||||
agentOS is not a replacement for sandboxes. It's designed to work alongside them. agentOS makes it easy to integrate agents into your backend with [host tools](/docs/agent-os/tools), [permissions](/docs/agent-os/permissions), the [LLM gateway](/docs/agent-os/llm-gateway), and orchestration. Sandbox mounting lets you connect a full sandbox environment when the workload needs it.
|
||||
|
||||
See [agentOS vs Sandbox](/docs/agent-os/versus-sandbox) for a detailed comparison.
|
||||
|
||||
## When to use a sandbox
|
||||
|
||||
- **Native binaries** not yet supported in the agentOS runtime.
|
||||
- **Browsers and desktop automation**: Playwright, Puppeteer, Selenium, or anything that needs a display server.
|
||||
- **Heavy compilation**: Large builds or native toolchains that require a full Linux environment.
|
||||
- **GUI applications**: Desktop apps, VNC sessions, or any workload that needs a graphical environment.
|
||||
- **Node.js packages with native extensions** (e.g. `sharp`, `bcrypt`, `better-sqlite3`) that require a full build toolchain.
|
||||
|
||||
## Getting started
|
||||
|
||||
The `@rivet-dev/agent-os-sandbox` package integrates through two mechanisms:
|
||||
|
||||
- **Filesystem mount**: Projects the sandbox into the VM as a native directory, like mounting a hard drive on your own machine. Read and write files through the mount directly.
|
||||
- **Toolkit**: Exposes sandbox process management as [host tools](/docs/agent-os/tools). Execute commands on the sandbox from within the VM.
|
||||
|
||||
Both are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code.
|
||||
|
||||
```bash
|
||||
npm install @rivet-dev/agent-os-sandbox sandbox-agent
|
||||
```
|
||||
|
||||
```ts @nocheck
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { DockerProvider } from "sandbox-agent/docker";
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import { createSandboxFs, createSandboxToolkit } from "@rivet-dev/agent-os-sandbox";
|
||||
|
||||
const sandbox = await SandboxAgent.start({
|
||||
sandbox: new DockerProvider(),
|
||||
});
|
||||
|
||||
const vm = await AgentOs.create({
|
||||
software: [common],
|
||||
mounts: [
|
||||
{
|
||||
path: "/sandbox",
|
||||
driver: createSandboxFs({ client: sandbox }),
|
||||
},
|
||||
],
|
||||
toolKits: [createSandboxToolkit({ client: sandbox })],
|
||||
});
|
||||
|
||||
// Write code via the filesystem. The /sandbox mount maps to the sandbox root.
|
||||
await vm.writeFile("/sandbox/app/index.ts", 'console.log("hello")');
|
||||
|
||||
// Run it via the toolkit. Commands execute inside the sandbox, so paths are
|
||||
// relative to the sandbox root (/app/index.ts), not the VM mount (/sandbox/app/index.ts).
|
||||
const result = await vm.exec("agentos-sandbox run-command --command node --json '{\"args\": [\"/app/index.ts\"]}'");
|
||||
```
|
||||
|
||||
## Tools reference
|
||||
|
||||
The toolkit exposes these commands inside the VM:
|
||||
|
||||
```bash
|
||||
# Run a command synchronously
|
||||
agentos-sandbox run-command --command "npm install" --cwd "/app"
|
||||
|
||||
# Start a background process
|
||||
agentos-sandbox create-process --command "npm" --json '{"args": ["run", "dev"]}'
|
||||
|
||||
# List running processes
|
||||
agentos-sandbox list-processes
|
||||
|
||||
# Get process output
|
||||
agentos-sandbox get-process-logs --id "proc_abc123"
|
||||
|
||||
# Stop or kill a process
|
||||
agentos-sandbox stop-process --id "proc_abc123"
|
||||
agentos-sandbox kill-process --id "proc_abc123"
|
||||
|
||||
# Send input to an interactive process
|
||||
agentos-sandbox send-input --id "proc_abc123" --input "yes"
|
||||
```
|
||||
|
||||
## Sandbox providers
|
||||
|
||||
The extension works with any [Sandbox Agent](https://sandboxagent.dev) provider. See the [Sandbox Agent documentation](https://sandboxagent.dev) for available providers and setup instructions.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Start with the default agentOS VM for all workloads. Only spin up a sandbox when you hit a task that genuinely requires one.
|
||||
- Sandboxes are billed per second of uptime. Spin them up on demand and tear them down when the task is done to minimize cost.
|
||||
- The hybrid model means your agent can handle both lightweight coding tasks and heavy system operations in the same session, using the right tool for each.
|
||||
- See [Tools](/docs/agent-os/tools) for how host tools work and how the agent calls them as CLI commands.
|
||||
- See [Security Model](/docs/agent-os/security-model) for details on the VM isolation model.
|
||||
|
||||
_Source doc path: /docs/agent-os/sandbox_
|
||||
@@ -0,0 +1,52 @@
|
||||
# Security Model
|
||||
|
||||
> Source: `src/content/docs/agent-os/security-model.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/security-model
|
||||
> Description: Trust boundaries, isolation guarantees, and the agentOS threat model.
|
||||
|
||||
---
|
||||
agentOS is in beta and still undergoing security review. The security model described here is subject to change.
|
||||
|
||||
## Deny by default
|
||||
|
||||
No syscalls are bound to the system by default. Everything is denied until explicitly opted in. Network access, filesystem mounts, process spawning, and all other capabilities must be configured by the host before the VM can use them.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
agentOS has two trust boundaries:
|
||||
|
||||
1. **Runtime boundary.** The VM isolate that runs agent code. All code inside the VM is untrusted. The isolate prevents access to the host process, host filesystem, and host network.
|
||||
2. **Host boundary.** Your application code that configures and manages the VM. You are responsible for hardening the host process, validating inputs, and managing secrets.
|
||||
|
||||
## VM isolation
|
||||
|
||||
Each agentOS actor runs in its own isolated VM:
|
||||
|
||||
- **Sandboxed execution.** All agent code runs inside a V8 isolate with WebAssembly. No code escapes the isolate boundary.
|
||||
- **Virtual filesystem.** The VM has its own filesystem. Agents cannot access host files unless explicitly mounted.
|
||||
- **Virtual network.** The VM has no direct access to the host network. Outbound requests are proxied through the host with configurable controls.
|
||||
- **Process isolation.** No host process is visible or accessible from inside the VM.
|
||||
|
||||
## What agentOS guarantees
|
||||
|
||||
- Agent code cannot read or write host files outside configured mounts
|
||||
- Agent code cannot make network requests except through the host proxy
|
||||
- Agent code cannot access host environment variables or secrets
|
||||
- Each actor's filesystem, sessions, and state are isolated from other actors
|
||||
- Resource limits (CPU, memory) are enforced at the VM level
|
||||
|
||||
## What you are responsible for
|
||||
|
||||
- Hardening the host process and deployment environment
|
||||
- Validating authentication tokens in `onBeforeConnect`
|
||||
- Scoping [permissions](/docs/agent-os/permissions) appropriately for your use case
|
||||
- Managing API keys and secrets on the host side (use the [LLM gateway](/docs/agent-os/llm-gateway) to avoid passing keys into the VM)
|
||||
- Configuring [resource limits and network controls](/docs/agent-os/security) to match your threat model
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Security configuration](/docs/agent-os/security) for resource limits, network control, and authentication setup
|
||||
- [Permissions](/docs/agent-os/permissions) for agent tool-use approval patterns
|
||||
- [agentOS vs Sandbox](/docs/agent-os/versus-sandbox) for when to escalate to a full sandbox
|
||||
|
||||
_Source doc path: /docs/agent-os/security-model_
|
||||
101
.agents/skills/ai-agent-workspace/reference/agent-os/security.md
Normal file
101
.agents/skills/ai-agent-workspace/reference/agent-os/security.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Security & Auth
|
||||
|
||||
> Source: `src/content/docs/agent-os/security.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/security
|
||||
> Description: Configure resource limits, network control, authentication, and filesystem isolation for agentOS.
|
||||
|
||||
---
|
||||
For the isolation model and trust boundaries, see [Security Model](/docs/agent-os/security-model).
|
||||
|
||||
## Resource limits
|
||||
|
||||
Restrict CPU and memory per actor to prevent runaway agents.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
software: [common, pi],
|
||||
maxMemoryMb: 512,
|
||||
maxCpuPercent: 50,
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Network control
|
||||
|
||||
Control which ports and hosts the VM can access.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = agentOs({
|
||||
options: {
|
||||
software: [common],
|
||||
loopbackExemptPorts: [8080, 3000],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Custom authentication
|
||||
|
||||
Use the `onBeforeConnect` hook to validate clients before they access the agent.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
onBeforeConnect: async (c, params) => {
|
||||
const isValid = await verifyToken(params.token);
|
||||
if (!isValid) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
},
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
|
||||
async function verifyToken(token: string): Promise<boolean> {
|
||||
// Your authentication logic
|
||||
return token === "valid-token";
|
||||
}
|
||||
```
|
||||
|
||||
## Permission system
|
||||
|
||||
Agents request permission before using tools. See [Permissions](/docs/agent-os/permissions) for auto-approve, selective approval, and human-in-the-loop patterns.
|
||||
|
||||
## Preview URL security
|
||||
|
||||
Preview URLs use randomly generated 32-character alphanumeric tokens with configurable expiration. See [Networking & Previews](/docs/agent-os/networking) for token management.
|
||||
|
||||
- Tokens are stored in SQLite and survive sleep/wake
|
||||
- Expired tokens are automatically cleaned up
|
||||
- Use `expireSignedPreviewUrl` to immediately revoke a token
|
||||
|
||||
## Filesystem isolation
|
||||
|
||||
Each VM has its own virtual filesystem. Files are isolated per actor instance.
|
||||
|
||||
- `/home/user` is persistent and survives sleep/wake
|
||||
- Mount boundaries prevent escape via symlinks or path traversal
|
||||
- Host directory mounts (if configured) prevent symlink escape beyond the mount point
|
||||
|
||||
_Source doc path: /docs/agent-os/security_
|
||||
461
.agents/skills/ai-agent-workspace/reference/agent-os/sessions.md
Normal file
461
.agents/skills/ai-agent-workspace/reference/agent-os/sessions.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Sessions
|
||||
|
||||
> Source: `src/content/docs/agent-os/sessions.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/sessions
|
||||
> Description: Create agent sessions, send prompts, stream responses, and replay event history.
|
||||
|
||||
---
|
||||
- **Create sessions** with any supported agent type
|
||||
- **Stream responses** in real time via `sessionEvent` subscriptions
|
||||
- **Replay events** with sequence numbers for reconnection and history
|
||||
- **Persist transcripts** automatically in SQLite across sleep/wake cycles
|
||||
- **Universal transcript format** using the Agent Communication Protocol (ACP)
|
||||
|
||||
Currently only [Pi](https://github.com/mariozechner/pi-coding-agent) is supported as an agent. Amp, Claude Code, Codex, and OpenCode are coming soon.
|
||||
|
||||
## Create a session
|
||||
|
||||
Use `createSession` to launch an agent inside the VM. Returns session metadata including capabilities and agent info.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
console.log(session.sessionId);
|
||||
console.log(session.capabilities);
|
||||
console.log(session.agentInfo);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
### `env`
|
||||
|
||||
Environment variables to pass to the agent process. The VM does not inherit from the host `process.env`, so API keys must be passed explicitly.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
```
|
||||
|
||||
### `cwd`
|
||||
|
||||
Working directory for the agent session inside the VM. Defaults to `/home/user`.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
cwd: "/home/user/project",
|
||||
});
|
||||
```
|
||||
|
||||
### `mcpServers`
|
||||
|
||||
Pass MCP servers to give the agent access to additional tools. MCP servers provide typed tool definitions that the agent's LLM can discover and call natively.
|
||||
|
||||
#### Local MCP server
|
||||
|
||||
Run an MCP server as a child process inside the VM.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
mcpServers: [
|
||||
{
|
||||
type: "local",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"],
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
#### Remote MCP server
|
||||
|
||||
Connect to an MCP server running outside the VM.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
mcpServers: [
|
||||
{
|
||||
type: "remote",
|
||||
url: "https://mcp.example.com/sse",
|
||||
headers: {
|
||||
Authorization: "Bearer my-token",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### `additionalInstructions`
|
||||
|
||||
Append custom instructions to the agent's system prompt.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
additionalInstructions: "Always write tests before implementation.",
|
||||
});
|
||||
```
|
||||
|
||||
### `skipOsInstructions`
|
||||
|
||||
Skip the base OS instructions injection. Tool documentation is still included even when this is `true`.
|
||||
|
||||
```ts @nocheck
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
skipOsInstructions: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Send a prompt
|
||||
|
||||
Use `sendPrompt` to send a message to an active session. The response contains the agent's reply.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
const response = await agent.sendPrompt(
|
||||
session.sessionId,
|
||||
"Create a TypeScript function that checks if a number is prime",
|
||||
);
|
||||
console.log(response);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Stream responses
|
||||
|
||||
Subscribe to `sessionEvent` to receive real-time streaming output from the agent.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Subscribe to session events before sending the prompt
|
||||
agent.on("sessionEvent", (data) => {
|
||||
console.log(`[${data.sessionId}]`, data.event.method, data.event.params);
|
||||
});
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "Explain how async/await works");
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Cancel a prompt
|
||||
|
||||
Use `cancelPrompt` to stop an in-progress prompt.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
// Start a long-running prompt
|
||||
const promptPromise = agent.sendPrompt(
|
||||
session.sessionId,
|
||||
"Refactor the entire codebase to use TypeScript strict mode",
|
||||
);
|
||||
|
||||
// Cancel after 10 seconds
|
||||
setTimeout(async () => {
|
||||
await agent.cancelPrompt(session.sessionId);
|
||||
}, 10_000);
|
||||
|
||||
const response = await promptPromise;
|
||||
console.log(response);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Resume, close, and destroy sessions
|
||||
|
||||
- `resumeSession` reconnects to a session that was suspended (e.g. after sleep)
|
||||
- `closeSession` gracefully closes a session
|
||||
- `destroySession` removes the session and all persisted data
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Resume a previously created session
|
||||
const resumed = await agent.resumeSession("session-id-from-earlier");
|
||||
|
||||
// Close without destroying persisted data
|
||||
await agent.closeSession(resumed.sessionId);
|
||||
|
||||
// Destroy session and all persisted events
|
||||
await agent.destroySession(resumed.sessionId);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
Change model, mode, and thought level on a live session.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
// Change model
|
||||
await agent.setModel(session.sessionId, "claude-sonnet-4-6");
|
||||
|
||||
// Change mode (e.g. "plan", "auto")
|
||||
await agent.setMode(session.sessionId, "plan");
|
||||
|
||||
// Change thought level
|
||||
await agent.setThoughtLevel(session.sessionId, "high");
|
||||
|
||||
// Query available options
|
||||
const modes = await agent.getModes(session.sessionId);
|
||||
console.log(modes);
|
||||
|
||||
const options = await agent.getConfigOptions(session.sessionId);
|
||||
console.log(options);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Replay events
|
||||
|
||||
Use `getSequencedEvents` to replay in-memory session events (for live reconnection while the VM is running), or `getSessionEvents` to replay from persisted storage (for transcript history, including when the VM is not running). See [Events](/docs/agent-os/events#event-replay) for details on the difference.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "Hello");
|
||||
|
||||
// Get all events
|
||||
const events = await agent.getEvents(session.sessionId);
|
||||
console.log(events);
|
||||
|
||||
// Get events with sequence numbers (for pagination/reconnection)
|
||||
const sequenced = await agent.getSequencedEvents(session.sessionId, {
|
||||
since: 0,
|
||||
});
|
||||
console.log(sequenced);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Persisted session history
|
||||
|
||||
Query session history from SQLite. Works even when the VM is not running.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// List all persisted sessions
|
||||
const sessions = await agent.listPersistedSessions();
|
||||
for (const s of sessions) {
|
||||
console.log(s.sessionId, s.agentType, s.createdAt);
|
||||
}
|
||||
|
||||
// Get full event history for a session
|
||||
const events = await agent.getSessionEvents(sessions[0].sessionId);
|
||||
for (const e of events) {
|
||||
console.log(e.seq, e.event.method, e.createdAt);
|
||||
}
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Multiple sessions
|
||||
|
||||
A single VM can run multiple sessions simultaneously. Each session has its own agent process but shares the same filesystem. Use different session IDs to manage them independently.
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
// Create two sessions in the same VM
|
||||
const coder = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
const reviewer = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
|
||||
// Coder writes code
|
||||
await agent.sendPrompt(coder.sessionId, "Write a REST API at /home/user/api.ts");
|
||||
|
||||
// Reviewer reads and reviews the same file
|
||||
await agent.sendPrompt(reviewer.sessionId, "Review /home/user/api.ts for issues");
|
||||
|
||||
// Close each session independently
|
||||
await agent.closeSession(coder.sessionId);
|
||||
await agent.closeSession(reviewer.sessionId);
|
||||
```
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Subscribe to `sessionEvent` **before** calling `sendPrompt` to avoid missing early events.
|
||||
- Use `getSequencedEvents` with `since` for reconnection. Track the last sequence number you processed.
|
||||
- Use `listPersistedSessions` and `getSessionEvents` to build transcript history UIs without requiring a running VM.
|
||||
- Call `closeSession` when done to release resources. Use `destroySession` only when you want to permanently delete session data.
|
||||
|
||||
_Source doc path: /docs/agent-os/sessions_
|
||||
@@ -0,0 +1,56 @@
|
||||
# Software
|
||||
|
||||
> Source: `src/content/docs/agent-os/software.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/software
|
||||
> Description: Install software packages and configure the commands available inside agentOS.
|
||||
|
||||
---
|
||||
agentOS starts with no commands installed. The `software` option lets you declare which packages to include. Each package provides one or more CLI commands.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @rivet-dev/agent-os-core @rivet-dev/agent-os-common
|
||||
```
|
||||
|
||||
`@rivet-dev/agent-os-common` is a meta-package that includes coreutils, sed, grep, gawk, findutils, diffutils, tar, and gzip. For a smaller footprint, install individual packages instead.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
|
||||
const vm = await AgentOs.create({
|
||||
software: [common],
|
||||
});
|
||||
|
||||
const result = await vm.exec("echo hello | grep hello");
|
||||
console.log(result.stdout); // "hello\n"
|
||||
|
||||
await vm.dispose();
|
||||
```
|
||||
|
||||
You can mix individual packages and meta-packages:
|
||||
|
||||
```ts @nocheck
|
||||
import { AgentOs } from "@rivet-dev/agent-os-core";
|
||||
import coreutils from "@rivet-dev/agent-os-coreutils";
|
||||
import grep from "@rivet-dev/agent-os-grep";
|
||||
import jq from "@rivet-dev/agent-os-jq";
|
||||
import ripgrep from "@rivet-dev/agent-os-ripgrep";
|
||||
|
||||
const vm = await AgentOs.create({
|
||||
software: [coreutils, grep, jq, ripgrep],
|
||||
});
|
||||
```
|
||||
|
||||
## Available Packages
|
||||
|
||||
Browse all available software packages on the [Registry](/agent-os/registry).
|
||||
|
||||
## Publishing Custom Packages
|
||||
|
||||
See the [agent-os-registry contributing guide](https://github.com/rivet-dev/agent-os/blob/main/registry/CONTRIBUTING.md) for how to add new software packages to the registry.
|
||||
|
||||
_Source doc path: /docs/agent-os/software_
|
||||
@@ -0,0 +1,66 @@
|
||||
# SQLite
|
||||
|
||||
> Source: `src/content/docs/agent-os/sqlite.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/sqlite
|
||||
> Description: Give agents access to a persistent SQLite database via host tools.
|
||||
|
||||
---
|
||||
Each agentOS actor has a built-in SQLite database that persists across sessions and sleep/wake cycles. Expose it to agents as a host tool so they can run arbitrary SQL queries.
|
||||
|
||||
## Example
|
||||
|
||||
Give the agent a single `sql` tool that executes any SQL query against the actor's SQLite database.
|
||||
|
||||
```ts @nocheck
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { db } from "rivetkit/db";
|
||||
import { toolKit, hostTool } from "@rivet-dev/agent-os-core";
|
||||
import { z } from "zod";
|
||||
|
||||
const actorDb = db({});
|
||||
|
||||
const sqlToolkit = toolKit({
|
||||
name: "db",
|
||||
description: "SQLite database",
|
||||
tools: {
|
||||
sql: hostTool({
|
||||
description: "Execute a SQL query against the actor's SQLite database. Use this for creating tables, inserting data, and querying data. Returns rows for SELECT queries.",
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe("SQL query to execute"),
|
||||
}),
|
||||
execute: async (input) => {
|
||||
const result = await actorDb.execute(input.query);
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
db: actorDb,
|
||||
options: { software: [common, pi], toolKits: [sqlToolkit] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
The agent calls it as a CLI command:
|
||||
|
||||
```bash
|
||||
agentos-db sql --query "CREATE TABLE notes (id INTEGER PRIMARY KEY, content TEXT, created_at INTEGER)"
|
||||
agentos-db sql --query "INSERT INTO notes (content, created_at) VALUES ('auth uses JWT with 24h expiry', 1711843200000)"
|
||||
agentos-db sql --query "SELECT * FROM notes WHERE content LIKE '%auth%'"
|
||||
```
|
||||
|
||||
The database persists in the actor's storage across sessions and sleep/wake cycles. The agent can create whatever schema it needs and build up structured data over time.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- See [SQLite](/docs/actors/sqlite) for the full SQLite API reference.
|
||||
- See [Tools](/docs/agent-os/tools) for how host tools work.
|
||||
|
||||
_Source doc path: /docs/agent-os/sqlite_
|
||||
@@ -0,0 +1,24 @@
|
||||
# System Prompt
|
||||
|
||||
> Source: `src/content/docs/agent-os/system-prompt.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/system-prompt
|
||||
> Description: How agentOS injects context into agent sessions.
|
||||
|
||||
---
|
||||
agentOS automatically injects a system prompt into every agent session that describes the VM environment and available tools. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.).
|
||||
|
||||
The base prompt lives at `/etc/agentos/instructions.md` inside the VM.
|
||||
|
||||
## Customization
|
||||
|
||||
```ts @nocheck
|
||||
const session = await vm.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
// Append custom instructions
|
||||
additionalInstructions: "Always write tests before implementation.",
|
||||
// Suppress the base OS prompt (tool docs are still injected)
|
||||
skipOsInstructions: true,
|
||||
});
|
||||
```
|
||||
|
||||
_Source doc path: /docs/agent-os/system-prompt_
|
||||
151
.agents/skills/ai-agent-workspace/reference/agent-os/tools.md
Normal file
151
.agents/skills/ai-agent-workspace/reference/agent-os/tools.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Tools
|
||||
|
||||
> Source: `src/content/docs/agent-os/tools.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/tools
|
||||
> Description: Expose custom tools to agents as CLI commands inside the VM.
|
||||
|
||||
---
|
||||
Expose your JavaScript functions to agents as CLI commands inside the VM.
|
||||
|
||||
- **Define tools on the host** with Zod input schemas
|
||||
- **Auto-generated CLI commands** installed at `/usr/local/bin/agentos-{toolkit}`
|
||||
- **Code mode compatible** for up to 80% token reduction since tool calls are just shell commands
|
||||
- **Tool list injected** into the agent's [system prompt](/docs/agent-os/system-prompt) automatically
|
||||
|
||||
## Getting started
|
||||
|
||||
Define a toolkit with Zod input schemas and pass it to `agentOs()`. Each tool becomes a CLI command inside the VM.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { toolKit, hostTool } from "@rivet-dev/agent-os-core";
|
||||
import { z } from "zod";
|
||||
|
||||
const weatherToolkit = toolKit({
|
||||
name: "weather",
|
||||
description: "Weather data tools",
|
||||
tools: {
|
||||
forecast: hostTool({
|
||||
description: "Get the weather forecast for a city",
|
||||
inputSchema: z.object({
|
||||
city: z.string().describe("City name"),
|
||||
days: z.number().optional().describe("Number of days"),
|
||||
}),
|
||||
execute: async (input) => {
|
||||
const res = await fetch(`https://api.weather.example/forecast?city=${input.city}&days=${input.days ?? 3}`);
|
||||
return res.json();
|
||||
},
|
||||
examples: [
|
||||
{ description: "3-day forecast for Paris", input: { city: "Paris", days: 3 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi],
|
||||
toolKits: [weatherToolkit],
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const agent = client.vm.getOrCreate(["my-agent"]);
|
||||
|
||||
const session = await agent.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agent.sendPrompt(session.sessionId, "What's the weather in Paris?");
|
||||
```
|
||||
|
||||
### Zod to CLI mapping
|
||||
|
||||
Zod schema fields are converted to CLI flags automatically. Field names are converted from camelCase to kebab-case.
|
||||
|
||||
| Zod type | CLI syntax | Example |
|
||||
|---|---|---|
|
||||
| `z.string()` | `--name value` | `--path /tmp/out.png` |
|
||||
| `z.number()` | `--name 42` | `--limit 5` |
|
||||
| `z.boolean()` | `--flag` / `--no-flag` | `--full-page` |
|
||||
| `z.enum(["a","b"])` | `--name a` | `--format json` |
|
||||
| `z.array(z.string())` | `--name a --name b` | `--tags foo --tags bar` |
|
||||
|
||||
Optional fields (via `.optional()`) become optional flags. Required fields are enforced at validation time.
|
||||
|
||||
### What the agent sees
|
||||
|
||||
When toolkits are registered, CLI shims are installed at `/usr/local/bin/agentos-{name}` inside the VM and the tool list is injected into the agent's [system prompt](/docs/agent-os/system-prompt).
|
||||
|
||||
The agent interacts with tools as shell commands:
|
||||
|
||||
```bash
|
||||
# List all available toolkits
|
||||
agentos list-tools
|
||||
|
||||
# List tools in a specific toolkit
|
||||
agentos list-tools weather
|
||||
|
||||
# Get help for a tool
|
||||
agentos-weather forecast --help
|
||||
|
||||
# Call a tool with flags
|
||||
agentos-weather forecast --city Paris --days 3
|
||||
|
||||
# Call a tool with inline JSON
|
||||
agentos-weather forecast --json '{"city":"Paris","days":3}'
|
||||
|
||||
# Call a tool with JSON from a file
|
||||
agentos-weather forecast --json-file /tmp/input.json
|
||||
|
||||
# Pipe JSON via stdin
|
||||
echo '{"city":"Paris"}' | agentos-weather forecast
|
||||
```
|
||||
|
||||
Tool responses are JSON:
|
||||
|
||||
```json
|
||||
{"ok":true,"result":{"temperature":22,"condition":"sunny"}}
|
||||
```
|
||||
|
||||
Error responses include an error code and message:
|
||||
|
||||
```json
|
||||
{"ok":false,"error":"VALIDATION_ERROR","message":"Expected string at \"city\", received number"}
|
||||
```
|
||||
|
||||
## Tools vs MCP servers
|
||||
|
||||
agentOS supports two ways to give agents access to external functionality: **host tools** and **MCP servers**. Both work, but they have different tradeoffs.
|
||||
|
||||
| | Host Tools | MCP Servers |
|
||||
|---|---|---|
|
||||
| **How it works** | Call JavaScript functions on the host directly | Connect to a standard MCP server |
|
||||
| **Authentication** | None required. Direct binding to the agent's OS. | Requires custom auth configuration per server |
|
||||
| **Code mode** | Built-in. Tools are exposed as CLI commands, so agents can call them inside scripts for up to 80% token reduction. | Requires extra work to make code mode work out of the box |
|
||||
| **Latency** | Near-zero. Bound directly to the host process. | Extra network hop to reach the MCP server |
|
||||
| **Setup** | Define tools in your actor code with Zod schemas | Configure any standard MCP server |
|
||||
|
||||
Use host tools when you want to expose your own JavaScript functions to agents. Use MCP servers when you want to connect to existing third-party services. See [Sessions](/docs/agent-os/sessions#mcpservers) for MCP server configuration.
|
||||
|
||||
## Security
|
||||
|
||||
Tool calls from the agent securely invoke your `execute()` functions on the host. Your functions run with full access to the host environment, so you can call databases, APIs, and services directly without proxying credentials into the VM. The agent never sees the credentials — it only sees the tool's input/output contract.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Keep tool descriptions concise. They are injected into the agent's system prompt and consume tokens.
|
||||
- Use `.describe()` on Zod fields to generate useful `--help` output.
|
||||
- Set explicit timeouts on long-running tools instead of relying on the 30s default.
|
||||
- Tools execute on the host with full access to the host environment. Do not expose tools that could compromise the host without appropriate safeguards.
|
||||
|
||||
_Source doc path: /docs/agent-os/tools_
|
||||
@@ -0,0 +1,54 @@
|
||||
# agentOS vs Sandbox
|
||||
|
||||
> Source: `src/content/docs/agent-os/versus-sandbox.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/versus-sandbox
|
||||
> Description: When to use the lightweight agentOS VM, a full sandbox, or both together.
|
||||
|
||||
---
|
||||
- **agentOS** is a lightweight VM that runs inside your process. Near-zero cold start, low memory, direct backend integration via [host tools](/docs/agent-os/tools).
|
||||
- **Sandboxes** are full Linux environments with root access, system packages, and native binary support.
|
||||
- **You can use both.** agentOS works with sandboxes through [sandbox mounting](/docs/agent-os/sandbox). Agents run in the lightweight VM by default and spin up a full sandbox on demand.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | agentOS VM | Full Sandbox |
|
||||
|---|---|---|
|
||||
| **Cost** | Very low. Runs in your process. | Pay per second of uptime. |
|
||||
| **Startup** | Near-zero cold start (~6 ms). | Seconds to spin up. |
|
||||
| **Backend integration** | Direct. [Host tools](/docs/agent-os/tools) call your functions with zero latency. | Indirect. Requires network calls back to your backend. |
|
||||
| **API keys** | Stay on the server via the [LLM gateway](/docs/agent-os/llm-gateway). | Must be injected into the sandbox environment. |
|
||||
| **Permissions** | Granular, deny-by-default. | Coarse-grained (container-level). |
|
||||
| **Infrastructure** | `npm install` | Vendor account + API keys. |
|
||||
| **Best for** | Coding, file manipulation, scripting, API calls, orchestration. | Browsers, desktop automation, native compilation, dev servers. |
|
||||
|
||||
## When to use each
|
||||
|
||||
### agentOS VM
|
||||
|
||||
Use the lightweight VM for most agent workloads:
|
||||
|
||||
- Coding and file editing
|
||||
- Running scripts and CLI tools
|
||||
- Calling APIs and services via host tools
|
||||
- Multi-agent orchestration and workflows
|
||||
- Tasks where backend integration matters (permissions, tool access, LLM routing)
|
||||
|
||||
### Full sandbox
|
||||
|
||||
Spin up a sandbox when the workload needs a real Linux kernel:
|
||||
|
||||
- Browsers and desktop automation (Playwright, Puppeteer, Selenium)
|
||||
- Heavy compilation and native toolchains
|
||||
- Dev servers with hot reload, databases, and system ports
|
||||
- GUI applications and VNC sessions
|
||||
|
||||
### Both together
|
||||
|
||||
Use agentOS with [sandbox mounting](/docs/agent-os/sandbox) for workflows that need both:
|
||||
|
||||
- Agent runs in the agentOS VM with full access to host tools and permissions
|
||||
- Sandbox spins up on demand for heavy tasks
|
||||
- Sandbox filesystem is mounted into the VM as a native directory
|
||||
- Agent reads and writes sandbox files the same way it reads local files
|
||||
|
||||
_Source doc path: /docs/agent-os/versus-sandbox_
|
||||
103
.agents/skills/ai-agent-workspace/reference/agent-os/webhooks.md
Normal file
103
.agents/skills/ai-agent-workspace/reference/agent-os/webhooks.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Webhooks
|
||||
|
||||
> Source: `src/content/docs/agent-os/webhooks.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/webhooks
|
||||
> Description: Trigger agent workflows from external webhooks using Hono and queues.
|
||||
|
||||
---
|
||||
Use a lightweight HTTP server to receive webhooks and queue them for agent processing. This example uses [Hono](https://hono.dev) to receive Slack webhooks and dispatch them to an agent via queues.
|
||||
|
||||
## Example: Slack webhook to agent
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, queue, setup } from "rivetkit";
|
||||
import { Hono } from "hono";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
// Actor that processes Slack messages via a queue
|
||||
const slackWorker = actor({
|
||||
queues: {
|
||||
messages: queue<{ channel: string; text: string; user: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
const agentHandle = c.actors.vm.getOrCreate(["slack-agent"]);
|
||||
|
||||
for await (const message of c.queue.iter()) {
|
||||
const { channel, text, user } = message.body;
|
||||
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
const response = await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Slack message from ${user} in #${channel}:\n\n${text}\n\nRespond helpfully.`,
|
||||
);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
|
||||
// Post the response back to Slack
|
||||
await fetch("https://slack.com/api/chat.postMessage", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({ channel, text: response }),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { slackWorker, vm } });
|
||||
registry.start();
|
||||
|
||||
// Hono server to receive Slack webhooks
|
||||
const app = new Hono();
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
app.post("/slack/events", async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
// Handle Slack URL verification
|
||||
if (body.type === "url_verification") {
|
||||
return c.json({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
// Queue the message for the agent
|
||||
if (body.event?.type === "message" && !body.event?.bot_id) {
|
||||
const worker = client.slackWorker.getOrCreate(["main"]);
|
||||
await worker.send("messages", {
|
||||
channel: body.event.channel,
|
||||
text: body.event.text,
|
||||
user: body.event.user,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. Slack sends an HTTP POST to `/slack/events`
|
||||
2. The Hono handler validates the event and pushes it to the actor's queue
|
||||
3. The queue processes messages one at a time, creating agent sessions for each
|
||||
4. The agent responds and the worker posts the reply back to Slack
|
||||
|
||||
The queue provides backpressure and durability. If the agent is busy, messages wait in the queue. If the server restarts, queued messages are replayed.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use [Queues](/docs/agent-os/queues) to decouple webhook ingestion from agent processing. This prevents slow agent responses from blocking the webhook endpoint.
|
||||
- Return `200` from the webhook handler immediately after queuing. External services like Slack have short timeout windows.
|
||||
- Store webhook secrets in environment variables, not in code.
|
||||
|
||||
_Source doc path: /docs/agent-os/webhooks_
|
||||
@@ -0,0 +1,163 @@
|
||||
# Workflow Automation
|
||||
|
||||
> Source: `src/content/docs/agent-os/workflows.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/agent-os/workflows
|
||||
> Description: Orchestrate multi-step agent tasks with durable workflows.
|
||||
|
||||
---
|
||||
- **Durable workflows** that survive crashes and restarts
|
||||
- **Multi-step orchestration** with sessions, file operations, and process execution
|
||||
- **Error handling and retry** via `ctx.step()` for each operation
|
||||
- **Agent chaining** where output of one session feeds into the next
|
||||
|
||||
## Basic workflow
|
||||
|
||||
Use the actor `workflow()` primitive to orchestrate a multi-step agent task. Each step is durable and will resume from where it left off after a restart.
|
||||
|
||||
Session creation and prompting must happen within the same step because sessions are ephemeral and won't survive a replay.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, setup, workflow } from "rivetkit";
|
||||
|
||||
const automator = actor({
|
||||
workflows: {
|
||||
fixBug: workflow<{ repo: string; issue: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.workflow.iter("fixBug")) {
|
||||
const { repo, issue } = message.body;
|
||||
const agentHandle = c.actors.vm.getOrCreate([`fix-${issue}`]);
|
||||
|
||||
// Step 1: Clone the repo
|
||||
await c.step("clone-repo", async () => {
|
||||
return agentHandle.exec(`git clone ${repo} /home/user/repo`);
|
||||
});
|
||||
|
||||
// Step 2: Agent fixes the bug (session lives within this step)
|
||||
await c.step("fix-bug", async () => {
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Fix the bug described in issue: ${issue}`,
|
||||
);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
});
|
||||
|
||||
// Step 3: Run tests
|
||||
const tests = await c.step("run-tests", async () => {
|
||||
return agentHandle.exec("cd /home/user/repo && npm test");
|
||||
});
|
||||
|
||||
console.log("Tests exit code:", tests.exitCode);
|
||||
await message.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { automator, vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.automator.getOrCreate(["main"]);
|
||||
|
||||
// Trigger the workflow
|
||||
await handle.send("fixBug", {
|
||||
repo: "https://github.com/example/repo.git",
|
||||
issue: "Fix the login redirect bug",
|
||||
});
|
||||
```
|
||||
|
||||
## Agent chaining
|
||||
|
||||
Output of one agent session feeds into the next. Each session is created and completed within its own step.
|
||||
|
||||
```ts @nocheck server.ts
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
import { actor, setup, workflow } from "rivetkit";
|
||||
|
||||
const pipeline = actor({
|
||||
workflows: {
|
||||
codeReview: workflow<{ filePath: string }>(),
|
||||
},
|
||||
run: async (c) => {
|
||||
for await (const message of c.workflow.iter("codeReview")) {
|
||||
const agentHandle = c.actors.vm.getOrCreate([`review-${Date.now()}`]);
|
||||
|
||||
// Step 1: Agent reviews code and writes findings to a file
|
||||
await c.step("review", async () => {
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Review the code at ${message.body.filePath} and write your findings to /home/user/review.md`,
|
||||
);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
});
|
||||
|
||||
// Step 2: Read the review from the filesystem
|
||||
const review = await c.step("read-review", async () => {
|
||||
const content = await agentHandle.readFile("/home/user/review.md");
|
||||
return new TextDecoder().decode(content);
|
||||
});
|
||||
|
||||
// Step 3: Second session applies fixes based on the review
|
||||
await c.step("fix", async () => {
|
||||
const session = await agentHandle.createSession("pi", {
|
||||
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
|
||||
});
|
||||
await agentHandle.sendPrompt(
|
||||
session.sessionId,
|
||||
`Apply the following review feedback:\n\n${review}`,
|
||||
);
|
||||
await agentHandle.closeSession(session.sessionId);
|
||||
});
|
||||
|
||||
await message.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { pipeline, vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```ts @nocheck client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./server";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
const handle = client.pipeline.getOrCreate(["main"]);
|
||||
|
||||
await handle.send("codeReview", { filePath: "/home/user/src/auth.ts" });
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Create and close sessions within the same step. Sessions are ephemeral and won't exist after a workflow replays.
|
||||
- Pass data between steps via the filesystem or step return values, not session state.
|
||||
- Keep step names stable across code changes. Renaming a step breaks replay for in-progress workflows.
|
||||
- Use separate actors for the workflow orchestrator and the agentOS VM.
|
||||
- See [Workflows](/docs/actors/workflows) for the full workflow API reference including timers, joins, and races.
|
||||
|
||||
_Source doc path: /docs/agent-os/workflows_
|
||||
103
.agents/skills/ai-agent-workspace/reference/cli.md
Normal file
103
.agents/skills/ai-agent-workspace/reference/cli.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# CLI
|
||||
|
||||
> Source: `src/content/docs/cli.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/cli
|
||||
> Description: Reference for the optional rivet CLI: deploy to Rivet Compute and run local dev for serverless platforms.
|
||||
|
||||
---
|
||||
The `rivet` CLI (`@rivetkit/cli`) is optional. You only need it for:
|
||||
|
||||
| Use case | Command |
|
||||
| --- | --- |
|
||||
| Deploy to Rivet Compute | `rivet deploy` |
|
||||
| Local dev for serverless platforms (Cloudflare, Supabase) | `rivet dev --provider <name>` |
|
||||
|
||||
Run it with your package runner:
|
||||
|
||||
```bash
|
||||
npx @rivetkit/cli <command>
|
||||
```
|
||||
|
||||
| Command | Description |
|
||||
| --- | --- |
|
||||
| `rivet dev` | Run a local engine and your handler's dev server. |
|
||||
| `rivet deploy` | Build and deploy the project to Rivet Cloud. |
|
||||
| `rivet engine` | Run the bundled `rivet-engine` binary directly. |
|
||||
| `rivet setup-ci` | Install the GitHub Actions deploy workflow. |
|
||||
|
||||
## `rivet dev`
|
||||
|
||||
Starts a local engine, spawns your dev server, and registers the serverless runner pointing at it. The engine keeps running across restarts; Ctrl-C stops only the dev server.
|
||||
|
||||
```bash
|
||||
rivet dev [--provider <serverless|cloudflare|supabase|none>] [--port N] [--fn-name NAME] [--url URL] [-- <command>...]
|
||||
```
|
||||
|
||||
`--provider` selects how the dev server is launched. Anything after `--` is appended to the preset command (or is the command to run when no provider is set).
|
||||
|
||||
| `--provider` | Spawns | Port |
|
||||
| --- | --- | --- |
|
||||
| _(omitted)_ | your `-- <command>` (needs `--port`) | from `--port` |
|
||||
| `serverless` | your `-- <command>` (gets `PORT`) | auto |
|
||||
| `cloudflare` | `wrangler dev` | `8787` |
|
||||
| `supabase` | `supabase functions serve` | `54321` |
|
||||
| `none` | nothing (engine only) | — |
|
||||
|
||||
For `cloudflare`, the CLI also passes the engine endpoint as `--var RIVET_ENDPOINT:...`, so the Worker connects back with no `wrangler.toml` config.
|
||||
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `--port` | Handler port. Required without a provider unless `--url` is set. |
|
||||
| `--fn-name` | Supabase function name (default `rivet`). |
|
||||
| `--url` | Explicit handler URL, overriding port and path. |
|
||||
| `--engine-binary` | Path to a `rivet-engine` binary. |
|
||||
|
||||
## `rivet deploy`
|
||||
|
||||
Builds and pushes your project's Docker image and upserts the managed pool, printing the dashboard URL. See [Deploying to Rivet Compute](/docs/deploy/rivet-compute).
|
||||
|
||||
```bash
|
||||
rivet deploy --token cloud_api_xxxxx
|
||||
```
|
||||
|
||||
The token is saved to `~/.rivet/credentials` (also read from `RIVET_CLOUD_TOKEN`), so later deploys can omit it.
|
||||
|
||||
| Flag | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `--token` | env / credentials | Rivet Cloud API token. |
|
||||
| `--namespace` | `production` | Cloud namespace. |
|
||||
| `--project` / `--org` | from token | Override project/org. |
|
||||
| `--dockerfile` | `Dockerfile` | Dockerfile to build. |
|
||||
| `--build-context` | `.` | Docker build context. |
|
||||
| `--env KEY=VAL` | — | Environment override, repeatable. |
|
||||
| `--image` | project slug | Image repository name. |
|
||||
| `--tag` | git short SHA | Image tag. |
|
||||
|
||||
## `rivet engine`
|
||||
|
||||
Runs the bundled `rivet-engine` binary directly, against the same local database and ports as `rivet dev`. Arguments are forwarded verbatim.
|
||||
|
||||
```bash
|
||||
rivet engine nuke # wipe local engine state
|
||||
rivet engine wf list # inspect workflows
|
||||
```
|
||||
|
||||
## `rivet setup-ci`
|
||||
|
||||
Installs `.github/workflows/rivet-deploy.yml`, which deploys to Rivet Cloud on push and pull request. Add `--force` to overwrite. Then set the token secret:
|
||||
|
||||
```bash
|
||||
gh secret set RIVET_CLOUD_TOKEN
|
||||
```
|
||||
|
||||
## Engine binary resolution
|
||||
|
||||
`rivet dev` and `rivet engine` resolve the `rivet-engine` binary from, in order: `--engine-binary`, `RIVET_ENGINE_BINARY_PATH`, a binary bundled next to the CLI, a local `target/{debug,release}` build, then an auto-downloaded release. Set `RIVETKIT_ENGINE_AUTO_DOWNLOAD=0` to require a local binary.
|
||||
|
||||
## Related
|
||||
|
||||
- [Deploying to Rivet Compute](/docs/deploy/rivet-compute)
|
||||
- [Cloudflare Workers Quickstart](/docs/actors/quickstart/cloudflare)
|
||||
- [Supabase Functions Quickstart](/docs/actors/quickstart/supabase)
|
||||
|
||||
_Source doc path: /docs/cli_
|
||||
@@ -0,0 +1,288 @@
|
||||
# Node.js & Bun
|
||||
|
||||
> Source: `src/content/docs/clients/javascript.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/clients/javascript
|
||||
> Description: Connect JavaScript apps to Rivet Actors.
|
||||
|
||||
---
|
||||
## Getting Started
|
||||
|
||||
See the [backend quickstart guide](/docs/actors/quickstart/backend) for getting started.
|
||||
|
||||
## Minimal Client
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>({
|
||||
endpoint: "https://my-namespace:pk_...@api.rivet.dev",
|
||||
});
|
||||
const counter = client.counter.getOrCreate(["my-counter"]);
|
||||
const count = await counter.increment(1);
|
||||
```
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Stateless vs Stateful
|
||||
|
||||
```typescript
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const handle = client.counter.getOrCreate(["my-counter"]);
|
||||
|
||||
// Stateless: each call is independent
|
||||
await handle.increment(1);
|
||||
|
||||
// Stateful: keep a connection open for realtime events
|
||||
const conn = handle.connect();
|
||||
conn.on("count", (value) => console.log(value));
|
||||
await conn.increment(1);
|
||||
```
|
||||
|
||||
## Getting Actors
|
||||
|
||||
```typescript
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const room = client.chatRoom.getOrCreate(["room-42"]);
|
||||
const existing = client.chatRoom.get(["room-42"]);
|
||||
|
||||
const created = await client.game.create(["game-1"], {
|
||||
input: { mode: "ranked" },
|
||||
});
|
||||
|
||||
const byId = client.chatRoom.getForId("actor-id");
|
||||
const resolvedId = await room.resolve();
|
||||
```
|
||||
|
||||
## Connection Parameters
|
||||
|
||||
```typescript params
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const chat = client.chatRoom.getOrCreate(["general"], {
|
||||
params: { authToken: "jwt-token-here" },
|
||||
});
|
||||
|
||||
const conn = chat.connect();
|
||||
```
|
||||
|
||||
```typescript getParams
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
async function getAuthToken(): Promise<string> {
|
||||
return "jwt-token-here";
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const chat = client.chatRoom.getOrCreate(["general"], {
|
||||
getParams: async () => ({
|
||||
authToken: await getAuthToken(),
|
||||
}),
|
||||
});
|
||||
|
||||
const conn = chat.connect();
|
||||
```
|
||||
|
||||
Use `params` for static connection parameters. Use `getParams` when the value can change between connection attempts, such as refreshing a JWT before each `.connect()` or reconnect.
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
```typescript
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const conn = client.chatRoom.getOrCreate(["general"]).connect();
|
||||
conn.on("message", (msg) => console.log(msg));
|
||||
conn.once("gameOver", () => console.log("done"));
|
||||
```
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
```typescript
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const conn = client.chatRoom.getOrCreate(["general"]).connect();
|
||||
|
||||
conn.onOpen(() => console.log("connected"));
|
||||
conn.onClose(() => console.log("disconnected"));
|
||||
conn.onError((err) => console.error("error:", err));
|
||||
conn.onStatusChange((status) => console.log("status:", status));
|
||||
|
||||
await conn.dispose();
|
||||
```
|
||||
|
||||
## Low-Level HTTP & WebSocket
|
||||
|
||||
For actors that implement `onRequest` or `onWebSocket`, call them directly:
|
||||
|
||||
```ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const handle = client.chatRoom.getOrCreate(["general"]);
|
||||
|
||||
const response = await handle.fetch("history");
|
||||
const history = await response.json();
|
||||
|
||||
const ws = await handle.webSocket("stream");
|
||||
ws.addEventListener("message", (event) => {
|
||||
console.log("message:", event.data);
|
||||
});
|
||||
ws.send("hello");
|
||||
```
|
||||
|
||||
## Calling from Backend
|
||||
|
||||
```typescript
|
||||
import { Hono } from "hono";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const app = new Hono();
|
||||
const client = createClient();
|
||||
|
||||
app.post("/increment/:name", async (c) => {
|
||||
const counterHandle = client.counter.getOrCreate([c.req.param("name")]);
|
||||
const newCount = await counterHandle.increment(1);
|
||||
return c.json({ count: newCount });
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { ActorError } from "rivetkit/client";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
|
||||
try {
|
||||
await client.user.getOrCreate(["user-123"]).updateUsername("ab");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.code, error.metadata);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
```ts client.ts
|
||||
import { createClient } from "rivetkit/client";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const client = createClient<typeof registry>("http://localhost:6420");
|
||||
|
||||
// Compound key: [org, room]
|
||||
client.chatRoom.getOrCreate(["org-acme", "general"]);
|
||||
```
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
actions: {
|
||||
getRoomInfo: (c) => ({ org: c.key[0], room: c.key[1] }),
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { chatRoom },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
Don't build keys with string interpolation like `"org:${userId}"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
`createClient()` automatically reads:
|
||||
|
||||
- `RIVET_ENDPOINT` (endpoint)
|
||||
- `RIVET_NAMESPACE`
|
||||
- `RIVET_TOKEN`
|
||||
- `RIVET_RUNNER`
|
||||
|
||||
Defaults to `http://localhost:6420` when unset. RivetKit runs on port 6420 by default.
|
||||
|
||||
### Endpoint Format
|
||||
|
||||
Endpoints support URL auth syntax:
|
||||
|
||||
```
|
||||
https://namespace:token@api.rivet.dev
|
||||
```
|
||||
|
||||
You can also pass the endpoint without auth and provide `RIVET_NAMESPACE` and `RIVET_TOKEN` separately. For serverless deployments, use your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints#url-auth-syntax) for details.
|
||||
|
||||
## Advanced
|
||||
|
||||
### Skip Ready Wait
|
||||
|
||||
Requests are normally held at the gateway until the actor is ready to accept traffic. An actor is not ready while it's still starting (before `onWake` finishes) or while it's in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence) (running `onSleep`, `waitUntil`, and pending disconnects).
|
||||
|
||||
Pass `skipReadyWait: true` on the [low-level HTTP and WebSocket APIs](#low-level-http--websocket) to deliver immediately and reach the actor's `onRequest` / `onWebSocket` handler in either window:
|
||||
|
||||
```ts @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const handle = client.chatRoom.getOrCreate(["general"]);
|
||||
|
||||
const response = await handle.fetch("/healthz", {
|
||||
skipReadyWait: true,
|
||||
});
|
||||
|
||||
const ws = await handle.webSocket("probe", undefined, {
|
||||
skipReadyWait: true,
|
||||
});
|
||||
```
|
||||
|
||||
Requests can still return transient lifecycle or gateway errors. Retry once the actor is available again.
|
||||
|
||||
- `actor.stopping`: the actor has fully stopped, i.e. the sleep grace period has ended but it has not yet restarted.
|
||||
- `guard.actor_stopped_while_waiting`: the request reached the actor tunnel, but the actor stopped before the gateway received a response.
|
||||
- `guard.tunnel_request_aborted`: the actor tunnel aborted the request before a response started.
|
||||
- `guard.tunnel_message_timeout`: the gateway dropped the in-flight tunnel request after its tunnel message timeout.
|
||||
- `guard.tunnel_response_closed`: the actor tunnel closed before sending a response.
|
||||
- `guard.gateway_response_start_timeout`: the gateway timed out waiting for the actor response to start.
|
||||
|
||||
## API Reference
|
||||
|
||||
**Package:** [rivetkit](https://www.npmjs.com/package/rivetkit)
|
||||
|
||||
See the [RivetKit client overview](/docs/clients).
|
||||
|
||||
- [`createClient`](/typedoc/functions/rivetkit.client_mod.createClient.html) - Create a client
|
||||
- [`Client`](/typedoc/types/rivetkit.mod.Client.html) - Client type
|
||||
|
||||
_Source doc path: /docs/clients/javascript_
|
||||
273
.agents/skills/ai-agent-workspace/reference/clients/react.md
Normal file
273
.agents/skills/ai-agent-workspace/reference/clients/react.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# React
|
||||
|
||||
> Source: `src/content/docs/clients/react.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/clients/react
|
||||
> Description: Connect React apps to Rivet Actors.
|
||||
|
||||
---
|
||||
## Getting Started
|
||||
|
||||
See the [React quickstart guide](/docs/actors/quickstart/react) for getting started.
|
||||
|
||||
## Install
|
||||
|
||||
## Minimal Client
|
||||
|
||||
```tsx Counter.tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const { useActor } = createRivetKit<typeof registry>({
|
||||
endpoint: "https://my-namespace:pk_...@api.rivet.dev",
|
||||
});
|
||||
|
||||
function Counter() {
|
||||
const { connection, connStatus } = useActor({ name: "counter", key: ["my-counter"] });
|
||||
|
||||
if (connStatus !== "connected" || !connection) return <div>Connecting...</div>;
|
||||
return <button onClick={() => connection.increment(1)}>+</button>;
|
||||
}
|
||||
```
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
c.broadcast("newCount", c.state.count);
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
## Stateless vs Stateful
|
||||
|
||||
```tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
|
||||
const { useActor } = createRivetKit();
|
||||
|
||||
function Counter() {
|
||||
const counter = useActor({ name: "counter", key: ["my-counter"] });
|
||||
|
||||
const increment = async () => {
|
||||
await counter.connection?.increment(1);
|
||||
};
|
||||
|
||||
return <button onClick={increment}>+</button>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Stateless: use createClient for one-off calls (SSR or utilities)
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
await client.counter.getOrCreate(["my-counter"]).increment(1);
|
||||
```
|
||||
|
||||
## Getting Actors
|
||||
|
||||
```tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const { useActor } = createRivetKit();
|
||||
|
||||
function ChatRoom() {
|
||||
const room = useActor({ name: "chatRoom", key: ["room-42"] });
|
||||
return <div>{room.connStatus}</div>;
|
||||
}
|
||||
|
||||
// For get/getOrCreate/create/getForId, use createClient
|
||||
const client = createClient();
|
||||
const handle = client.chatRoom.getOrCreate(["room-42"]);
|
||||
const existing = client.chatRoom.get(["room-42"]);
|
||||
const created = await client.game.create(["game-1"], { input: { mode: "ranked" } });
|
||||
const byId = client.chatRoom.getForId("actor-id");
|
||||
const resolvedId = await handle.resolve();
|
||||
```
|
||||
|
||||
## Connection Parameters
|
||||
|
||||
```tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
|
||||
const { useActor } = createRivetKit();
|
||||
|
||||
function Chat() {
|
||||
const chat = useActor({
|
||||
name: "chatRoom",
|
||||
key: ["general"],
|
||||
params: { authToken: "jwt-token-here" },
|
||||
});
|
||||
|
||||
return <div>{chat.connStatus}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
```tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
|
||||
const { useActor } = createRivetKit();
|
||||
|
||||
function Chat() {
|
||||
const chat = useActor({ name: "chatRoom", key: ["general"] });
|
||||
|
||||
chat.useEvent("message", (msg) => {
|
||||
console.log("message:", msg);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
```tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
|
||||
const { useActor } = createRivetKit();
|
||||
|
||||
function CounterStatus() {
|
||||
const actor = useActor({ name: "counter", key: ["my-counter"] });
|
||||
|
||||
if (actor.connStatus === "connected") {
|
||||
console.log("connected");
|
||||
}
|
||||
|
||||
if (actor.error) {
|
||||
console.error(actor.error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Low-Level HTTP & WebSocket
|
||||
|
||||
Use the JavaScript client for raw HTTP and WebSocket access:
|
||||
|
||||
```tsx @nocheck
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const handle = client.chatRoom.getOrCreate(["general"]);
|
||||
|
||||
const response = await handle.fetch("history");
|
||||
const history = await response.json();
|
||||
|
||||
const ws = await handle.webSocket("stream");
|
||||
ws.addEventListener("message", (event) => {
|
||||
console.log("message:", event.data);
|
||||
});
|
||||
ws.send("hello");
|
||||
```
|
||||
|
||||
## Calling from Backend
|
||||
|
||||
Use the JavaScript client on your backend (Node.js/Bun). See the [JavaScript client docs](/docs/clients/javascript).
|
||||
|
||||
## Error Handling
|
||||
|
||||
```tsx
|
||||
import { ActorError } from "rivetkit/client";
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
|
||||
const { useActor } = createRivetKit();
|
||||
|
||||
function Profile() {
|
||||
const actor = useActor({ name: "user", key: ["user-123"] });
|
||||
|
||||
const updateUsername = async () => {
|
||||
try {
|
||||
await actor.connection?.updateUsername("ab");
|
||||
} catch (error) {
|
||||
if (error instanceof ActorError) {
|
||||
console.log(error.code, error.metadata);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={updateUsername}>Update</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
```tsx ChatRoom.tsx
|
||||
import { createRivetKit } from "@rivetkit/react";
|
||||
import type { registry } from "./index";
|
||||
|
||||
const { useActor } = createRivetKit<typeof registry>("http://localhost:6420");
|
||||
|
||||
function ChatRoom() {
|
||||
const room = useActor({ name: "chatRoom", key: ["org-acme", "general"] });
|
||||
return <div>{room.connStatus}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```ts index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const chatRoom = actor({
|
||||
state: { messages: [] as string[] },
|
||||
actions: {
|
||||
getRoomInfo: (c) => ({ org: c.key[0], room: c.key[1] }),
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { chatRoom },
|
||||
});
|
||||
|
||||
registry.start();
|
||||
```
|
||||
|
||||
Don't build keys with string interpolation like `"org:${userId}"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
`createRivetKit()` (and the underlying `createClient()` instance) automatically read:
|
||||
|
||||
- `RIVET_ENDPOINT`
|
||||
- `RIVET_NAMESPACE`
|
||||
- `RIVET_TOKEN`
|
||||
- `RIVET_RUNNER`
|
||||
|
||||
Defaults to `http://localhost:6420` when unset. RivetKit runs on port 6420 by default.
|
||||
|
||||
### Endpoint Format
|
||||
|
||||
Endpoints support URL auth syntax:
|
||||
|
||||
```
|
||||
https://namespace:token@api.rivet.dev
|
||||
```
|
||||
|
||||
You can also pass the endpoint without auth and provide `RIVET_NAMESPACE` and `RIVET_TOKEN` separately. For serverless deployments, use your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints#url-auth-syntax) for details.
|
||||
|
||||
## API Reference
|
||||
|
||||
**Package:** [@rivetkit/react](https://www.npmjs.com/package/@rivetkit/react)
|
||||
|
||||
- [`createRivetKit`](/docs/clients/react) - Create hooks for React
|
||||
- [`useActor`](/docs/clients/react) - Hook for actor instances
|
||||
|
||||
_Source doc path: /docs/clients/react_
|
||||
454
.agents/skills/ai-agent-workspace/reference/clients/swift.md
Normal file
454
.agents/skills/ai-agent-workspace/reference/clients/swift.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# Swift
|
||||
|
||||
> Source: `src/content/docs/clients/swift.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/clients/swift
|
||||
> Description: Connect Swift apps to Rivet Actors.
|
||||
|
||||
---
|
||||
## Install
|
||||
|
||||
Add the Swift package dependency and import `RivetKitClient`:
|
||||
|
||||
```swift
|
||||
// Package.swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/rivet-dev/rivetkit-swift", from: "2.0.0")
|
||||
]
|
||||
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
.product(name: "RivetKitClient", package: "rivetkit-swift")
|
||||
]
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
## Minimal Client
|
||||
|
||||
### Endpoint URL
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(
|
||||
endpoint: "https://my-namespace:pk_...@api.rivet.dev"
|
||||
)
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||||
let count: Int = try await handle.action("increment", 1, as: Int.self)
|
||||
```
|
||||
|
||||
### Explicit Fields
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(
|
||||
endpoint: "https://api.rivet.dev",
|
||||
namespace: "my-namespace",
|
||||
token: "pk_..."
|
||||
)
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||||
let count: Int = try await handle.action("increment", 1, as: Int.self)
|
||||
```
|
||||
|
||||
## Stateless vs Stateful
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||||
|
||||
// Stateless: each call is independent
|
||||
let current: Int = try await handle.action("getCount", as: Int.self)
|
||||
print("Current count: \(current)")
|
||||
|
||||
// Stateful: keep a connection open for realtime events
|
||||
let conn = handle.connect()
|
||||
|
||||
// Subscribe to events using AsyncStream
|
||||
let eventTask = Task {
|
||||
for await count in await conn.events("count", as: Int.self) {
|
||||
print("Event: \(count)")
|
||||
}
|
||||
}
|
||||
|
||||
_ = try await conn.action("increment", 1, as: Int.self)
|
||||
|
||||
eventTask.cancel()
|
||||
await conn.dispose()
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Getting Actors
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
struct GameInput: Encodable {
|
||||
let mode: String
|
||||
}
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
// Get or create an actor
|
||||
let room = client.getOrCreate("chatRoom", ["room-42"])
|
||||
|
||||
// Get an existing actor (fails if not found)
|
||||
let existing = client.get("chatRoom", ["room-42"])
|
||||
|
||||
// Create a new actor with input
|
||||
let created = try await client.create(
|
||||
"game",
|
||||
["game-1"],
|
||||
options: CreateOptions(input: GameInput(mode: "ranked"))
|
||||
)
|
||||
|
||||
// Get actor by ID
|
||||
let byId = client.getForId("chatRoom", "actor-id")
|
||||
|
||||
// Resolve actor ID
|
||||
let resolvedId = try await room.resolve()
|
||||
print("Resolved ID: \(resolvedId)")
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
Actions support positional overloads for 0–5 args:
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||||
|
||||
let count: Int = try await handle.action("getCount")
|
||||
let updated: String = try await handle.action("rename", "new-name")
|
||||
let ok: Bool = try await handle.action("setScore", "user-1", 42)
|
||||
|
||||
print("Count: \(count), Updated: \(updated), OK: \(ok)")
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
If you need more than 5 arguments, use the raw JSON fallback:
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||||
|
||||
let args: [JSONValue] = [
|
||||
.string("user-1"),
|
||||
.number(.int(42)),
|
||||
.string("extra"),
|
||||
.string("more"),
|
||||
.string("args"),
|
||||
.string("here")
|
||||
]
|
||||
let ok: Bool = try await handle.action("setScore", args: args, as: Bool.self)
|
||||
print("OK: \(ok)")
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Connection Parameters
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
struct ConnParams: Encodable {
|
||||
let authToken: String
|
||||
}
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
let chat = client.getOrCreate(
|
||||
"chatRoom",
|
||||
["general"],
|
||||
options: GetOrCreateOptions(params: ConnParams(authToken: "jwt-token-here"))
|
||||
)
|
||||
|
||||
let conn = chat.connect()
|
||||
|
||||
// Use the connection...
|
||||
for await status in await conn.statusChanges() {
|
||||
print("Status: \(status.rawValue)")
|
||||
if status == .connected {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await conn.dispose()
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
|
||||
|
||||
// Subscribe to events using AsyncStream
|
||||
let messageTask = Task {
|
||||
for await (from, body) in await conn.events("message", as: (String, String).self) {
|
||||
print("\(from): \(body)")
|
||||
}
|
||||
}
|
||||
|
||||
// For one-time events, break after receiving
|
||||
let gameOverTask = Task {
|
||||
for await _ in await conn.events("gameOver", as: Void.self) {
|
||||
print("done")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Let it run for a bit
|
||||
try await Task.sleep(for: .seconds(5))
|
||||
|
||||
// Cancel when done
|
||||
messageTask.cancel()
|
||||
gameOverTask.cancel()
|
||||
await conn.dispose()
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
Event streams support 0–5 typed arguments. If you need raw values or more than 5 arguments, use `JSONValue`:
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
|
||||
|
||||
let rawTask = Task {
|
||||
for await args in await conn.events("message") {
|
||||
print(args)
|
||||
}
|
||||
}
|
||||
|
||||
try await Task.sleep(for: .seconds(5))
|
||||
rawTask.cancel()
|
||||
await conn.dispose()
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
|
||||
|
||||
// Monitor status changes (immediately yields current status)
|
||||
let statusTask = Task {
|
||||
for await status in await conn.statusChanges() {
|
||||
print("status: \(status.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor errors
|
||||
let errorTask = Task {
|
||||
for await error in await conn.errors() {
|
||||
print("error: \(error.group).\(error.code)")
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor open/close events
|
||||
let openTask = Task {
|
||||
for await _ in await conn.opens() {
|
||||
print("connected")
|
||||
}
|
||||
}
|
||||
|
||||
let closeTask = Task {
|
||||
for await _ in await conn.closes() {
|
||||
print("disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
// Check current status
|
||||
let current = await conn.currentStatus
|
||||
print("Current status: \(current.rawValue)")
|
||||
|
||||
// Let it run for a bit
|
||||
try await Task.sleep(for: .seconds(5))
|
||||
|
||||
// Cleanup
|
||||
statusTask.cancel()
|
||||
errorTask.cancel()
|
||||
openTask.cancel()
|
||||
closeTask.cancel()
|
||||
await conn.dispose()
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Low-Level HTTP & WebSocket
|
||||
|
||||
For actors that implement `onRequest` or `onWebSocket`, you can call them directly:
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let handle = client.getOrCreate("chatRoom", ["general"])
|
||||
|
||||
// Raw HTTP request
|
||||
let response = try await handle.fetch("history")
|
||||
let history: [String] = try response.json([String].self)
|
||||
print("History: \(history)")
|
||||
|
||||
// Raw WebSocket connection
|
||||
let websocket = try await handle.websocket(path: "stream")
|
||||
try await websocket.send(text: "hello")
|
||||
let message = try await websocket.receive()
|
||||
print("Received: \(message)")
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Calling from Backend
|
||||
|
||||
Use the same client in server-side Swift (Vapor, Hummingbird, etc.):
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
let handle = client.getOrCreate("counter", ["server-counter"])
|
||||
let count: Int = try await handle.action("increment", 1, as: Int.self)
|
||||
print("Count: \(count)")
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
do {
|
||||
_ = try await client.getOrCreate("user", ["user-123"])
|
||||
.action("updateUsername", "ab", as: String.self)
|
||||
} catch let error as ActorError {
|
||||
print("Error code: \(error.code)")
|
||||
print("Metadata: \(String(describing: error.metadata))")
|
||||
}
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
If you need an untyped response, you can decode to `JSONValue`:
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
let handle = client.getOrCreate("data", ["raw"])
|
||||
|
||||
let value: JSONValue = try await handle.action("getRawPayload")
|
||||
print("Raw value: \(value)")
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
```swift
|
||||
import RivetKitClient
|
||||
|
||||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||||
let client = RivetKitClient(config: config)
|
||||
|
||||
// Use compound keys for hierarchical addressing
|
||||
let room = client.getOrCreate("chatRoom", ["org-acme", "general"])
|
||||
let actorId = try await room.resolve()
|
||||
print("Actor ID: \(actorId)")
|
||||
|
||||
await client.dispose()
|
||||
```
|
||||
|
||||
Don't build keys with string interpolation like `"org:\(userId)"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
`ClientConfig` reads optional values from environment variables:
|
||||
|
||||
- `RIVET_NAMESPACE` - Namespace (can also be in endpoint URL)
|
||||
- `RIVET_TOKEN` - Authentication token (can also be in endpoint URL)
|
||||
- `RIVET_RUNNER` - Runner name (defaults to `"default"`)
|
||||
|
||||
The `endpoint` parameter is always required. There is no default endpoint.
|
||||
|
||||
### Endpoint Format
|
||||
|
||||
Endpoints support URL auth syntax:
|
||||
|
||||
```
|
||||
https://namespace:token@api.rivet.dev
|
||||
```
|
||||
|
||||
You can also pass the endpoint without auth and provide `RIVET_NAMESPACE` and `RIVET_TOKEN` separately. For serverless deployments, set the endpoint to your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints#url-auth-syntax) for details.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Client
|
||||
- `RivetKitClient(config:)` - Create a client with a config
|
||||
- `ClientConfig` - Configure endpoint, namespace, and token
|
||||
- `client.get()` / `getOrCreate()` / `getForId()` / `create()` - Get actor handles
|
||||
- `client.dispose()` - Dispose the client and all connections
|
||||
|
||||
### ActorHandle
|
||||
- `handle.action(name, args..., as:)` - Stateless action call
|
||||
- `handle.connect()` - Create a stateful connection
|
||||
- `handle.resolve()` - Get the actor ID
|
||||
- `handle.getGatewayUrl()` - Get the raw gateway URL
|
||||
- `handle.fetch(path, request:)` - Raw HTTP request
|
||||
- `handle.websocket(path:)` - Raw WebSocket connection
|
||||
|
||||
### ActorConnection
|
||||
- `conn.action(name, args..., as:)` - Action call over WebSocket
|
||||
- `conn.events(name, as:)` - AsyncStream of typed events
|
||||
- `conn.statusChanges()` - AsyncStream of status changes
|
||||
- `conn.errors()` - AsyncStream of connection errors
|
||||
- `conn.opens()` - AsyncStream that yields on connection open
|
||||
- `conn.closes()` - AsyncStream that yields on connection close
|
||||
- `conn.currentStatus` - Current connection status
|
||||
- `conn.dispose()` - Close the connection
|
||||
|
||||
### Types
|
||||
- `ActorConnStatus` - Connection status enum (`.idle`, `.connecting`, `.connected`, `.disconnected`, `.disposed`)
|
||||
- `ActorError` - Typed actor errors with `group`, `code`, `message`, `metadata`
|
||||
- `JSONValue` - Raw JSON value for untyped responses
|
||||
|
||||
_Source doc path: /docs/clients/swift_
|
||||
359
.agents/skills/ai-agent-workspace/reference/clients/swiftui.md
Normal file
359
.agents/skills/ai-agent-workspace/reference/clients/swiftui.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# SwiftUI
|
||||
|
||||
> Source: `src/content/docs/clients/swiftui.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/clients/swiftui
|
||||
> Description: Build SwiftUI apps with Rivet Actors.
|
||||
|
||||
---
|
||||
## Install
|
||||
|
||||
Add the Swift package dependency and import `RivetKitSwiftUI`:
|
||||
|
||||
```swift
|
||||
// Package.swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/rivet-dev/rivetkit-swift", from: "2.0.0")
|
||||
]
|
||||
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
.product(name: "RivetKitSwiftUI", package: "rivetkit-swift")
|
||||
]
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
`RivetKitSwiftUI` re-exports `RivetKitClient` and `SwiftUI`, so a single import covers both.
|
||||
|
||||
## Minimal Client
|
||||
|
||||
```swift HelloWorldApp.swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct HelloWorldApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.rivetKit(endpoint: "https://my-namespace:pk_...@api.rivet.dev")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```swift ContentView.swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@Actor("counter", key: ["my-counter"]) private var counter
|
||||
@State private var count = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("\(count)")
|
||||
.font(.system(size: 64, weight: .bold, design: .rounded))
|
||||
|
||||
Button("Increment") {
|
||||
counter.send("increment", 1)
|
||||
}
|
||||
.disabled(!counter.isConnected)
|
||||
}
|
||||
.task {
|
||||
count = (try? await counter.action("getCount")) ?? 0
|
||||
}
|
||||
.onActorEvent(counter, "newCount") { (newCount: Int) in
|
||||
count = newCount
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Actor Options
|
||||
|
||||
The `@Actor` property wrapper always uses get-or-create semantics and accepts:
|
||||
|
||||
- `name` (required)
|
||||
- `key` as `String` or `[String]` (required)
|
||||
- `params` (optional connection parameters)
|
||||
- `createWithInput` (optional creation input)
|
||||
- `createInRegion` (optional creation hint)
|
||||
- `enabled` (toggle connection lifecycle)
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct ConnParams: Encodable {
|
||||
let authToken: String
|
||||
}
|
||||
|
||||
struct ChatView: View {
|
||||
@Actor(
|
||||
"chatRoom",
|
||||
key: ["general"],
|
||||
params: ConnParams(authToken: "jwt-token"),
|
||||
enabled: true
|
||||
) private var chat
|
||||
|
||||
var body: some View {
|
||||
Text("Chat: \(chat.connStatus.rawValue)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct CounterView: View {
|
||||
@Actor("counter", key: ["my-counter"]) private var counter
|
||||
@State private var count = 0
|
||||
@State private var name = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Count: \(count)")
|
||||
Text("Name: \(name)")
|
||||
|
||||
Button("Fetch") {
|
||||
Task {
|
||||
count = try await counter.action("getCount")
|
||||
name = try await counter.action("rename", "new-name")
|
||||
}
|
||||
}
|
||||
|
||||
Button("Increment") {
|
||||
counter.send("increment", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct GameView: View {
|
||||
@Actor("game", key: ["game-1"]) private var game
|
||||
@State private var count = 0
|
||||
@State private var isGameOver = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Count: \(count)")
|
||||
if isGameOver {
|
||||
Text("Game Over!")
|
||||
}
|
||||
}
|
||||
.onActorEvent(game, "newCount") { (newCount: Int) in
|
||||
count = newCount
|
||||
}
|
||||
.onActorEvent(game, "gameOver") {
|
||||
isGameOver = true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Event Streams
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct ChatView: View {
|
||||
@Actor("chatRoom", key: ["general"]) private var chat
|
||||
@State private var messages: [String] = []
|
||||
|
||||
var body: some View {
|
||||
List(messages, id: \.self) { message in
|
||||
Text(message)
|
||||
}
|
||||
.task {
|
||||
for await message in chat.events("message", as: String.self) {
|
||||
messages.append(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Connection Status
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct StatusView: View {
|
||||
@Actor("counter", key: ["my-counter"]) private var counter
|
||||
@State private var count = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Status: \(counter.connStatus.rawValue)")
|
||||
|
||||
if counter.connStatus == .connected {
|
||||
Text("Connected!")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
|
||||
Button("Fetch via Handle") {
|
||||
Task {
|
||||
if let handle = counter.handle {
|
||||
count = try await handle.action("getCount", as: Int.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(!counter.isConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct UserView: View {
|
||||
@Actor("user", key: ["user-123"]) private var user
|
||||
@State private var errorMessage: String?
|
||||
@State private var username = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField("Username", text: $username)
|
||||
|
||||
Button("Update Username") {
|
||||
Task {
|
||||
do {
|
||||
let _: String = try await user.action("updateUsername", username)
|
||||
} catch let error as ActorError {
|
||||
errorMessage = "\(error.code): \(String(describing: error.metadata))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.onActorError(user) { error in
|
||||
errorMessage = "\(error.group).\(error.code): \(error.message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
```swift
|
||||
import RivetKitSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct OrgChatView: View {
|
||||
@Actor("chatRoom", key: ["org-acme", "general"]) private var room
|
||||
|
||||
var body: some View {
|
||||
Text("Room: \(room.connStatus.rawValue)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Don't build keys with string interpolation like `"org:\(userId)"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
Call `.rivetKit(endpoint:)` or `.rivetKit(client:)` once at the root of your view tree:
|
||||
|
||||
```swift
|
||||
// With endpoint string (recommended for most apps)
|
||||
@main
|
||||
struct MyApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.rivetKit(endpoint: "https://my-namespace:pk_...@api.rivet.dev")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With custom client (for advanced configuration)
|
||||
@main
|
||||
struct MyApp: App {
|
||||
private let client = RivetKitClient(
|
||||
config: try! ClientConfig(endpoint: "https://api.rivet.dev", token: "pk_...")
|
||||
)
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.rivetKit(client: client)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using `.rivetKit(endpoint:)`, the client is created once and cached per endpoint. When using `.rivetKit(client:)`, store the client as a property on `App` (not inside `body`) since SwiftUI can call `body` multiple times.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
`ClientConfig` reads optional values from environment variables:
|
||||
|
||||
- `RIVET_NAMESPACE` - Namespace (can also be in endpoint URL)
|
||||
- `RIVET_TOKEN` - Authentication token (can also be in endpoint URL)
|
||||
- `RIVET_RUNNER` - Runner name (defaults to `"default"`)
|
||||
|
||||
The endpoint is always required. There is no default endpoint.
|
||||
|
||||
### Endpoint Format
|
||||
|
||||
Endpoints support URL auth syntax:
|
||||
|
||||
```
|
||||
https://namespace:token@api.rivet.dev
|
||||
```
|
||||
|
||||
You can also pass the endpoint without auth and provide `RIVET_NAMESPACE` and `RIVET_TOKEN` separately. For serverless deployments, set the endpoint to your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints#url-auth-syntax) for details.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Property Wrapper
|
||||
- `@Actor(name, key:, params:, createWithInput:, createInRegion:, enabled:)` - SwiftUI property wrapper for actor connections
|
||||
|
||||
### View Modifiers
|
||||
- `.rivetKit(endpoint:)` - Configure client with an endpoint URL (creates cached client)
|
||||
- `.rivetKit(client:)` - Configure client with a custom instance
|
||||
- `.onActorEvent(actor, event) { ... }` - Subscribe to actor events (supports 0–5 typed args)
|
||||
- `.onActorError(actor) { error in ... }` - Handle actor errors
|
||||
|
||||
### ActorObservable
|
||||
- `actor.action(name, args..., as:)` - Async action call
|
||||
- `actor.send(name, args...)` - Fire-and-forget action
|
||||
- `actor.events(name, as:)` - AsyncStream of typed events
|
||||
- `actor.connStatus` - Current connection status
|
||||
- `actor.isConnected` - Whether connected
|
||||
- `actor.handle` - Underlying `ActorHandle` (optional)
|
||||
- `actor.connection` - Underlying `ActorConnection` (optional)
|
||||
- `actor.error` - Most recent error (optional)
|
||||
|
||||
### Types
|
||||
- `ActorConnStatus` - Connection status enum (`.idle`, `.connecting`, `.connected`, `.disconnected`, `.disposed`)
|
||||
- `ActorError` - Typed actor errors with `group`, `code`, `message`, `metadata`
|
||||
|
||||
_Source doc path: /docs/clients/swiftui_
|
||||
@@ -0,0 +1,181 @@
|
||||
# AI Agent Workspaces
|
||||
|
||||
> Source: `src/content/cookbook/ai-agent-workspace.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/ai-agent-workspace
|
||||
> Description: Give every AI agent its own computer: a persistent workspace with a filesystem, processes, shells, networking, and agent sessions on a lightweight in-process OS.
|
||||
|
||||
---
|
||||
Patterns for giving every AI agent its own computer with [agentOS](/docs/agent-os): one Rivet Actor per agent that owns a portable, lightweight in-process OS running on Wasm and V8. Use it for code interpreters that keep state between runs, agents that ship artifacts behind shareable preview URLs, per-user dev environments, and scheduled maintenance agents. agentOS is in preview and the API is subject to change.
|
||||
|
||||
This entry is about giving an agent a workspace. For conversation memory, message queues, and streaming chat patterns, see [AI Agent](/cookbook/ai-agent/).
|
||||
|
||||
## Starter Code
|
||||
|
||||
The [agent-os](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os) collection is reference code, one sub-example per capability; treat it as patterns to copy into your project rather than a turnkey app. The [agent-os-e2e](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os-e2e) example is the complete end-to-end walkthrough.
|
||||
|
||||
| Example | Starter Code | Use When |
|
||||
| --- | --- | --- |
|
||||
| Hello World | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/hello-world) | You want the minimal loop: boot a VM lazily on the first action, write a file, read it back. |
|
||||
| Filesystem | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/filesystem) | The agent needs the full file surface: recursive listing, stat, move, delete, and custom mounts. |
|
||||
| Git | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/git) | The agent works with real git repos inside the workspace: init, commit, branch, and clone via `exec`. |
|
||||
| Processes | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/processes) | The agent runs shell commands with pipes and long-lived spawned programs. |
|
||||
| Network | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/network) | The agent serves HTTP inside the VM and you need `vmFetch` or signed preview URLs. |
|
||||
| Cron | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/cron) | The workspace runs scheduled commands or recurring agent work. |
|
||||
| Tools | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/tools) | You want your backend functions exposed as CLI commands inside the workspace. |
|
||||
| Agent Session | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/agent-session) | You drive a Pi coding agent session inside the workspace. Requires `ANTHROPIC_API_KEY`. |
|
||||
| Sandbox Mounting | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/sandbox) | The agent needs native binaries or a real OS, mounted into the VM from a Docker-backed sandbox. Requires Docker. |
|
||||
| End-to-End Walkthrough | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os-e2e) | You want one runnable script covering files, processes, preview URLs, and a streaming Pi agent session. |
|
||||
|
||||
## Setup
|
||||
|
||||
The whole backend is one registry with one `agentOs()` actor:
|
||||
|
||||
```typescript
|
||||
import { agentOs } from "rivetkit/agent-os";
|
||||
import { setup } from "rivetkit";
|
||||
import common from "@rivet-dev/agent-os-common";
|
||||
import pi from "@rivet-dev/agent-os-pi";
|
||||
|
||||
const vm = agentOs({
|
||||
options: { software: [common, pi] },
|
||||
});
|
||||
|
||||
export const registry = setup({ use: { vm } });
|
||||
registry.start();
|
||||
```
|
||||
|
||||
See the [Quickstart](/docs/agent-os/quickstart) for the client side and project layout.
|
||||
|
||||
## Workspace Model
|
||||
|
||||
- **One actor per workspace, key as identity.** `client.vm.getOrCreate(["my-agent"])` gives each agent its own workspace; key by user id for per-user dev environments. Each workspace has its own filesystem, processes, and networking with no shared state and no cross-contamination (see the [overview](/docs/agent-os)).
|
||||
- **Software packages choose what is installed.** agentOS starts with no commands installed. The `software` option installs packages such as `@rivet-dev/agent-os-common` (a meta-package of Wasm command-line tools: coreutils, sed, grep, gawk, findutils, diffutils, tar, and gzip), `@rivet-dev/agent-os-git` (git), and `@rivet-dev/agent-os-pi` (the Pi coding agent). See [Software](/docs/agent-os/software).
|
||||
- **The VM boots lazily and sleeps when idle.** The first action boots the VM (clients see a `vmBooted` event); when nothing is active, the actor sleeps and broadcasts `vmShutdown`, then wakes on the next action.
|
||||
|
||||
What survives a sleep/wake cycle (see [Persistence](/docs/agent-os/persistence)):
|
||||
|
||||
| Data | Across sleep/wake |
|
||||
| --- | --- |
|
||||
| Session transcripts and event history | Persist in actor SQLite as events stream. `listPersistedSessions` and `getSessionEvents` read them back without booting the VM, and `resumeSession` picks a session back up in a rebooted VM. |
|
||||
| Signed preview URL tokens | Persist in actor SQLite. Requests are validated against the stored token and the VM reboots lazily to serve them, so preview URLs keep working after sleep. |
|
||||
| Files | Persist when the mount is backed by a persistent driver (database-backed, S3, or a sandbox mount). In-memory mounts come back empty on wake. |
|
||||
| Processes, shells, and cron jobs | Do not persist. Restart long-running processes and reschedule cron jobs on wake (recommended extension). |
|
||||
|
||||
The actor holds itself awake while sessions, processes, shells, or hooks are active, then sleeps after a grace period.
|
||||
|
||||
## Capability Tour
|
||||
|
||||
| Area | Use It For | Key Actions | Docs | Example |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Filesystem | Give the agent a file tree to read and write | `readFile`, `writeFile`, `mkdir`, `readdir`, `move` | [Filesystem](/docs/agent-os/filesystem) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/filesystem) |
|
||||
| Processes | Run commands and long-lived programs | `exec`, `spawn`, `waitProcess`, `killProcess` | [Processes](/docs/agent-os/processes) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/processes) |
|
||||
| Shells | Interactive terminals with streamed output | `openShell`, `writeShell`, `resizeShell`, `closeShell` | [Processes](/docs/agent-os/processes) | No standalone example |
|
||||
| Networking and preview URLs | Reach services inside the VM and share them externally | `vmFetch`, `createSignedPreviewUrl`, `expireSignedPreviewUrl` | [Networking](/docs/agent-os/networking) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/network) |
|
||||
| Cron | Scheduled commands and recurring agent sessions | `scheduleCron`, `listCronJobs`, `cancelCronJob` | [Cron](/docs/agent-os/cron) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/cron) |
|
||||
| Agent sessions | Drive a coding agent inside the workspace | `createSession`, `sendPrompt`, `resumeSession`, `closeSession` | [Sessions](/docs/agent-os/sessions) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/agent-session) |
|
||||
|
||||
Two details worth knowing up front:
|
||||
|
||||
- `createSignedPreviewUrl` returns a relative path plus the token and expiry. Build the full URL with the client handle's `getGatewayUrl()` method; it is a client method, not an actor action.
|
||||
- Schedule cron jobs through the actor with the `exec` and `session` action types only. Callback cron actions are defined in server code and do not serialize through `listCronJobs`.
|
||||
|
||||
## Driving a Coding Agent Session
|
||||
|
||||
Only the Pi agent (`@rivet-dev/agent-os-pi`) is currently supported as a session agent; Amp, Claude Code, Codex, and OpenCode are coming soon. See [Sessions](/docs/agent-os/sessions).
|
||||
|
||||
1. `createSession("pi", { env: { ANTHROPIC_API_KEY } })` returns a `sessionId`. The VM does not inherit the host `process.env`, so API keys are passed explicitly per session or kept server-side through the [LLM gateway](/docs/agent-os/llm-gateway).
|
||||
2. Open a realtime connection and subscribe to `sessionEvent` to stream the agent's output, such as message chunks, as it works.
|
||||
3. `sendPrompt(sessionId, ...)` starts a turn; `cancelPrompt` stops one in flight.
|
||||
4. When the agent asks to use a tool, clients receive a `permissionRequest` event and answer with `respondPermission`, or the server auto-approves with the `onPermissionRequest` config hook (see [Permissions](/docs/agent-os/permissions)).
|
||||
5. Transcripts are persisted automatically in the universal transcript format (Agent Communication Protocol, ACP). After sleep, `resumeSession` continues a session in the rebooted VM, and `listPersistedSessions` plus `getSessionEvents` read history without booting the VM at all.
|
||||
|
||||
## Host Tools
|
||||
|
||||
Expose your backend functions to the agent as CLI commands inside the workspace. Define a toolkit with `toolKit()` and `hostTool()` (Zod-schema'd JavaScript functions on the host), pass it via `agentOs({ options: { toolKits: [...] } })`, and it is installed as a command such as `agentos-weather forecast --city Paris --days 3` and injected into the agent's system prompt. The agent calls your backend with no HTTP endpoints or MCP servers to stand up, and CLI-shaped tools are code mode compatible for large token savings. See [Tools](/docs/agent-os/tools) and the [tools example](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os/src/tools).
|
||||
|
||||
## When to Mount a Full Sandbox
|
||||
|
||||
agentOS is not a replacement for sandboxes; it is designed to work alongside them. When a workspace needs native binaries, browsers, compilation, or desktop automation, use sandbox mounting: start a Docker-backed sandbox with `SandboxAgent.start({ sandbox: docker() })`, project its filesystem into the VM as a native directory (for example `/sandbox`) with `createSandboxFs`, and expose sandbox process control as host tools with `createSandboxToolkit`. Filesystem actions like `writeFile` and `readFile` project transparently through the mount while heavy workloads run in the container.
|
||||
|
||||
See [Sandbox Mounting](/docs/agent-os/sandbox) for the hybrid model and [agentOS vs Sandboxes](/docs/agent-os/versus-sandbox) for when each side wins: the lightweight VM has a near-zero cold start (~6 ms) and installs with `npm install`, while sandboxes are full Linux environments billed per second of uptime.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Topology | One `vm[workspaceId]` actor per agent or per user; the actor key is the workspace identity. |
|
||||
| Ingress | Actor actions for files, processes, networking, cron, and sessions; a realtime connection for streamed events. |
|
||||
| Streaming | `sessionEvent` per agent event, `processOutput` and `processExit` for spawned processes, `shellData` for interactive shells. |
|
||||
| Persistence | Session transcripts, event history, and preview tokens in actor SQLite; files persist through persistent mounts. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `vm[workspaceId]`, for example `client.vm.getOrCreate(["my-agent"])`
|
||||
- **Responsibility**: Owns one workspace. Boots the VM lazily on the first action, serves all capability actions, proxies signed preview URL requests into the VM's virtual network, and persists sessions and tokens to actor SQLite.
|
||||
- **Actions** (grouped; the most load-bearing of each area)
|
||||
- Filesystem: `readFile`, `writeFile`, `mkdir`, `readdir`, `readdirRecursive`, `stat`, `exists`, `move`, `deleteFile`
|
||||
- Processes: `exec`, `spawn`, `writeProcessStdin`, `waitProcess`, `listProcesses`, `killProcess`
|
||||
- Shells: `openShell`, `writeShell`, `resizeShell`, `closeShell`
|
||||
- Network: `vmFetch`, `createSignedPreviewUrl`, `expireSignedPreviewUrl`
|
||||
- Cron: `scheduleCron`, `listCronJobs`, `cancelCronJob`
|
||||
- Sessions: `createSession`, `sendPrompt`, `cancelPrompt`, `respondPermission`, `resumeSession`, `closeSession`, `destroySession`, `listPersistedSessions`, `getSessionEvents`
|
||||
- **Queues**
|
||||
- None
|
||||
- **Events**
|
||||
- `vmBooted`
|
||||
- `vmShutdown`
|
||||
- `sessionEvent`
|
||||
- `permissionRequest`
|
||||
- `processOutput`
|
||||
- `processExit`
|
||||
- `shellData`
|
||||
- `cronEvent`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `agent_os_sessions` and `agent_os_session_events` (session metadata plus seq-ordered transcript events)
|
||||
- `agent_os_preview_tokens` (signed preview URL tokens with expiry)
|
||||
- `agent_os_fs_entries` (file content for database-backed mounts)
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant A as vm actor
|
||||
participant V as agentOS VM
|
||||
participant P as Pi session
|
||||
|
||||
C->>A: getOrCreate(["my-agent"])
|
||||
C->>A: writeFile("/tmp/hello.txt", ...)
|
||||
Note over A,V: first action boots the VM
|
||||
A-->>C: vmBooted
|
||||
C->>A: exec("echo hello | tr a-z A-Z")
|
||||
A->>V: run command
|
||||
V-->>A: {exitCode: 0, stdout}
|
||||
C->>A: spawn("node", ["/tmp/server.mjs"])
|
||||
C->>A: createSignedPreviewUrl(8080, 60)
|
||||
A-->>C: {path, token, expiresAt}
|
||||
C->>A: fetch(gatewayUrl + path)
|
||||
Note over A: token checked in SQLite, request proxied into the VM network
|
||||
C->>A: createSession("pi", {env})
|
||||
A->>P: start session
|
||||
C->>A: sendPrompt(sessionId, ...)
|
||||
loop streamed agent output
|
||||
P-->>A: agent event
|
||||
A-->>C: sessionEvent
|
||||
end
|
||||
Note over A: idle, sleeps after grace period (vmShutdown)
|
||||
C->>A: resumeSession(sessionId)
|
||||
Note over A,V: wake reboots the VM, restoring transcripts, preview tokens, and persistent mounts
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- **Authenticate connections**: Add the `onBeforeConnect` hook in the `agentOs()` config so only authorized callers reach a workspace. Signed preview URL requests deliberately skip it because the token is the credential; browsers navigating a preview URL cannot supply actor connection params.
|
||||
- **Gate agent tool use with permissions**: Session permission requests broadcast as `permissionRequest` events for human-in-the-loop approval via `respondPermission`, or run a server-side `onPermissionRequest` policy for automated pipelines. See [Permissions](/docs/agent-os/permissions).
|
||||
- **Treat preview URLs as bearer credentials**: Tokens are randomly generated 32-character values with a default expiry of 1 hour and a maximum of 24; revoke early with `expireSignedPreviewUrl`. Preview responses carry permissive CORS headers, so do not serve private data on a preview port without app-level auth.
|
||||
- **Keep LLM credentials off the browser**: Create sessions from trusted server code with the key in `createSession` env, or keep keys entirely server-side with the [LLM gateway](/docs/agent-os/llm-gateway). Session keys are injected into the session environment inside the VM and are never stored in actor config or SQLite.
|
||||
- **Treat mounted sandboxes as their own trust boundary**: A mounted sandbox is a full Linux environment outside the workspace's Wasm and V8 isolate. Scope what its network and filesystem can reach before projecting it into an agent's VM.
|
||||
- **Set resource and cost limits**: Cap per-workspace memory and CPU (`maxMemoryMb`, `maxCpuPercent`, see [Security](/docs/agent-os/security)). Active sessions, processes, and shells hold the actor awake, so add per-workspace session caps and token budgets as a recommended extension.
|
||||
|
||||
_Source doc path: /cookbook/ai-agent-workspace_
|
||||
128
.agents/skills/ai-agent-workspace/reference/cookbook/ai-agent.md
Normal file
128
.agents/skills/ai-agent-workspace/reference/cookbook/ai-agent.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# AI Agent
|
||||
|
||||
> Source: `src/content/cookbook/ai-agent.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/ai-agent
|
||||
> Description: Build an AI agent backend with persistent memory: one Rivet Actor per conversation, queued message handling, and streaming LLM responses as realtime events.
|
||||
|
||||
---
|
||||
Patterns for building AI agent backends with RivetKit, where each conversation is one Rivet Actor that owns its memory, its message queue, and its streaming output.
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start with one of the working examples on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent) and adapt it. The sections below describe the flagship `ai-agent` example unless a variant is called out explicitly.
|
||||
|
||||
| Variant | Starter Code | Use When |
|
||||
| --- | --- | --- |
|
||||
| Queue-driven AI SDK agent | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent) | You want a streaming chat agent where each conversation keeps its own persistent memory and processes one message at a time. |
|
||||
| Sandbox coding agent | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/sandbox-coding-agent) | The agent should run a coding agent (Codex by default) inside an isolated [sandbox](/docs/actors/sandbox) via Docker, Daytona, or E2B. |
|
||||
| Durable streams agent (experimental) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/experimental-durable-streams-ai-agent) | You want replayable, restart-safe prompt and response delivery through durable streams instead of actor state and events. |
|
||||
| Agent with a workspace (agentOS) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/agent-os) | The agent needs its own persistent computer: a filesystem, processes, shells, and preview URLs. See the cookbook: [AI Agent Workspaces](/cookbook/ai-agent-workspace/). |
|
||||
|
||||
## Conversation Memory
|
||||
|
||||
Use one actor per conversation, keyed by a conversation or agent id (see [Actor Keys](/docs/actors/keys)). The agent actor's persistent [state](/docs/actors/state) is the conversation memory: in the `ai-agent` example, `messages` and `status` live in JSON actor state and survive sleep and restarts with no external database. Every model call rebuilds the prompt from `c.state.messages` plus a system prompt, so memory and inference input are the same data.
|
||||
|
||||
| Variant | Where Memory Lives | Persisted State Fields |
|
||||
| --- | --- | --- |
|
||||
| `ai-agent` | JSON actor state | `messages`, `status` |
|
||||
| `sandbox-coding-agent` | JSON actor state plus the sandbox ACP session | `messages`, `status`, `sessionId` |
|
||||
| `experimental-durable-streams-ai-agent` | Durable streams; the actor stores only its conversation id and a read cursor | `conversationId`, `promptStreamOffset` |
|
||||
|
||||
## Message Handling
|
||||
|
||||
In the `ai-agent` example, the client pushes user input onto the agent's `message` [queue](/docs/actors/queues) with `agent.connection.send("message", { text, sender })`. This is a queue push, not an action call. The actor's `run` hook (see [Lifecycle](/docs/actors/lifecycle)) consumes the queue serially with `for await (const queued of c.queue.iter())`.
|
||||
|
||||
Serial queue consumption is the per-conversation concurrency guarantee: at most one in-flight model call per actor, with no extra locking. The `status` field (`thinking` while a model call is in flight) is UI signal only; the run loop is the actual lock. The loop also checks `c.aborted` inside the token stream so shutdown exits gracefully.
|
||||
|
||||
| Variant | Message Ingress | Serialization Guarantee |
|
||||
| --- | --- | --- |
|
||||
| `ai-agent` | `message` queue pushed via `connection.send` | `run` hook pops one queued message at a time with `c.queue.iter()`. |
|
||||
| `sandbox-coding-agent` | `sendMessage` [action](/docs/actors/actions), no queue | Each call awaits the sandbox round trip before broadcasting the result. |
|
||||
| `experimental-durable-streams-ai-agent` | Durable prompt stream long-polled from `onWake` | `promptStreamOffset` is persisted per chunk, so restarts resume without reprocessing prompts. |
|
||||
|
||||
## Streaming Responses
|
||||
|
||||
The `ai-agent` actor broadcasts a `response` [event](/docs/actors/events) for every model text delta. The payload carries `messageId`, the per-token `delta`, the cumulative `content`, and a `done` flag (plus `error` on failure), so clients can either append deltas or idempotently replace the message by `messageId` using `content`. The example frontend replaces by `messageId`, which tolerates dropped events. The terminal broadcast has an empty `delta`, the full `content`, and `done: true`.
|
||||
|
||||
Because the assistant message object lives in `c.state.messages` and is mutated in place during streaming, partial content persists if the actor restarts mid-stream. The example broadcasts once per AI SDK delta with no throttling; batching or throttling deltas is a recommended extension for high-traffic deployments, not something the example implements.
|
||||
|
||||
Variant differences: `sandbox-coding-agent` sends a single `response` broadcast with `done: true` after the sandbox finishes (no incremental streaming), and `experimental-durable-streams-ai-agent` appends per-token chunks to a durable response stream, then broadcasts `responseComplete` or `responseError`.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Topology | `agentManager["primary"]` singleton directory plus one `agent[agentId]` actor per conversation. |
|
||||
| Ingress | Client pushes `AgentQueueMessage` payloads onto the agent's `message` queue with `connection.send`. |
|
||||
| Streaming | One `response` broadcast per model delta, terminal broadcast with `done: true`. |
|
||||
| Memory | Full transcript and status in JSON actor state; no external database. |
|
||||
|
||||
The manager creates `AgentInfo` records and warms each agent through [actor-to-actor communication](/docs/actors/communicating-between-actors): `createAgent` calls `c.client<typeof registry>()`, then `client.agent.getOrCreate([info.id])` and awaits `getStatus()` so the conversation actor exists before the client connects. The sandbox variant extends this topology with a `codingSandbox` actor that shares the agent's key (`codingSandbox.getOrCreate([c.key[0]])`), so the agent-to-sandbox mapping is implicit in the key space.
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `agentManager["primary"]`
|
||||
- **Responsibility**: Directory actor. Creates `AgentInfo` records, lists agents, and warms each agent actor via `c.client()`.
|
||||
- **Actions**
|
||||
- `createAgent`
|
||||
- `listAgents`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `agents`
|
||||
|
||||
- **Key**: `agent[agentId]`
|
||||
- **Responsibility**: One actor per conversation. Holds the full message history and status, consumes queued user messages in its `run` loop, calls the model via the AI SDK, and broadcasts streaming deltas.
|
||||
- **Actions**
|
||||
- `getHistory`
|
||||
- `getStatus`
|
||||
- **Queues**
|
||||
- `message`
|
||||
- **Events**
|
||||
- `messageAdded`
|
||||
- `status`
|
||||
- `response`
|
||||
- **State**
|
||||
- JSON
|
||||
- `messages`
|
||||
- `status`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant AM as agentManager
|
||||
participant A as agent
|
||||
participant LLM as Model API
|
||||
|
||||
C->>AM: createAgent(name)
|
||||
AM->>A: getOrCreate([info.id]) + getStatus()
|
||||
AM-->>C: AgentInfo
|
||||
C->>A: connection.send("message", {text, sender})
|
||||
Note over A: run loop pops queue via c.queue.iter()
|
||||
A-->>C: messageAdded (user message)
|
||||
A-->>C: messageAdded (assistant placeholder)
|
||||
A-->>C: status (thinking)
|
||||
A->>LLM: streamText(system prompt + history)
|
||||
loop each text delta
|
||||
LLM-->>A: delta
|
||||
A-->>C: response {messageId, delta, content, done: false}
|
||||
end
|
||||
A-->>C: response {delta: "", content, done: true}
|
||||
A-->>C: status (idle)
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
The examples ship without auth so they stay minimal. Apply this baseline before exposing an agent backend.
|
||||
|
||||
- **API keys stay server-side**: `OPENAI_API_KEY` (or `ANTHROPIC_API_KEY`) is read by the AI SDK inside the actor process. The key never reaches the browser; clients only talk to the actor over RivetKit. The sandbox variant forwards keys into the sandbox env, never to the client.
|
||||
- **Add authentication**: The examples have no auth, so anyone who reaches the server can create agents, list them, and message any agent whose key they can guess. Add `onBeforeConnect` or `createConnState` checks with scoped tokens as a recommended extension. See [Authentication](/docs/actors/authentication).
|
||||
- **Validate and rate-limit queue payloads**: The example only skips bodies without a string `text`. Enforce payload size limits, schema validation, and per-connection rate limits as a recommended extension.
|
||||
- **Derive sender identity server-side**: The example trusts the client-supplied `sender` field verbatim. Bind sender identity to the authenticated connection instead.
|
||||
- **Cap or trim message history**: The example sends the full transcript on every model call with no cap. Trim or summarize old messages as a recommended extension so prompts and state stay bounded.
|
||||
- **Set cost ceilings per conversation**: Add per-agent token budgets and quotas as a recommended extension. The sandbox variant runs real compute, so also enforce per-user sandbox quotas and restrict sandbox network egress.
|
||||
|
||||
_Source doc path: /cookbook/ai-agent_
|
||||
@@ -0,0 +1,114 @@
|
||||
# Chat Room
|
||||
|
||||
> Source: `src/content/cookbook/chat-room.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/chat-room
|
||||
> Description: Build a realtime chat room backend with Rivet Actors: one actor per room, SQLite-backed message history, and WebSocket broadcast to every connected client.
|
||||
|
||||
---
|
||||
Patterns for building a chat room backend with RivetKit: room-scoped actors, persistent message history, and realtime delivery over WebSocket connections.
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start with the working example on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/chat-room) and adapt it. The backend is a single `chatRoom` actor; the frontend is a React app using `@rivetkit/react` (see the [React quickstart](/docs/actors/quickstart/react)).
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Room model | One `chatRoom` actor per room key. The frontend defaults the key to `general`; typing a different room name connects to a different actor. |
|
||||
| History | SQLite `messages` table created in `db({ onMigrate })`, read back with `ORDER BY id ASC`. |
|
||||
| Delivery | `sendMessage` inserts the row, then broadcasts a typed `newMessage` event to every connected client. |
|
||||
| Identity | None in the example. `sender` is a plain action argument; production should bind identity to the connection. |
|
||||
|
||||
## Room-Per-Actor Model
|
||||
|
||||
Each room is one Rivet Actor instance, addressed by [key](/docs/actors/keys). The client calls `useActor({ name: "chatRoom", key: [roomId] })`, which gets-or-creates the actor for that room. This gives you:
|
||||
|
||||
- **Isolation**: each room's history and connections are fully scoped to its key. Switching the room input re-keys the hook and connects to a different actor with separate history.
|
||||
- **A single serialized writer**: all `sendMessage` calls for one room run through one actor, so message ordering is consistent without locks. The SQLite `AUTOINCREMENT` id is the canonical order, which is why `getHistory` sorts by `id` rather than by timestamp.
|
||||
- **Natural scaling**: rooms spread across the cluster independently. A hot room does not slow down other rooms.
|
||||
|
||||
## Message History Storage
|
||||
|
||||
This example stores history in the actor's SQLite database, not in JSON state. Pick based on history size and query needs:
|
||||
|
||||
| Approach | Use When | Implementation Guidance |
|
||||
| --- | --- | --- |
|
||||
| [SQLite](/docs/actors/sqlite) (what this example uses) | Large or long-lived history that needs ordering, caps, pagination, or search | Create the `messages` table in `db({ onMigrate })`, insert with parameterized queries (`c.db.execute("INSERT ... VALUES (?, ?, ?)", ...)`), and read with `ORDER BY id ASC`. History survives actor sleep and scales past what you want in memory. |
|
||||
| [JSON state](/docs/actors/state) | Small recent history, for example the last 50 to 100 messages | Push onto a `messages` array in actor state and trim to a cap on every send. Simplest option, but the whole history lives in memory and there is no query layer, so it only fits bounded recent-history use cases. |
|
||||
|
||||
## Broadcast Delivery
|
||||
|
||||
New messages reach connected clients through a typed [event](/docs/actors/events):
|
||||
|
||||
- The actor declares `events: { newMessage: event() }`, where `Message` is `{ sender, text, timestamp }`.
|
||||
- The `sendMessage` [action](/docs/actors/actions) builds the message with a server-side `Date.now()` timestamp, inserts it into the `messages` table, then calls `c.broadcast("newMessage", message)` and returns the message to the caller.
|
||||
- Each client subscribes with `useEvent("newMessage", ...)` and appends to its local list. The sender renders its own message through the same broadcast path as everyone else, so all clients stay on one code path.
|
||||
- History load is connection-gated: once the connection is ready, the client calls `getHistory()` once to render the backlog, then relies on events for everything after.
|
||||
|
||||
Use `c.broadcast(...)` for room-wide messages. For private or per-recipient payloads (such as DMs inside a room), send on the individual connection instead, which is a recommended extension beyond this example.
|
||||
|
||||
## Typing Indicators And Presence (Extension)
|
||||
|
||||
The example does not implement typing indicators, presence, or join/leave handling of any kind. There is no `createConnState`, `onConnect`, or `onDisconnect` in the code. If you need them, add them as ephemeral [connection](/docs/actors/connections) behavior:
|
||||
|
||||
- **Keep it ephemeral**: store the username and typing flag in per-connection state, never in SQLite or persisted actor state. Presence is derived from live connections and should disappear with them.
|
||||
- **Broadcast on change only**: emit a typing event when a user starts or stops typing, and a presence event from `onConnect` / `onDisconnect`, rather than polling or ticking.
|
||||
- **Expire on the client**: clear a typing indicator after a short client-side timeout so a dropped connection never leaves a stuck "is typing" row.
|
||||
|
||||
## Per-User Inbox (Extension)
|
||||
|
||||
For offline delivery, DMs, unread counts, or notification fanout, add a `userInbox[userId]` actor per user. This is an extension beyond the example:
|
||||
|
||||
- The room actor forwards each message to the inbox actor of every member via [actor-to-actor calls](/docs/actors/communicating-between-actors), so users who are not connected to the room still accumulate messages.
|
||||
- The inbox actor owns per-user unread state and serves it when the user comes online, independent of which rooms they are in.
|
||||
- DMs become a degenerate room: either a `chatRoom` keyed by the sorted pair of user ids, or direct inbox-to-inbox delivery if you do not need shared history semantics.
|
||||
|
||||
## Actors
|
||||
|
||||
- **Key**: `chatRoom[roomId]`
|
||||
- **Responsibility**: Owns one chat room. Persists the room's message history in its SQLite database and broadcasts each new message to every connected client.
|
||||
- **Actions**
|
||||
- `sendMessage`
|
||||
- `getHistory`
|
||||
- **Queues**
|
||||
- None
|
||||
- **Events**
|
||||
- `newMessage`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `messages` table: `id` (autoincrement primary key), `sender`, `text`, `timestamp`
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Client A
|
||||
participant B as Client B
|
||||
participant R as chatRoom
|
||||
|
||||
A->>R: connect with key [roomId]
|
||||
Note over R: every start runs onMigrate (CREATE TABLE IF NOT EXISTS messages)
|
||||
A->>R: getHistory()
|
||||
R-->>A: Message[] ordered by id
|
||||
B->>R: connect with key [roomId]
|
||||
B->>R: getHistory()
|
||||
R-->>B: Message[] ordered by id
|
||||
A->>R: sendMessage(sender, text)
|
||||
Note over R: INSERT row with server timestamp
|
||||
R-->>A: newMessage (broadcast)
|
||||
R-->>B: newMessage (broadcast)
|
||||
A->>R: disconnect
|
||||
Note over R: history stays in SQLite for the next connection
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
The example is intentionally minimal and skips all of the following. Add them before production:
|
||||
|
||||
- **Auth before join**: any client can join any room by knowing its name, and `sender` is arbitrary client input on every call. Validate a token during [connection auth](/docs/actors/authentication), bind identity to [connection state](/docs/actors/connections), and check room membership before serving history. Never trust a sender name passed as an action argument.
|
||||
- **Message length clamps**: the example accepts empty messages and has no length limit. Trim server-side, reject empty text, and clamp to a maximum length.
|
||||
- **Per-connection rate limiting**: rate limit `sendMessage` per connection to stop spam and broadcast amplification.
|
||||
- **Server-side timestamps and ids**: the example already does this correctly. `timestamp` comes from `Date.now()` inside the action and `id` from SQLite `AUTOINCREMENT`. Keep it that way; never accept client-supplied timestamps or ids.
|
||||
- **History caps**: `getHistory` returns every row with no limit. Add a `LIMIT` plus pagination, and prune or archive old rows so a long-lived room cannot grow unbounded.
|
||||
- **Parameterized queries**: the example already inserts with `?` placeholders. Keep all user-supplied text out of SQL string interpolation.
|
||||
|
||||
_Source doc path: /cookbook/chat-room_
|
||||
@@ -0,0 +1,157 @@
|
||||
# Collaborative Text Editor
|
||||
|
||||
> Source: `src/content/cookbook/collaborative-text-editor.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/collaborative-text-editor
|
||||
> Description: Build a collaborative text editor backend with Yjs CRDTs and Rivet Actors: per-document actors relay sync and awareness updates and persist snapshots.
|
||||
|
||||
---
|
||||
Patterns for building a Yjs server on RivetKit: CRDT document sync, presence and cursors, and snapshot persistence, with one Rivet Actor per document acting as a relay.
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start with the working example on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/collaborative-document) and adapt it to your editor. It ships a React frontend with a plain textarea, remote cursor overlays, and a workspace document index.
|
||||
|
||||
| Use Case | Starter Code | Common Examples |
|
||||
| --- | --- | --- |
|
||||
| Shared document editing | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/collaborative-document) | Notion-style docs, shared notes, pair-writing tools, form co-editing |
|
||||
|
||||
## CRDT vs OT
|
||||
|
||||
Two families of algorithms solve concurrent text editing. The choice decides what your server has to do.
|
||||
|
||||
| Dimension | CRDT (Yjs) | Operational Transformation |
|
||||
| --- | --- | --- |
|
||||
| Conflict resolution model | Commutative merges. Updates apply in any order on any peer and converge to the same result. | Server transforms each operation against every concurrent operation. Correctness depends on a central sequencer. |
|
||||
| Offline support | Strong. Clients keep editing locally and merge buffered updates on reconnect. | Weak. Long-lived divergence makes transformation chains complex and fragile. |
|
||||
| Server role | Relay plus persistence. The server applies opaque updates and rebroadcasts them. It never needs to understand document semantics. | Authoritative transformer. The server must implement transformation logic for every operation type. |
|
||||
| Library maturity | Yjs is mature and widely deployed, with bindings for ProseMirror, CodeMirror, Monaco, and others. | Production-grade implementations are mostly proprietary (Google Docs) or aging (ShareDB). |
|
||||
|
||||
The example uses Yjs because CRDTs let the server stay a relay-style Rivet Actor. The actor applies each incoming update to a server-side `Y.Doc` so it can persist the merged state and serve late joiners, but it never transforms operations or arbitrates conflicts. Ordering does not matter because Yjs merges are commutative.
|
||||
|
||||
## Document Actor Model
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Topology | One `document[workspaceId, documentId]` actor per document plus one `documentList[workspaceId]` coordinator per workspace. |
|
||||
| Sync model | Each client holds a local `Y.Doc`. The document actor relays incremental Yjs updates as broadcast [events](/docs/actors/events) and keeps a server-side merged copy in vars. |
|
||||
| Persistence | Full merged Yjs snapshot overwritten in one binary [actor KV](/docs/actors/kv) key (`yjs:doc`) on every sync update. Document metadata lives in JSON [state](/docs/actors/state). |
|
||||
| Queues | None. The example is purely [actions](/docs/actors/actions) plus broadcast events. |
|
||||
| Presence | Yjs Awareness relayed through the same `applyUpdate` action. Per-connection `connState` tracks asserted awareness clientIds for disconnect cleanup. |
|
||||
|
||||
The two-actor split follows the coordinator pattern from [Design Patterns](/docs/actors/design-patterns): the coordinator owns discovery and creation, and each document actor owns one document's realtime state. Multi-part [keys](/docs/actors/keys) scope both actors to a workspace.
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `document[workspaceId, documentId]`
|
||||
- **Responsibility**: Applies incoming sync and awareness updates to a server-side `Y.Doc` and `Awareness`, persists the merged Yjs snapshot to actor KV, and broadcasts updates to all connected collaborators.
|
||||
- **Actions**
|
||||
- `getContent`
|
||||
- `applyUpdate`
|
||||
- `getAwareness`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON metadata only: `title`, `createdAt`, `updatedAt`
|
||||
- Binary KV key `yjs:doc` holding the full merged Yjs snapshot
|
||||
- Ephemeral vars: the live `Y.Doc` and `Awareness`, created in `createVars` and rehydrated from KV on actor start
|
||||
- Per-connection `connState`: `clientIds` of awareness clients asserted by that connection
|
||||
|
||||
- **Key**: `documentList[workspaceId]`
|
||||
- **Responsibility**: Coordinator for one workspace. Creates document actors through the actor-to-actor client and maintains the index of document summaries.
|
||||
- **Actions**
|
||||
- `createDocument`
|
||||
- `listDocuments`
|
||||
- `deleteDocument`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `documents` array of `DocumentSummary` entries (`id`, `title`, `createdAt`, `updatedAt`)
|
||||
|
||||
The coordinator's `createDocument` generates a UUID, then explicitly creates the document actor with `c.client<typeof registry>()` and passes `{ title, createdAt }` as creation [input](/docs/actors/input), which the document actor's `createState` consumes. See [Communicating Between Actors](/docs/actors/communicating-between-actors) for the actor-to-actor client.
|
||||
|
||||
## Update Relay
|
||||
|
||||
A single `applyUpdate(update, kind, clientId?)` action handles both update kinds. Updates cross the action boundary as `number[]` byte arrays and are converted back to `Uint8Array` on each side.
|
||||
|
||||
| Kind | Server Applies To | Persists | Broadcasts |
|
||||
| --- | --- | --- | --- |
|
||||
| `"sync"` | `c.vars.doc` via `Y.applyUpdate` with origin `"client"` | Full merged snapshot to KV key `yjs:doc`, then bumps `updatedAt` | `sync` event carrying the incremental update |
|
||||
| `"awareness"` | `c.vars.awareness` via `applyAwarenessUpdate` with origin `"client"` | Nothing. Presence is ephemeral. | `awareness` event carrying the update |
|
||||
|
||||
Note the asymmetry on the sync branch: the broadcast carries only the small incremental update, while the KV write stores the full merged document re-encoded with `Y.encodeStateAsUpdate`.
|
||||
|
||||
Yjs origin tags are the echo guards that keep the relay loop-free:
|
||||
|
||||
| Origin Tag | Set Where | Effect |
|
||||
| --- | --- | --- |
|
||||
| `"local"` | Client edits inside `doc.transact(..., "local")` | The client's update listener fires and sends `applyUpdate` to the actor. |
|
||||
| `"client"` | Server applying an incoming update to its `Y.Doc` or `Awareness` | Marks the change as client-originated on the server copy. |
|
||||
| `"remote"` | Client applying broadcast events or initial sync data | Update listeners early-return on `"remote"`, so a client never re-sends its own echo. |
|
||||
|
||||
On connect or reconnect, the client calls `getContent` and `getAwareness`, then applies both results to its local `Y.Doc` and `Awareness` with origin `"remote"`. After that, every change flows through `applyUpdate` and the broadcast events.
|
||||
|
||||
## Awareness And Presence
|
||||
|
||||
Presence (user names, colors, cursor positions) rides on the Yjs Awareness protocol instead of actor state:
|
||||
|
||||
- Clients set presence with `awareness.setLocalStateField` for the `user` and `cursor` fields. The awareness update listener encodes the change and sends `applyUpdate(update, "awareness", awareness.clientID)`.
|
||||
- The actor records each asserted `clientId` in that connection's `connState.clientIds`, applies the update to the server-side `Awareness`, and broadcasts the `awareness` event to all peers. See [Connections](/docs/actors/connections) for per-connection state.
|
||||
- `onDisconnect` reads the connection's `clientIds`, calls `removeAwarenessStates` on the server-side `Awareness`, and broadcasts the encoded removal so every remaining client drops the departed user's cursor. See [Lifecycle](/docs/actors/lifecycle) for the hook.
|
||||
|
||||
Because the actor tracks which awareness clientIds belong to which connection, presence cleanup is automatic on disconnect with no client cooperation required.
|
||||
|
||||
## Persistence And Compaction
|
||||
|
||||
The example persists with a full-snapshot overwrite: on every `"sync"` update, the actor re-encodes the entire merged document with `Y.encodeStateAsUpdate` and overwrites the single binary KV key `yjs:doc`. There is no append-only update log and no separate compaction job. Compaction is implicit because `Y.encodeStateAsUpdate` emits one compact merged representation of the document, so Yjs merge semantics keep the stored blob compact on their own.
|
||||
|
||||
| Property | Full-Snapshot Overwrite (the example) |
|
||||
| --- | --- |
|
||||
| Write cost | One full-document KV write per sync update, so every keystroke rewrites the whole blob. |
|
||||
| Read cost | One binary KV read in `createVars` rehydrates the document on actor start. |
|
||||
| Crash safety | The last completed `applyUpdate` is durable. No log replay needed. |
|
||||
| Sweet spot | Small to medium documents where simplicity beats write amplification. |
|
||||
|
||||
**Recommended extension (not in the example)**: for large documents or very high edit rates, switch to appending incremental updates to a KV update log and writing a merged snapshot only periodically (for example every N updates). Boot becomes snapshot plus log replay, and the snapshot write becomes the explicit compaction step that truncates the log. Adopt this only when full-snapshot writes become the measured bottleneck or the blob approaches KV value size limits.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Client A
|
||||
participant B as Client B
|
||||
participant DL as documentList
|
||||
participant D as document
|
||||
|
||||
A->>DL: listDocuments()
|
||||
A->>DL: createDocument(title)
|
||||
DL->>D: create([workspaceId, documentId], input)
|
||||
DL-->>A: DocumentSummary
|
||||
A->>D: connect
|
||||
B->>D: connect
|
||||
Note over D: createVars rehydrates Y.Doc from KV "yjs:doc"
|
||||
A->>D: getContent() + getAwareness()
|
||||
D-->>A: encoded doc + awareness state
|
||||
Note over A: local edit with origin "local"
|
||||
A->>D: applyUpdate(update, "sync")
|
||||
Note over D: apply with origin "client", overwrite KV snapshot
|
||||
D-->>B: sync event (incremental update)
|
||||
Note over B: apply with origin "remote", no echo
|
||||
A->>D: applyUpdate(update, "awareness", clientId)
|
||||
D-->>B: awareness event
|
||||
B-->>D: disconnect
|
||||
Note over D: onDisconnect removes B's awareness clientIds
|
||||
D-->>A: awareness event (removal)
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
The example ships with no authentication or authorization. Harden it with this baseline before production. None of these are implemented in the example.
|
||||
|
||||
- **Authenticate before connect**: Anyone who knows or guesses a workspace ID can connect, and because `useActor` implicitly getOrCreates, connecting with a nonexistent workspace ID silently creates a blank `documentList` coordinator. Add connection auth so unauthenticated clients never reach an actor. See [Authentication](/docs/actors/authentication).
|
||||
- **Per-document access control**: Validate that the authenticated user is allowed to access the specific `[workspaceId, documentId]` key, not just any document.
|
||||
- **Cap and rate limit `applyUpdate`**: Update payloads are unvalidated `number[]` arrays with no size limit, and the example client sends one action per keystroke and per cursor move with zero throttling. Enforce payload size caps and per-connection rate limits on the server, and debounce on the client.
|
||||
- **Do not trust client-asserted awareness clientIds**: The `clientId` argument to `applyUpdate` is client-supplied and trusted as-is. Derive or verify presence identity from connection-scoped server state instead.
|
||||
- **Destroy actors and KV on delete**: `deleteDocument` only filters the entry out of the coordinator's index. The document actor and its KV snapshot are orphaned. On delete, also destroy the document actor and its storage, with a permission check on who may delete.
|
||||
|
||||
_Source doc path: /cookbook/collaborative-text-editor_
|
||||
@@ -0,0 +1,141 @@
|
||||
# Cron Jobs and Scheduled Tasks
|
||||
|
||||
> Source: `src/content/cookbook/cron-jobs.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/cron-jobs
|
||||
> Description: Durable cron jobs with Rivet Actors: schedule.after and schedule.at timers survive restarts and crashes, plus re-arming recurring jobs and idempotent handlers.
|
||||
|
||||
---
|
||||
Patterns for running durable cron jobs and scheduled tasks on Rivet Actors. Actor schedules are persistent timers owned by the engine, so a job keeps its deadline through actor sleep, restarts, upgrades, deploys, and crashes.
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start from the working [Scheduling example on GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/scheduling). It implements a reminder service with one-shot timers, a React frontend, and live trigger events.
|
||||
|
||||
## The Scheduling API
|
||||
|
||||
The full API is documented in [Scheduling](/docs/actors/schedule). There are two methods, both available on the actor context:
|
||||
|
||||
| Method | Behavior |
|
||||
| --- | --- |
|
||||
| `c.schedule.after(duration, actionName, ...args)` | Runs the named action after `duration` milliseconds. |
|
||||
| `c.schedule.at(timestamp, actionName, ...args)` | Runs the named action at an exact epoch timestamp in milliseconds. |
|
||||
|
||||
Key properties:
|
||||
|
||||
- **Durable**: The schedule is persisted by the engine and the timer survives actor sleep, restart, upgrade, and crash. If the actor is asleep at the deadline, the engine wakes it to run the action. See [Lifecycle](/docs/actors/lifecycle).
|
||||
- **Plain actions as callbacks**: The scheduled callback is an ordinary [action](/docs/actors/actions) on the same actor, invoked by name. Arguments after the action name are forwarded positionally, for example `c.schedule.after(delayMs, "triggerReminder", reminder.id)`.
|
||||
- **No cancellation API**: Rivet does not currently support canceling a scheduled action. The pattern is a tombstone guard: remove the entry from [state](/docs/actors/state) and have the scheduled action no-op when it cannot find its entry. The example's `cancelReminder` and `triggerReminder` actions implement exactly this.
|
||||
|
||||
## Recurring Jobs Via Re-Arm
|
||||
|
||||
`c.schedule` is one-shot, so recurring jobs are built by having the scheduled action re-arm itself at the end of each run:
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const dailyReport = actor({
|
||||
state: { lastRunAt: 0 },
|
||||
actions: {
|
||||
runReport: (c) => {
|
||||
// Do the job's work, then record the run.
|
||||
c.state.lastRunAt = Date.now();
|
||||
// Re-arm the next run before returning.
|
||||
c.schedule.after(DAY_MS, "runReport");
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Arm the first run from `onCreate` or a setup action; after that, the action keeps the chain alive by rescheduling itself.
|
||||
|
||||
Re-arming with `after` measures the next run from the end of the current one, so the cadence drifts later by the job's runtime on every cycle. If runs must stay aligned to a fixed cadence, re-arm with `c.schedule.at(c.state.lastRunAt + DAY_MS, "runReport")` instead.
|
||||
|
||||
The Scheduling example itself only uses one-shot reminders. A real re-arm implementation lives in the [idle world actor](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/idle/), where the `collectProduction` action credits production, updates `lastCollectedAt`, and calls a `scheduleCollection` helper that re-arms with `c.schedule.after(delayMs, "collectProduction", { buildingId })`. It also shows catch-up handling: if the action runs late, it computes how many whole intervals elapsed since the last run and credits them in one batch before re-arming.
|
||||
|
||||
For multi-step jobs that need retries and progress tracking inside a single run, consider [Workflows](/docs/actors/workflows) instead of chaining schedules.
|
||||
|
||||
## Durability Comparison
|
||||
|
||||
| Approach | Timer Durability | Horizontal Scaling | Deploys And Restarts |
|
||||
| --- | --- | --- | --- |
|
||||
| `setTimeout` / `setInterval` | In-process memory only | Every replica arms its own timer, so jobs run once per instance | All pending timers are lost on restart or crash |
|
||||
| `node-cron` and similar libraries | In-process memory only | Every instance runs the job unless you add external locking | Schedule resets on deploy; runs missed during downtime are skipped |
|
||||
| External cron service | Lives outside your app | Needs a public HTTP endpoint plus its own dedupe and retry state | Survives your deploys but is separate infrastructure to operate |
|
||||
| Rivet Actor scheduling | Persisted by the engine as a durable timer | Exactly one actor per key, so the timer is armed once rather than once per replica | Survives actor sleep, restart, upgrade, and crash |
|
||||
|
||||
## Idempotency
|
||||
|
||||
A scheduled action can fire more than you expect: a crash between doing the work and re-arming can cause the action to run again, and because schedules cannot be cancelled, an action can fire for an entry that was already removed. Design handlers so a duplicate firing is harmless:
|
||||
|
||||
- **Store a run marker in state**: Keep a `lastRunAt` timestamp or a sequence number in actor state and update it inside the action. On each firing, compute elapsed time since the marker and skip or batch accordingly. The idle world actor's `collectProduction` does this with `lastCollectedAt` and whole-interval batching.
|
||||
- **Tombstone guard for cancelled entries**: The example's `triggerReminder` looks the reminder up in `c.state.reminders` first and returns with a warning if it is gone, so a fire-after-cancel is a safe no-op.
|
||||
- **Keep work and marker updates in the same action**: Actor state writes are persisted with the action, so updating the marker in the same handler that does the work keeps the two consistent.
|
||||
|
||||
## Topology
|
||||
|
||||
| Topology | Use When | Example Key |
|
||||
| --- | --- | --- |
|
||||
| Singleton job actor | One global job such as a nightly report or cleanup pass | `job["daily-report"]` |
|
||||
| Actor per scheduled entity | Per-user or per-resource timers such as reminders, trials, or billing periods | `reminder[userId]` |
|
||||
|
||||
The Scheduling example uses a single shared `reminderActor["main"]` key for demo simplicity. For production reminder systems, prefer one actor per user so timers, state, and load are isolated per entity. See [Keys](/docs/actors/keys).
|
||||
|
||||
## Reminder Service Example
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Scheduling | One-shot timers armed with `c.schedule.after(delayMs, "triggerReminder", reminder.id)` or `c.schedule.at(timestamp, "triggerReminder", reminder.id)`. |
|
||||
| State | JSON state holding `reminders` and `completedCount`; the scheduled action mutates state when it fires. |
|
||||
| Events | `triggerReminder` broadcasts a `reminderTriggered` event to all connected clients. See [Events](/docs/actors/events). |
|
||||
| Cancellation | `cancelReminder` only removes the reminder from state; the scheduled action may still fire and no-ops via a state lookup guard. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `reminderActor["main"]`
|
||||
- **Responsibility**: Stores reminders in persistent state, arms a future self-action per reminder via `c.schedule`, marks reminders completed when the scheduled action fires, and broadcasts `reminderTriggered` to connected clients.
|
||||
- **Actions**
|
||||
- `scheduleReminder`
|
||||
- `scheduleReminderAt`
|
||||
- `triggerReminder`
|
||||
- `getReminders`
|
||||
- `cancelReminder`
|
||||
- `getStats`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `reminders`
|
||||
- `completedCount`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant R as reminderActor
|
||||
participant E as Engine
|
||||
|
||||
C->>R: scheduleReminder(message, delayMs)
|
||||
R->>E: schedule.after(delayMs, triggerReminder, id)
|
||||
Note over E: schedule persisted + alarm armed
|
||||
R-->>C: reminder
|
||||
Note over R: actor sleeps
|
||||
E->>R: alarm fires, actor wakes
|
||||
Note over R: triggerReminder(id) runs
|
||||
R-->>C: reminderTriggered event
|
||||
Note over R: recurring jobs re-arm here with schedule.after
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
The example is intentionally open: any client can connect to the shared `["main"]` key and schedule or cancel anything. Treat all of the following as required extensions for production:
|
||||
|
||||
- **Validate schedule inputs**: Clamp `delayMs` and `timestamp` from clients. Reject negative delays, timestamps in the past, and absurdly far-future deadlines, and bound message or payload sizes.
|
||||
- **Never schedule client-chosen actions**: Expose specific actions like `scheduleReminder` that internally arm a fixed callback. Do not pass a client-supplied action name or unchecked args into `c.schedule`.
|
||||
- **Authenticate and scope keys**: Add connection [authentication](/docs/actors/authentication) and use per-user actor keys instead of one global key, so users cannot read or cancel each other's schedules.
|
||||
- **Prune completed entries**: The example's `reminders` array grows without bound. Remove or archive completed entries so state stays small.
|
||||
- **Use stable IDs**: Generate entry IDs with `crypto.randomUUID()` rather than timestamp-plus-random strings.
|
||||
|
||||
_Source doc path: /cookbook/cron-jobs_
|
||||
@@ -0,0 +1,152 @@
|
||||
# Live Cursors and Presence
|
||||
|
||||
> Source: `src/content/cookbook/live-cursors.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/live-cursors
|
||||
> Description: Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling.
|
||||
|
||||
---
|
||||
Patterns for building live cursors, multiplayer presence, and realtime cursor sharing with RivetKit. One room actor fans cursor positions out to every connected client, keyed per room with [actor keys](/docs/actors/keys).
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start with one of the two working variants on GitHub. Both implement the same collaborative cursor canvas with persistent text labels; they differ only in transport.
|
||||
|
||||
| Variant | Starter Code | Transport | Presence Storage |
|
||||
| --- | --- | --- | --- |
|
||||
| `cursors` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/cursors) | Typed [actions](/docs/actors/actions) and [events](/docs/actors/events) over the RivetKit connection | `connState` per connection |
|
||||
| `cursors-raw-websocket` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/cursors-raw-websocket) | Raw [`onWebSocket` handler](/docs/actors/websocket-handler) with a custom JSON message protocol | Socket map in `createVars` |
|
||||
|
||||
Use `cursors` by default: typed actions, typed events, and automatic connection tracking cover most apps with less code. Use `cursors-raw-websocket` when you need full control of the wire format, for example a custom JSON or binary protocol, or clients that do not use the RivetKit client library.
|
||||
|
||||
## Connection State vs Persistent State
|
||||
|
||||
Presence is ephemeral by definition. A cursor position is only meaningful while its connection is alive, so it belongs in per-connection storage, not in persistent actor state. Persistent state is reserved for data that must survive disconnects and actor restarts.
|
||||
|
||||
| Data | Where It Lives | Why |
|
||||
| --- | --- | --- |
|
||||
| Cursor position | `connState` (`cursors`) or the `createVars` socket map (`cursors-raw-websocket`) | Scoped to one connection and discarded with it. Stale presence cannot accumulate in storage. |
|
||||
| Text labels (`textLabels`) | Persistent actor `state` in both variants | Canvas content must survive disconnects and actor restarts. |
|
||||
|
||||
In the `cursors` variant, `updateCursor` writes `c.conn.state.cursor` and `getRoomState` rebuilds the presence snapshot by iterating `c.conns.values()`, so the cursor map is always derived from live connections rather than stored. See [Connections](/docs/actors/connections) for `connState` and [State](/docs/actors/state) for persistence semantics.
|
||||
|
||||
## Presence Lifecycle
|
||||
|
||||
- **Join**: The `cursors-raw-websocket` variant pushes an `init` message with the current `{ cursors, textLabels }` snapshot as soon as a socket connects. The `cursors` variant has no explicit join broadcast; the client calls the `getRoomState` action once after connecting to seed its local maps, and peers first see a new user on that user's first `cursorMoved` broadcast.
|
||||
- **Move**: Every `updateCursor` call writes the connection's presence entry, then broadcasts `cursorMoved` to all connections, including the sender.
|
||||
- **Leave**: The `cursors` variant handles leave in `onDisconnect`, broadcasting `cursorRemoved` with the connection's last cursor. The raw variant does the same from the socket `close` listener, then deletes the session from the `vars.websockets` map. Clients delete that user from their local cursor map, so stale cursors disappear the moment a tab closes.
|
||||
|
||||
See [Lifecycle](/docs/actors/lifecycle) for `onDisconnect` and `createVars`.
|
||||
|
||||
## Update Throttling
|
||||
|
||||
Neither example throttles. Both frontends send a cursor update on every raw `mousemove` event with no debounce or interval cap. That is fine for a demo, but a fast mouse on a high-refresh display can emit hundreds of events per second per user. The patterns below are recommended production hardening on top of the starter code, not something the examples implement.
|
||||
|
||||
| Layer | Pattern | Guidance |
|
||||
| --- | --- | --- |
|
||||
| Client (smoothness) | Throttle to 20-30Hz | Sample the latest pointer position every 33-50ms and send only that. Drop intermediate moves, but always flush the final position so cursors settle at the true location. Interpolate between received positions on the rendering side. |
|
||||
| Server (enforcement) | Per-connection rate limit | Track the last accepted update timestamp per connection and drop or coalesce updates arriving faster than your cap. Client throttles are cooperative; the actor is the enforcement boundary. |
|
||||
|
||||
## Actors
|
||||
|
||||
- **Key**: `cursorRoom[roomId]` (the frontend defaults `roomId` to `"general"`)
|
||||
- **Responsibility**: Holds per-connection cursor presence in `connState`, persists shared text labels in actor state, and broadcasts cursor and text updates to all connections.
|
||||
- **Actions**
|
||||
- `updateCursor`
|
||||
- `updateText`
|
||||
- `removeText`
|
||||
- `getRoomState`
|
||||
- **Events**
|
||||
- `cursorMoved`
|
||||
- `cursorRemoved`
|
||||
- `textUpdated`
|
||||
- `textRemoved`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `textLabels` (persistent)
|
||||
- `connState.cursor` per connection (ephemeral)
|
||||
|
||||
- **Key**: `cursorRoom[roomId]` (resolved via `client.cursorRoom.getOrCreate(roomId)`)
|
||||
- **Responsibility**: Exposes a raw WebSocket endpoint, tracks live sockets and their cursors in a `createVars` map keyed by a `sessionId` query parameter, persists text labels, and manually fans JSON frames out to every socket.
|
||||
- **Actions**
|
||||
- `getOrCreate` (stub returning `{ status: "ok" }`; the frontend resolves the actor ID with the client handle's `getOrCreate(roomId).resolve()`, which creates the actor without dispatching this action)
|
||||
- `getRoomState`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `textLabels` (persistent)
|
||||
- `vars.websockets` map of `sessionId` to socket and cursor (in-memory, lost on restart)
|
||||
|
||||
The raw variant defines no RivetKit events. Its message names are `type` fields on raw JSON frames:
|
||||
|
||||
| Direction | Message `type` | Payload |
|
||||
| --- | --- | --- |
|
||||
| Client to server | `updateCursor` | `{ userId, x, y }` |
|
||||
| Client to server | `updateText` | `{ id, userId, text, x, y }` |
|
||||
| Client to server | `removeText` | `{ id }` |
|
||||
| Server to client | `init` | `{ cursors, textLabels }` snapshot on connect |
|
||||
| Server to client | `cursorMoved`, `textUpdated`, `textRemoved`, `cursorRemoved` | The corresponding cursor, label, or ID payload |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### cursors (Actions + Events)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Client A
|
||||
participant R as cursorRoom
|
||||
participant B as Other Clients
|
||||
|
||||
A->>R: connect via useActor (cursorRoom[roomId])
|
||||
A->>R: getRoomState()
|
||||
R-->>A: {cursors, textLabels}
|
||||
loop every mouse move
|
||||
A->>R: updateCursor(userId, x, y)
|
||||
Note over R: write c.conn.state.cursor
|
||||
R-->>B: cursorMoved (broadcast)
|
||||
end
|
||||
A->>R: updateText(id, userId, text, x, y)
|
||||
Note over R: upsert persistent state.textLabels
|
||||
R-->>B: textUpdated (broadcast)
|
||||
Note over A: tab closes
|
||||
Note over R: onDisconnect reads conn.state.cursor
|
||||
R-->>B: cursorRemoved (broadcast)
|
||||
```
|
||||
|
||||
### cursors-raw-websocket
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Client A
|
||||
participant R as cursorRoom
|
||||
participant B as Other Clients
|
||||
|
||||
A->>R: getOrCreate(roomId).resolve()
|
||||
R-->>A: actorId
|
||||
A->>R: open WebSocket /gateway/{actorId}/websocket?sessionId=...
|
||||
Note over R: close 1008 if sessionId is missing
|
||||
Note over R: store socket in vars.websockets
|
||||
R-->>A: init {cursors, textLabels}
|
||||
loop every mouse move
|
||||
A->>R: {type: "updateCursor"} frame
|
||||
Note over R: update session cursor in vars
|
||||
R-->>B: cursorMoved frame
|
||||
end
|
||||
Note over A: socket closes
|
||||
R-->>B: cursorRemoved frame
|
||||
Note over R: delete session from vars.websockets
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Both examples ship without authentication so the presence pattern stays readable. Everything below is recommended hardening for production, not behavior the examples implement.
|
||||
|
||||
- **Identity**: Bind presence identity to the connection (`c.conn.id` in the actions variant, a server-generated session ID in the raw variant). Never trust a client-supplied `userId`; in the examples it is a random client-generated string, so any client can impersonate or remove any cursor.
|
||||
- **Authorization**: Authorize label mutations by owner. In the examples, `updateText` accepts arbitrary `id` and `userId` arguments and `removeText` accepts an arbitrary `id`, so any client can edit or delete any label.
|
||||
- **Input validation**: Clamp `x` and `y` to canvas bounds, cap text label length, and cap the total `textLabels` count so persistent state cannot grow unbounded.
|
||||
- **Rate limiting**: Enforce a per-connection cap on `updateCursor` (for example 30Hz) and on label writes, as described in [Update Throttling](#update-throttling).
|
||||
- **Protocol strictness (raw variant)**: Validate message shape before use and close the socket on malformed JSON instead of logging and continuing. Reject duplicate `sessionId` values rather than silently overwriting another session's socket entry.
|
||||
|
||||
_Source doc path: /cookbook/live-cursors_
|
||||
@@ -0,0 +1,713 @@
|
||||
# Multiplayer Game
|
||||
|
||||
> Source: `src/content/cookbook/multiplayer-game.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/multiplayer-game
|
||||
> Description: Pragmatic patterns for building multiplayer games: matchmaking, tick loops, realtime state, interest management, and validation.
|
||||
|
||||
---
|
||||
Patterns for building multiplayer games with RivetKit, intended as a practical checklist you can adapt per genre.
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start with one of the working examples on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/) and adapt it to your game. Do not start from scratch for matchmaking and lifecycle flows.
|
||||
|
||||
| Game Classification | Starter Code | Common Examples |
|
||||
| --- | --- | --- |
|
||||
| Battle Royale | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/battle-royale/) | Fortnite, Apex Legends, PUBG, Warzone |
|
||||
| Arena | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/arena/) | Call of Duty TDM/FFA, Halo Slayer, Counter-Strike casual, VALORANT unrated, Overwatch Quick Play, Rocket League |
|
||||
| IO Style | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/io-style/) | Agar.io, Slither.io, surviv.io |
|
||||
| Open World | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/open-world/) | Minecraft survival servers, Rust-like worlds, MMO zone/chunk worlds |
|
||||
| Party | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/party/) | Fall Guys private lobbies, custom game rooms, social party sessions |
|
||||
| Physics 2D | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-2d/) | Top-down physics brawlers, 2D arena games, platform fighters |
|
||||
| Physics 3D | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-3d/) | Physics sandbox sessions, 3D arena games, movement playgrounds |
|
||||
| Ranked | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/ranked/) | Chess ladders, competitive card games, duel arena ranked queues |
|
||||
| Turn-Based | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/turn-based/) | Chess correspondence, Words With Friends, async board games |
|
||||
| Idle | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/idle/) | Cookie Clicker, Idle Miner Tycoon, Adventure Capitalist |
|
||||
|
||||
## Server Simulation
|
||||
|
||||
### Game Loop And Tick Rates
|
||||
|
||||
| Pattern | Use When | Implementation Guidance |
|
||||
| --- | --- | --- |
|
||||
| Fixed realtime loop | Battle Royale, Arena, IO Style, Open World, Ranked | Run in `run` with `sleep(tickMs)` and exit on `c.aborted`. |
|
||||
| Action-driven updates | Party, Turn-Based | Mutate and broadcast only on actions/events rather than scheduled ticks. |
|
||||
| Coarse offline progression | Any mode with idle progression | Use `c.schedule.after(...)` with coarse windows (for example 5 to 15 minutes) and apply catch-up from elapsed wall clock time. |
|
||||
|
||||
### Physics
|
||||
|
||||
Start with custom kinematic logic for simple games. Switch to a full physics engine when you need joints, stacked bodies, high collision density, or complex shapes (rotated polygons, capsules, convex hulls, triangle meshes).
|
||||
|
||||
Pick one engine per simulation. Keep frontend-only libs out of backend simulation paths and treat server state as authoritative.
|
||||
|
||||
| Dimension | Primary Engine | Fallback Engines | Example Code |
|
||||
| --- | --- | --- | --- |
|
||||
| 2D | `@dimforge/rapier2d` | `planck-js`, `matter-js` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-2d/) |
|
||||
| 3D | `@dimforge/rapier3d` | `cannon-es`, `ammo.js` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-3d/) |
|
||||
|
||||
### Spatial Indexing
|
||||
|
||||
For non-physics spatial queries, use a dedicated index instead of naive `O(n^2)` checks:
|
||||
|
||||
| Index Type | Recommendation |
|
||||
| --- | --- |
|
||||
| AABB index | For AOI, visibility, and non-collider entities, use `rbush` for dynamic sets or `flatbush` for static-ish sets. |
|
||||
| Point index | For nearest-neighbor or within-radius queries, use `d3-quadtree`. |
|
||||
|
||||
## Networking & State Sync
|
||||
|
||||
### Netcode
|
||||
|
||||
| Model | When To Use | Implementation |
|
||||
| --- | --- | --- |
|
||||
| Hybrid (client movement, server combat) | Shooters, action sports, ranked duels | Client owns movement and sends capped-rate position updates. Server validates for anti-cheat. Combat (projectiles, hits, damage) is fully server-authoritative. |
|
||||
| Server-authoritative with interpolation | IO Style, persistent worlds | Client sends input commands. Server simulates on fixed ticks and publishes authoritative snapshots. Client interpolates between snapshots. |
|
||||
| Server-authoritative (basic logic) | Turn-based, event-driven | Server validates and applies discrete actions (turns, phase transitions, votes). Client displays confirmed state. |
|
||||
|
||||
### Realtime Data Model
|
||||
|
||||
- **Snapshots and diffs**: Publish state as events. Send a full snapshot on join/resync, then per-tick diffs for regular updates.
|
||||
- **Batch per tick**: Keep events small and typed. Batch high-frequency updates per tick.
|
||||
- **Avoid UI framework state for game updates**: Use `requestAnimationFrame` or a Canvas/Three.js loop for simulation, not React state. Reserve UI framework state for menus, HUD, and forms.
|
||||
- **Broadcast vs per-connection**: Use `c.broadcast(...)` for shared updates and `conn.send(...)` for private/per-player data.
|
||||
|
||||
### Shared Simulation Logic
|
||||
|
||||
Shared simulation logic runs on both the client and the server. For example, an `applyInput(state, input, dt)` function that integrates velocity and clamps to world bounds can run on the client for prediction and on the server for validation.
|
||||
|
||||
- **Hybrid modes**: Client runs shared movement as primary authority, server runs it for anti-cheat validation.
|
||||
- **Server-authoritative modes**: Client uses shared logic for interpolation and prediction only.
|
||||
- **Keep it pure**: Movement integration, input transforms, collision helpers, and constants only.
|
||||
- **Put shared code in `src/shared/`**: Keep deterministic helpers in `src/shared/sim/*` with no side effects.
|
||||
|
||||
### Interest Management
|
||||
|
||||
Control what each client receives to reduce bandwidth and prevent information leaks.
|
||||
|
||||
#### Per-Player Replication Filters
|
||||
|
||||
- **Filter by relevance**: Send each client only state relevant to that player (proximity, line-of-sight, team, or game phase).
|
||||
- **Shooters and action games**: Limit replication by proximity and optional field-of-view checks.
|
||||
- **Server-side only**: Clients should never receive data they should not see.
|
||||
|
||||
#### Sharded Worlds
|
||||
|
||||
- **Partition large worlds**: Use chunk actors keyed by `worldId:chunkX:chunkY`.
|
||||
- **Subscribe to nearby chunks**: Clients connect only to nearby partitions (for example a 3x3 chunk window).
|
||||
- **Use sparingly**: Only when the world is large and state-heavy (sandbox builders, MMOs), not as a default for small matches.
|
||||
|
||||
## Backend Infrastructure
|
||||
|
||||
### Persistence
|
||||
|
||||
- **In-memory state**: Best for realtime game state that changes every tick (player positions, inputs, match phase, scores).
|
||||
- **SQLite (`rivetkit/db`)**: Better for large or table-like state that needs queries, indexes, or long-term persistence (tiles, inventory, matchmaking pools). Serialize DB work through a queue since multiple actions can hit the same actor concurrently.
|
||||
|
||||
### Matchmaking Patterns
|
||||
|
||||
Common building blocks used across the architecture patterns below.
|
||||
|
||||
#### Actor Topology
|
||||
|
||||
| Primitive | Use When | Typical Ownership |
|
||||
| --- | --- | --- |
|
||||
| `matchmaker["main"]` + `match[matchId]` | Session-based multiplayer (battle royale, arena, ranked, party, turn-based) | Matchmaker owns discovery/assignment. Match owns lifecycle and gameplay state. |
|
||||
| `chunk[worldId,chunkX,chunkY]` | Large continuous worlds that need sharding | Each chunk owns local players, chunk state, and local simulation. |
|
||||
| `world[playerId]` | Per-player progression loops (idle/solo world state) | Per-player resources, buildings, timers, and progression. |
|
||||
| `player[username]` | Canonical profile/rating reused across matches | Durable player stats (for example rating and win/loss). |
|
||||
| `leaderboard["main"]` | Shared rankings across many matches/players | Global ordered score rows and top lists. |
|
||||
|
||||
#### Queueing Strategy
|
||||
|
||||
- Multiple players can hit the matchmaker at the same time, so actions like find/create, queue/unqueue, and close need to be serialized through actor queues to avoid races.
|
||||
- Match-local actions (gameplay, scoring) do not need queueing unless they write back to the matchmaker.
|
||||
|
||||
## Security And Anti-Cheat
|
||||
|
||||
Start with this baseline, then harden further for competitive or high-risk environments.
|
||||
|
||||
### Baseline Checklist
|
||||
|
||||
- **Identity**: Use `c.conn.id` as the authoritative transport identity. Treat `playerId`/`username` in params as untrusted input and bind through server-issued assignment/join tickets.
|
||||
- **Authorization**: Validate the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions).
|
||||
- **Input validation**: Clamp sizes/lengths, validate enums, and validate usernames (length, allowed chars, avoid unbounded Unicode).
|
||||
- **Rate limiting**: Per-connection rate limits for spammy actions (chat, join/leave, fire, movement updates).
|
||||
- **State integrity**: Server recomputes derived state (scores, win conditions, placements). Never allow client-authoritative changes to inventory/currency/leaderboard totals.
|
||||
|
||||
### Movement Validation
|
||||
|
||||
For any mode with client-authoritative movement (hybrid flows), clients may send position/rotation updates for smoothness, but the server must:
|
||||
|
||||
- Enforce max delta per update (speed cap) based on elapsed time.
|
||||
- Reject or clamp teleports.
|
||||
- Enforce world bounds (and basic collision if applicable).
|
||||
- Rate limit update frequency (for example 20Hz max).
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
Each game type below starts with a quick summary table, then details actors and lifecycle.
|
||||
|
||||
### Battle Royale
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | Immediate routing to the fullest non-started lobby (oldest tie-break); players wait in lobby until capacity, then the match starts. |
|
||||
| Netcode | Hybrid. Client owns movement, camera, and local prediction. Server owns zone state, projectiles, hit resolution, eliminations, loot, and final placement. |
|
||||
| Tick Rate | 10 ticks/sec (`100ms`) with a fixed loop for zone progression and lifecycle checks. |
|
||||
| Physics | Client owns movement with server anti-cheat validation; projectiles, hits, and damage are server-authoritative. Use `@dimforge/rapier3d` for 3D or `@dimforge/rapier2d` for top-down 2D. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `matchmaker["main"]`
|
||||
- **Responsibility**: Finds or creates lobbies, tracks pending reservations, and maintains occupancy.
|
||||
- **Actions**
|
||||
- `findMatch`
|
||||
- `pendingPlayerConnected`
|
||||
- `updateMatch`
|
||||
- `closeMatch`
|
||||
- **Queues**
|
||||
- `findMatch`
|
||||
- `pendingPlayerConnected`
|
||||
- `updateMatch`
|
||||
- `closeMatch`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `matches`
|
||||
- `pending_players`
|
||||
- `player_count` includes connected and pending players
|
||||
|
||||
- **Key**: `match[matchId]`
|
||||
- **Responsibility**: Runs lobby/live/finished phases, owns player state, zone progression, and eliminations.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- Movement and combat actions
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `phase`
|
||||
- `players`
|
||||
- `zone`
|
||||
- `eliminations`
|
||||
- `snapshot data`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
C->>MM: findMatch()
|
||||
alt no open lobby
|
||||
MM->>M: create(matchId)
|
||||
end
|
||||
MM-->>C: {matchId, playerId}
|
||||
C->>M: connect(playerId)
|
||||
M->>MM: pendingPlayerConnected(matchId, playerId)
|
||||
MM-->>M: accepted
|
||||
Note over M: lobby countdown -> live
|
||||
M-->>C: snapshot + shoot events
|
||||
M->>MM: closeMatch(matchId)
|
||||
```
|
||||
|
||||
### Arena
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | Mode-based fixed-capacity queues (`duo`, `squad`, `ffa`) that build only full matches and pre-assign teams (except FFA). |
|
||||
| Netcode | Hybrid. Client owns movement plus prediction and smoothing. Server owns team or FFA assignment, projectiles, hit resolution, phase transitions, and scoring. |
|
||||
| Tick Rate | 20 ticks/sec (`50ms`) with a tighter loop for live team and FFA snapshots. |
|
||||
| Physics | Medium to high intensity; client movement with server validation and server-authoritative combat/entities. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `matchmaker["main"]`
|
||||
- **Responsibility**: Runs mode queues, builds full matches, assigns teams, and publishes assignments.
|
||||
- **Actions**
|
||||
- `queueForMatch`
|
||||
- `unqueueForMatch`
|
||||
- `matchCompleted`
|
||||
- **Queues**
|
||||
- `queueForMatch`
|
||||
- `unqueueForMatch`
|
||||
- `matchCompleted`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `player_pool`
|
||||
- `matches`
|
||||
- `assignments` keyed by connection and player
|
||||
|
||||
- **Key**: `match[matchId]`
|
||||
- **Responsibility**: Runs match phases and in-match player/team state for score and win conditions.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- Gameplay actions
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `phase`
|
||||
- `players`
|
||||
- `team assignments`
|
||||
- `score and win state`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
C->>MM: queueForMatch(mode)
|
||||
Note over MM: enqueue in player_pool
|
||||
Note over MM: fill when capacity reached
|
||||
MM->>M: create(matchId, assignments)
|
||||
Note over MM: persist assignments
|
||||
MM-->>C: assignmentReady
|
||||
C->>M: connect(playerId)
|
||||
Note over M: waiting -> live when all players connect
|
||||
M->>MM: matchCompleted(matchId)
|
||||
```
|
||||
|
||||
### IO Style
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | Open-lobby routing to the fullest room below capacity; room counts are heartbeated and new lobbies are auto-created when needed. |
|
||||
| Netcode | Server-authoritative with interpolation. Client sends input intents and interpolates. Server owns movement, bounds, room membership, and canonical snapshots. |
|
||||
| Tick Rate | 10 ticks/sec (`100ms`) with lightweight periodic room snapshots. |
|
||||
| Physics | Low to medium intensity; server-authoritative kinematic movement, escalating to a physics engine only when collisions get complex. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `matchmaker["main"]`
|
||||
- **Responsibility**: Routes players into the fullest open lobby and tracks reservations and occupancy.
|
||||
- **Actions**
|
||||
- `findLobby`
|
||||
- `pendingPlayerConnected`
|
||||
- `updateMatch`
|
||||
- `closeMatch`
|
||||
- **Queues**
|
||||
- `findLobby`
|
||||
- `pendingPlayerConnected`
|
||||
- `updateMatch`
|
||||
- `closeMatch`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `matches`
|
||||
- `pending_players`
|
||||
- Occupancy includes pending reservations
|
||||
|
||||
- **Key**: `match[matchId]`
|
||||
- **Responsibility**: Runs per-match movement simulation and broadcasts snapshots.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- `setInput`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `players`
|
||||
- `inputs`
|
||||
- `movement state`
|
||||
- `snapshot cache`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
C->>MM: findLobby()
|
||||
alt no open lobby
|
||||
MM->>M: create(matchId)
|
||||
end
|
||||
MM-->>C: {matchId, playerId}
|
||||
C->>M: connect(playerId)
|
||||
M->>MM: pendingPlayerConnected(matchId, playerId)
|
||||
MM-->>M: accepted
|
||||
Note over M: fixed tick simulation
|
||||
M-->>C: snapshot events
|
||||
M->>MM: closeMatch(matchId)
|
||||
```
|
||||
|
||||
### Open World
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | Client-driven chunk routing from world coordinates, with nearby chunk windows preloaded via adjacent chunk connections. |
|
||||
| Netcode | Hybrid for sandbox (client movement with validation) or server-authoritative for MMO-like flows. Server owns chunk routing, persistence, and canonical world state. |
|
||||
| Tick Rate | 10 ticks/sec per chunk actor (`100ms`), so load scales with active chunks. |
|
||||
| Physics | Medium to high at scale; chunk-local simulation can be server-authoritative (MMO-like) or client movement with server validation (sandbox-like). |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `chunk[worldId,chunkX,chunkY]`
|
||||
- **Responsibility**: Owns chunk-local players, blocks, movement tick, and chunk membership.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- `enterChunk`
|
||||
- `addPlayer`
|
||||
- `setInput`
|
||||
- `leaveChunk`
|
||||
- `removePlayer`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `connections`
|
||||
- `players`
|
||||
- `blocks` scoped to one chunk key
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant CH as chunk
|
||||
|
||||
Note over C: resolve chunk keys from world position
|
||||
loop each visible chunk
|
||||
C->>CH: connect(worldId, chunkX, chunkY, playerId)
|
||||
Note over CH: store connection metadata
|
||||
end
|
||||
C->>CH: enterChunk/addPlayer
|
||||
loop movement updates
|
||||
C->>CH: setInput(...)
|
||||
CH-->>C: snapshot
|
||||
end
|
||||
C->>CH: leaveChunk/removePlayer or disconnect
|
||||
Note over CH: remove membership and metadata
|
||||
```
|
||||
|
||||
### Party
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | Host-created private party flow using party codes and explicit joins. |
|
||||
| Netcode | Server-authoritative (basic logic). Server owns membership, host permissions, and phase transitions. |
|
||||
| Tick Rate | No continuous tick; updates are event-driven (`join`, `start`, `finish`). |
|
||||
| Physics | Low intensity for lobby-first flows; usually no dedicated physics or indexing unless you add realtime mini-games. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `matchmaker["main"]`
|
||||
- **Responsibility**: Handles party create/join flow, validates join tickets, and tracks party size.
|
||||
- **Actions**
|
||||
- `createParty`
|
||||
- `joinParty`
|
||||
- `verifyJoin`
|
||||
- `updatePartySize`
|
||||
- `closeParty`
|
||||
- **Queues**
|
||||
- `createParty`
|
||||
- `joinParty`
|
||||
- `verifyJoin`
|
||||
- `updatePartySize`
|
||||
- `closeParty`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `parties`
|
||||
- `join_tickets` for party lookup and join validation
|
||||
|
||||
- **Key**: `match[matchId]`
|
||||
- **Responsibility**: Owns party members, host role, ready flags, and phase transitions.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- `startGame`
|
||||
- `finishGame`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `members`
|
||||
- `host`
|
||||
- `ready state`
|
||||
- `phase`
|
||||
- `party events`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
### Host Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as Host Client
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
H->>MM: createParty()
|
||||
MM-->>H: {matchId, partyCode, playerId, joinToken}
|
||||
H->>M: connect(playerId, joinToken)
|
||||
M->>MM: verifyJoin(...)
|
||||
MM-->>M: allowed
|
||||
M->>MM: updatePartySize(playerCount)
|
||||
H->>M: startGame() / finishGame()
|
||||
M->>MM: closeParty(matchId)
|
||||
```
|
||||
|
||||
### Joiner Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant J as Joiner Client
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
J->>MM: joinParty(partyCode)
|
||||
MM-->>J: {matchId, playerId, joinToken}
|
||||
J->>M: connect(playerId, joinToken)
|
||||
M->>MM: verifyJoin(...)
|
||||
MM-->>M: allowed / denied
|
||||
M->>MM: updatePartySize(playerCount)
|
||||
```
|
||||
|
||||
### Ranked
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | ELO-based queue pairing with a widening search window as wait time increases. |
|
||||
| Netcode | Hybrid. Client owns movement with local prediction and interpolation. Server owns projectiles, hit resolution, match results, and rating updates. |
|
||||
| Tick Rate | 20 ticks/sec (`50ms`) with fixed live ticks for deterministic pacing and broadcast cadence. |
|
||||
| Physics | Medium to high intensity; client movement with server validation and server-authoritative combat/hit resolution. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `matchmaker["main"]`
|
||||
- **Responsibility**: Runs rating-based queueing, pairing, assignment persistence, and completion fanout.
|
||||
- **Actions**
|
||||
- `queueForMatch`
|
||||
- `unqueueForMatch`
|
||||
- `matchCompleted`
|
||||
- **Queues**
|
||||
- `queueForMatch`
|
||||
- `unqueueForMatch`
|
||||
- `matchCompleted`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `player_pool`
|
||||
- `matches`
|
||||
- `assignments` with rating window and connection scoping
|
||||
|
||||
- **Key**: `match[matchId]`
|
||||
- **Responsibility**: Runs ranked match phase, score, and winner reporting.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- Gameplay actions
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `phase`
|
||||
- `players`
|
||||
- `score`
|
||||
- `winner`
|
||||
- `completion payload`
|
||||
|
||||
- **Key**: `player[username]`
|
||||
- **Responsibility**: Stores canonical player MMR and win/loss profile.
|
||||
- **Actions**
|
||||
- `initialize`
|
||||
- `getRating`
|
||||
- `applyMatchResult`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `rating`
|
||||
- `wins`
|
||||
- `losses`
|
||||
- `match counters`
|
||||
|
||||
- **Key**: `leaderboard["main"]`
|
||||
- **Responsibility**: Stores and serves top-ranked players.
|
||||
- **Actions**
|
||||
- `updatePlayer`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- SQLite
|
||||
- Leaderboard score rows
|
||||
- Top-list ordering
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant MM as matchmaker
|
||||
participant P as player
|
||||
participant M as match
|
||||
participant LB as leaderboard
|
||||
|
||||
C->>MM: queueForMatch(username)
|
||||
MM->>P: initialize/getRating
|
||||
P-->>MM: rating
|
||||
Note over MM: store queue row + retry pairing
|
||||
MM->>M: create(matchId, assigned players)
|
||||
MM-->>C: assignmentReady
|
||||
C->>M: connect(username)
|
||||
M->>MM: matchCompleted(...)
|
||||
MM->>P: applyMatchResult(...)
|
||||
MM->>LB: updatePlayer(...)
|
||||
Note over MM: remove matches + assignments rows
|
||||
```
|
||||
|
||||
### Turn-Based
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | Async private-invite and public-queue pairing in the same pattern. |
|
||||
| Netcode | Server-authoritative (basic logic). Client can draft moves before submit. Server owns turn ownership, committed move log, turn order, and completion state. |
|
||||
| Tick Rate | No continuous tick; move submission and turn transitions drive updates. |
|
||||
| Physics | Very low intensity; no realtime physics loop, just discrete rules validation. Indexing is optional and mostly for board or query convenience at scale. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `matchmaker["main"]`
|
||||
- **Responsibility**: Handles private invite and public queue pairing for async matches.
|
||||
- **Actions**
|
||||
- `createGame`
|
||||
- `joinByCode`
|
||||
- `queueForMatch`
|
||||
- `unqueueForMatch`
|
||||
- `closeMatch`
|
||||
- **Queues**
|
||||
- `createGame`
|
||||
- `joinByCode`
|
||||
- `queueForMatch`
|
||||
- `unqueueForMatch`
|
||||
- `closeMatch`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `matches`
|
||||
- `player_pool`
|
||||
- `assignments` for invite and queue mapping
|
||||
|
||||
- **Key**: `match[matchId]`
|
||||
- **Responsibility**: Owns board state, turn order, move validation, and final result.
|
||||
- **Actions**
|
||||
- `connect`
|
||||
- `makeMove`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- `board`
|
||||
- `turns`
|
||||
- `players`
|
||||
- `connection presence`
|
||||
- `result`
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
### Public Queue
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Client A
|
||||
participant B as Client B
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
A->>MM: queueForMatch()
|
||||
B->>MM: queueForMatch()
|
||||
Note over MM: pair first two queued players
|
||||
MM->>M: create(matchId) + seed X/O players
|
||||
MM-->>A: assignment/match info
|
||||
MM-->>B: assignment/match info
|
||||
A->>M: connect(playerId)
|
||||
B->>M: connect(playerId)
|
||||
A->>M: makeMove()
|
||||
B->>M: makeMove()
|
||||
opt all players disconnected for timeout
|
||||
Note over M: destroy after idle timeout
|
||||
end
|
||||
M->>MM: closeMatch(matchId)
|
||||
```
|
||||
|
||||
### Private Invite
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Client A
|
||||
participant B as Client B
|
||||
participant MM as matchmaker
|
||||
participant M as match
|
||||
|
||||
A->>MM: createGame()
|
||||
MM-->>A: {matchId, playerId, inviteCode}
|
||||
B->>MM: joinByCode(inviteCode)
|
||||
MM->>M: create(matchId) + seed X/O players
|
||||
MM-->>A: assignment/match info
|
||||
MM-->>B: assignment/match info
|
||||
A->>M: connect(playerId)
|
||||
B->>M: connect(playerId)
|
||||
A->>M: makeMove()
|
||||
B->>M: makeMove()
|
||||
M->>MM: closeMatch(matchId)
|
||||
```
|
||||
|
||||
### Idle
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Matchmaking | No matchmaker; each player uses a direct per-player actor and a shared leaderboard actor. |
|
||||
| Netcode | Server-authoritative (basic logic). Client owns UI and build intent. Server owns resources, production rates, building validation, and leaderboard totals. |
|
||||
| Tick Rate | No continuous tick; use `c.schedule.after(...)` for coarse intervals and compute offline catch-up from elapsed wall time. |
|
||||
| Physics | None for standard idle loops; transitions are discrete (`build`, `collect`, `upgrade`) and do not need spatial indexing. |
|
||||
|
||||
**Actors**
|
||||
|
||||
- **Key**: `world[playerId]`
|
||||
- **Responsibility**: Owns one player's progression, buildings, production scheduling, and state updates.
|
||||
- **Actions**
|
||||
- `initialize`
|
||||
- `build`
|
||||
- `collectProduction`
|
||||
- **Queues**
|
||||
- None
|
||||
- **State**
|
||||
- JSON
|
||||
- Per-player buildings
|
||||
- `resources`
|
||||
- `timers`
|
||||
- `progression state`
|
||||
|
||||
- **Key**: `leaderboard["main"]`
|
||||
- **Responsibility**: Stores global scores and serves leaderboard updates.
|
||||
- **Actions**
|
||||
- `updateScore`
|
||||
- **Queues**
|
||||
- `updateScore`
|
||||
- **State**
|
||||
- SQLite
|
||||
- `scores` table keyed by player
|
||||
- Current leaderboard totals
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant W as world
|
||||
participant LB as leaderboard
|
||||
|
||||
C->>W: getOrCreate(playerId) + initialize()
|
||||
Note over W: seed state + schedule collection
|
||||
W-->>C: stateUpdate
|
||||
loop gameplay loop
|
||||
C->>W: build() / collectProduction()
|
||||
W->>LB: updateScore(...)
|
||||
Note over LB: upsert scores
|
||||
LB-->>C: leaderboardUpdate
|
||||
W-->>C: stateUpdate
|
||||
end
|
||||
```
|
||||
|
||||
_Source doc path: /cookbook/multiplayer-game_
|
||||
@@ -0,0 +1,119 @@
|
||||
# Database per Tenant
|
||||
|
||||
> Source: `src/content/cookbook/per-tenant-database.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/per-tenant-database
|
||||
> Description: Multi-tenant data isolation with one Rivet Actor per tenant: the actor key is the tenant id, so each tenant gets its own isolated dataset and migrations.
|
||||
|
||||
---
|
||||
Patterns for database-per-tenant architectures with RivetKit. Instead of one shared database with a `tenant_id` column on every table, each tenant gets its own Rivet Actor, and that actor owns the tenant's entire dataset.
|
||||
|
||||
## Starter Code
|
||||
|
||||
Start with the working example on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database) and adapt it. The example stores each tenant's dataset in JSON actor state and serves a React dashboard with live event updates.
|
||||
|
||||
| Topic | Summary |
|
||||
| --- | --- |
|
||||
| Isolation | One `companyDatabase` actor per tenant, keyed by company name. Switching tenants swaps the entire dataset. |
|
||||
| State | JSON actor state holding `employees` and `projects` arrays plus timestamps. No SQLite, no queues, no scheduling. |
|
||||
| Realtime | Every write action mutates state, then broadcasts a typed event (`employeeAdded`, `projectAdded`) to all connected clients of that tenant. |
|
||||
| Auth | None. The sign-in screen is cosmetic. Production guidance is in the [security checklist](#security-checklist). |
|
||||
|
||||
## The Isolation Model
|
||||
|
||||
The actor key is the tenant id. The client connects with `useActor({ name: "companyDatabase", key: [companyName] })` and the actor reads `c.key[0]` in `createState` to seed that tenant's dataset. This gives you:
|
||||
|
||||
- **One actor per tenant**: `companyDatabase[tenantId]` addresses exactly one actor instance. Two tenants can never share an actor.
|
||||
- **One dataset per tenant**: All reads and writes go through that actor's [state](/docs/actors/state), so there is no shared table with a `tenant_id` column to filter incorrectly. Cross-tenant leaks require constructing the wrong key, not forgetting a `WHERE` clause.
|
||||
- **No key injection**: Keys are arrays, not interpolated strings. `key: [tenantId]` cannot be escaped the way `"tenant:" + tenantId` string concatenation can. See [Keys](/docs/actors/keys).
|
||||
|
||||
The example's test ([tests/per-tenant-database.test.ts](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database/tests/per-tenant-database.test.ts)) proves the isolation: data written to `companyDatabase["Alpha Co"]` never appears in `companyDatabase["Beta Co"]`.
|
||||
|
||||
## Choosing a State Backend
|
||||
|
||||
The example uses plain JSON actor state. The same key-equals-tenant model works with any actor state backend.
|
||||
|
||||
| Backend | Use When | Docs | Working Code |
|
||||
| --- | --- | --- | --- |
|
||||
| JSON actor state | Small datasets, simple reads, whole dataset fits comfortably in memory. What the example uses. | [State](/docs/actors/state) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database) |
|
||||
| Actor SQLite (`rivetkit/db`) | Tables, indexes, SQL queries, larger-than-memory data, per-tenant relational schema. | [SQLite](/docs/actors/sqlite) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/kitchen-sink/src/actors/state/sqlite-raw.ts) |
|
||||
| SQLite + Drizzle | Typed schema, query builder, and generated migration files on top of actor SQLite. | [SQLite + Drizzle](/docs/actors/sqlite-drizzle) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/kitchen-sink/src/actors/state/sqlite-drizzle/) |
|
||||
|
||||
With either SQLite option, every tenant gets its own embedded SQLite database, since the database is scoped to the actor and the actor is scoped to the tenant.
|
||||
|
||||
## Migrations
|
||||
|
||||
The per-tenant example has no migrations because JSON state has no schema. When you adopt SQLite, migrations run per tenant database:
|
||||
|
||||
- **Raw SQL**: `db({ onMigrate })` runs your migration SQL inside a SQLite savepoint before the actor serves traffic. If `onMigrate` throws, all migration SQL rolls back atomically and the actor does not start. See [SQLite](/docs/actors/sqlite).
|
||||
- **Drizzle**: `drizzle-kit` generates migration files from your typed schema, and `db({ schema, migrations })` applies them when the actor wakes. See [SQLite + Drizzle](/docs/actors/sqlite-drizzle).
|
||||
|
||||
Because each tenant has its own database, migrations roll out per actor as each tenant's actor wakes, rather than as one large migration against a shared database.
|
||||
|
||||
## Tenant Id Must Come From Auth
|
||||
|
||||
The example's sign-in is cosmetic: the client picks any company string and that string becomes the actor key, so any visitor can read and write any tenant's data. Do not ship this. As a required production extension (not implemented by the example):
|
||||
|
||||
- Derive the tenant id from a verified credential, such as a JWT claim, never from user input.
|
||||
- Validate the credential against `c.key` in `onBeforeConnect` (pass/fail) or `createConnState` (store the verified user on connection state). See [Authentication](/docs/actors/authentication) and [Connections](/docs/actors/connections).
|
||||
- Add per-action permission checks on top of connection-level auth. See [Access Control](/docs/actors/access-control).
|
||||
|
||||
## Actors
|
||||
|
||||
- **Key**: `companyDatabase[companyName]` (single-element array key; `c.key[0]` is the company name)
|
||||
- **Responsibility**: One actor per tenant. Holds that company's employees and projects in persistent state, serves reads and writes via actions, and broadcasts mutations to connected clients.
|
||||
- **Actions**
|
||||
- `addEmployee`
|
||||
- `listEmployees`
|
||||
- `addProject`
|
||||
- `listProjects`
|
||||
- `getStats`
|
||||
- **Queues**
|
||||
- None
|
||||
- **Events**
|
||||
- `employeeAdded`
|
||||
- `projectAdded`
|
||||
- **State**
|
||||
- JSON
|
||||
- `company_name`
|
||||
- `employees`
|
||||
- `projects`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
Every write action follows the same mutate-then-broadcast shape: push the record into `c.state`, bump `updated_at`, broadcast the typed event, return the record. See [Actions](/docs/actors/actions) and [Events](/docs/actors/events).
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Tenant A client
|
||||
participant DA as companyDatabase A
|
||||
participant B as Tenant B client
|
||||
participant DB as companyDatabase B
|
||||
|
||||
Note over A: authenticate and derive tenant id
|
||||
A->>DA: connect with key [tenantA]
|
||||
Note over DA: createState seeds company_name, employees, projects
|
||||
A->>DA: listEmployees() + listProjects() + getStats()
|
||||
A->>DA: addEmployee(name, role)
|
||||
DA-->>A: employeeAdded event
|
||||
B->>DB: connect with key [tenantB]
|
||||
Note over DB: separate actor, separate dataset
|
||||
B->>DB: listEmployees()
|
||||
DB-->>B: tenant B data only
|
||||
```
|
||||
|
||||
In the example, the "authenticate" step is a free-text company picker. The rest of the flow matches the diagram: `createState` seeds the dataset on first creation, the dashboard loads with `listEmployees`, `listProjects`, and `getStats`, and every connected client of the same tenant receives `employeeAdded` and `projectAdded` events.
|
||||
|
||||
## Security Checklist
|
||||
|
||||
The example ships with none of these. Apply all of them before production.
|
||||
|
||||
- **Tenant identity**: Derive the tenant id from a verified JWT claim, never from a client-supplied string.
|
||||
- **Connection validation**: In `onBeforeConnect` or `createConnState`, verify the credential's tenant claim matches `c.key` and reject mismatches.
|
||||
- **Per-action authorization**: Check the caller's role before mutating actions (`addEmployee`, `addProject`), not just at connect time. See [Access Control](/docs/actors/access-control).
|
||||
- **Input validation**: Clamp name and role lengths and validate enums. The example only trims input and substitutes fallback defaults.
|
||||
- **Key construction**: Always pass the tenant id as an array element (`key: [tenantId]`). Never interpolate tenant ids into key strings, and never build keys from one tenant's input to address another tenant's actor.
|
||||
- **Growth limits**: As a recommended extension, cap or paginate the `employees` and `projects` arrays. The example lets them grow unboundedly in JSON state; move to [SQLite](/docs/actors/sqlite) when the dataset outgrows memory.
|
||||
|
||||
_Source doc path: /cookbook/per-tenant-database_
|
||||
@@ -0,0 +1,136 @@
|
||||
# Deploying Rivet in a VPC or Air-Gapped Network
|
||||
|
||||
> Source: `src/content/cookbook/vpc-air-gapped.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/vpc-air-gapped
|
||||
> Description: Run Rivet entirely inside your own perimeter: single-binary or Docker Compose install, file system storage with no database infrastructure, and no outbound telemetry by default.
|
||||
|
||||
---
|
||||
Patterns for running self-hosted Rivet inside a private network: a VPC without internet egress, an on-premises rack, or a fully air-gapped environment. The engine is one service, the recommended single-node storage backend is the local file system, and the engine makes no outbound connections by default. Self-hosting is the only Rivet deployment model that supports air-gapped networks; see the [Self-Hosting Overview](/docs/self-hosting) for the full comparison with BYOC.
|
||||
|
||||
## What Runs Inside the Perimeter
|
||||
|
||||
A self-hosted deployment has three components, all of which live inside your network:
|
||||
|
||||
| Component | Role | Inside the perimeter |
|
||||
| --- | --- | --- |
|
||||
| Your backend | Your application server, including the runner that executes actor code | Yes |
|
||||
| Rivet Engine | Orchestration service that manages actor lifecycle, routes messages, and serves the dashboard and APIs | Yes |
|
||||
| Storage | Persistence for actor state. Local file system for single-node, PostgreSQL or FoundationDB for multi-node | Yes |
|
||||
|
||||
There is no license server, no Rivet Cloud account, and no callback to `rivet.dev`. Clients inside the perimeter reach actors through the engine's gateway over your private network. See [Architecture](/docs/self-hosting#architecture).
|
||||
|
||||
## Single-Binary Install
|
||||
|
||||
The engine compiles to a single `rivet-engine` binary. Build it from source outside the perimeter, then copy the binary across the boundary:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rivet-dev/rivet.git
|
||||
cd rivet
|
||||
cargo build --release -p rivet-engine
|
||||
# Copy target/release/rivet-engine into the perimeter.
|
||||
```
|
||||
|
||||
Prebuilt binaries are coming soon; see [Installing Rivet Engine](/docs/self-hosting/install) for current options.
|
||||
|
||||
Run it with the file system backend, which stores everything on local disk and is the production-ready choice for single-node deployments. The [File System](/docs/self-hosting/filesystem) docs list air-gapped environments as a primary use case because it needs no database infrastructure:
|
||||
|
||||
```bash
|
||||
RIVET__database__file_system__path="/var/lib/rivet/data" ./rivet-engine
|
||||
```
|
||||
|
||||
Configuration can also come from files. The engine discovers config at `/etc/rivet/config.json` on Linux (JSON, JSON5, JSONC, YAML, and YML are all supported), and `--config` overrides the path. Environment variables use the `RIVET__` prefix with `__` as the separator. See [Configuration](/docs/self-hosting/configuration).
|
||||
|
||||
The engine serves its own dashboard on port `6420`, so inspection and namespace management work with nothing but a browser inside the perimeter.
|
||||
|
||||
## Docker Compose Deployment
|
||||
|
||||
For Docker hosts without registry access, move the engine image across the boundary the standard way:
|
||||
|
||||
```bash
|
||||
# Outside the perimeter.
|
||||
docker pull rivetdev/engine:latest
|
||||
docker save rivetdev/engine:latest -o rivet-engine.tar
|
||||
# Inside the perimeter.
|
||||
docker load -i rivet-engine.tar
|
||||
```
|
||||
|
||||
Then run the engine and your app together in one Compose file:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
rivet-engine:
|
||||
image: rivetdev/engine:latest
|
||||
ports:
|
||||
- "6420:6420"
|
||||
volumes:
|
||||
- rivet-data:/data
|
||||
environment:
|
||||
RIVET__FILE_SYSTEM__PATH: "/data"
|
||||
restart: unless-stopped
|
||||
|
||||
my-app:
|
||||
build: .
|
||||
environment:
|
||||
RIVET_ENDPOINT: "http://default:admin@rivet-engine:6420"
|
||||
depends_on:
|
||||
- rivet-engine
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
rivet-data:
|
||||
```
|
||||
|
||||
`RIVET_ENDPOINT` uses the format `http://namespace:token@host:port` and tells your app to connect to the engine as a runner instead of running standalone. After both services start, register your runner with the engine through the dashboard or its API. The full walkthrough, including PostgreSQL setup for multi-node deployments, is in [Docker Compose](/docs/self-hosting/docker-compose).
|
||||
|
||||
## No Outbound Telemetry
|
||||
|
||||
The engine exports traces and metrics only when you opt in with OpenTelemetry. Export is disabled unless `RIVET_OTEL_ENABLED=1` is set, and the export target defaults to a local collector at `http://localhost:4317`. With no configuration, nothing crosses the perimeter.
|
||||
|
||||
When you want observability, keep it inside the network:
|
||||
|
||||
- Set `RIVET_OTEL_ENABLED=1` and point `RIVET_OTEL_GRPC_ENDPOINT` at a collector you run inside the perimeter.
|
||||
- Adjust `RIVET_OTEL_SAMPLER_RATIO` to control trace sampling.
|
||||
- Use the engine's health endpoint for liveness and readiness probes.
|
||||
|
||||
See the [Production Checklist](/docs/self-hosting/production-checklist) for monitoring guidance.
|
||||
|
||||
## Embedding Rivet in a Customer's Environment
|
||||
|
||||
If you ship software that runs inside your customers' VPCs, the same setup turns Rivet into an internal component of your product rather than a service your customers must reach over the internet:
|
||||
|
||||
- **Ship the engine next to your app.** Add `rivetdev/engine` to the Compose file or chart you already deliver. Your app finds it over the private network via `RIVET_ENDPOINT`, so one artifact deploys the whole stack.
|
||||
- **One namespace per install.** The endpoint URL carries the namespace and token (`http://namespace:token@host:port`), so a single image works across customer deployments. See [Endpoints](/docs/general/endpoints).
|
||||
- **Generate a strong admin token per install.** Replace the default token and keep it server-side. Never include the admin token in `RIVET_PUBLIC_ENDPOINT` or anywhere clients can read it.
|
||||
- **Public endpoint only when needed.** `RIVET_PUBLIC_ENDPOINT` with a public (`pk_`) token is only required when browser clients connect to actors in [serverless runtime mode](/docs/general/runtime-modes). Backend-only deployments can skip it entirely.
|
||||
- **TLS at the customer's edge.** Terminate TLS with the customer's reverse proxy or load balancer in front of the engine.
|
||||
|
||||
## Scaling Past One Node
|
||||
|
||||
| Backend | Use when | Status |
|
||||
| --- | --- | --- |
|
||||
| [File System](/docs/self-hosting/filesystem) (RocksDB-based) | Single-node deployments, including air-gapped installs | Production-ready, single node only |
|
||||
| [PostgreSQL](/docs/self-hosting/postgres) | Multi-node deployments | Recommended for multi-node today, but experimental |
|
||||
| FoundationDB | Largest production deployments | [Enterprise](/sales) |
|
||||
|
||||
For multi-node deployments, run two or more engine nodes behind a load balancer and add NATS for pub/sub, which replaces the default PostgreSQL `LISTEN`/`NOTIFY` path at high throughput. Neither is needed for a single-node file system install. See the [Production Checklist](/docs/self-hosting/production-checklist).
|
||||
|
||||
## Perimeter Checklist
|
||||
|
||||
- **Admin token**: Generate a strong, random token for engine authentication and verify it is not exposed to clients.
|
||||
- **TLS termination**: Encrypt connections to the engine via a reverse proxy or load balancer.
|
||||
- **No public exposure**: Keep port `6420` reachable only from inside the perimeter unless clients outside it genuinely need access.
|
||||
- **Health checks**: Configure liveness and readiness probes against the engine health endpoint.
|
||||
- **Telemetry**: Leave OpenTelemetry export off, or point it at a collector inside the network.
|
||||
- **Backups**: With the file system backend, back up the data directory. With PostgreSQL, configure automated backups and failover.
|
||||
|
||||
## Full Configuration
|
||||
|
||||
- [Self-Hosting Overview](/docs/self-hosting) for architecture and the self-host vs BYOC comparison
|
||||
- [Installing Rivet Engine](/docs/self-hosting/install) for Docker, binary, and source installs
|
||||
- [Docker Container](/docs/self-hosting/docker-container) and [Docker Compose](/docs/self-hosting/docker-compose) for container deployments
|
||||
- [Kubernetes](/docs/self-hosting/kubernetes) for cluster deployments
|
||||
- [Configuration](/docs/self-hosting/configuration) for every option and the full JSON schema
|
||||
- [Endpoints](/docs/general/endpoints) for connecting your backend and clients
|
||||
- [Production Checklist](/docs/self-hosting/production-checklist) before going live
|
||||
|
||||
_Source doc path: /cookbook/vpc-air-gapped_
|
||||
@@ -0,0 +1,13 @@
|
||||
# Deploying to AWS ECS
|
||||
|
||||
> Source: `src/content/docs/deploy/aws-ecs.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/aws-ecs
|
||||
> Description: Run your backend on Amazon ECS with Fargate.
|
||||
|
||||
---
|
||||
See the [AWS ECS template](https://github.com/rivet-dev/template-aws-ecs) for deployment instructions:
|
||||
|
||||
- [Deploy with Terraform](https://github.com/rivet-dev/template-aws-ecs#option-a-deploy-with-terraform)
|
||||
- [Deploy with AWS CLI](https://github.com/rivet-dev/template-aws-ecs#option-b-deploy-with-aws-cli)
|
||||
|
||||
_Source doc path: /docs/deploy/aws-ecs_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Deploy To Amazon Web Services Lambda
|
||||
|
||||
> Source: `src/content/docs/deploy/aws-lambda.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/aws-lambda
|
||||
> Description: _AWS Lambda is coming soon_
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Source doc path: /docs/deploy/aws-lambda_
|
||||
@@ -0,0 +1,53 @@
|
||||
# Deploying to Cloudflare Workers
|
||||
|
||||
> Source: `src/content/docs/deploy/cloudflare.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/cloudflare
|
||||
> Description: Deploy an existing Rivet project to Cloudflare Workers.
|
||||
|
||||
---
|
||||
This guide covers deploying an existing Rivet project to Cloudflare Workers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Cloudflare account](https://dash.cloudflare.com/)
|
||||
- [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) configured for your account
|
||||
- A Rivet namespace from the [Rivet Dashboard](https://dashboard.rivet.dev/) or a self-hosted Rivet Engine
|
||||
|
||||
## Steps
|
||||
|
||||
### Set up your project
|
||||
|
||||
Follow the [Cloudflare Workers Quickstart](/docs/actors/quickstart/cloudflare) to set up your project locally.
|
||||
|
||||
### Configure Wrangler
|
||||
|
||||
Set your Rivet connection values as Worker variables. Find `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` in the [Rivet Dashboard](https://dashboard.rivet.dev/) under **Settings → Namespace → Advanced → Backend Configuration** and copy them in.
|
||||
|
||||
```toml wrangler.toml
|
||||
name = "rivetkit-cloudflare"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-04-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[vars]
|
||||
RIVET_ENDPOINT = "https://your-namespace:sk_...@api.rivet.dev"
|
||||
RIVET_PUBLIC_ENDPOINT = "https://your-namespace@api.rivet.dev"
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
```sh
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
### Register the Serverless Runner URL
|
||||
|
||||
After deploy, set the Worker URL with the `/api/rivet` path as the serverless runner URL in Rivet.
|
||||
|
||||
## Related
|
||||
|
||||
- [Cloudflare Workers Quickstart](/docs/actors/quickstart/cloudflare)
|
||||
- [Deploying to Supabase Functions](/docs/deploy/supabase)
|
||||
- [SQLite](/docs/actors/sqlite)
|
||||
|
||||
_Source doc path: /docs/deploy/cloudflare_
|
||||
136
.agents/skills/ai-agent-workspace/reference/deploy/freestyle.md
Normal file
136
.agents/skills/ai-agent-workspace/reference/deploy/freestyle.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Deploying to Freestyle
|
||||
|
||||
> Source: `src/content/docs/deploy/freestyle.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/freestyle
|
||||
> Description: Deploy RivetKit app to Freestyle.sh, a cloud platform for running AI-generated code with built-in security and scalability.
|
||||
|
||||
---
|
||||
Freestyle provides built-in security for running untrusted AI-generated code, making it ideal for AI agent applications. Using Rivet, it is easy to deploy your vibe-coded or user-provided RivetKit backends straight to Freestyle.
|
||||
|
||||
- [Freestyle + Rivet](https://github.com/rivet-dev/rivet/tree/main/examples/freestyle) — Complete example of deploying RivetKit app to Freestyle.sh.
|
||||
|
||||
## Setup
|
||||
|
||||
### Install packages
|
||||
|
||||
Install RivetKit and Hono and create your registry:
|
||||
|
||||
```bash
|
||||
npm install rivetkit hono
|
||||
```
|
||||
|
||||
### Configure serverless driver
|
||||
|
||||
Update your server code to run the registry serverless with Deno.
|
||||
|
||||
```typescript index.ts @hide
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
export const counter = actor({
|
||||
state: { count: 0 },
|
||||
actions: {
|
||||
increment: (c, x: number) => {
|
||||
c.state.count += x;
|
||||
return c.state.count;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const registry = setup({
|
||||
use: { counter },
|
||||
});
|
||||
registry.start();
|
||||
```
|
||||
|
||||
```typescript server.ts @nocheck
|
||||
import { registry } from "./index";
|
||||
|
||||
// Freestyle uses Deno under the hood for web deployments
|
||||
// @ts-ignore Deno is a Freestyle runtime global
|
||||
Deno.serve((request: Request) => registry.handler(request));
|
||||
```
|
||||
|
||||
The `Deno.serve` API is provided by Freestyle's runtime environment. The `@ts-ignore` comment suppresses TypeScript errors for this platform-specific API.
|
||||
|
||||
### Deploy to Freestyle
|
||||
|
||||
Deploy your application to Freestyle with the correct configuration. Create a deployment script or add this to your existing deployment process:
|
||||
|
||||
```typescript @nocheck
|
||||
const FREESTYLE_DOMAIN = "my-domain.style.dev"; // Change to your desired Freestyle domain
|
||||
|
||||
declare const freestyle: any;
|
||||
declare const buildDir: string;
|
||||
|
||||
const res = await freestyle.deployWeb(buildDir, {
|
||||
envVars: {
|
||||
FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`,
|
||||
RIVET_RUNNER_KIND: "serverless",
|
||||
// For self-hosted instances:
|
||||
// RIVET_ENDPOINT: "http://127.0.0.1:6420",
|
||||
RIVET_ENDPOINT: "api.rivet.dev",
|
||||
},
|
||||
timeout: 60 * 5, // Increases max request lifetime on the runner
|
||||
entrypoint: "server.ts", // File which starts serverless runner
|
||||
domains: [FREESTYLE_DOMAIN],
|
||||
build: false,
|
||||
});
|
||||
```
|
||||
|
||||
Details on `buildDir` and other settings are available on [Freestyle docs](https://docs.freestyle.sh/web/web).
|
||||
|
||||
Run this deployment script to push your application to Freestyle.
|
||||
|
||||
**Deployment Configuration:**
|
||||
|
||||
- `timeout: 60 * 5` - Set timeout to 5 minutes for actor operations - it's important to keep this high
|
||||
- `entrypoint: "server.ts"` - Entry point file with your serverless setup
|
||||
- `domains` - Your Freestyle domain(s)
|
||||
- `build: false` - Disable build if you're pre-building your assets
|
||||
|
||||
### Configure runner
|
||||
|
||||
Update the runner configuration on the Rivet side to connect with your Freestyle deployment. Create a configuration script and run it after your Freestyle deployment is live:
|
||||
|
||||
```typescript @nocheck
|
||||
import { RivetClient } from "@rivetkit/engine-api-full";
|
||||
|
||||
const rivet = new RivetClient({
|
||||
environment: "https://api.rivet.dev",
|
||||
token: process.env.RIVET_API_TOKEN,
|
||||
});
|
||||
|
||||
const FREESTYLE_DOMAIN = "my-domain.style.dev"; // Change to your desired Freestyle domain
|
||||
const RIVET_NAMESPACE = "my-rivet-namespace"; // Change to your Rivet namespace
|
||||
|
||||
await rivet.runnerConfigsUpsert("freestyle-runner", {
|
||||
namespace: RIVET_NAMESPACE,
|
||||
datacenters: {
|
||||
default: {
|
||||
serverless: {
|
||||
url: `https://${FREESTYLE_DOMAIN}/start`,
|
||||
runnersMargin: 1,
|
||||
minRunners: 1,
|
||||
maxRunners: 1,
|
||||
slotsPerRunner: 1,
|
||||
// Must be shorter than Freestyle request `timeout` config
|
||||
requestLifespan: 60 * 5 - 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Execute this configuration script to register your Freestyle deployment with Rivet.
|
||||
|
||||
**Runner Configuration:**
|
||||
|
||||
- `url` - Freestyle deployment URL with `/start` endpoint
|
||||
- `runnersMargin` - Buffer of runners to maintain
|
||||
- `minRunners/maxRunners` - Scaling limits
|
||||
- `slotsPerRunner` - Concurrent actors per runner
|
||||
- `requestLifespan` - Request timeout (slightly less than Freestyle timeout)
|
||||
|
||||
Once executed, Rivet will be connected to your Freestyle serverless instance.
|
||||
|
||||
_Source doc path: /docs/deploy/freestyle_
|
||||
@@ -0,0 +1,74 @@
|
||||
# Deploying to Google Cloud Run
|
||||
|
||||
> Source: `src/content/docs/deploy/gcp-cloud-run.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/gcp-cloud-run
|
||||
> Description: Deploy your RivetKit app to Google Cloud Run.
|
||||
|
||||
---
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Google Cloud project with Cloud Run and Artifact Registry enabled
|
||||
- `gcloud` CLI authenticated (`gcloud auth login`) and project set (`gcloud config set project YOUR_PROJECT`)
|
||||
- Artifact Registry repository or Container Registry enabled
|
||||
- Your RivetKit app
|
||||
- If you don't have one, see the [Quickstart](/docs/actors/quickstart) page or our [Examples](https://github.com/rivet-dev/rivet/tree/main/examples)
|
||||
- Access to the [Rivet Cloud](https://dashboard.rivet.dev/) or a [self-hosted Rivet Engine](/docs/general/self-hosting)
|
||||
|
||||
### Package Your App
|
||||
|
||||
Create a `Dockerfile` in your project root:
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
ENV PORT=8080
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
### Build and Push the Image
|
||||
|
||||
Use Cloud Build to build and push the image. Replace the region and repository with your own.
|
||||
|
||||
```bash
|
||||
gcloud builds submit --tag us-central1-docker.pkg.dev/YOUR_PROJECT/rivetkit-app/rivetkit-app:latest
|
||||
```
|
||||
|
||||
### Set Environment Variables
|
||||
|
||||
After creating your project on the Rivet dashboard, select Google Cloud Run as your provider. You'll be provided `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` environment variables to use when deploying.
|
||||
|
||||
### Deploy to Cloud Run
|
||||
|
||||
Deploy the service to Cloud Run, passing the Rivet environment variables. Adjust the region and image as needed.
|
||||
|
||||
```bash
|
||||
gcloud run deploy rivetkit-app \
|
||||
--image us-central1-docker.pkg.dev/YOUR_PROJECT/rivetkit-app/rivetkit-app:latest \
|
||||
--region us-central1 \
|
||||
--allow-unauthenticated \
|
||||
--min-instances 1 \
|
||||
--set-env-vars RIVET_ENDPOINT=<your-rivet-endpoint>,RIVET_PUBLIC_ENDPOINT=<your-rivet-public-endpoint>
|
||||
```
|
||||
|
||||
### Connect to Rivet
|
||||
|
||||
1. After deployment, note the service URL (e.g. `https://rivetkit-app-xxxxx-uc.a.run.app`)
|
||||
2. On the Rivet dashboard, paste your URL with the `/api/rivet` path into the connect form (e.g. `https://rivetkit-app-xxxxx-uc.a.run.app/api/rivet`)
|
||||
3. Click "Done"
|
||||
|
||||
### Verify
|
||||
|
||||
Confirm the service is running:
|
||||
|
||||
```bash
|
||||
gcloud run services describe rivetkit-app --region us-central1 --format 'value(status.conditions[?type="Ready"].status)'
|
||||
```
|
||||
|
||||
Your app should appear as connected on the Rivet dashboard once the service reports ready.
|
||||
|
||||
_Source doc path: /docs/deploy/gcp-cloud-run_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Deploying to Hetzner
|
||||
|
||||
> Source: `src/content/docs/deploy/hetzner.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/hetzner
|
||||
> Description: Please see the VM & Bare Metal guide.
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Source doc path: /docs/deploy/hetzner_
|
||||
121
.agents/skills/ai-agent-workspace/reference/deploy/kubernetes.md
Normal file
121
.agents/skills/ai-agent-workspace/reference/deploy/kubernetes.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Deploying to Kubernetes
|
||||
|
||||
> Source: `src/content/docs/deploy/kubernetes.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/kubernetes
|
||||
> Description: Deploy your RivetKit app to any Kubernetes cluster.
|
||||
|
||||
---
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A Kubernetes cluster with `kubectl` access (EKS, GKE, k3s, etc.)
|
||||
- Container registry credentials (Docker Hub, GHCR, GCR, etc.)
|
||||
- Your RivetKit app
|
||||
- If you don't have one, see the [Quickstart](/docs/actors/quickstart) page or our [Examples](https://github.com/rivet-dev/rivet/tree/main/examples)
|
||||
- Access to the [Rivet Cloud](https://dashboard.rivet.dev/) or a [self-hosted Rivet Engine](/docs/general/self-hosting)
|
||||
|
||||
### Package Your App
|
||||
|
||||
Create a `Dockerfile` in your project root:
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
ENV PORT=8080
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
### Build and Push the Image
|
||||
|
||||
```bash
|
||||
docker build -t registry.example.com/your-team/rivetkit-app:latest .
|
||||
docker push registry.example.com/your-team/rivetkit-app:latest
|
||||
```
|
||||
|
||||
Replace `registry.example.com/your-team` with your registry path. Auth with `docker login` first if needed.
|
||||
|
||||
### Set Environment Variables
|
||||
|
||||
After creating your project on the Rivet dashboard, select Kubernetes as your provider. You'll be provided `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` environment variables.
|
||||
|
||||
Create a `rivetkit-secrets.yaml` for your environment variables:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: rivetkit-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
RIVET_ENDPOINT: <your-rivet-endpoint>
|
||||
RIVET_PUBLIC_ENDPOINT: <your-rivet-public-endpoint>
|
||||
```
|
||||
|
||||
### Deploy to Kubernetes
|
||||
|
||||
Create a `deployment.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rivetkit-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: rivetkit-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: rivetkit-app
|
||||
spec:
|
||||
# Allow enough time for actors to gracefully stop on SIGTERM.
|
||||
# The runner waits up to 30m for actors to finish.
|
||||
# Add buffer for runner shutdown overhead after actors stop.
|
||||
# See: /docs/actors/versions#graceful-shutdown-sigterm
|
||||
terminationGracePeriodSeconds: 2100
|
||||
containers:
|
||||
- name: rivetkit-app
|
||||
image: registry.example.com/your-team/rivetkit-app:latest
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: rivetkit-secrets
|
||||
```
|
||||
|
||||
Apply both manifests:
|
||||
|
||||
```bash
|
||||
kubectl apply -f rivetkit-secrets.yaml
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
|
||||
### Connect to Rivet
|
||||
|
||||
1. Add a `Service` and Ingress to expose your app externally (e.g. `my-app.example.com`)
|
||||
2. On the Rivet dashboard, paste your domain with the `/api/rivet` path into the connect form (e.g. `https://my-app.example.com/api/rivet`)
|
||||
3. Click "Done"
|
||||
|
||||
Rivet envoys connect to your app over long-lived WebSockets, and your app's clients (browsers, SDKs) do the same. Default Ingress and cloud load balancer idle timeouts (typically 30 to 60 seconds) drop these connections and cause reconnect storms.
|
||||
|
||||
Raise the idle / read / send timeout on every Ingress and load balancer in front of your app to at least 1 hour (`3600` seconds). Examples:
|
||||
|
||||
- **NGINX Ingress**: `nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"` and `nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"`
|
||||
- **AWS Load Balancer Controller (ALB)**: `alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=3600`
|
||||
- **GCE Ingress (GKE)**: set `timeoutSec: 3600` on the `BackendConfig` referenced by the Service.
|
||||
|
||||
### Verify
|
||||
|
||||
Check that the pod is running:
|
||||
|
||||
```bash
|
||||
kubectl get pods -l app=rivetkit-app
|
||||
```
|
||||
|
||||
Your app should appear as connected on the Rivet dashboard once the pod is ready.
|
||||
|
||||
_Source doc path: /docs/deploy/kubernetes_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user