Compare commits
156 Commits
sai/change
...
ax
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52b90825a1 | ||
|
|
6364c33499 | ||
|
|
db9afd7a24 | ||
|
|
ce16813fbc | ||
|
|
95c23db4fc | ||
|
|
08bc5ae259 | ||
|
|
4dc878b8cb | ||
|
|
3e3f06403f | ||
|
|
8bd3953440 | ||
|
|
bb4bd9e0d3 | ||
|
|
f208d4e4b8 | ||
|
|
fc1d0b6916 | ||
|
|
fc8cfabacb | ||
|
|
ce1f33aa69 | ||
|
|
c644ec8d01 | ||
|
|
88f53f550d | ||
|
|
54a5993274 | ||
|
|
def3fc949a | ||
|
|
9f7dd3c1a6 | ||
|
|
a8b2ff5e2e | ||
|
|
40e0f7e1eb | ||
|
|
bf2300a6be | ||
|
|
0657037c84 | ||
|
|
64a783b445 | ||
|
|
ed28943e7a | ||
|
|
9e148489f0 | ||
|
|
25f86d94cc | ||
|
|
7668fa69cc | ||
|
|
3ffa1cfc7c | ||
|
|
18eb150d7d | ||
|
|
e1b0b731e0 | ||
|
|
d428a2492b | ||
|
|
fe0fd9b16c | ||
|
|
fc1fcf5d44 | ||
|
|
3ae72864bd | ||
|
|
0d5d54caa8 | ||
|
|
830bcc4756 | ||
|
|
0e56a462cd | ||
|
|
9fb293a539 | ||
|
|
062c00f53c | ||
|
|
a7e70c9b2a | ||
|
|
526ed59776 | ||
|
|
fd3980c6bf | ||
|
|
d8a4bbe804 | ||
|
|
9eb6bcd25f | ||
|
|
a907539810 | ||
|
|
601aca73c2 | ||
|
|
ffecff3857 | ||
|
|
0d7162544b | ||
|
|
092a9793ea | ||
|
|
5ee0a8d50e | ||
|
|
420676f2d7 | ||
|
|
24d82e2a06 | ||
|
|
d47fa0e96a | ||
|
|
1e7c893985 | ||
|
|
f9ebcb4a01 | ||
|
|
dceaa2b417 | ||
|
|
a4f121e190 | ||
|
|
4edd456b5b | ||
|
|
4d9f6da41b | ||
|
|
a53029cf7a | ||
|
|
eede4c10ba | ||
|
|
35169672e1 | ||
|
|
359d9e2285 | ||
|
|
05a3baaac3 | ||
|
|
4cc40cb2e4 | ||
|
|
814df02be9 | ||
|
|
d7f6cbdcdc | ||
|
|
a8d946b6a9 | ||
| 0e32c35515 | |||
|
|
005b26fa32 | ||
| cb7484912c | |||
|
|
cc47007fa9 | ||
|
|
302fd159df | ||
| cfdb2efc70 | |||
|
|
6da82f0ed8 | ||
|
|
789d26bb8d | ||
|
|
4605dd7c92 | ||
|
|
d8383a788e | ||
|
|
58ff7942bd | ||
|
|
39b27a229f | ||
|
|
3bfa9ac65e | ||
|
|
2a0487aa6e | ||
|
|
48200a11df | ||
|
|
d4d5620c18 | ||
|
|
d4745591a9 | ||
| a17aa5c283 | |||
|
|
e744ac4687 | ||
|
|
19f771ea71 | ||
|
|
a11ed988fb | ||
|
|
360f0d4829 | ||
|
|
c1719c1708 | ||
|
|
50ccf4e418 | ||
|
|
e166b666e6 | ||
|
|
a9e8f0731f | ||
|
|
8ddecc098f | ||
|
|
e31f50af32 | ||
|
|
22ebb89ff0 | ||
|
|
ba8794b072 | ||
|
|
d645d3e08b | ||
|
|
fd207eb067 | ||
|
|
5df8633eae | ||
|
|
2ce96b0bab | ||
|
|
113c755e23 | ||
|
|
03fc32bd6d | ||
|
|
8f0b915f4b | ||
|
|
1324316fee | ||
| b5c40755cf | |||
|
|
fb438ba9a7 | ||
|
|
9d54470774 | ||
|
|
a6bf07829b | ||
|
|
78c5a39999 | ||
|
|
45e9fb1b77 | ||
|
|
7f3cc840bf | ||
|
|
43e7c9050a | ||
|
|
6be2b3a67c | ||
|
|
a9a52e1078 | ||
|
|
7caad057db | ||
|
|
d343587c2b | ||
|
|
a17d53cb41 | ||
|
|
5d6ba558ab | ||
|
|
38c73847e0 | ||
|
|
b49ca236cf | ||
|
|
0607d13d4b | ||
|
|
c0cded1324 | ||
|
|
2aa7150716 | ||
| 609badc4ed | |||
|
|
7974bd5f3e | ||
|
|
ae8f24a8cb | ||
|
|
f3d44ebcfa | ||
|
|
7365322197 | ||
|
|
e44759d7fe | ||
|
|
224e98d0bc | ||
|
|
477e54240d | ||
|
|
732c458fd4 | ||
|
|
1d18ec1b32 | ||
| ab9426b8fc | |||
|
|
f35b381477 | ||
|
|
4002a12d69 | ||
|
|
d3381f978f | ||
|
|
6d86c8e538 | ||
|
|
3836d70704 | ||
|
|
5b0bf35175 | ||
|
|
5d5139dc95 | ||
|
|
535e9dde62 | ||
|
|
c6f3cd330a | ||
|
|
ca967fcc1c | ||
|
|
e9260cbd17 | ||
|
|
5d1598a807 | ||
|
|
52510d1107 | ||
|
|
aa1c89a310 | ||
|
|
f73e4407c0 | ||
|
|
608f468219 | ||
|
|
a405fcf785 | ||
|
|
6494a61ed8 | ||
|
|
070c6c0a91 |
43
.agents/skills/ax-agent-context/SKILL.md
Normal file
43
.agents/skills/ax-agent-context/SKILL.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: ax-agent-context
|
||||
description: This skill helps an LLM pick the right AxAgent context tool for a job - contextMap for recurring corpora, contextPolicy presets for within-run trajectory compaction, agent.optimize for offline GEPA instruction/demo tuning, agent.playbook for an evolving context playbook (offline evolve + online update), and recall/memories + skills for per-turn retrieval. Use when the user asks "which context feature should I use", confuses contextMap with contextPolicy or memory, or wants a decision guide for long-context agents. For contextPolicy/contextMap codegen use ax-agent-rlm; for recall/skills use ax-agent-memory-skills; for agent.optimize or agent.playbook use ax-agent-optimize.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxAgent Context Selection (@ax-llm/ax)
|
||||
|
||||
Use this skill to route a context-management need to the right AxAgent tool, then open the matching codegen skill. AxAgent manages four distinct context objects; choosing the wrong one is the usual mistake. Do not write tutorial prose; pick the tool and hand off.
|
||||
|
||||
## Pick The Right Context Tool
|
||||
|
||||
| Need | Object | Scope | Use | Next skill |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Many tasks over the same large corpus (repo, doc set, dataset) | Context map | recurring corpus, persists across runs | `contextMap` | `ax-agent-rlm` |
|
||||
| One long run whose own history must stay under control | Trajectory compaction | this run only | `contextPolicy: { preset, budget }` | `ax-agent-rlm` |
|
||||
| Evolve task strategy from examples or live feedback | Context playbook | a stage, offline + online | `agent.playbook(...)` | `ax-agent-optimize` |
|
||||
| Tune the prompt/instructions/demos offline | Instruction text | a program, offline | `agent.optimize(...)` (GEPA) | `ax-agent-optimize` |
|
||||
| Pull task-relevant facts or guides for a turn | Retrieval | one turn | `recall(...)` / skills | `ax-agent-memory-skills` |
|
||||
|
||||
## Defaults
|
||||
|
||||
- Recurring corpus + many different questions -> `contextMap` (persistent orientation cache).
|
||||
- One long multi-turn run with prompt pressure -> `contextPolicy: { preset: 'checkpointed', budget: 'balanced' }`; move to `lean` for very long runs with strong models, `full` for short tasks or weak models.
|
||||
- Evolve a context playbook -> `agent.playbook(...)` (offline from examples, or online from live feedback).
|
||||
- Tune instructions/demos offline -> `agent.optimize(...)` (GEPA).
|
||||
- Fetch facts or guides on demand -> `recall(...)` for memories, `discover({ skills })` for skill guides.
|
||||
- A single oversized input value (a pasted doc, a big JSON blob) -> do nothing; `autoUpgrade` (ON by default) keeps it runtime-only with a prompt preview. Reach for `contextFields` only when you want a specific inline policy or the value is a large required non-string field. See `ax-agent-rlm`.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Do not use `contextMap` to compress a single run's history. That is `contextPolicy`.
|
||||
- Do not use `contextPolicy` to carry knowledge across runs. That is `contextMap`.
|
||||
- Do not hand-build a strategy playbook in the prompt. Evolve it with `agent.playbook(...)`.
|
||||
- Do not stuff a whole corpus into the prompt every run. Use a context map plus on-demand `recall(...)`.
|
||||
- Do not confuse runtime skills (`discover({ skills })` guides) with these installable codegen skills.
|
||||
|
||||
## See Also
|
||||
|
||||
- `ax-agent-rlm` - contextPolicy presets, context maps, and runtime sessions.
|
||||
- `ax-agent-memory-skills` - recall, memories, and dynamic skill loading.
|
||||
- `ax-agent-optimize` - GEPA via `agent.optimize(...)` and the context playbook via `agent.playbook(...)`.
|
||||
- `ax-agent` - core agent shape and the final/clarification protocol.
|
||||
443
.agents/skills/ax-agent-memory-skills/SKILL.md
Normal file
443
.agents/skills/ax-agent-memory-skills/SKILL.md
Normal file
@@ -0,0 +1,443 @@
|
||||
---
|
||||
name: ax-agent-memory-skills
|
||||
description: This skill helps an LLM generate correct AxAgent memory retrieval, context-map, and dynamic skill-loading code using @ax-llm/ax. Use when the user asks about contextMap, AxAgentContextMap, onMemoriesSearch, memoriesCatalog, recall(...), inputs.memories, onLoadedMemories, onUsedMemories, onSkillsSearch, skillsCatalog, AxAgentCatalogSkill, discover({ skills }), onLoadedSkills, onUsedSkills, preloaded skills, preloading memories at forward time, relevanceRanking hints, loaded memory/skill IDs, or carrying memories across forward() calls.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxAgent Memory And Skills Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill when an agent needs a persistent context map, task-relevant memory retrieval, or skill guides loaded into the executor prompt on demand. For ordinary agent setup use `ax-agent`. For RLM runtime policy use `ax-agent-rlm`. For callbacks and telemetry use `ax-agent-observability`.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use a static `skillsCatalog` / `memoriesCatalog` when the skill guides or memories fit in a plain array — Ax then backs `discover({ skills })` / `recall(...)` with a built-in deterministic local search and no host search code is needed.
|
||||
- Use `onSkillsSearch` / `onMemoriesSearch` when retrieval needs a real backend (vector DB, BM25 service, KV). A host callback always takes precedence over the catalog's built-in search.
|
||||
- Use `contextMap` when repeated runs inspect the same long external context and should accumulate a small orientation cache automatically.
|
||||
- `recall(...)` is available to distiller and executor stages when `onMemoriesSearch` or a non-empty `memoriesCatalog` is set.
|
||||
- `discover({ skills })` is available to the executor when `onSkillsSearch` or a non-empty `skillsCatalog` is set.
|
||||
- With `skillsCatalog`, the executor prompt also gains a static `### Available Skills` index (id + name + description), so skill discovery is targeted instead of blind.
|
||||
- Both `recall(...)` and `discover({ skills })` return `void`. The loaded content appears on the next turn.
|
||||
- Use `onLoadedMemories` / `onLoadedSkills` to observe what got loaded.
|
||||
- Use `onUsedMemories` / `onUsedSkills` to track what the actor says it actually relied on.
|
||||
- Child agents do not inherit memory or skills search callbacks; wire them explicitly on every agent that needs the capability.
|
||||
|
||||
## Context Map
|
||||
|
||||
Use `contextMap` when repeated runs ask different questions over the same long context, document set, or repository. The map is prompt-resident orientation knowledge: structure, concepts, constants, parsing schema, reusable aggregate results, and concrete error patterns. It is not a task-specific answer cache.
|
||||
|
||||
Runnable example: [`src/examples/rlm-context-map-live.ts`](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-context-map-live.ts) demonstrates a provider-backed context-map update, `onUpdate` snapshot persistence, finite evolve, and frozen map reuse.
|
||||
|
||||
When `contextMap` is configured:
|
||||
|
||||
- Ax injects the current map into the distiller prompt.
|
||||
- Ax updates the map once after each successful completed `forward(...)`.
|
||||
- By default the map evolves forever. For a finite warmup, create the map with `{ infiniteEvolve: false, evolveSteps: N }`; after `N` successful updates it is still injected but no longer updated.
|
||||
- Failed runs, aborts, and clarification requests do not update the map.
|
||||
- Use `onUpdate` to persist `result.map.snapshot()` outside the agent.
|
||||
|
||||
```typescript
|
||||
import { agent, AxAgentContextMap } from '@ax-llm/ax';
|
||||
|
||||
const map = new AxAgentContextMap(savedSnapshot, {
|
||||
maxChars: 4000,
|
||||
infiniteEvolve: false,
|
||||
evolveSteps: 10,
|
||||
});
|
||||
|
||||
const myAgent = agent('context:string, query:string -> answer:string', {
|
||||
contextFields: ['context'],
|
||||
contextMap: {
|
||||
map,
|
||||
onUpdate: ({ map }) => saveSnapshot(map.snapshot()),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Types:
|
||||
|
||||
```typescript
|
||||
type AxAgentContextMapConfig = {
|
||||
map?: AxAgentContextMap | AxAgentContextMapSnapshot | string;
|
||||
onUpdate?: (result: AxAgentContextMapUpdateResult) => void | Promise<void>;
|
||||
};
|
||||
|
||||
type AxAgentContextMapOptions = {
|
||||
maxChars?: number;
|
||||
infiniteEvolve?: boolean;
|
||||
evolveSteps?: number;
|
||||
};
|
||||
```
|
||||
|
||||
## Memory Search
|
||||
|
||||
Use `onMemoriesSearch` when the agent needs to pull task-relevant context such as user preferences, prior decisions, project facts, or past conversations from an external store (vector DB, BM25, KV). The actor decides what to load, when, and how much.
|
||||
|
||||
When `onMemoriesSearch` is set, the distiller and executor stages gain:
|
||||
|
||||
1. An `inputs.memories` field. In JS this is an array of `{ id, content }` entries the actor reads directly. In the prompt, the same entries render as markdown blocks with `ID: \`...\`` lines, matching the Loaded Skills ID style. Each `content` is opaque markdown; frontmatter is not parsed.
|
||||
2. A `recall(searches: string[]): void` global the actor `await`s to load more entries. Recalled entries are appended to `inputs.memories` and visible from the next turn onward. `recall()` returns nothing.
|
||||
|
||||
The responder stage does not receive memories.
|
||||
|
||||
### Enabling
|
||||
|
||||
```typescript
|
||||
import { agent } from '@ax-llm/ax';
|
||||
import type { AxAgentMemoriesSearchFn } from '@ax-llm/ax';
|
||||
|
||||
const onMemoriesSearch: AxAgentMemoriesSearchFn = async (
|
||||
searches,
|
||||
alreadyLoaded
|
||||
) => {
|
||||
// `searches` is the full array passed to recall(...). Batch your
|
||||
// store lookup in one round-trip.
|
||||
// `alreadyLoaded` is the current inputs.memories snapshot. Filter
|
||||
// against it to skip duplicates.
|
||||
const skip = new Set(alreadyLoaded.map((m) => m.id));
|
||||
const fresh = await myVectorDB.searchBatch(searches, { topK: 3 });
|
||||
return fresh.filter((m) => !skip.has(m.id));
|
||||
};
|
||||
|
||||
const myAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
onMemoriesSearch,
|
||||
});
|
||||
```
|
||||
|
||||
Each memory result must be:
|
||||
|
||||
```typescript
|
||||
type AxAgentMemoryResult = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Static catalog (no callback)
|
||||
|
||||
If the memory set fits in a plain array, skip the callback entirely: pass `memoriesCatalog` and Ax backs `recall(...)` with a built-in deterministic local search (idf-weighted token overlap over `id` + content; not regex, not embeddings). The `alreadyLoaded` contract is preserved — entries already on `inputs.memories` are excluded before ranking.
|
||||
|
||||
```typescript
|
||||
const myAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
memoriesCatalog: [
|
||||
{ id: 'deploy-window', content: 'Prod deploys only on Tuesday afternoons.' },
|
||||
{ id: 'user-prefs', content: 'User prefers concise answers.' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- If both `memoriesCatalog` and `onMemoriesSearch` are set, the host callback handles all `recall(...)` searches; the catalog still powers the advisory `relevanceRanking` hint.
|
||||
- Catalog content is NOT preloaded into the prompt; entries load only when recalled.
|
||||
- The built-in search is lexical. For semantic retrieval over large stores, supply `onMemoriesSearch` instead.
|
||||
|
||||
### Preloading memories at forward time
|
||||
|
||||
To seed specific memories for one run (no recall round-trip), pass them as the `memories` input value. They render on `inputs.memories` from the first turn and merge with anything recalled later (deduped by `id`).
|
||||
|
||||
```typescript
|
||||
await myAgent.forward(ai, {
|
||||
task: 'Plan the deploy',
|
||||
memories: [{ id: 'deploy-window', content: 'Prod deploys only on Tuesday afternoons.' }],
|
||||
});
|
||||
```
|
||||
|
||||
### Actor usage
|
||||
|
||||
```javascript
|
||||
// Turn 1: kick off one batched lookup.
|
||||
await recall(['user preferences', 'project constraints']);
|
||||
|
||||
// Turn 2+: matched entries are now visible on inputs.memories.
|
||||
const prefs = inputs.memories.find((m) => m.id === 'user-prefs-v2');
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Pass all memory queries in one `await recall([...])` call.
|
||||
- Do not loop `recall()` calls or wrap them in `Promise.all(...)`.
|
||||
- Read `inputs.memories` on the next turn to see what landed.
|
||||
- `recall()` invokes `onMemoriesSearch` with `(searches, alreadyLoaded)` and returns `void`.
|
||||
- Results land on `inputs.memories` for subsequent turns and render in the prompt as:
|
||||
|
||||
```markdown
|
||||
### Memory
|
||||
|
||||
ID: `mem:user-prefs-v2`
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
- Entries are deduped by `id` (last-write-wins) and sorted by `id` for prefix-cache stability.
|
||||
- Memories loaded by the distiller thread automatically to the executor. No second `recall()` is needed for those entries.
|
||||
- `recall()` may be called multiple times across turns; results accumulate for that run.
|
||||
- `inputs.memories` lifetime is one `.forward()` call. It resets between calls.
|
||||
|
||||
## Carrying Memories Across `.forward()` Calls
|
||||
|
||||
To preserve continuity across calls, persist memories in your store and recall them again on the next call. If you want to replay anything loaded on a prior run, observe loads with `onLoadedMemories`.
|
||||
|
||||
```typescript
|
||||
const carried = new Map<string, string>();
|
||||
|
||||
const myAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
onMemoriesSearch: async (searches) => {
|
||||
const fresh = await myVectorDB.searchBatch(searches, { topK: 3 });
|
||||
const carriedAsResults = [...carried.entries()].map(([id, content]) => ({
|
||||
id,
|
||||
content,
|
||||
}));
|
||||
return [...carriedAsResults, ...fresh];
|
||||
},
|
||||
onLoadedMemories: (results) => {
|
||||
for (const r of results) carried.set(r.id, r.content);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Skills Search
|
||||
|
||||
Use `onSkillsSearch` when the agent needs to load skill guides such as usage instructions, operational guides, or domain conventions into the executor's system prompt on demand. The actor decides which skills to fetch and when, so you do not pre-render every skill into every prompt.
|
||||
|
||||
When `onSkillsSearch` is set, the distiller and executor stages gain:
|
||||
|
||||
1. A "Loaded Skills" section in the system prompt that renders matched skill bodies with stable `ID:` values sorted by `id`.
|
||||
2. A `discover({ skills })` path the actor `await`s to load more skills. Loaded entries appear in the next turn's prompt. `discover(...)` returns nothing.
|
||||
|
||||
Skills the distiller loads carry over to the executor automatically. The responder does not see skills.
|
||||
|
||||
### Enabling
|
||||
|
||||
```typescript
|
||||
import { agent } from '@ax-llm/ax';
|
||||
import type { AxAgentSkillsSearchFn } from '@ax-llm/ax';
|
||||
|
||||
// Each result is { id?: string; name: string; content: string }.
|
||||
// If id is omitted, Ax falls back to name.
|
||||
const onSkillsSearch: AxAgentSkillsSearchFn = async (searches) => {
|
||||
return mySkillStore.resolveBatch(searches, {
|
||||
// Recommended backend order: exact id, exact name, then broader search.
|
||||
// This lets the actor pass one simple string and keeps lookup policy host-side.
|
||||
strategy: ['id', 'name', 'search'],
|
||||
topK: 2,
|
||||
});
|
||||
};
|
||||
|
||||
const myAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
onSkillsSearch,
|
||||
});
|
||||
```
|
||||
|
||||
Each skill result is:
|
||||
|
||||
```typescript
|
||||
type AxAgentSkillResult = {
|
||||
id?: string;
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Static catalog (no callback)
|
||||
|
||||
If the skill set fits in a plain array, skip the callback entirely: pass `skillsCatalog` and Ax backs `discover({ skills })` with a built-in deterministic local search (idf-weighted token overlap over `id` + `name`×2 + `description`×2 + the first 600 chars of `content`; not regex, not embeddings). The executor prompt also gains a static, cache-stable `### Available Skills` index (id + name + description, sorted by id), so the actor searches by known ids instead of guessing.
|
||||
|
||||
```typescript
|
||||
import type { AxAgentCatalogSkill } from '@ax-llm/ax';
|
||||
|
||||
const catalog: AxAgentCatalogSkill[] = [
|
||||
{
|
||||
id: 'release-checklist',
|
||||
name: 'Release checklist',
|
||||
description: 'Steps for shipping a package release safely', // high-signal for matching
|
||||
content: '1. Bump version\n2. Run tests\n3. Tag and publish',
|
||||
},
|
||||
];
|
||||
|
||||
const myAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
skillsCatalog: catalog,
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
type AxAgentCatalogSkill = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
content: string;
|
||||
};
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- If both `skillsCatalog` and `onSkillsSearch` are set, the host callback handles all `discover({ skills })` searches; the catalog still powers the `### Available Skills` index and the advisory `relevanceRanking` hint.
|
||||
- Catalog content is NOT preloaded into the prompt (unlike `skills`); entries load only when matched. Use `skills` for guides that must always be in context, `skillsCatalog` for a larger set loaded on demand.
|
||||
- The built-in search is lexical. For semantic retrieval over large stores, supply `onSkillsSearch` instead.
|
||||
|
||||
### Actor usage
|
||||
|
||||
```javascript
|
||||
// Pass all skill queries in one call.
|
||||
await discover({ skills: ['release-checklist', 'incident-response'] });
|
||||
|
||||
// Next turn: loaded skill bodies render under the "Loaded Skills"
|
||||
// system-prompt section.
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `discover({ skills })` invokes `onSkillsSearch` with the raw search strings and returns `void`.
|
||||
- Resolve each raw string backend-side: prefer an exact `id` match, then an exact `name` match, then fuzzy/full-text search. The actor should not have to choose `id:` vs `name:` syntax.
|
||||
- Matched skills land under "Loaded Skills" for the next turn.
|
||||
- Entries are deduped by `id` (last-write-wins) and sorted by `id` for prefix-cache stability.
|
||||
- If a skill result omits `id`, its trimmed `name` is used as the id for backwards compatibility.
|
||||
- Skills persist on the agent's `currentSkillsPromptState` across `.forward()` calls, unlike memories.
|
||||
- Use `agent.getState()` / `setState(...)` to serialize/restore loaded skills.
|
||||
- `discover({ skills })` may be called multiple times across turns. Within one turn, batch all skill queries in a single call.
|
||||
- Child agents do not inherit `onSkillsSearch`; wire it explicitly per agent.
|
||||
|
||||
## Preloading Skills
|
||||
|
||||
If the caller already knows which skills are relevant, pass them up front instead of round-tripping through `discover({ skills })`.
|
||||
|
||||
- Init-time: `skills` on `AxAgentOptions` seeds the executor prompt at agent creation. They survive `setState(...)` resets.
|
||||
- Forward-time: `skills` on `forward(ai, values, { skills })` merge in at the start of that call. Distiller and responder ignore forward-time skills.
|
||||
|
||||
Both accept the same shape `onSkillsSearch` returns: `readonly AxAgentSkillResult[]`. Forward-time skills override init-time skills by `id`. `onLoadedSkills` is not fired for preset skills; that callback is for runtime `discover({ skills })` analytics.
|
||||
|
||||
```typescript
|
||||
const releaseAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
skills: [
|
||||
{
|
||||
id: 'release-checklist',
|
||||
name: 'release-checklist',
|
||||
content: '...',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await releaseAgent.forward(
|
||||
ai,
|
||||
{ task: 'Prepare release notes' },
|
||||
{
|
||||
skills: [
|
||||
{
|
||||
id: 'incident-response',
|
||||
name: 'incident-response',
|
||||
content: '...',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
You can use `skills` without setting `onSkillsSearch` at all. That is useful for static guides where the actor never needs to fetch more.
|
||||
|
||||
## Advisory Relevance Hints (`relevanceRanking`)
|
||||
|
||||
`relevanceRanking` is ON by default — leave it unset; set `relevanceRanking: false` to opt out. The default was flipped after its A/B gate passed (substance-judged, 49 runs per variant per model: small-model first-lookup precision 24%→90% and answer accuracy 14%→29%; frontier-model control accuracy 63%→88% with fewer turns). The generated language ports implement the same advisory hint contract through AxIR Core.
|
||||
|
||||
When enabled, a deterministic local ranker scores the agent's discoverable capabilities against the task once per `forward(...)` and injects a short advisory `### Likely Relevant` shortlist into the executor turn — modules (needs `functionDiscovery`), catalog skills (needs `skillsCatalog`), and catalog memories (needs `memoriesCatalog`). The hint is non-authoritative: the full lists still apply and the actor may `discover`/`recall` anything else.
|
||||
|
||||
```typescript
|
||||
const myAgent = agent('task:string -> answer:string', {
|
||||
contextFields: [],
|
||||
functionDiscovery: true,
|
||||
skillsCatalog: catalog,
|
||||
relevanceRanking: true, // or { topK: 3, minScore: 0.08 }
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Default is ON across TypeScript and generated language ports; domains still self-gate on their prerequisites (`functionDiscovery` for modules, catalogs for skills/memories), so agents without those see no change. Everything else in this skill (catalog search, the Available Skills index) is independent of the flag.
|
||||
- The shortlist rides a dynamic, non-cached prompt field; the cached system prompt stays byte-stable across tasks.
|
||||
- On low confidence the ranker emits nothing rather than guessing.
|
||||
- Memory hint entries include an ~80-char content snippet; very short memories may be usable from the hint alone without a `recall(...)` (such use is not visible to `onUsedMemories`).
|
||||
- Observe outcomes via the `relevance_ranking` context event (see `ax-agent-observability`).
|
||||
|
||||
## Loaded And Used Tracking
|
||||
|
||||
`onLoadedMemories` reports what `recall(...)` loaded. `onLoadedSkills` reports what `discover({ skills })` loaded. To track what the actor says it actually relied on, use `onUsedMemories` / `onUsedSkills`.
|
||||
|
||||
```typescript
|
||||
const used: AxAgentUsedMemory[] = [];
|
||||
|
||||
await myAgent.forward(
|
||||
ai,
|
||||
{ task: 'Make a personal plan' },
|
||||
{
|
||||
onUsedMemories: (items) => used.push(...items),
|
||||
}
|
||||
);
|
||||
|
||||
used; // [{ id, reason, stage }]
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- The actor can only report memory IDs already present in `inputs.memories`.
|
||||
- The actor can only report skill IDs already present in Loaded Skills.
|
||||
- Unknown values are dropped.
|
||||
- When tracking is enabled, the actor sees `await used(id, reason?)`; this is the actor-side declaration mechanism.
|
||||
- `used(...)` resolves against loaded memory IDs and loaded skill IDs.
|
||||
- If memory IDs and skill IDs can collide, namespace them in your application, for example `mem:abc` and `skill:planning`.
|
||||
- Python, Go, and Java accept these observers directly in their agent option maps. C++ wraps them with `register_agent_observer(...)`; Rust wraps them with `agent_observer(...)`. The returned marker can be used in constructor or forward option maps, and observer failures are ignored in every language.
|
||||
|
||||
Types:
|
||||
|
||||
```typescript
|
||||
onMemoriesSearch?: AxAgentMemoriesSearchFn;
|
||||
onLoadedMemories?: (
|
||||
results: readonly AxAgentMemoryResult[]
|
||||
) => void | Promise<void>;
|
||||
onUsedMemories?: (
|
||||
usedMemories: readonly AxAgentUsedMemory[]
|
||||
) => void | Promise<void>;
|
||||
|
||||
onSkillsSearch?: AxAgentSkillsSearchFn;
|
||||
onLoadedSkills?: (
|
||||
results: readonly AxAgentSkillResult[]
|
||||
) => void | Promise<void>;
|
||||
onUsedSkills?: (
|
||||
usedSkills: readonly AxAgentUsedSkill[]
|
||||
) => void | Promise<void>;
|
||||
|
||||
contextMap?: AxAgentContextMapConfig;
|
||||
skills?: readonly AxAgentSkillResult[];
|
||||
skillsCatalog?: readonly AxAgentCatalogSkill[];
|
||||
memoriesCatalog?: readonly AxAgentMemoryResult[];
|
||||
relevanceRanking?: boolean | { topK?: number; minScore?: number };
|
||||
```
|
||||
|
||||
## Persisting Agent State Across Languages
|
||||
|
||||
TypeScript uses `getState()` / `setState()` for the actor runtime snapshot. The generated packages keep their legacy `GetState` / `SetState` (or language-shaped equivalents) as bare-runtime compatibility methods. Use `ExportRuntimeState` / `RestoreRuntimeState` in generated packages when you need the complete portable agent snapshot, including loaded skills and constructor-preset reapplication after restore. Do not interchange the two snapshot shapes.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch this for full working code:
|
||||
|
||||
- [RLM Memories and Skills](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-memories-and-skills.ts) - `onMemoriesSearch` + `recall()` and `onSkillsSearch` + `discover({ skills })` with load observability and actual usage tracking via `onUsedMemories` / `onUsedSkills`
|
||||
- [Skills + Memory Ops Assistant](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/skills-and-memory-assistant.ts) - an on-call assistant that recalls past decisions from a memory store and loads the right runbook skill on demand (also ported to Python, Go, Rust, Java, and C++ under `src/examples/<lang>/long-agents/`). All six languages support the native `onMemoriesSearch` / `onSkillsSearch` host callbacks, passed in the agent options at construction (Go/Java use native function values, Rust a `agent_with_search_callbacks` constructor, C++ a `register_*_search` helper); a static `memory_search_results` / `skill_search_results` config is also available.
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not assign the result of `await recall(...)` or `await discover(...)`; both return `void`.
|
||||
- Do not call `recall()` from the responder stage.
|
||||
- Do not call `discover({ skills })` from the responder stage.
|
||||
- Do not loop `recall()` calls or wrap them in `Promise.all(...)`.
|
||||
- Do not loop `discover()` calls or wrap them in `Promise.all(...)`.
|
||||
- Do not assume child agents inherit `onMemoriesSearch` or `onSkillsSearch`.
|
||||
- Do not pass `onMemoriesSearch` results via shared fields as a workaround; use `recall(...)`.
|
||||
- Do not assume `inputs.memories` persists across `.forward()` calls.
|
||||
- Do not use `onLoadedMemories` / `onLoadedSkills` as proof that the actor relied on an item; use `onUsedMemories` / `onUsedSkills` for actual-use tracking.
|
||||
- Do not write an `onSkillsSearch` / `onMemoriesSearch` callback that just scans a static array; pass the array as `skillsCatalog` / `memoriesCatalog` instead.
|
||||
- Do not rely on the built-in catalog search for semantic matching over large stores; it is lexical token overlap — supply a host callback for embeddings/vector search.
|
||||
- Do not confuse `skills` (always preloaded into the prompt) with `skillsCatalog` (searchable, loaded on demand).
|
||||
398
.agents/skills/ax-agent-observability/SKILL.md
Normal file
398
.agents/skills/ax-agent-observability/SKILL.md
Normal file
@@ -0,0 +1,398 @@
|
||||
---
|
||||
name: ax-agent-observability
|
||||
description: This skill helps an LLM generate correct AxAgent observability code using @ax-llm/ax. Use when the user asks about axGlobals.onUsage, usageContext, centralized or multi-tenant usage accounting, actorTurnCallback, onContextEvent, agentStatusCallback, onFunctionCall, reportSuccess, reportFailure, getChatLog(), getUsage(), resetUsage(), debug traces, progress updates, or telemetry for AxAgent runs.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxAgent Observability Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill when an agent needs runtime visibility, progress reporting, tracing, usage accounting, or chat-log access. For ordinary agent setup use `ax-agent`. For RLM runtime policy use `ax-agent-rlm`. For memories and dynamic skill loading use `ax-agent-memory-skills`.
|
||||
|
||||
## Choose The Smallest Hook
|
||||
|
||||
- Need a quick prompt/runtime trace during development -> start with `debug: true`.
|
||||
- Need structured per-turn code, raw runtime result, formatted output, provider thoughts, or actor stage -> use `actorTurnCallback`.
|
||||
- Need context-pressure and compaction telemetry -> use `onContextEvent`.
|
||||
- Need real-time task progress emitted by actor code -> use `agentStatusCallback`.
|
||||
- Need every runtime function call before execution -> use `onFunctionCall`.
|
||||
- Need model prompts/responses after a run -> use `getChatLog()`.
|
||||
- Need centralized chat/embed usage across APIs, users, agents, and services -> use `axGlobals.onUsage` plus `usageContext`.
|
||||
- Need token usage by actor/responder -> use `getUsage()` and `resetUsage()`.
|
||||
- Need usage split by context and task stages -> use `getStagedUsage()`.
|
||||
- Need Ax program traces -> use `getTraces()`.
|
||||
- Do not add multiple hooks unless the user clearly needs each output stream.
|
||||
|
||||
## Global Runtime Defaults
|
||||
|
||||
OpenTelemetry and debug defaults come from the shared Ax runtime surface:
|
||||
|
||||
```typescript
|
||||
import { axGlobals, axCreateDefaultColorLogger } from '@ax-llm/ax';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
|
||||
axGlobals.tracer = trace.getTracer('agent-app');
|
||||
axGlobals.debug = true;
|
||||
axGlobals.logger = axCreateDefaultColorLogger();
|
||||
axGlobals.onUsage = (event) => usageQueue.enqueue(event);
|
||||
```
|
||||
|
||||
These globals are live defaults for future AI, AxGen, AxFlow, and agent-internal model calls. Per-call or explicitly configured options still override `axGlobals`. Use AxAgent callbacks below when the caller needs structured agent-turn events rather than OpenTelemetry spans or debug logs.
|
||||
|
||||
## Centralized Usage Observer
|
||||
|
||||
Use the process-wide usage observer for application accounting across many agents, API routes, tenants, and users. Keep `getUsage()` for inspecting one agent instance after a run.
|
||||
|
||||
```typescript
|
||||
import { axGlobals } from '@ax-llm/ax';
|
||||
|
||||
axGlobals.onUsage = (event) => {
|
||||
usageQueue.enqueue(event); // Must return immediately.
|
||||
};
|
||||
|
||||
await supportAgent.forward(
|
||||
llm,
|
||||
{ query: request.body.query },
|
||||
{
|
||||
usageContext: {
|
||||
tenantId: auth.tenantId,
|
||||
userId: auth.userId,
|
||||
requestId: request.id,
|
||||
runId: crypto.randomUUID(),
|
||||
feature: 'support-chat',
|
||||
attributes: { environment: 'production' },
|
||||
},
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- The observer receives one immutable normalized event for each completed chat or embedding call that reports provider usage. A fully consumed stream emits once; an unconsumed or cancelled stream may not emit.
|
||||
- Events include `operation`, `ai`, `model`, normalized `tokens`, `streaming`, optional `context`, and available session or remote request IDs.
|
||||
- Put stable attribution such as application or environment in AI service-level `usageContext`. Put tenant, user, request, run, and feature attribution in per-call or per-forward `usageContext`.
|
||||
- Per-call context overrides service defaults. `attributes` are shallow-merged.
|
||||
- The observer is best-effort and fail-open. Ax does not await it and ignores observer failures, so synchronously enqueue and persist or aggregate out of band.
|
||||
- The registration is process-wide. A new assignment replaces the previous observer; set `axGlobals.onUsage = undefined` during test teardown or shutdown when appropriate.
|
||||
- In multi-process or serverless deployments, send events to a shared durable pipeline. Do not treat an in-memory total as application-wide accounting.
|
||||
- Keep identifiers opaque and attributes low-cardinality. Avoid prompts, responses, secrets, and other sensitive payloads.
|
||||
- Token events do not estimate currency cost. Apply a versioned provider/model pricing table downstream.
|
||||
|
||||
For direct AI calls and the complete event shape, also read `ax-ai`.
|
||||
|
||||
## Actor Turn Callback
|
||||
|
||||
Use `actorTurnCallback` when the caller needs structured telemetry for each actor turn.
|
||||
|
||||
What it gives you:
|
||||
|
||||
- `code`: the normalized JavaScript code the actor produced
|
||||
- `stage`: which actor produced the turn (`distiller` or `executor`)
|
||||
- `result`: the raw untruncated runtime return value from executing that code
|
||||
- `output`: the formatted action-log output string after Ax normalizes and truncates it for prompt replay
|
||||
- `thought`: the actor model's `thought` field when `showThoughts` is enabled and the provider returns one
|
||||
- `executorResult`: the full actor payload returned by the current actor stage, kept under this historical field name for compatibility
|
||||
- `isError`: whether the execution path for that turn was treated as an error
|
||||
- `usage`: token usage for this actor turn only
|
||||
- `model`: model used for this turn when explicitly set through `executorModelPolicy`
|
||||
- `chatLogMessages`: raw ChatML conversation for this turn, populated only when an actor turn callback is set
|
||||
|
||||
Use it for:
|
||||
|
||||
- debug UIs that want to show code plus raw runtime results
|
||||
- tracing and analytics
|
||||
- capturing `thought` for internal diagnostics when supported by the provider
|
||||
- storing per-turn execution artifacts without scraping the prompt/action log
|
||||
|
||||
Important:
|
||||
|
||||
- `output` is not raw stdout; it is the formatted replay string used in the action log.
|
||||
- `result` is the raw runtime result before Ax applies type-aware serialization and budget-proportional truncation.
|
||||
- `thought` is optional and only appears when the underlying `AxGen` call had `showThoughts` enabled and the provider actually returned a thought field.
|
||||
- `actionLogEntryCount` and `guidanceLogEntryCount` reflect the live log sizes after the turn is processed, including resumed runs.
|
||||
- `actorTurnCallback` fires for the configured agent instance. Child agents passed through `functions: [...]` should define their own callback if you need their internal actor turns; use `onFunctionCall` on the parent to observe the parent-side child-agent invocation.
|
||||
|
||||
Good pattern:
|
||||
|
||||
```typescript
|
||||
const supportAgent = agent('query:string -> answer:string', {
|
||||
contextFields: ['query'],
|
||||
runtime,
|
||||
actorTurnCallback: ({
|
||||
stage,
|
||||
turn,
|
||||
actionLogEntryCount,
|
||||
guidanceLogEntryCount,
|
||||
code,
|
||||
result,
|
||||
output,
|
||||
thought,
|
||||
isError,
|
||||
usage,
|
||||
model,
|
||||
}) => {
|
||||
console.log({
|
||||
turn,
|
||||
stage,
|
||||
model,
|
||||
actionLogEntryCount,
|
||||
guidanceLogEntryCount,
|
||||
isError,
|
||||
code,
|
||||
rawResult: result,
|
||||
replayOutput: output,
|
||||
thought,
|
||||
usage,
|
||||
});
|
||||
},
|
||||
executorOptions: {
|
||||
model: 'gpt-5.4-mini',
|
||||
showThoughts: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Callback type:
|
||||
|
||||
```typescript
|
||||
actorTurnCallback?: (turn: {
|
||||
stage: 'distiller' | 'executor';
|
||||
turn: number;
|
||||
actionLogEntryCount: number;
|
||||
guidanceLogEntryCount: number;
|
||||
executorResult: Record<string, unknown>;
|
||||
code: string;
|
||||
result: unknown;
|
||||
output: string;
|
||||
isError: boolean;
|
||||
thought?: string;
|
||||
usage?: AxProgramUsage[];
|
||||
model?: string;
|
||||
chatLogMessages?: ReadonlyArray<{ role: string; content: string }>;
|
||||
}) => void | Promise<void>;
|
||||
|
||||
actorTurnCallback?: (turn: {
|
||||
stage: 'distiller' | 'executor';
|
||||
turn: number;
|
||||
actionLogEntryCount: number;
|
||||
guidanceLogEntryCount: number;
|
||||
executorResult: Record<string, unknown>;
|
||||
code: string;
|
||||
result: unknown;
|
||||
output: string;
|
||||
isError: boolean;
|
||||
thought?: string;
|
||||
usage?: AxProgramUsage[];
|
||||
model?: string;
|
||||
chatLogMessages?: ReadonlyArray<{ role: string; content: string }>;
|
||||
}) => void | Promise<void>; // deprecated alias
|
||||
```
|
||||
|
||||
## Context Event Observability
|
||||
|
||||
Use `onContextEvent` when the caller needs structured telemetry about prompt pressure and compaction. It does not change model behavior directly; it is for logs, evals, and dashboards.
|
||||
|
||||
Events:
|
||||
|
||||
- `budget_check`: character-based prompt pressure before an actor turn, with detailed metrics kept out of the actor prompt
|
||||
- `checkpoint_created` / `checkpoint_cleared`: checkpoint lifecycle events with covered turns and reason
|
||||
- `tombstone_created`: compact resolved-error summary creation
|
||||
- `relevance_ranking`: emitted once per ranked domain per forward when `relevanceRanking` is enabled; carries `domain` (`'modules' | 'skills' | 'memories'`), the `shortlist` (`{ id, score }[]`, most relevant first), and `suppressed` (true when the low-confidence guard emitted no hint)
|
||||
- `field_auto_promoted`: emitted once per field per run when `autoUpgrade` keeps an oversized undeclared input value runtime-only; carries `fieldName`, `originalChars`, and `promptPreviewChars` (undefined when no inline preview was kept)
|
||||
|
||||
To measure whether the advisory hint helps, join per forward: `relevance_ranking.shortlist` ids against what the actor then loaded — for modules the internal `discover` calls (`onFunctionCall` with `kind: 'internal'`, `name: 'discover'`, `args.request`) plus the module part of external `qualifiedName`s; for skills `onLoadedSkills` / `used(id)`; for memories `onLoadedMemories` / `used(id)`.
|
||||
|
||||
Rules:
|
||||
|
||||
- `contextPressure` in the actor prompt is intentionally compact (`ok`, `watch`, `critical` plus one short instruction).
|
||||
- Budget metrics are character-based for provider neutrality and are exposed through `onContextEvent`, not the actor prompt.
|
||||
- Callback errors are swallowed so telemetry cannot break the agent run.
|
||||
- Do not scrape actor prompts for pressure metrics.
|
||||
|
||||
```typescript
|
||||
const supportAgent = agent('query:string -> answer:string', {
|
||||
contextFields: ['query'],
|
||||
runtime,
|
||||
contextPolicy: { preset: 'checkpointed', budget: 'balanced' },
|
||||
onContextEvent: (event) => {
|
||||
if (event.kind === 'budget_check') {
|
||||
console.log(event.pressure, event.mutablePromptChars);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Type:
|
||||
|
||||
```typescript
|
||||
onContextEvent?: (event: AxAgentContextEvent) => void | Promise<void>;
|
||||
```
|
||||
|
||||
## Agent Status Callback
|
||||
|
||||
Use `agentStatusCallback` when the caller wants real-time progress updates from the actor. When set, the actor can call `await reportSuccess(message)` and `await reportFailure(message)` in its JavaScript turns.
|
||||
|
||||
```typescript
|
||||
const supportAgent = agent('query:string -> answer:string', {
|
||||
contextFields: ['query'],
|
||||
runtime,
|
||||
agentStatusCallback: (message, status) => {
|
||||
console.log(`[${status}] ${message}`);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `agentStatusCallback` receives `(message: string, status: 'success' | 'failed')`.
|
||||
- When set, the actor prompt automatically includes `reportSuccess(message)` and `reportFailure(message)` as available runtime functions.
|
||||
- The actor is instructed to keep the user updated on task progress.
|
||||
- `reportSuccess` and `reportFailure` are reserved runtime names when the callback is configured.
|
||||
- Child agents inherit the callback via the RLM config.
|
||||
|
||||
Type:
|
||||
|
||||
```typescript
|
||||
agentStatusCallback?: (
|
||||
message: string,
|
||||
status: 'success' | 'failed'
|
||||
) => void | Promise<void>;
|
||||
```
|
||||
|
||||
## On Function Call
|
||||
|
||||
Use `onFunctionCall` when the caller wants to observe every function call the actor makes from the JS runtime. It fires before the underlying function runs.
|
||||
|
||||
```typescript
|
||||
const supportAgent = agent('query:string -> answer:string', {
|
||||
contextFields: ['query'],
|
||||
runtime,
|
||||
functions: [helperAgent, lookupOrderTool],
|
||||
onFunctionCall: ({ name, qualifiedName, args, kind }) => {
|
||||
console.log(`[${kind}] ${qualifiedName}`, args);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Receives `{ name, qualifiedName, args, kind }`.
|
||||
- `name` is the bare function name, e.g. `'lookupOrder'`.
|
||||
- `qualifiedName` is the namespaced name as the actor sees it, e.g. `'tools.lookupOrder'`; for un-namespaced runtime globals it equals `name`.
|
||||
- `args` is the resolved positional/named arguments object (`Record<string, unknown>`).
|
||||
- `kind` is `'external'` for caller-registered `functions`.
|
||||
- `kind` is `'internal'` for agent-injected globals: child agents, `discover`, `recall`, and `used`.
|
||||
- Fires once per call, before the function executes.
|
||||
- Errors thrown inside the callback are swallowed so they cannot break the actor loop.
|
||||
- This is independent from the DSP-layer `onFunctionCall` on `AxProgramForwardOptions`; that hook is for LLM tool-calls and never fires under AxAgent because AxAgent injects functions as runtime globals.
|
||||
|
||||
Type:
|
||||
|
||||
```typescript
|
||||
onFunctionCall?: (call: {
|
||||
name: string;
|
||||
qualifiedName: string;
|
||||
args: Record<string, unknown>;
|
||||
kind: 'internal' | 'external';
|
||||
}) => void | Promise<void>;
|
||||
```
|
||||
|
||||
## Chat Log, Usage, And Traces
|
||||
|
||||
`AxAgent` exposes actor and responder sub-programs. `getChatLog()` returns the same flat `AxChatLogEntry[]` shape as `AxGen` and `AxFlow`; use each entry's optional `name` field to distinguish `distiller`, `executor`, and `responder`. `getUsage()` returns token usage split by actor/responder.
|
||||
|
||||
### getChatLog()
|
||||
|
||||
Returns the full normalized chat history after any `.forward()` call. Each entry is one `ai.chat()` round-trip. Actor stages accumulate one entry per turn; the responder typically has one entry.
|
||||
|
||||
```typescript
|
||||
const log = myAgent.getChatLog();
|
||||
|
||||
for (const entry of log) {
|
||||
console.log(entry.name, entry.model);
|
||||
for (const msg of entry.messages) {
|
||||
console.log(`[${msg.role}]`, msg.content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each `AxChatLogEntry` captures the full prompt sent to the model and its response:
|
||||
|
||||
```typescript
|
||||
type AxChatLogMessage =
|
||||
| { role: 'system'; content: string }
|
||||
| { role: 'user'; content: string }
|
||||
| { role: 'assistant'; content: string }
|
||||
| { role: 'tool'; name: string; content: string };
|
||||
|
||||
type AxChatLogEntry = {
|
||||
name?: string; // e.g. "distiller", "executor", "responder"
|
||||
model: string;
|
||||
messages: AxChatLogMessage[];
|
||||
modelUsage?: AxProgramUsage;
|
||||
stage?: 'ctx' | 'task';
|
||||
};
|
||||
```
|
||||
|
||||
### getUsage()
|
||||
|
||||
Returns token usage split by actor/responder. Each sub-array contains one `AxProgramUsage` entry per model/run, merged by `(ai, model)` key.
|
||||
|
||||
```typescript
|
||||
const usage = myAgent.getUsage();
|
||||
// { actor: AxProgramUsage[], responder: AxProgramUsage[] }
|
||||
|
||||
console.log('Actor tokens:', usage.actor[0]?.tokens);
|
||||
console.log('Responder tokens:', usage.responder[0]?.tokens);
|
||||
```
|
||||
|
||||
### getStagedUsage()
|
||||
|
||||
Returns usage split by pipeline stage. The `ctx` stage has the distiller actor only; the `task` stage has the executor actor plus responder.
|
||||
|
||||
```typescript
|
||||
const staged = myAgent.getStagedUsage();
|
||||
console.log(staged.ctx?.actor);
|
||||
console.log(staged.task.actor);
|
||||
console.log(staged.task.responder);
|
||||
```
|
||||
|
||||
### getTraces()
|
||||
|
||||
Returns Ax program traces for the agent pipeline. Use it when the caller needs trace data rather than chat messages or token summaries.
|
||||
|
||||
```typescript
|
||||
const traces = myAgent.getTraces();
|
||||
```
|
||||
|
||||
### resetUsage()
|
||||
|
||||
Resets both actor and responder usage at once:
|
||||
|
||||
```typescript
|
||||
myAgent.resetUsage();
|
||||
```
|
||||
|
||||
Type signatures:
|
||||
|
||||
```typescript
|
||||
// AxAgent
|
||||
agent.getChatLog(): readonly AxChatLogEntry[]
|
||||
agent.getUsage(): { actor: AxProgramUsage[]; responder: AxProgramUsage[] }
|
||||
agent.getStagedUsage(): { ctx?: AxAgentUsage; task: AxAgentUsage }
|
||||
agent.getTraces(): AxProgramTrace[]
|
||||
agent.resetUsage(): void
|
||||
|
||||
// AxGen / AxFlow
|
||||
gen.getChatLog(): readonly AxChatLogEntry[]
|
||||
gen.getUsage(): AxProgramUsage[]
|
||||
```
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not add both `debug: true` and `actorTurnCallback` unless the user wants both unstructured prompt/runtime visibility and structured telemetry.
|
||||
- Do not scrape actor prompts or action logs when a callback exposes the data directly.
|
||||
- Do not let observability callback failures break the agent run; Ax swallows callback errors for telemetry hooks.
|
||||
- Do not use DSP-layer `onFunctionCall` when the user wants AxAgent runtime function calls.
|
||||
- Do not enable `showThoughts` unless the user needs provider thought diagnostics and the provider supports it.
|
||||
- Do not use `getUsage()` as the centralized source of truth across shared agents or processes.
|
||||
- Do not perform database or network work inline in `axGlobals.onUsage`; enqueue and return.
|
||||
368
.agents/skills/ax-agent-optimize/SKILL.md
Normal file
368
.agents/skills/ax-agent-optimize/SKILL.md
Normal file
@@ -0,0 +1,368 @@
|
||||
---
|
||||
name: ax-agent-optimize
|
||||
description: This skill helps an LLM generate correct AxAgent tuning and evaluation code using @ax-llm/ax. Use when the user asks about agent.optimize(...), judgeOptions, eval datasets, optimization targets, saved optimizedProgram artifacts, or agent optimization guidance.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxAgent Optimize Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill for `agent.optimize(...)` workflows. Prefer short, modern, copyable patterns. Do not repeat general agent-authoring guidance unless the user needs it. For generic `ax(...)` or `flow(...)` tuning with top-level `optimize(...)`, use the `ax-gepa` skill instead.
|
||||
|
||||
Your job is to help the model choose a good optimization setup for the user's actual goal:
|
||||
|
||||
- If the user wants better tool use, prefer action-aware tasks and either a deterministic metric or the built-in judge depending on how objective the scoring is.
|
||||
- If the user wants better wording only, responder optimization may be enough.
|
||||
- If the user wants reusable improvements, include artifact save/load.
|
||||
- If the user wants cost, tool-use, or child-agent delegation behavior improved, make the eval tasks expose those tradeoffs explicitly.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use `agent.optimize(...)` only after the agent is already configured and runnable.
|
||||
- Prefer the built-in judge path first for normal agent tuning. Most users should start with tasks that include `input` and `criteria`, then let `agent.optimize(...)` use its default actor target and judge-based metric.
|
||||
- Keep top-level `optimize(program, train, metric, options)` for non-agent generators and flows; do not rewrite normal agent task-record examples to the generic helper.
|
||||
- Prefer a deterministic custom `metric` only when success is easy to score from the prediction and task record.
|
||||
- Add `judgeAI` plus `judgeOptions` when the judge should run on a stronger or separate model than the agent runtime model.
|
||||
- Only reach for a plain typed `AxGen` evaluator when the user needs LLM-as-judge behavior outside the built-in `agent.optimize(...)` flow.
|
||||
- Default optimize target is the actor path; do not surface `target` unless the user clearly wants responder-only tuning or explicit program IDs.
|
||||
- Use eval-safe tools or in-memory mocks because optimization replays tasks many times.
|
||||
- Prefer precise tool return schemas such as `f.object(...)` over vague `f.json(...)` whenever the agent must reason about returned fields.
|
||||
- Prefer task wording with canonical entity names like "the Atlas project" instead of ambiguous labels like "Atlas" when ambiguity could trigger pointless clarification.
|
||||
- Save artifacts with `axSerializeOptimizedProgram(result.optimizedProgram!)`, then restore with `axDeserializeOptimizedProgram(saved)` and `agent.applyOptimization(...)`.
|
||||
- For browser-safe persistence, let the caller store the serialized JSON anywhere they want such as localStorage, IndexedDB, or a backend.
|
||||
- If `bootstrap` is enabled, bootstrapped demos are persisted inside `result.optimizedProgram.demos`; raw failed traces are not saved in v1.
|
||||
- Auto-promoted context fields (large undeclared inputs kept runtime-only by `autoUpgrade`) appear in captured traces/demos as their truncated preview string, not the full value — same as declared truncate-style `contextFields`. This is expected; do not treat the shortened value as a bug in the saved demos.
|
||||
- For first examples, pass a plain task array instead of splitting into `train` and `validation` unless the user already has a holdout set.
|
||||
- GEPA-backed `agent.optimize(...)` now optimizes generic components exposed by the selected target programs; `target: 'actor'` only tunes actor components, `target: 'responder'` only tunes responder components, and `target: 'all'` broadens the component set.
|
||||
- `result.optimizedProgram.componentMap` is the canonical saved artifact for agent GEPA runs. It may include actor instructions, descriptions, tool descriptions/names, templates, or runtime primitives depending on what the selected target exposes.
|
||||
- When child-agent delegation matters, expose the child agents as named functions and tune against realistic call/no-call tasks.
|
||||
|
||||
## Decision Guide
|
||||
|
||||
Pick the optimization shape from the user's need:
|
||||
|
||||
- "Make the agent use tools correctly" -> keep the default actor target and use `expectedActions` and `forbiddenActions`.
|
||||
- "Make final answers read better" -> consider `target: 'responder'`, but only if the task is not mostly tool-selection or clarification behavior.
|
||||
- "Make the whole agent better" -> use the default actor target first; only broaden target selection when the user clearly wants that extra scope.
|
||||
- "Tune child-agent delegation" -> use tasks that exercise when to call the child agent, when to call normal tools, and when to answer directly.
|
||||
- "Compare before and after" -> include a held-out task plus artifact save/load and replay.
|
||||
- "Repair the tasks it keeps failing, without eroding what works" -> this is playbook territory, not GEPA: use the agent-bound playbook evolve method (in TypeScript, `agent.playbook().evolve(dataset)`) to mine failures into verified playbook bullets under a held-out gate. Python, Java, C++, Go, and Rust expose the same loop with native method casing; see `ax-playbook`. `optimize(...)` maximizes a metric by tuning instructions and demos; playbook evolution grows durable rules.
|
||||
|
||||
Choose task design carefully:
|
||||
|
||||
- Prefer a small number of realistic tasks over broad but vague datasets.
|
||||
- Prefer concrete criteria over generic "be helpful" language.
|
||||
- Prefer explicit action expectations when correctness depends on tools, recipients, dates, or side effects.
|
||||
- Prefer eval-safe mocks anytime the task touches email, scheduling, external APIs, or persistence.
|
||||
|
||||
## Make Agents Optimizable
|
||||
|
||||
Optimization works much better when the agent and dataset remove avoidable ambiguity:
|
||||
|
||||
- Prefer typed tool outputs over free-form JSON blobs so the actor can rely on exact field names.
|
||||
- Tell the actor the exact tool fields it may use when payload shape matters.
|
||||
- Explicitly ban invented fields if the model has any reason to guess hidden IDs or alternate key names.
|
||||
- If a child agent needs parent values, declare those fields in the child signature and pass them explicitly at the call site.
|
||||
- For specialist synthesis, tell the agent what narrowed context should be passed to the child agent.
|
||||
- Keep `maxSubAgentCalls` small in examples unless the user is explicitly testing broad fan-out behavior.
|
||||
- Use canonical, unambiguous task wording so the model does not burn turns asking for fake clarification.
|
||||
- In JS-runtime agents, require raw runnable JavaScript only. Ban `javascript:` prefixes, mixed prose/code, and multi-snippet turns.
|
||||
|
||||
Good pattern:
|
||||
|
||||
- tool schema says exactly what fields exist
|
||||
- task names the exact entity to look up
|
||||
- actor prompt says which fields to extract before calling a child agent
|
||||
- metric or judge penalizes unnecessary child-agent calls and tool misuse
|
||||
|
||||
Bad pattern:
|
||||
|
||||
- tool returns `json` with an underspecified shape
|
||||
- task uses overloaded names like `Atlas` without clarifying whether that is a project, team, or account
|
||||
- child agent is expected to infer hidden parent state that was never passed in its call arguments
|
||||
- code agent is allowed to mix natural language with JavaScript in the same turn
|
||||
|
||||
## Metric vs Judge
|
||||
|
||||
Choose the scoring path based on how objectively the task can be measured:
|
||||
|
||||
- Use a custom `metric` when you can score success directly from `prediction` and `example`.
|
||||
- Use the built-in agent judge when success depends on a full-run qualitative review across tool choices, clarifications, and final output.
|
||||
- Use `judgeOptions.description` to tell the built-in judge what to value most.
|
||||
- Use helper-based judge code only when the user is not inside `agent.optimize(...)` and still wants LLM judging.
|
||||
|
||||
Quick rules:
|
||||
|
||||
- Tool correctness with exact expected calls or forbidden calls: prefer a deterministic metric first.
|
||||
- Simple extraction or classification with known correct answers: prefer a deterministic metric.
|
||||
- Open-ended assistant quality, nuanced clarification behavior, or broad synthesis quality: prefer the built-in judge.
|
||||
- GEPA or optimizer flows outside agents that still need LLM judging: use a plain typed `AxGen` evaluator.
|
||||
|
||||
Important:
|
||||
|
||||
- A custom `metric` overrides the built-in judge path entirely.
|
||||
- Do not introduce a dedicated judge abstraction in new examples; prefer a plain typed `AxGen`.
|
||||
- Do not add both a custom `metric` and judge guidance unless the user explicitly wants two separate scoring systems and understands only the custom metric drives optimization.
|
||||
- If the user builds a plain `AxGen` judge metric, prefer a numeric `score:number` output over a string tier when possible. It is simpler and less fragile in practice.
|
||||
|
||||
## Canonical Pattern
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AxAIGoogleGeminiModel,
|
||||
AxJSRuntime,
|
||||
axDefaultOptimizerLogger,
|
||||
agent,
|
||||
ai,
|
||||
f,
|
||||
fn,
|
||||
axDeserializeOptimizedProgram,
|
||||
axSerializeOptimizedProgram,
|
||||
} from '@ax-llm/ax';
|
||||
|
||||
const tools = [
|
||||
fn('sendEmail')
|
||||
.namespace('email')
|
||||
.description('Send an email message')
|
||||
.arg('to', f.string('Recipient email address'))
|
||||
.arg('body', f.string('Email body text'))
|
||||
.returns(
|
||||
f.object({
|
||||
sent: f.boolean('Whether the email was sent'),
|
||||
to: f.string('Recipient email address'),
|
||||
})
|
||||
)
|
||||
.handler(async ({ to }) => ({ sent: true, to }))
|
||||
.build(),
|
||||
];
|
||||
|
||||
const studentAI = ai({
|
||||
name: 'google-gemini',
|
||||
apiKey: process.env.GOOGLE_APIKEY!,
|
||||
config: { model: AxAIGoogleGeminiModel.Gemini31FlashLite, temperature: 0.2 },
|
||||
});
|
||||
|
||||
const judgeAI = ai({
|
||||
name: 'google-gemini',
|
||||
apiKey: process.env.GOOGLE_APIKEY!,
|
||||
config: { model: AxAIGoogleGeminiModel.Gemini35Flash, temperature: 1.0 },
|
||||
});
|
||||
|
||||
const assistant = agent('query:string -> answer:string', {
|
||||
ai: studentAI,
|
||||
judgeAI,
|
||||
contextFields: [],
|
||||
runtime: new AxJSRuntime(),
|
||||
functions: tools,
|
||||
contextPolicy: { preset: 'checkpointed', budget: 'balanced' },
|
||||
judgeOptions: {
|
||||
description: 'Prefer correct tool use over polished wording.',
|
||||
model: 'judge-model',
|
||||
},
|
||||
});
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
input: { query: 'Send an email to Jim saying good morning.' },
|
||||
criteria: 'Use the email tool and send the message to Jim.',
|
||||
expectedActions: ['email.sendEmail'],
|
||||
},
|
||||
];
|
||||
|
||||
const result = await assistant.optimize(tasks, {
|
||||
maxMetricCalls: 12,
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
const saved = axSerializeOptimizedProgram(result.optimizedProgram!);
|
||||
const restored = axDeserializeOptimizedProgram(saved);
|
||||
assistant.applyOptimization(restored);
|
||||
```
|
||||
|
||||
## Minimal Normal-User Pattern
|
||||
|
||||
Start here unless the user clearly needs a hand-built scorer:
|
||||
|
||||
```typescript
|
||||
const tasks = [
|
||||
{
|
||||
input: { query: 'Send an email to Jim saying good morning.' },
|
||||
criteria: 'Use the email tool and send the message to Jim.',
|
||||
expectedActions: ['email.sendEmail'],
|
||||
},
|
||||
];
|
||||
|
||||
const result = await assistant.optimize(tasks);
|
||||
assistant.applyOptimization(result.optimizedProgram!);
|
||||
```
|
||||
|
||||
- `target` defaults to actor optimization.
|
||||
- `metric` defaults to the built-in LLM judge.
|
||||
- `judgeAI` is optional; if omitted, the agent falls back to its configured judge model or runtime model.
|
||||
- `bootstrap: true` is a good next step for tool-heavy agents when you want GEPA to start from successful traces from the provided tasks.
|
||||
- The one thing users still need is realistic task records with clear `criteria`.
|
||||
|
||||
## Deterministic Metric Pattern
|
||||
|
||||
Use this when the task has crisp correctness and cost/behavior tradeoffs:
|
||||
|
||||
```typescript
|
||||
const result = await assistant.optimize(tasks, {
|
||||
target: 'actor',
|
||||
metric: ({ prediction, example }) => {
|
||||
if (prediction.completionType !== 'final' || !prediction.output) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
|
||||
if (prediction.output.answer.includes('Jim')) score += 0.4;
|
||||
|
||||
if (
|
||||
prediction.functionCalls.some(
|
||||
(call) => call.qualifiedName === 'email.sendEmail'
|
||||
)
|
||||
) {
|
||||
score += 0.4;
|
||||
}
|
||||
|
||||
if (prediction.turnCount <= 3) {
|
||||
score += 0.2;
|
||||
}
|
||||
|
||||
return score;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Use this pattern when:
|
||||
|
||||
- the task has a known correct answer or exact action pattern
|
||||
- tool count, child-agent calls, or turn count must be measured explicitly
|
||||
- you want repeatable, low-variance optimization runs
|
||||
|
||||
## Built-In Judge Pattern
|
||||
|
||||
Use this when the agent behavior needs holistic review:
|
||||
|
||||
```typescript
|
||||
const result = await assistant.optimize(tasks, {
|
||||
judgeAI,
|
||||
judgeOptions: {
|
||||
model: AxAIGoogleGeminiModel.Gemini35Flash,
|
||||
description:
|
||||
'Be strict about unnecessary child-agent calls, weak clarifications, and incorrect tool choices.',
|
||||
},
|
||||
maxMetricCalls: 12,
|
||||
});
|
||||
```
|
||||
|
||||
Use this pattern when:
|
||||
|
||||
- task quality is open-ended or hard to score exactly
|
||||
- the final answer quality matters together with the action trace
|
||||
- the user wants a judge to consider clarifications, tool errors, and overall completion quality
|
||||
|
||||
## Plain `AxGen` Judge Pattern
|
||||
|
||||
Use this only when the user needs LLM judging outside the built-in `agent.optimize(...)` path:
|
||||
|
||||
```typescript
|
||||
import { AxGen, s } from '@ax-llm/ax';
|
||||
|
||||
const judgeGen = new AxGen(
|
||||
s(`
|
||||
taskInput:json "Task input",
|
||||
candidateOutput:json "Candidate output",
|
||||
expectedOutput?:json "Optional reference output"
|
||||
->
|
||||
score:number "Normalized score from 0 to 1"
|
||||
`)
|
||||
);
|
||||
judgeGen.setInstruction(
|
||||
'Score the candidate output from 0 to 1. Reward correctness and task completion. Return only the score field.'
|
||||
);
|
||||
|
||||
const metric = async ({ prediction, example }) => {
|
||||
const result = await judgeGen.forward(judgeAI, {
|
||||
taskInput: example,
|
||||
candidateOutput: prediction,
|
||||
expectedOutput: example.expectedOutput,
|
||||
});
|
||||
|
||||
return Math.max(0, Math.min(1, result.score));
|
||||
};
|
||||
|
||||
const result = await optimizer.compile(program, train, metric, {
|
||||
validationExamples: validation,
|
||||
});
|
||||
```
|
||||
|
||||
Use this pattern when:
|
||||
|
||||
- the user is optimizing an `AxGen`, flow, or another program directly
|
||||
- the user wants LLM judging without the higher-level `agent.optimize(...)` wrapper
|
||||
- the user wants to inspect judge results directly, not just a numeric score
|
||||
|
||||
## Dataset And Judge Rules
|
||||
|
||||
- Pass already-loaded tasks. Do not invent a benchmark loader unless the user asks for one.
|
||||
- Use `expectedActions` and `forbiddenActions` when tool correctness matters.
|
||||
- `judgeOptions` mirrors normal forward options and supports extra judge guidance through `description`.
|
||||
- The built-in judge scores from the full agent run, not just the final reply. It can see completion type, clarification payload, final output, action log, normalized function calls, tool errors, and turn count.
|
||||
- If the user provides a custom `metric`, that overrides the built-in judge path.
|
||||
- If the user provides an LLM-based custom metric, keep the output schema as small as possible and prefer a direct numeric score.
|
||||
|
||||
Decision rules:
|
||||
|
||||
- Prefer a custom metric when the user has deterministic business scoring, exact action expectations, or explicit cost tradeoffs.
|
||||
- Prefer the built-in judge when the user wants practical assistant-quality tuning and does not already have a trusted metric.
|
||||
- Prefer a plain typed `AxGen` evaluator when the user is not calling `agent.optimize(...)` but still wants LLM judging.
|
||||
- Prefer `judgeOptions.description` to steer the judge toward the user's real priority, such as tool correctness, brevity, groundedness, or policy compliance.
|
||||
|
||||
## Eval Semantics
|
||||
|
||||
- MCP/UCP evaluation defaults to replay or sandbox mode. A live client is rejected unless `mcpEvaluation: 'live'` is explicit.
|
||||
- Use `ax-mcp` for recording/replay transport setup and MCP side-effect policy.
|
||||
- Use `AxMCPRecordingTransport` to capture a real session once and `AxMCPReplayTransport` for deterministic optimization/evaluation.
|
||||
- Replay normalized MCP notifications and task transitions through
|
||||
`AxEventRuntime`; do not leave a live subscription active in a default
|
||||
optimization run.
|
||||
- Action traces include qualified MCP/UCP operations, approvals, task transitions, raw protocol errors, and business outcomes for judges and deterministic metrics.
|
||||
- `agent.optimize(...)` runs each evaluation rollout from a clean continuation state.
|
||||
- Saved runtime state from `getState()` and `setState(...)` is not used during eval rollouts.
|
||||
- During optimize/eval, `askClarification(...)` is treated as a scored evaluation outcome instead of going through the responder.
|
||||
- For clarification outcomes in custom metrics, expect `prediction.completionType === 'askClarification'`, populated `prediction.clarification`, and absent `prediction.output`.
|
||||
- For final outcomes in custom metrics, expect `prediction.completionType === 'final'` and populated `prediction.output`.
|
||||
- `target: 'responder'` still works, but clarification-heavy tasks are usually low-signal for responder optimization.
|
||||
|
||||
## Delegation Optimization Notes
|
||||
|
||||
- Prefer explicit child agents in `functions: [...]` for specialist delegation. Their calls appear as normal function-call records.
|
||||
- When delegation behavior matters, tune against the same child-agent/tool structure you expect in production.
|
||||
- Tell the actor which fields to pass to the child agent and which tasks should stay local.
|
||||
- For synthesis-style tasks, specify the desired delegation pattern explicitly, for example "call `team.writer(...)` only after narrowing tool output in JS."
|
||||
- Penalize unnecessary child-agent calls directly in the metric or judge prompt.
|
||||
- If one training task keeps collapsing to zero, inspect that task first instead of adding more optimizer rounds. Most failures come from task ambiguity, weak tool schemas, or vague delegation guidance rather than GEPA itself.
|
||||
|
||||
## Artifacts And Replay
|
||||
|
||||
- Save `result.optimizedProgram` if the user wants portable artifacts.
|
||||
- Restore artifacts with `new AxOptimizedProgramImpl(...)`, then call `agent.applyOptimization(...)`.
|
||||
- Preserve the full optimized program when saving GEPA artifacts; `componentMap` reapplies the learned strings.
|
||||
- For demonstrations, use fresh eval-safe tool state for baseline, optimize, and restored replay so side effects do not leak across phases.
|
||||
- If the user wants to show improvement, run a held-out task before optimization, then replay it on a freshly restored optimized agent.
|
||||
|
||||
## Examples
|
||||
|
||||
- [RLM Agent Optimize](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-agent-optimize.ts) — Gemini office-assistant tuning with save/load
|
||||
- [AxAgent GEPA Component Optimization](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/axagent-gepa-optimization.ts) — compact support-agent GEPA run with deterministic metric and artifact replay
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not optimize against production tools with real side effects unless the user explicitly wants that.
|
||||
- Do not recommend responder-only optimization by default for clarification-heavy workflows.
|
||||
- Do not omit artifact save/load steps when the user asks for reusable optimized configurations.
|
||||
- Do not introduce a dedicated judge class or helper abstraction in new agent-optimize examples; prefer the built-in judge path or a plain typed `AxGen`.
|
||||
- Do not rely on vague `json` tool returns when the agent must reason about specific fields across tool or child-agent calls.
|
||||
- Do not leave child-agent inputs implicit. If the child needs a fact, pass it explicitly.
|
||||
- Do not let code-generation agents mix prose and JavaScript if the user is optimizing runtime behavior.
|
||||
501
.agents/skills/ax-agent-rlm/SKILL.md
Normal file
501
.agents/skills/ax-agent-rlm/SKILL.md
Normal file
@@ -0,0 +1,501 @@
|
||||
---
|
||||
name: ax-agent-rlm
|
||||
description: This skill helps an LLM generate correct AxAgent RLM/runtime code using @ax-llm/ax. Use when the user asks about RLM code execution, AxJSRuntime, contextFields, contextPolicy, liveRuntimeState, promptLevel, stage prompt controls, executorModelPolicy, maxRuntimeChars, agent.test(...), llmQuery(...), recursionOptions, or long-running agent runtime behavior.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxAgent RLM Runtime Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill for code-runtime agents and `llmQuery(...)` semantic-helper behavior. For ordinary agent setup, child agents, tool namespaces, clarification, and `bubbleErrors`, use `ax-agent`. For callbacks and logs, use `ax-agent-observability`. For memories and skill loading, use `ax-agent-memory-skills`.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use `agent(...)`, not `new AxAgent(...)`.
|
||||
- In stdout-mode RLM, use one observable `console.log(...)` step per non-final actor turn.
|
||||
- Rely on `autoUpgrade` (ON by default) for oversized inputs you did not declare in `contextFields`: any input value over ~8k serialized chars is kept runtime-only automatically, with a 1,200-char prompt preview plus a `contextMetadata` line, while the full value stays live in the runtime as `inputs.<field>`. Declare a field in `contextFields` only when you want a specific inline policy (`promptMaxChars` / `keepInPromptChars`) or need a large required non-string field kept out of the prompt (those are left inline by auto-upgrade).
|
||||
- Default to `contextPolicy: { preset: 'checkpointed', budget: 'balanced' }` for most RLM tasks.
|
||||
- Prefer `contextPolicy: { preset: 'adaptive', budget: 'balanced' }` when older successful turns should collapse sooner while live runtime state stays visible.
|
||||
- Use `contextMap` for recurring long-context corpora when the distiller should start future runs with a small persisted orientation cache.
|
||||
- Prefer `promptLevel: 'default'` for normal use.
|
||||
- Use `promptLevel: 'detailed'` when you want extra anti-pattern examples and tighter teaching scaffolding in the actor prompt.
|
||||
- Prefer `executorModelPolicy` when the actor may need to upgrade after repeated error turns or discovery in specific namespaces without also upgrading the responder.
|
||||
- Use explicit child agents in `functions: [...]` when the task needs specialist agents with their own tools/runtime.
|
||||
- Use `llmQuery(...)` only for focused semantic questions over narrowed context; it does not spawn a tool-using child AxAgent.
|
||||
- Prefer `maxSubAgentCalls` only when you need an explicit cap on `llmQuery(...)` sub-query usage.
|
||||
|
||||
## Mental Model
|
||||
|
||||
`AxAgent` is a three-stage pipeline. Each `forward()` call walks the stages in order:
|
||||
|
||||
```text
|
||||
distiller (RLM actor) -> executor (RLM actor) -> responder (synthesizer)
|
||||
```
|
||||
|
||||
- **distiller** always runs first. It sees all original inputs so it can understand and normalize the task; declared `contextFields` stay runtime-only when present. It distils relevant evidence by writing runtime-language code in a multi-turn loop, then calls the runtime-exposed `final(request, evidence)` primitive. The request becomes the executor's `inputs.executorRequest`; it must be self-contained and restate the concrete action, target, and constraints, not vague wording like "do it". The distiller should expand the original user task with facts found in context, including follow-ups like "yes, do it". When no `contextFields` are configured, it still performs request normalization over the original inputs with `contextFields: []`. **The distiller has no tools and is not a capability gate.**
|
||||
- **executor** runs unless the distiller skipped it (below). It receives non-context inputs plus `inputs.executorRequest`, a compact `distilledContextSummary` prompt field, and the real evidence live as `inputs.distilledContext` from the distiller's `final(request, evidence)` payload. Declared or auto-promoted context fields stay runtime-readable as `inputs.<field>` when `contextMetadata` lists them, but their raw contents are not pasted into the executor prompt. The executor owns tool use, decides whether to call its available functions or finish directly from distilled evidence, and reports actual tool results or failures.
|
||||
- **responder** always runs last. It synthesizes the user's output signature from whichever upstream actor finished the run and must not contradict tool evidence gathered upstream.
|
||||
|
||||
### Direct respond (executor skip)
|
||||
|
||||
With `directResponse: 'auto'` (the default), the distiller can end the run with the `respond(task, evidence)` primitive when the task needs no user-provided functions — the executor stage is skipped entirely (zero executor model calls) and the responder synthesizes straight from the distiller's evidence. Unlike `final`, whose evidence stays live in the shared session by reference, `respond`'s evidence crosses into the responder prompt (budgeted by `maxEvidenceChars`), and the distiller's runtime variables are exported as the cross-run state exactly as the executor's would have been.
|
||||
|
||||
- **Static agents** (no `functions`, no child agents) run respond-only: `final` is not offered to the distiller and every run is distiller → responder.
|
||||
- **Agents with functions** get `respond` alongside `final` under a conservative covenant: only for tasks answered purely by reading/synthesizing provided context, never when a listed function/module domain covers the need, never for current/live/fresh-state asks (context may be stale — tools are the source of truth for "now"), never for side effects. Landing-gate eval (both pinned models, 3 repeats): 0 false skips on tool-required tasks including a stale-context trap, 100% skip recall on pure context Q&A.
|
||||
- `directResponse: 'off'` removes the primitive from the prompt and the runtime, and the pipeline rejects a respond payload outright.
|
||||
|
||||
Treat both actor stages as long-running code runtime sessions that the actor steers over multiple turns, not as fresh script generators on every turn. `AxJSRuntime` is the default; custom runtimes set `language` so the actor code field becomes `<language>Code` such as `pythonCode` while JavaScript keeps the legacy `javascriptCode`.
|
||||
|
||||
- Successful code leaves variables, functions, imports, and computed values available in the runtime session.
|
||||
- The actor should continue from existing runtime state instead of recreating prior work.
|
||||
- `actionLog`, `liveRuntimeState`, and checkpoint summaries only control what the actor can see again in the prompt.
|
||||
- Rebuild state only after an explicit runtime restart notice or when you intentionally need to overwrite a value.
|
||||
|
||||
## RLM Actor Code Rules
|
||||
|
||||
Use these rules when generating actor JavaScript for RLM in `AxJSRuntime` stdout mode. For custom runtimes, follow the runtime's `getUsageInstructions()`, primitive overrides, and callable formatter instead.
|
||||
|
||||
- Treat each actor turn as exactly one observable step.
|
||||
- Inspect what already exists before recomputing it. If a prior turn successfully created a value, prefer reusing that runtime value.
|
||||
- If you need to inspect a value, compute it or read it, `console.log(...)` it, and stop immediately after that `console.log(...)`.
|
||||
- On the next turn, continue from the existing runtime state and use the logged result from `Action Log` only as evidence for what happened.
|
||||
- If the prompt contains `Live Runtime State`, treat it as the canonical view of current variables.
|
||||
- Errors from child-agent or tool calls appear in `Action Log`; inspect them and fix the code on the next turn.
|
||||
- Non-final turns should contain exactly one `console.log(...)`.
|
||||
- Final turns should call `await final(outputGenerationTask, context)` or `await askClarification(...)` without `console.log(...)`.
|
||||
- Do not write a complete multi-step program in one actor turn.
|
||||
- Do not combine `console.log(...)` with `await final(...)` or `await askClarification(...)` in the same actor turn.
|
||||
- Inside actor-authored JavaScript, `await final(...)` and `await askClarification(...)` end the current turn immediately; code after them is dead code.
|
||||
- Do not re-declare or recompute values just because older turns are summarized; only rebuild after an explicit runtime restart or when you intentionally want a new value.
|
||||
- Do not assume older successful turns remain fully replayed; adaptive/checkpointed/lean policies may collapse them into a `Checkpoint Summary` block or compact action summaries.
|
||||
|
||||
Small reuse example:
|
||||
|
||||
Turn 1:
|
||||
|
||||
```javascript
|
||||
const customers = await kb.findCustomers({ segment: 'active' });
|
||||
console.log(customers.length);
|
||||
```
|
||||
|
||||
Turn 2:
|
||||
|
||||
```javascript
|
||||
const topCustomers = customers.slice(0, 3);
|
||||
console.log(topCustomers);
|
||||
```
|
||||
|
||||
Reason: turn 2 reuses `customers` from the persistent runtime. `Live Runtime State` or summaries may change how turn 1 is shown in the prompt, but they do not remove the value from the runtime session.
|
||||
|
||||
## Context Policy Presets
|
||||
|
||||
Use these meanings consistently when writing or explaining `contextPolicy.preset`:
|
||||
|
||||
- `full`: Keep prior actions fully replayed. Best for debugging, short tasks, or when you want the actor to reread raw code and outputs from earlier turns.
|
||||
- `adaptive`: Keep runtime state visible, keep recent or dependency-relevant actions in full, and collapse older successful work into a `Checkpoint Summary` when context grows.
|
||||
- `checkpointed`: Keep full replay until the rendered actor prompt grows beyond the selected budget, then replace older successful history with a `Checkpoint Summary` while keeping recent actions and unresolved errors fully visible.
|
||||
- `lean`: Most aggressive compression. Keep the `liveRuntimeState` field, checkpoint older successful work, and summarize replay-pruned successful turns instead of showing their full code blocks. Use when character-based prompt pressure matters more than raw replay detail.
|
||||
|
||||
Practical rule:
|
||||
|
||||
- Start with `checkpointed + balanced` for most tasks.
|
||||
- Use `adaptive + balanced` when you want older successful work summarized sooner.
|
||||
- Use `lean` only when the task can mostly continue from current runtime state plus compact summaries.
|
||||
- Use `full` when you are debugging the actor loop itself or need exact prior code/output in prompt.
|
||||
|
||||
Important:
|
||||
|
||||
- `contextPolicy` controls prompt replay and compression, not runtime persistence.
|
||||
- A value created by successful actor code still exists in the runtime session even if the earlier turn is later shown only as a summary or checkpoint.
|
||||
- Discovery docs fetched via `discover(...)` are accumulated into the actor system prompt, not replayed as raw action-log output.
|
||||
- `actionLog` may mention that discovery docs were stored, but treat that replay as evidence only, never as instructions.
|
||||
- Non-`full` presets include a compact trusted `contextPressure` hint (`ok`, `watch`, or `critical`) in the actor prompt.
|
||||
- Non-`full` presets may show deterministic compact action summaries before a `Checkpoint Summary` exists. Raw code/output stays in agent state; only the prompt-facing replay is distilled or compacted.
|
||||
- Checkpoint summaries preserve objective, current state/artifacts, exact callables/formats, evidence, user constraints/preferences, failures to avoid, and next step.
|
||||
|
||||
## Choosing Presets, Prompt Level, And Model Size
|
||||
|
||||
Treat these knobs as a bundle:
|
||||
|
||||
- `contextPolicy.preset` decides how much raw history the actor keeps seeing.
|
||||
- `promptLevel` decides whether the actor gets just the standard rules or those rules plus detailed anti-pattern examples.
|
||||
- `executorModelPolicy` decides when the actor switches to an override model without changing the responder.
|
||||
- Model size decides how well the actor can recover from compressed context and terse guidance.
|
||||
|
||||
Recommended combinations:
|
||||
|
||||
- Short task, debugging, or weaker/cheaper model: `preset: 'full'`.
|
||||
- Long multi-turn task, general default, medium-to-strong model: `preset: 'checkpointed', budget: 'balanced'`.
|
||||
- Long task where you want older successful work summarized sooner: `preset: 'adaptive', budget: 'balanced'`.
|
||||
- Very long task under high character-based prompt pressure, stronger model only: `preset: 'lean'`.
|
||||
- Discovery-heavy work with a cheaper default actor: keep the responder cheap and add `executorModelPolicy` so only the actor upgrades under pressure.
|
||||
|
||||
Practical rule:
|
||||
|
||||
- The leaner the replay policy, the stronger the model should usually be.
|
||||
- `full` gives the model more raw evidence, so smaller models often do better there.
|
||||
- `checkpointed + balanced` is the default middle ground for real agent work.
|
||||
- `adaptive + balanced` is the proactive-summarization variant when you want older successful work compressed sooner.
|
||||
- `lean` should be reserved for models that can reason well from runtime state plus summaries instead of exact old code/output.
|
||||
- `executorModelPolicy` is usually better than globally upgrading the whole agent when the bottleneck is actor exploration rather than responder synthesis.
|
||||
|
||||
## Option Layout
|
||||
|
||||
Use these top-level controls consistently:
|
||||
|
||||
- `recursionOptions.ai`: routes `llmQuery(...)` sub-query calls to a different AI service than the parent run.
|
||||
- `recursionOptions.model`, `modelConfig`, and other forward options: tune the AxGen call used by `llmQuery(...)`.
|
||||
- `maxSubAgentCalls`: shared `llmQuery(...)` sub-query budget across the whole run. Default is `100`.
|
||||
- `maxBatchedLlmQueryConcurrency`: caps batched `llmQuery([...])` concurrency.
|
||||
- `maxRuntimeChars`: runtime/output truncation ceiling for console logs, tool results, and interpreter output replay. The effective limit is computed dynamically each turn based on remaining context budget.
|
||||
- `summarizerOptions`: default model/options for the internal checkpoint summarizer.
|
||||
- `contextPolicy`: replay/checkpointing/compression policy.
|
||||
- `contextMap`: optional persistent orientation cache injected into the distiller and updated once after each successful run. `AxAgentContextMap` evolves indefinitely by default; use `{ infiniteEvolve: false, evolveSteps: N }` on the map object for finite warmup followed by reuse.
|
||||
- `contextOptions`: distiller-stage forward options.
|
||||
- `autoUpgrade`: smart defaults, ON by default. Auto-enables `functionDiscovery` for large tool catalogs and keeps oversized undeclared input values runtime-only with a truncated prompt preview. Set `false` to opt out, or tune per side: `{ functionDiscovery?: boolean | { aboveFunctionDocChars }, contextFields?: boolean | { promoteAboveChars, previewChars } }`. Explicit `functionDiscovery` and declared `contextFields` always win.
|
||||
- `executorOptions`: executor-stage forward options such as `description`, `model`, `modelConfig`, `thinkingTokenBudget`, and `showThoughts`.
|
||||
- `executorModelPolicy`: executor-only model override rules based on consecutive error turns or discovery fetches from listed namespaces.
|
||||
- `responderOptions`: responder-stage forward options.
|
||||
- `judgeOptions`: built-in judge options for `agent.optimize(...)`; for tuning workflows use `ax-agent-optimize`.
|
||||
|
||||
Canonical shape:
|
||||
|
||||
```typescript
|
||||
const researchAgent = agent('query:string -> answer:string', {
|
||||
contextFields: ['query'],
|
||||
runtime,
|
||||
recursionOptions: {
|
||||
model: 'gpt-5.4-mini',
|
||||
},
|
||||
maxRuntimeChars: 3000,
|
||||
summarizerOptions: {
|
||||
model: 'gpt-5.4-mini',
|
||||
modelConfig: { temperature: 0.1, maxTokens: 180 },
|
||||
},
|
||||
contextPolicy: {
|
||||
preset: 'checkpointed',
|
||||
budget: 'balanced',
|
||||
},
|
||||
contextOptions: {
|
||||
model: 'gpt-5.4-mini',
|
||||
maxTurns: 3,
|
||||
},
|
||||
executorOptions: {
|
||||
description: 'Use tools first and keep JS steps small.',
|
||||
model: 'gpt-5.4-mini',
|
||||
},
|
||||
executorModelPolicy: [
|
||||
{
|
||||
model: 'gpt-5.4',
|
||||
aboveErrorTurns: 2,
|
||||
namespaces: ['db', 'kb'],
|
||||
},
|
||||
],
|
||||
responderOptions: {
|
||||
model: 'gpt-5.4-mini',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
- `maxRuntimeChars` sets the truncation ceiling and is separate from `contextPolicy.budget`.
|
||||
- `summarizerOptions` tunes only the internal checkpoint summarizer. It does not change actor or responder model selection.
|
||||
- `executorModelPolicy` only switches the actor model. It does not change `responderOptions.model`.
|
||||
- `llmQuery(...)` uses `recursionOptions.ai` when set, otherwise it falls back to the parent `.forward(ai, ...)` service.
|
||||
- `recursionOptions` configures the AxGen semantic sub-query used by `llmQuery(...)`; it does not create a child AxAgent and cannot give the sub-query tools.
|
||||
- `executorModelPolicy` entries are ordered from weaker to stronger. If multiple rules match, the last matching entry wins.
|
||||
- If one entry defines `namespaces`, any successful `discover(...)` function-definition fetch from one of those namespaces marks the rule as matched starting on the next actor turn.
|
||||
- Do not add `recursionOptions` unless the user needs different model/options for `llmQuery(...)`.
|
||||
|
||||
## Dynamic Output Truncation
|
||||
|
||||
Runtime output truncation is budget-proportional and type-aware:
|
||||
|
||||
- Early turns with little action-log pressure use the full `maxRuntimeChars` ceiling.
|
||||
- As the action log fills toward `targetPromptChars`, the limit decays linearly down to 15% of the ceiling, hard-floored at 400 chars.
|
||||
- Large arrays keep the first 3 and last 2 items, with the middle replaced by `... [N hidden items]`.
|
||||
- Deep objects replace nested values beyond depth 3 with `[Object]` or `[Array(N)]`.
|
||||
- Error stack traces keep the first 3 and last 1 stack frames.
|
||||
- Simple values use standard `JSON.stringify` passthrough.
|
||||
|
||||
Users do not need to configure this behavior. `maxRuntimeChars` sets the upper bound; the dynamic system only reduces it.
|
||||
|
||||
## Stage Prompt Controls
|
||||
|
||||
The pipeline has three peer stage-config bags: `contextOptions` (distiller), `executorOptions` (executor), and `responderOptions` (responder). Each accepts the same shape: `description`, `model`, `modelConfig`, `excludeFields`, plus other forward options.
|
||||
|
||||
Key fields:
|
||||
|
||||
- `contextOptions.description`: append extra distiller-specific instructions.
|
||||
- `executorOptions.description`: append extra executor-specific instructions; this is the typical place for tool-use guidance.
|
||||
- `responderOptions.description`: append extra responder-specific instructions.
|
||||
- `contextOptions.model` / `executorOptions.model` / `responderOptions.model`: split model choice across stages.
|
||||
- `contextOptions.ai` / `executorOptions.ai` / `responderOptions.ai`: override the AI service for a specific stage.
|
||||
- `executorModelPolicy`: auto-switch only the executor when the run is on a consecutive error streak or discovery fetches land in specific namespaces.
|
||||
|
||||
Good split-model pattern:
|
||||
|
||||
```typescript
|
||||
const researchAgent = agent('query:string -> answer:string', {
|
||||
contextFields: ['query'],
|
||||
runtime,
|
||||
contextPolicy: { preset: 'checkpointed', budget: 'balanced' },
|
||||
executorOptions: {
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
responderOptions: {
|
||||
model: 'gpt-5.4-mini',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Model guidance:
|
||||
|
||||
- Put the stronger model on the actor when the task depends on multi-turn exploration, discovery, runtime state reuse, or compressed replay.
|
||||
- Put the stronger model on the responder only when the hard part is final synthesis/formatting rather than exploration.
|
||||
- For cost-sensitive setups, a common pattern is stronger actor plus cheaper responder.
|
||||
- Prefer `executorModelPolicy` over globally upgrading the whole agent when the actor only needs help after context grows or the run starts thrashing.
|
||||
|
||||
Prompt/cache shape:
|
||||
|
||||
- Actor turns are compact observable turns, not replayed chat transcripts.
|
||||
- Stable system prompt: role/stage rules, primitive descriptions, static module list, always-included callable signatures, output contract, and field definitions.
|
||||
- Cached working inputs: task inputs, inline context, `contextMetadata`, `contextMap`, `memories`, `executorRequest`, `distilledContextSummary`, `discoveredToolDocs`, `loadedSkills`, and `summarizedActorLog`.
|
||||
- Dynamic turn tail: `guidanceLog`, `actionLog`, `liveRuntimeState`, and `contextPressure`.
|
||||
- Prefer one compact inspection per non-final turn. Never combine inspection output with `final(...)` or `askClarification(...)`.
|
||||
|
||||
Invalid actor turn:
|
||||
|
||||
```javascript
|
||||
await discover(['kb.findSnippets']);
|
||||
const snippets = await kb.findSnippets({ topic: 'severity' });
|
||||
await final("Summarize severity findings", { snippets });
|
||||
```
|
||||
|
||||
Reason: this mixes observation and follow-up work in one turn. `discover(...)` returns `void`; read the next prompt's "Discovered Tool Docs" section before calling the function.
|
||||
|
||||
## AxJSRuntime Security
|
||||
|
||||
Default `new AxJSRuntime()` is hardened: no network, no filesystem, no child process, dynamic `import()` blocked, intrinsics frozen, `ShadowRealm` locked to `undefined`, worker IPC locked in browser/Deno/Bun, Bun workers use `smol: true`, and on Node 20+ the OS Permission Model auto-engages where available.
|
||||
|
||||
Threat model: this is defense-in-depth for LLM-authored code, not a container or VM boundary. Host callbacks and granted runtime permissions remain the authority boundary; keep durable secrets and privileged effects in host-side functions.
|
||||
|
||||
Permission enum (`AxJSRuntimePermission`):
|
||||
`NETWORK`, `STORAGE`, `CODE_LOADING`, `COMMUNICATION`, `TIMING`, `WORKERS`, `FILESYSTEM`, `CHILD_PROCESS`.
|
||||
|
||||
Options quick reference:
|
||||
|
||||
- `permissions?: readonly AxJSRuntimePermission[]`: default `[]`; opt in capabilities.
|
||||
- `blockDynamicImport?: boolean`: default `true`.
|
||||
- `allowedModules?: readonly string[]`: default `[]`; narrow dynamic-import allowlist gate. Allowlisted specifiers are attempted, but full Node module namespace passthrough depends on Node vm semantics.
|
||||
- `freezeIntrinsics?: boolean`: default `true`.
|
||||
- `blockShadowRealm?: boolean`: default `true`.
|
||||
- `lockWorkerIPC?: boolean`: default `true`.
|
||||
- `preventGlobalThisExtensions?: boolean`: default `false`; opt-in and breaks top-level persistence.
|
||||
- `useNodePermissionModel?: boolean | 'auto'`: default `'auto'`.
|
||||
- `nodePermissionAllowlist?: { fsRead?; fsWrite?; childProcess?; addons?; wasi? }`.
|
||||
- `resourceLimits?: { maxOldGenerationSizeMb?; maxYoungGenerationSizeMb?; codeRangeSizeMb?; stackSizeMb? }`.
|
||||
- `allowDenoRemoteImport?: boolean`: default `false`.
|
||||
- `allowUnsafeNodeHostAccess?: boolean`: default `false`.
|
||||
|
||||
Recipes:
|
||||
|
||||
```typescript
|
||||
new AxJSRuntime();
|
||||
|
||||
new AxJSRuntime({ permissions: [AxJSRuntimePermission.NETWORK] });
|
||||
|
||||
new AxJSRuntime({
|
||||
permissions: [AxJSRuntimePermission.FILESYSTEM],
|
||||
allowedModules: ['node:fs', 'node:fs/promises', 'node:path'],
|
||||
useNodePermissionModel: 'auto',
|
||||
nodePermissionAllowlist: {
|
||||
fsRead: ['/app/data'],
|
||||
fsWrite: ['/app/data'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rules for the LLM author:
|
||||
|
||||
- Default to `new AxJSRuntime()` with no options unless the user asked for a specific capability.
|
||||
- When the user asks for `fetch`, add `permissions: [AxJSRuntimePermission.NETWORK]`.
|
||||
- When the user asks for filesystem access, prefer host-side tool functions. If direct runtime filesystem access is required, add `permissions: [AxJSRuntimePermission.FILESYSTEM]`, scope with `nodePermissionAllowlist` when the user names a directory, and treat `allowedModules` as an import allowlist gate rather than a portability guarantee.
|
||||
- Do not disable `freezeIntrinsics`, `blockShadowRealm`, or `lockWorkerIPC` unless the user explicitly asks.
|
||||
- Treat `allowUnsafeNodeHostAccess: true` as a red flag; only use it when the user is authoring trusted code in their own process.
|
||||
- `preventGlobalThisExtensions: true` breaks top-level `var`/`let`/`const` persistence across turns; never set it for stdout-mode RLM where persistence is load-bearing.
|
||||
- On Deno, `blockDynamicImport` is a no-op; the defense is the worker permission sandbox. Pass `allowDenoRemoteImport: true` only if remote module loading is genuinely required.
|
||||
|
||||
## Custom Code Runtimes
|
||||
|
||||
Implement `AxCodeRuntime` when the actor should write a language other than JavaScript.
|
||||
|
||||
- Set `language` to the model-facing language name. JavaScript aliases (`JavaScript`, `js`, `ecmascript`) keep `javascriptCode`; other values derive lower-camel code fields such as `pythonCode` or `cSharpCode`.
|
||||
- Keep execution inside `createSession(globals, options)`. AxAgent passes `inputs`, `llmQuery`, `final`, `askClarification`, progress callbacks, memory/discovery primitives, and namespaced tools as host globals; the runtime decides how those globals appear in the target language.
|
||||
- Put language syntax, output behavior, persistence semantics, and completion-call examples in `getUsageInstructions()`.
|
||||
- Use `getPrimitiveOverrides()` to describe language-native calls for built-in primitives, and `formatCallable()` to describe language-native calls for tools and child agents.
|
||||
- Implement `inspectGlobals()` on sessions when `contextPolicy` should show live runtime state for non-JavaScript runtimes; otherwise AxAgent will not run JavaScript fallback inspection snippets.
|
||||
|
||||
## RLM Test Harness
|
||||
|
||||
Use `agent.test(code, contextFieldValues?, options?)` when the user wants to validate runtime snippets against the actual AxAgent runtime environment without running the full actor/responder loop. With `AxJSRuntime`, those snippets are JavaScript.
|
||||
|
||||
```typescript
|
||||
import { AxJSRuntime, agent, f, fn } from '@ax-llm/ax';
|
||||
|
||||
const runtime = new AxJSRuntime();
|
||||
|
||||
const tools = [
|
||||
fn('sum')
|
||||
.description('Return the sum of the provided numeric values')
|
||||
.namespace('math')
|
||||
.arg('values', f.number('Value to add').array())
|
||||
.returns(f.number('Sum of all values'))
|
||||
.handler(async ({ values }) =>
|
||||
values.reduce((total, value) => total + value, 0)
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
|
||||
const toolHarness = agent('query:string -> answer:string', {
|
||||
contextFields: [],
|
||||
runtime,
|
||||
functions: tools,
|
||||
contextPolicy: { preset: 'checkpointed', budget: 'balanced' },
|
||||
});
|
||||
|
||||
const toolOutput = await toolHarness.test(
|
||||
'console.log(await math.sum({ values: [3, 5, 8] }))'
|
||||
);
|
||||
|
||||
console.log(toolOutput);
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `test(...)` creates a fresh runtime session per call.
|
||||
- Context-field snippets run in the context/distiller runtime and expose `inputs` plus non-colliding top-level aliases for configured `contextFields`.
|
||||
- Tool snippets should use an agent with no `contextFields`, or test the executor stage directly, so namespaced functions, child agents, and `llmQuery(...)` are in scope.
|
||||
- In `AxJSRuntime`, do not rely on calling `inspectRuntime()` from inside `test(...)` snippets yet; prefer checking runtime globals directly inside the snippet.
|
||||
- It returns the formatted runtime output string.
|
||||
- It throws on runtime failures instead of returning LLM-style error strings.
|
||||
- Do not call `final(...)` or `askClarification(...)` inside `test(...)` snippets.
|
||||
- Pass only `contextFields` values to `test(...)`; it is not a general way to inject arbitrary non-context inputs.
|
||||
- If the snippet uses `llmQuery(...)`, provide an AI service through the agent config or `options.ai`.
|
||||
|
||||
## `llmQuery(...)` Rules
|
||||
|
||||
Available forms:
|
||||
|
||||
- `await llmQuery(query, context?)`
|
||||
- `await llmQuery({ query, context? })`
|
||||
- `await llmQuery([{ query, context }, ...])`
|
||||
|
||||
Rules:
|
||||
|
||||
- `llmQuery(...)` forwards only the explicit `context` argument.
|
||||
- Parent inputs, runtime variables, tool results, and discovered docs are not automatically available to `llmQuery(...)`; include any needed facts in `context`.
|
||||
- `llmQuery(...)` is a direct semantic helper backed by an AxGen sub-query. It does not create a child AxAgent, does not run an actor runtime session, and does not have access to tools or discovery.
|
||||
- Use batched `llmQuery([...])` only for independent semantic questions. Use serial calls when later work depends on earlier results.
|
||||
- Pass compact named object context instead of huge raw parent payloads.
|
||||
- Do not assume anything other than the returned string comes back from `llmQuery(...)`.
|
||||
- `maxSubAgentCalls` is a shared budget for `llmQuery(...)` sub-queries across the top-level run.
|
||||
- Single-call `llmQuery(...)` may return `[ERROR] ...` on non-abort failures.
|
||||
- Batched `llmQuery([...])` returns per-item `[ERROR] ...`.
|
||||
- If a result starts with `[ERROR]`, inspect or branch on it instead of assuming success.
|
||||
|
||||
Minimal example:
|
||||
|
||||
```javascript
|
||||
const summary = await llmQuery('Summarize this incident', inputs.context);
|
||||
if (summary.startsWith('[ERROR]')) {
|
||||
console.log(summary);
|
||||
} else {
|
||||
console.log(summary);
|
||||
}
|
||||
```
|
||||
|
||||
Parallel semantic review example:
|
||||
|
||||
```javascript
|
||||
const narrowedIncidents = incidents.map((incident) => ({
|
||||
id: incident.id,
|
||||
timeline: incident.timeline,
|
||||
notes: incident.notes.slice(0, 1200),
|
||||
}));
|
||||
|
||||
const [severityReview, followupReview] = await llmQuery([
|
||||
{
|
||||
query:
|
||||
'Use discovery and available tools to review severity policy alignment. Return compact findings.',
|
||||
context: {
|
||||
incidents: narrowedIncidents,
|
||||
rubric: 'severity-policy',
|
||||
},
|
||||
},
|
||||
{
|
||||
query:
|
||||
'Use discovery and available tools to review postmortem and follow-up obligations. Return compact findings.',
|
||||
context: {
|
||||
incidents: narrowedIncidents,
|
||||
rubric: 'postmortem-followup',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const merged = await llmQuery(
|
||||
'Merge these delegated reviews into one manager-ready summary with next steps.',
|
||||
{
|
||||
severityReview,
|
||||
followupReview,
|
||||
audience: inputs.audience,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Delegation decision guide:
|
||||
|
||||
- **JS-only**: deterministic logic such as filter, sort, count, regex, or date math -> do it inline.
|
||||
- **Single-shot semantic**: needs LLM reasoning but no tools or multi-step exploration -> single `llmQuery(...)` with narrow context.
|
||||
- **Specialist/tool delegation**: needs its own tools, discovery, runtime, or reusable role -> create a child `agent(...)` and pass it in `functions: [...]`.
|
||||
- **Parallel semantic fan-out**: two or more independent semantic-only subtasks -> batched `llmQuery([...])`.
|
||||
|
||||
Context handling:
|
||||
|
||||
- Always narrow with JS before delegating. Never pass raw `inputs.*`.
|
||||
- Name context keys semantically, e.g. `{ emails: filtered, rubric: 'classify-urgency' }`.
|
||||
- Estimate total sub-query calls before fanning out. `maxSubAgentCalls` is shared across the run.
|
||||
|
||||
Patterns:
|
||||
|
||||
- Fan-Out / Fan-In: JS narrows into categories -> `llmQuery([...])` fans out per category -> JS or one more `llmQuery(...)` merges semantic results.
|
||||
- Pipeline: serial `llmQuery(...)` calls where each depends on the prior result.
|
||||
- Specialist tool use: call child agents or tools via their namespaced function globals, e.g. `await team.writer({ draft })`.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [RLM](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm.ts) - RLM basic
|
||||
- [RLM Long Task](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-long-task.ts) - RLM context policy
|
||||
- [RLM Discovery](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-discovery.ts) - discovery mode, grouped tools, child agents as functions, and semantic `llmQuery(...)`
|
||||
- [RLM Adaptive Replay](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-adaptive-replay.ts) - adaptive replay
|
||||
|
||||
Flagship real-world long-agents (also ported to Python, Go, Rust, Java, and C++ under `src/examples/<lang>/long-agents/`; run with `npm run example -- <lang> <path>`):
|
||||
|
||||
- [Incident Log Forensics](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/incident-log-forensics.ts) - large-context log forensics over `contextFields` (Gemini)
|
||||
- [Codebase Peek Map](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/codebase-peek-map.ts) - Peek-paper context-map orientation over a large repo snapshot
|
||||
- [Data Analyst with Tools](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/data-analyst-with-tools.ts) - large data dictionary in `contextFields` + typed warehouse tools the model queries instead of inlining
|
||||
- [Smart Defaults Agent](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/smart-defaults-agent.ts) - oversized undeclared context auto-promoted runtime-only, with relevance hints and runtime tools
|
||||
- [Self-Improving Lab](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/self-improving-lab.ts) - many-tool agent that runs experiments, grades them with an independent verifier, and distills verified rules into memory
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not write a full multi-step RLM actor program in one turn.
|
||||
- Do not combine `console.log(...)` with `final(...)`.
|
||||
- Do not assume old successful turns stay fully replayed under adaptive/checkpointed/lean policies.
|
||||
- Do not rebuild runtime state just because a prior turn was summarized.
|
||||
- Do not describe `llmQuery(...)` as spawning a tool-using child AxAgent.
|
||||
- Do not assume parent inputs are available to `llmQuery(...)` unless passed in `context`.
|
||||
- Do not ignore `[ERROR] ...` results from `llmQuery(...)`.
|
||||
- Do not grant `AxJSRuntime` permissions unless the user asked for the capability.
|
||||
693
.agents/skills/ax-agent/SKILL.md
Normal file
693
.agents/skills/ax-agent/SKILL.md
Normal file
@@ -0,0 +1,693 @@
|
||||
---
|
||||
name: ax-agent
|
||||
description: This skill helps an LLM generate correct core AxAgent code using @ax-llm/ax. Use when the user asks about agent(), child agents, namespaced functions, discovery mode, clarification, bubbleErrors, host-side final/clarification protocol, or ordinary agent runtime behavior. For MCP clients, native runtime modules, subscriptions, tasks, or authentication use ax-mcp alongside this skill. For RLM/code-runtime work use ax-agent-rlm; for callbacks and telemetry use ax-agent-observability; for recall/memory/skill loading use ax-agent-memory-skills; for agent.optimize(...) use ax-agent-optimize.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxAgent Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill to generate small, correct `AxAgent` code. Prefer modern factory-style APIs and copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||
|
||||
Your job is to choose the smallest correct `AxAgent` shape for the user's needs:
|
||||
|
||||
- If the user wants a normal tool-using assistant, keep the config minimal.
|
||||
- If the user wants long-running code execution, use the `ax-agent-rlm` skill.
|
||||
- If the user wants callbacks, logs, tracing, or usage data, use the `ax-agent-observability` skill.
|
||||
- If the user wants dynamic memory retrieval or skill-guide loading, use the `ax-agent-memory-skills` skill.
|
||||
- If the user wants tuning or eval with `agent.optimize(...)`, use the `ax-agent-optimize` skill.
|
||||
- If the user wants MCP transports, authentication, catalogs, subscriptions,
|
||||
tasks, Apps, or event-driven wake/resume, use the `ax-mcp` skill.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use `agent(...)`, not `new AxAgent(...)`.
|
||||
- Prefer string signatures or `f()` signatures over hand-written signature objects.
|
||||
- Put `ai`, `judgeAI`, and `agentIdentity` on the `agent(...)` config when you want instance defaults or child-agent metadata.
|
||||
- Prefer `fn(...)` for host-side function definitions instead of hand-writing JSON Schema objects.
|
||||
- Prefer namespaced functions such as `utils.search(...)` or `kb.find(...)`.
|
||||
- Pass child agents directly in `functions: [...]`. They land under their `agentIdentity.namespace` (or `utils` if unset), exactly like a `fn()` tool.
|
||||
- If discovery is enabled, call `discover(...)` before using callables whose docs are not already in the prompt.
|
||||
- Use explicit child agents in `functions: [...]` for specialist delegation; do not model that as recursive `llmQuery(...)`.
|
||||
- Add `bubbleErrors` only for fatal infrastructure errors that should abort `.forward()`.
|
||||
|
||||
## Decision Guide
|
||||
|
||||
Map user intent to agent shape before writing code:
|
||||
|
||||
- "Use tools and answer" -> plain `agent(...)` with local functions, no extra observability.
|
||||
- "Need child agents with distinct responsibilities" -> add child agents to the parent's `functions: [...]` list and set each child's `agentIdentity.namespace` when you want a specific runtime call site such as `team.writer(...)`.
|
||||
- "Need tool discovery because names/schemas are not stable" -> enable discovery and generate discovery-first actor code.
|
||||
- "Need certain errors to escape the agent loop" -> add `bubbleErrors` with error classes; those errors propagate through function handlers, actor code, and `llmQuery(...)` sub-queries to `.forward()`.
|
||||
- "Inspect large context with code", "RLM", or "`llmQuery(...)`" -> use `ax-agent-rlm`.
|
||||
- "Need debugging, traces, progress updates, tool-call logs, chat logs, or usage" -> use `ax-agent-observability`.
|
||||
- "Need memories, recall, dynamic skill guides, `discover({ skills })`, or loaded/used tracking" -> use `ax-agent-memory-skills`.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- Use `agent(...)` factory syntax for new code.
|
||||
- If an actor response contains multiple fenced code blocks, the runtime rejects the whole turn without executing any block and asks for one executable program.
|
||||
- Add child agents to the parent's `functions: [...]` list. Each child's `agentIdentity.namespace` (or `utils`, the default) determines the runtime call site, e.g. `await team.writer({...})`.
|
||||
- If discovery is enabled, call `discover(...)` before using callables whose docs are not already in the prompt.
|
||||
- `autoUpgrade` is ON by default: large tool catalogs auto-enable discovery, and oversized undeclared input values are auto-kept runtime-only with a truncated prompt preview. Explicit `functionDiscovery` and declared `contextFields` always win; set `autoUpgrade: false` to opt out.
|
||||
- `directResponse` is ON by default (`'auto'`): when a task needs no user-provided functions, the distiller ends the run with `respond(task, evidence)` and the executor stage is skipped (zero executor model calls). Function-less agents run respond-only every time; agents with functions offer `respond` under a conservative covenant (no live/fresh-state asks, no side effects, nothing a listed function/module domain covers). Set `directResponse: 'off'` to always run the executor.
|
||||
- If a host-side `AxAgentFunction` needs to end the current actor turn, use `extra.protocol.final(...)` or `extra.protocol.askClarification(...)`.
|
||||
- In public `forward()` and `streamingForward()` flows, `askClarification(...)` throws `AxAgentClarificationError`; it does not go through the responder.
|
||||
- When resuming after clarification, prefer `error.getState()` from the thrown `AxAgentClarificationError`, then call `agent.setState(savedState)` before the next `forward(...)`.
|
||||
- Errors listed in `bubbleErrors` bypass actor-loop catch blocks and propagate directly to the caller of `.forward()`.
|
||||
- Child agents receive only the arguments the actor passes. Pass parent fields explicitly via `inputs.<field>` or use `inputUpdateCallback` when many calls need the same value.
|
||||
- Audio input fields are transcribed before agent planner/executor/responder stages by default; internal agent stages receive text transcripts, not base64 audio.
|
||||
|
||||
## Canonical Pattern
|
||||
|
||||
```typescript
|
||||
import { agent, ai, f } from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
});
|
||||
|
||||
const assistant = agent(
|
||||
f()
|
||||
.input('query', f.string())
|
||||
.output('answer', f.string())
|
||||
.build(),
|
||||
{
|
||||
agentIdentity: {
|
||||
name: 'Assistant',
|
||||
description: 'Answers user questions',
|
||||
},
|
||||
contextFields: [],
|
||||
}
|
||||
);
|
||||
|
||||
const result = await assistant.forward(llm, { query: 'What is TypeScript?' });
|
||||
console.log(result.answer);
|
||||
```
|
||||
|
||||
## Audio Inputs And Speech Outputs
|
||||
|
||||
Agents can accept audio inputs and return scripted speech artifacts. The runtime transcribes audio input fields before internal stages run, then synthesizes `:audio` outputs after the final structured response is selected.
|
||||
|
||||
```typescript
|
||||
const voiceAgent = agent(
|
||||
'recording:audio, question:string -> speech:audio, summary:string',
|
||||
{
|
||||
agentIdentity: {
|
||||
name: 'Voice Assistant',
|
||||
description: 'Answers spoken requests',
|
||||
},
|
||||
contextFields: [],
|
||||
}
|
||||
);
|
||||
|
||||
const result = await voiceAgent.forward(
|
||||
llm,
|
||||
{
|
||||
recording: { data: base64Wav, format: 'wav' },
|
||||
question: 'What should I do next?',
|
||||
},
|
||||
{
|
||||
speech: {
|
||||
transcribe: { model: 'gpt-4o-mini-transcribe' },
|
||||
speak: { voice: 'alloy', format: 'mp3' },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log(result.summary);
|
||||
console.log(result.speech.data);
|
||||
```
|
||||
|
||||
Use direct `ax(...)` or `.chat()` if the model should receive native audio instead of a transcript-first agent pipeline.
|
||||
|
||||
## Child Agents As Tools
|
||||
|
||||
Child agents are passed in the parent's `functions` list. There is no separate `agents` option for new code. Each child agent's `agentIdentity.namespace` (or `utils`, the default) determines where it lands in the actor runtime. With `AxJSRuntime`, that produces JavaScript call sites such as `team.writer(...)`:
|
||||
|
||||
```typescript
|
||||
const writer = agent('draft:string -> revision:string', {
|
||||
agentIdentity: {
|
||||
name: 'Writer',
|
||||
description: 'Polishes drafts',
|
||||
namespace: 'team',
|
||||
},
|
||||
contextFields: [],
|
||||
});
|
||||
|
||||
const coordinator = agent('query:string -> answer:string', {
|
||||
functions: [writer],
|
||||
contextFields: [],
|
||||
});
|
||||
```
|
||||
|
||||
Generated runtime call:
|
||||
|
||||
```javascript
|
||||
const result = await team.writer({ draft: '...' });
|
||||
```
|
||||
|
||||
Without `agentIdentity.namespace`, the child lands under `utils.<name>` like any other tool:
|
||||
|
||||
```javascript
|
||||
const result = await utils.writer({ draft: '...' });
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Add child agents to `functions: [...]`, the same array as `fn(...)` tools.
|
||||
- Set `agentIdentity.namespace` on the child to control its runtime call site.
|
||||
- `onFunctionCall` observers receive `kind: 'internal'` for agent-derived calls and `kind: 'external'` for user-registered tools.
|
||||
|
||||
### Reserved namespace names
|
||||
|
||||
The agent runtime injects a fixed set of globals into the runtime session. These names cannot be used as `agentIdentity.namespace` values or as agent-function namespaces.
|
||||
|
||||
```text
|
||||
inputs
|
||||
llmQuery
|
||||
final
|
||||
askClarification
|
||||
reportSuccess
|
||||
reportFailure
|
||||
inspectRuntime
|
||||
discover
|
||||
recall
|
||||
```
|
||||
|
||||
Pick any other lowercase identifier such as `utils`, `kb`, `tools`, `team`, or `db`.
|
||||
|
||||
## Tool Functions And Namespaces
|
||||
|
||||
```typescript
|
||||
import { agent, f, fn } from '@ax-llm/ax';
|
||||
|
||||
const findSnippets = fn('findSnippets')
|
||||
.description('Find handbook snippets by topic')
|
||||
.namespace('kb')
|
||||
.arg('topic', f.string('Topic keyword'))
|
||||
.returns(f.string('Matching snippet').array())
|
||||
.example({
|
||||
title: 'Find severity guidance',
|
||||
code: 'await kb.findSnippets({ topic: "severity" });',
|
||||
})
|
||||
.handler(async ({ topic }) => [])
|
||||
.build();
|
||||
|
||||
const analyst = agent('query:string -> answer:string', {
|
||||
functions: [findSnippets],
|
||||
contextFields: [],
|
||||
});
|
||||
```
|
||||
|
||||
Generated runtime call:
|
||||
|
||||
```javascript
|
||||
const snippets = await kb.findSnippets({ topic: 'severity' });
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Prefer namespaced functions.
|
||||
- Default function namespace is `utils` when no namespace is set.
|
||||
- With `AxJSRuntime`, use the runtime call shape `await <namespace>.<name>({...})`. Custom runtimes should expose equivalent namespaced calls through their own `formatCallable()` guidance.
|
||||
- `.arg()` and `.returns()` can use Ax field helpers or any Standard Schema v1 validator directly.
|
||||
|
||||
## Grouped Function Modules
|
||||
|
||||
For discovery mode, group functions into modules using the `AxAgentFunctionGroup` shape when you want a clean namespace tree such as `kb.find(...)` or `metrics.score(...)` without setting `namespace` on every individual `fn(...)`:
|
||||
|
||||
```typescript
|
||||
const parent = agent('query:string -> answer:string', {
|
||||
functions: [
|
||||
{
|
||||
namespace: 'kb',
|
||||
title: 'Knowledge Base',
|
||||
selectionCriteria: 'Use for handbook and documentation lookups.',
|
||||
description: 'Knowledge base lookups',
|
||||
functions: [findSnippetsFn, searchPagesFn],
|
||||
},
|
||||
{
|
||||
namespace: 'workflow',
|
||||
title: 'Workflow Controls',
|
||||
description: 'Small control functions the actor should always see',
|
||||
alwaysInclude: true,
|
||||
functions: [completeFn],
|
||||
},
|
||||
],
|
||||
functionDiscovery: true,
|
||||
contextFields: [],
|
||||
});
|
||||
```
|
||||
|
||||
Attach MCP/UCP clients through the native execution context. Ax initializes them once, exposes `mcp.<namespace>` / `ucp.<namespace>` runtime modules, and propagates them through actor stages, `llmQuery`, RLM, and child agents:
|
||||
|
||||
Use `ax-mcp` for constructing those clients, transport/authentication policy,
|
||||
server-initiated handlers, resource subscriptions, task continuations, and
|
||||
recording/replay. Keep this section focused on Agent attachment and discovery.
|
||||
|
||||
```typescript
|
||||
const parent = agent('query:string -> answer:string', {
|
||||
mcp: [memoryClient, searchClient],
|
||||
mcpInheritance: 'all',
|
||||
functionDiscovery: true,
|
||||
contextFields: [],
|
||||
});
|
||||
|
||||
// A child can restrict inheritance to selected namespaces or `none`.
|
||||
await parent.forward(llm, { query }, { mcpInheritance: ['memory'] });
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- A group is `{ namespace, title, description, functions: [...] }`.
|
||||
- `selectionCriteria` is optional but useful in discovery mode; it tells the actor when to choose that module.
|
||||
- The group's `namespace`, `title`, `selectionCriteria`, and `description` show up in `discover(...)` module docs.
|
||||
- `relevanceRanking` (default ON — set `false` to opt out): a deterministic local ranker that injects an advisory `### Likely Relevant` shortlist into the executor turn (dynamic, non-cached field — the cached prompt stays byte-stable). Enabled by default after its A/B gate passed on both small and frontier models and implemented in the generated language ports through AxIR Core. Details in `ax-agent-memory-skills`; outcomes observable via the `relevance_ranking` context event (`ax-agent-observability`).
|
||||
- Add `alwaysInclude: true` to a group when discovery mode is on but the actor should always see that group's full callable definitions inline in the prompt.
|
||||
- Keep `functions: [...]` either flat or grouped. Runtime validation rejects mixed plain function entries and group objects.
|
||||
- In flat mode, pass `fn(...)` tools and child agents directly.
|
||||
- In grouped mode, put callable entries inside groups. To expose a child agent inside a group, use `childAgent.getFunction()`.
|
||||
- Do not place MCP clients in `functions`; use `mcp` so tasks, resources, subscriptions, elicitation, sampling, authorization, cancellation, and protocol metadata remain available.
|
||||
- To wake an Agent from a resource subscription, use `AxMCPEventSource` and an
|
||||
explicit authenticated `wake` route. MCP sessions are not tenant identity;
|
||||
supply identity from the application's authenticated token mapping.
|
||||
- An endpoint does not imply a resource URI. Inspect `client.inspectCatalog()`
|
||||
and choose an explicit `resourceSubscriptions` policy. Omission means none;
|
||||
`'all'` selects all discovered concrete resources; selectors can use names,
|
||||
descriptions, MIME types, URIs, and annotations. Templates are not expanded.
|
||||
- Map the event with a signature-aware `.wakeInput(...)` plan, or reuse an
|
||||
`eventInput()` plan. Callback `mapInput` is still signature-validated and
|
||||
cannot inject undeclared Agent fields. Use multiple matching routes to wake
|
||||
multiple Agents with independent state, authorization, retries, and runs.
|
||||
- To wake from a UCP lifecycle webhook, use `AxUCPWebhookEventSource` and map
|
||||
verified profile/account state to Ax tenant identity after request
|
||||
verification. Never derive tenant identity from the order payload.
|
||||
|
||||
## Host-Side Completion From Functions
|
||||
|
||||
Use this pattern when the actor should call a namespaced function, but the host-side function implementation should decide to end the turn:
|
||||
|
||||
```typescript
|
||||
import { f, fn } from '@ax-llm/ax';
|
||||
|
||||
const finishReply = fn('finishReply')
|
||||
.description('Complete the actor turn with the final reply text')
|
||||
.namespace('workflow')
|
||||
.arg('reply', f.string('Final reply text'))
|
||||
.returns(f.string('Final reply text'))
|
||||
.handler(async ({ reply }, extra) => {
|
||||
extra?.protocol?.final(reply);
|
||||
return reply;
|
||||
})
|
||||
.build();
|
||||
|
||||
const askForOrderId = fn('askForOrderId')
|
||||
.description('Complete the actor turn by requesting clarification')
|
||||
.namespace('workflow')
|
||||
.arg('question', f.string('Clarification question'))
|
||||
.returns(f.string('Clarification question'))
|
||||
.handler(async ({ question }, extra) => {
|
||||
extra?.protocol?.askClarification(question);
|
||||
return question;
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `extra.protocol` is only available when the function call comes from an active AxAgent actor runtime session.
|
||||
- Use `extra.protocol.final(...)`, `extra.protocol.askClarification(...)`, or `extra.protocol.guideAgent(...)` only inside host-side function handlers.
|
||||
- Inside actor-authored runtime code, use the runtime globals `final(...)` and `askClarification(...)` with the syntax documented by the active runtime.
|
||||
- `extra.protocol.guideAgent(...)` is handler-only internal control flow. It stops the current actor turn and appends trusted guidance to `guidanceLog` for the next iteration.
|
||||
- `askClarification(...)` accepts either a simple string or a structured object with `question` plus optional UI hints such as `type: 'date' | 'number' | 'single_choice' | 'multiple_choice'` and `choices`.
|
||||
|
||||
## Clarification And Resume State
|
||||
|
||||
Use this pattern when the actor should pause for user input and continue later from the same runtime state.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AxAgentClarificationError,
|
||||
AxJSRuntime,
|
||||
agent,
|
||||
ai,
|
||||
} from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
});
|
||||
|
||||
const tripAgent = agent('request:string, answer?:string -> reply:string', {
|
||||
contextFields: [],
|
||||
runtime: new AxJSRuntime(),
|
||||
});
|
||||
|
||||
let savedState = tripAgent.getState();
|
||||
|
||||
try {
|
||||
await tripAgent.forward(llm, {
|
||||
request: 'Plan a Lisbon trip',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxAgentClarificationError) {
|
||||
console.log(error.question);
|
||||
savedState = error.getState();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (savedState) {
|
||||
tripAgent.setState(savedState);
|
||||
const resumed = await tripAgent.forward(llm, {
|
||||
request: 'Plan a Lisbon trip',
|
||||
answer: 'June 1-5',
|
||||
});
|
||||
console.log(resumed.reply);
|
||||
}
|
||||
```
|
||||
|
||||
Public flow rules:
|
||||
|
||||
- `forward()` and `streamingForward()` throw `AxAgentClarificationError` when the actor calls `askClarification(...)`.
|
||||
- Successful `final(...)` completions always continue through the responder in public flows.
|
||||
- `AxAgentClarificationError.question` is the user-facing question text.
|
||||
- `AxAgentClarificationError.clarification` is the normalized structured payload.
|
||||
- `AxAgentClarificationError.getState()` returns the saved continuation state captured at throw time.
|
||||
- `agent.getState()` and `agent.setState(...)` export or restore continuation state on the agent instance.
|
||||
- `test(...)` is different: it returns structured completion payloads for harness/debug use instead of throwing clarification exceptions.
|
||||
|
||||
Structured clarification payloads:
|
||||
|
||||
- String shorthand is allowed: `askClarification("What dates should I use?")`.
|
||||
- Structured form is preferred for richer chat UIs:
|
||||
|
||||
```javascript
|
||||
askClarification({
|
||||
question: 'Which route should I use?',
|
||||
type: 'single_choice',
|
||||
choices: ['Fastest', 'Scenic'],
|
||||
});
|
||||
```
|
||||
|
||||
- Supported `type` values are `text`, `number`, `date`, `single_choice`, and `multiple_choice`.
|
||||
- `single_choice` payloads with missing, empty, or malformed `choices` are downgraded to a plain clarification question instead of failing the turn.
|
||||
- `multiple_choice` payloads must include at least two valid choices; otherwise the actor turn fails with a corrective runtime error.
|
||||
- Choice entries may be strings or `{ label, value? }` objects.
|
||||
- Invalid clarification payloads such as a missing `question` are actor-turn runtime errors, not successful clarification completions.
|
||||
|
||||
State notes:
|
||||
|
||||
- `runtimeBindings` restores execution state; `runtimeEntries`, `actionLogEntries`, and `checkpointState` restore prompt context.
|
||||
- Resume does not create a fake rehydration action-log turn; provenance still points to the original actor code that set the value.
|
||||
- Only serializable/structured-clone-friendly values are guaranteed to round-trip through `getState()` / `setState(...)`.
|
||||
- Reserved runtime globals such as `inputs`, tools, and protocol helpers are rebuilt fresh and are not part of saved state.
|
||||
- Treat one agent instance as conversation-scoped when using `setState(...)`; do not share one mutable resumed instance across unrelated concurrent conversations.
|
||||
|
||||
## Bubble Errors
|
||||
|
||||
Use `bubbleErrors` when certain exceptions thrown inside function handlers or `llmQuery(...)` sub-query calls should propagate all the way out to `.forward()` instead of being caught by the actor loop and returned as `[ERROR]` strings.
|
||||
|
||||
```typescript
|
||||
import { agent, f, fn } from '@ax-llm/ax';
|
||||
|
||||
class DatabaseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'DatabaseError';
|
||||
}
|
||||
}
|
||||
|
||||
const dbTool = fn('queryUsers')
|
||||
.description('Query the user database')
|
||||
.namespace('db')
|
||||
.arg('filter', f.string('Filter expression'))
|
||||
.returns(f.string('JSON result'))
|
||||
.handler(async ({ filter }) => {
|
||||
if (!isConnected()) throw new DatabaseError('DB connection refused');
|
||||
return JSON.stringify(await db.query(filter));
|
||||
})
|
||||
.build();
|
||||
|
||||
const myAgent = agent('query:string -> answer:string', {
|
||||
contextFields: [],
|
||||
functions: [dbTool],
|
||||
bubbleErrors: [DatabaseError],
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `bubbleErrors` takes an array of Error constructor classes, checked via `instanceof`.
|
||||
- A matching error thrown inside a function handler, during actor code execution, or inside an `llmQuery(...)` sub-query propagates immediately to `.forward()`.
|
||||
- Use `bubbleErrors` for fatal infrastructure errors such as DB down, auth failure, or quota exceeded.
|
||||
- Do not use `bubbleErrors` for expected recoverable errors; let those return as `[ERROR] ...` strings so the actor can handle them.
|
||||
- `AxAgentClarificationError` and `AxAIServiceAbortedError` always bubble up unconditionally.
|
||||
|
||||
## Unified Final Signal
|
||||
|
||||
There are two ways to end a successful run through the responder:
|
||||
|
||||
1. In actor JS code, call `final(message)` when no extra context object is needed, or `final(task, context)` when you gathered evidence.
|
||||
2. In function handlers, use `extra.protocol.final(...)` with the same one-arg or two-arg forms.
|
||||
|
||||
Rules:
|
||||
|
||||
- Use `final(message)` when the actor already knows the answer and no extra context object is needed.
|
||||
- Use `final(task, context)` when context was gathered and needs synthesis into output fields.
|
||||
- In function handlers, use `extra.protocol.final(...)` instead of a separate respond API.
|
||||
- The responder still runs for both successful `final(...)` forms.
|
||||
- Use `askClarification(...)` when the user must provide more information to continue.
|
||||
|
||||
## Discovery Mode
|
||||
|
||||
Enable discovery mode when you want the actor to discover modules and fetch callable definitions on demand:
|
||||
|
||||
```typescript
|
||||
const analyst = agent('context:string, query:string -> answer:string', {
|
||||
agentIdentity: {
|
||||
name: 'Analyst',
|
||||
description: 'Analyzes long context',
|
||||
namespace: 'team',
|
||||
},
|
||||
contextFields: ['context'],
|
||||
functions: [writer, ...tools],
|
||||
functionDiscovery: true,
|
||||
});
|
||||
```
|
||||
|
||||
Discovery API:
|
||||
|
||||
- `await discover(item: string): void`
|
||||
- `await discover(items: string[]): void`
|
||||
- `await discover({ tools?: string | string[], skills?: string | string[] }): void` when `onSkillsSearch` is configured
|
||||
|
||||
Discovery returns `void`; fetched docs render in the next executor prompt.
|
||||
|
||||
Rules:
|
||||
|
||||
- `discover('kb')` loads a module callable list when `kb` is a discoverable module.
|
||||
- `discover('kb.findSnippets')` loads a full callable definition.
|
||||
- `discover('lookup')` resolves as `utils.lookup`.
|
||||
- `discover({ tools: ['kb'], skills: ['release-checklist'] })` loads tool docs and skill bodies in one turn.
|
||||
- Call one batched `discover(...)` with every module, callable, and skill you need.
|
||||
- Do not split discovery into separate calls or wrap discovery in `Promise.all(...)`.
|
||||
- Read the next prompt's "Discovered Tool Docs" and "Loaded Skills" sections.
|
||||
- If a guessed call fails, stop guessing nearby names. Run `discover(...)` for that module or function and call only the exact discovered qualified name.
|
||||
|
||||
## Threading Parent Fields Into Child Agents
|
||||
|
||||
If a child agent requires a parent field such as `audience`, declare it on the child's signature and pass it explicitly when calling the child from the actor:
|
||||
|
||||
```typescript
|
||||
const writingCoach = agent('draft:string, audience:string -> revision:string', {
|
||||
agentIdentity: {
|
||||
name: 'Writing Coach',
|
||||
description: 'Polishes summaries for a target audience',
|
||||
namespace: 'team',
|
||||
},
|
||||
contextFields: [],
|
||||
});
|
||||
|
||||
const analyst = agent('context:string, audience:string, query:string -> answer:string', {
|
||||
functions: [writingCoach],
|
||||
contextFields: ['context'],
|
||||
});
|
||||
```
|
||||
|
||||
Generated runtime call:
|
||||
|
||||
```javascript
|
||||
const polished = await team.writingCoach({
|
||||
draft: summary,
|
||||
audience: inputs.audience,
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Pass parent fields explicitly via the call site.
|
||||
- If many children need the same field on every call, use `inputUpdateCallback` to inject the value before each executor turn.
|
||||
- Do not assume auto-propagation; child agents receive only the args the actor passes.
|
||||
|
||||
## Core API Reference
|
||||
|
||||
Factory shape:
|
||||
|
||||
```typescript
|
||||
agent(signature, {
|
||||
ai,
|
||||
judgeAI,
|
||||
agentIdentity,
|
||||
contextFields,
|
||||
functions,
|
||||
functionDiscovery,
|
||||
autoUpgrade,
|
||||
playbook,
|
||||
citations,
|
||||
...agentOptions,
|
||||
});
|
||||
```
|
||||
|
||||
- `ai` is an optional default service for the agent instance; `.forward(ai, ...)` can still pass the runtime service.
|
||||
- `judgeAI` is the optional default judge/teacher service used by optimize flows.
|
||||
- `agentIdentity` controls the user-facing agent identity and child-agent function metadata.
|
||||
|
||||
```typescript
|
||||
agentIdentity?: {
|
||||
name: string;
|
||||
description: string;
|
||||
namespace?: string;
|
||||
}
|
||||
```
|
||||
|
||||
- `name` is normalized to camelCase for child-agent function names.
|
||||
- `name` and `description` are included in the actor and responder prompts as the user-facing agent identity.
|
||||
- `namespace` changes the child-agent module from default `utils` to a custom module such as `team`.
|
||||
|
||||
Each `contextFields` entry is either a plain field name string or an object controlling how much of the value is inlined into the distiller prompt:
|
||||
|
||||
- `{ field, promptMaxChars: N }`: inline only when the serialized value is at most `N` chars; otherwise omit it from the prompt and keep it runtime-only.
|
||||
- `{ field, keepInPromptChars: N, reverseTruncate?: boolean }`: always inline a truncated string excerpt; `reverseTruncate: true` keeps the last `N` chars.
|
||||
|
||||
Use `promptMaxChars` when partial data is worse than no data. Use `keepInPromptChars` when a prefix or suffix alone is useful. The two options are mutually exclusive on one field.
|
||||
|
||||
### Auto-upgrade defaults
|
||||
|
||||
`autoUpgrade` is ON by default: the agent applies both knobs above on the user's behalf based on character counts, so forgetting them no longer floods prompts.
|
||||
|
||||
- Function discovery: when `functionDiscovery` is left unset and the estimated inline docs of discoverable functions exceed ~10k chars, discovery is enabled automatically. An explicit `functionDiscovery: true | false` always wins.
|
||||
- Context fields: per run, an undeclared input value whose serialized size exceeds 8k chars is kept runtime-only like a declared context field — the prompt gets a 1,200-char truncated preview plus a `contextMetadata` entry, while the full value stays addressable as `inputs.<field>` in the code runtime (the responder stage gets the same preview). Fields declared in `contextFields` keep their declared config.
|
||||
|
||||
```typescript
|
||||
autoUpgrade?: boolean | {
|
||||
functionDiscovery?: boolean | { aboveFunctionDocChars?: number }; // default 10_000
|
||||
contextFields?: boolean | { promoteAboveChars?: number; previewChars?: number }; // 8_000 / 1_200
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Set `autoUpgrade: false` (or disable one side) to restore fully manual behavior.
|
||||
- Values in required non-string fields (arrays, objects, numbers, media) are never auto-promoted — declare those in `contextFields` explicitly when they can be large.
|
||||
- Each promotion emits a `field_auto_promoted` context event (`onContextEvent`) with the field name, original size, and preview size; use it to observe what was kept out of the prompt.
|
||||
|
||||
### Learning And Citations
|
||||
|
||||
These construction-time options are portable across TypeScript and the generated
|
||||
Python, Java, C++, Go, and Rust packages (both default off):
|
||||
|
||||
- `playbook`: attach an ACE playbook at construction. `learn` is on by default — after each run that produced failure signals (error turns, dead-ends, failing tool calls) one bounded update curates durable avoidance rules that ride the next run's actor prompt; zero LLM cost on clean runs. TypeScript seeds a prior session with `playbook: { playbook: snapshot }`; generated packages accept their full `{ playbook, artifact }` snapshot under `seed`. Persist via `onUpdate`, read the live handle with `getPlaybook()` (or the language-shaped equivalent), gate with `learn: { minSignals, dedupe }`, or disable with `learn: false`. To grow the same playbook from a task set with a held-out verify gate, use the agent-bound playbook evolve method — see `ax-playbook`.
|
||||
- `citations`: add an optional `evidenceCitations: string[]` responder output listing which evidence entries (top-level keys of the curated evidence, plus memory ids) the answer relied on. Validated in-pipeline — the model cannot cite evidence it never collected (existence, not entailment). Pass `true`, or `{ field?, surface?: 'output' | 'hidden', includeMemoryIds?, onCitations? }`.
|
||||
|
||||
Stage guidance is portable too. `setInstruction` replaces the stage-owned actor
|
||||
instruction and `addActorInstruction` appends an additive rule. Both are real
|
||||
optimization components; rebuilding the split programs no longer discards them.
|
||||
Generated packages use their normal casing conventions (`set_instruction` in
|
||||
Python/C++/Rust and `SetInstruction` in Go).
|
||||
|
||||
The generated-language observer and evolve spellings are:
|
||||
|
||||
| Language | Citations observer | Verified agent playbook evolve |
|
||||
|---|---|---|
|
||||
| Python | `citations.onCitations` | `agent.playbook().evolve(dataset, options)` |
|
||||
| Java | `citations.onCitations` (`Consumer`) | `agent.playbook(null).evolve(dataset, options)` |
|
||||
| C++ | `set_citations_observer(...)` | `agent.get_playbook()->evolve(dataset, options)` |
|
||||
| Go | `citations.onCitations` (`func([]Value)`) | `agent.GetPlaybook().EvolveAgent(ctx, dataset, options)` |
|
||||
| Rust | `set_citations_observer(...)` | `playbook.evolve_agent(&mut agent, client, dataset, options)` |
|
||||
|
||||
C++ and Rust use `set_playbook_observer(...)` for construction-time learning
|
||||
updates; Python, Java, and Go accept the `playbook.onUpdate` callback in their
|
||||
native configuration map.
|
||||
|
||||
## Public Surface
|
||||
|
||||
Use these method groups as the compact AxAgent surface map:
|
||||
|
||||
- Running: `forward(ai, values, options?)` and `streamingForward(ai, values, options?)`.
|
||||
- Forward-time agent options: `skills`, `onUsedMemories`, and `onUsedSkills`; use `ax-agent-memory-skills` for details.
|
||||
- State and control: `getState()`, `setState(state?)`, `getContextMap()`, `setContextMap(map?)`, `stop()`, `getSignature()`, `setSignature(signature)`, `getFunction()`, `getId()`, and `setId(id)`. Context-map evolve policy lives on `AxAgentContextMap` (`infiniteEvolve`, `evolveSteps`, `maxChars`), not on the agent config. See [`src/examples/rlm-context-map-live.ts`](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-context-map-live.ts) for provider-backed persistence and finite-evolve usage.
|
||||
- Observability: `getChatLog()`, `getUsage()`, `getStagedUsage()`, `resetUsage()`, and `getTraces()`; use `ax-agent-observability` for details.
|
||||
- Demos and tuning: `setDemos(...)`, `namedPrograms()`, `namedProgramInstances()`, `optimize(...)`, `applyOptimization(...)`, `getOptimizableComponents()`, and `applyOptimizedComponents(...)`; use `ax-agent-optimize` for tuning details.
|
||||
- Learning: `playbook()` returns an agent-aware playbook handle (`update(...)`, `render()`, state/load methods, and verified dataset evolution); `getPlaybook()` reads the current handle. Generated packages expose the same behavior with language-shaped method names. Use `ax-playbook` for details.
|
||||
|
||||
Rules:
|
||||
|
||||
- `getFunction()` requires `agentIdentity` because the agent needs function metadata when used as a child tool.
|
||||
- Prefer `.forward(...)` for normal runs and `.streamingForward(...)` only when the caller needs streamed responder output.
|
||||
- `setSignature(...)` must preserve configured `contextFields`; it throws if a configured context field is missing from the new signature.
|
||||
- Treat low-level optimization component methods as advanced hooks; normal examples should use `agent.optimize(...)` and `agent.applyOptimization(...)`.
|
||||
|
||||
## Tuning Hand-off
|
||||
|
||||
When the user wants `agent.optimize(...)`, judge configuration, eval datasets, saved optimization artifacts, or optimization guidance, use `ax-agent-optimize`.
|
||||
|
||||
Keep this skill focused on building and running agents. For tuning work:
|
||||
|
||||
- use eval-safe tools
|
||||
- treat `judgeOptions` as part of the optimize workflow
|
||||
- choose an objective `metric` when scoring is mechanical; use the built-in judge only when run quality needs qualitative review
|
||||
- keep runtime authoring guidance here and optimization guidance in `ax-agent-optimize`
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [Agent](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/agent.ts) - basic agent
|
||||
- [Functions](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/function.ts) - function validation
|
||||
- [Food Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/food-search.ts) - API tools
|
||||
- [Smart Home](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/smart-home.ts) - state management
|
||||
- [Customer Support](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/customer-support.ts) - classification agent
|
||||
- [Abort Patterns](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/abort-patterns.ts) - abort handling
|
||||
- [Smart Defaults Agent](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/long-agents/smart-defaults-agent.ts) - auto-upgrade context promotion, relevance hints, and runtime tools
|
||||
- [Portable Playbook Evolve](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/python/optimization/agent-playbook-evolve.py) - Python construction-time learning, validated citations, stage guidance, and verified task-set evolution (parallel Java/C++/Go/Rust examples live beside it)
|
||||
|
||||
RLM examples are listed in `ax-agent-rlm`. Memory/skills examples are listed in `ax-agent-memory-skills`.
|
||||
|
||||
## Event-Driven Agents
|
||||
|
||||
`AxEventRuntime` can wake or resume an Agent while preserving its logical
|
||||
state. Use `createProgram(instance)` for multi-tenant Agents; one mutable Agent
|
||||
object must not serve multiple instance keys concurrently. Clarification and
|
||||
remote task completion are represented as owned continuations, not synthetic
|
||||
user turns.
|
||||
|
||||
Declare the Agent signature on
|
||||
`eventTarget('id').createProgram(signature, factory)` and map event values with
|
||||
`eventPath`. The runtime verifies every created Agent against that signature
|
||||
before invoking it. Fan-out uses multiple matching routes so each Agent keeps
|
||||
its own authorization, instance serialization, retry policy, and run record.
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not use `new AxAgent(...)` for new code unless explicitly required.
|
||||
- Do not assume child agents are always under `agents.*`.
|
||||
- Do not guess function names in discovery mode.
|
||||
- Do not write a full multi-step RLM actor program in one turn; use `ax-agent-rlm`.
|
||||
- Do not combine `console.log(...)` with `final(...)`.
|
||||
- Do not add `bubbleErrors` for ordinary recoverable tool errors.
|
||||
- Do not call `discover()` from the distiller or responder stages.
|
||||
- Do not assign or inspect the return value of `await discover(...)`; read the next prompt instead.
|
||||
- Do not loop `discover()` calls or wrap them in `Promise.all`.
|
||||
497
.agents/skills/ax-ai/SKILL.md
Normal file
497
.agents/skills/ax-ai/SKILL.md
Normal file
@@ -0,0 +1,497 @@
|
||||
---
|
||||
name: ax-ai
|
||||
description: This skill helps an LLM generate correct AI provider setup and configuration code using @ax-llm/ax. Use when the user asks about ai(), providers, models, routing, adaptive balancing, presets, embeddings, batch audio with ai.transcribe() or ai.speak(), extended thinking, context caching, or mentions OpenAI/Anthropic/Google/Azure/DeepSeek/Mistral/Cohere/Reka/Grok with @ax-llm/ax.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AI Provider Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill to generate AI provider setup, configuration, and chat code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
```typescript
|
||||
import { ai } from '@ax-llm/ax';
|
||||
|
||||
const openai = ai({ name: 'openai', apiKey: 'sk-...' });
|
||||
const claude = ai({ name: 'anthropic', apiKey: 'sk-ant-...' });
|
||||
const gemini = ai({ name: 'google-gemini', apiKey: 'AIza...' });
|
||||
const azure = ai({ name: 'azure-openai', apiKey: 'your-key', resourceName: 'your-resource', deploymentName: 'gpt-5-4-mini' });
|
||||
const deepseek = ai({ name: 'deepseek', apiKey: 'sk-...' });
|
||||
const mistral = ai({ name: 'mistral', apiKey: 'your-key' });
|
||||
const cohere = ai({ name: 'cohere', apiKey: 'your-key' });
|
||||
const custom = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.PROVIDER_API_KEY,
|
||||
apiURL: 'https://example.com/v1',
|
||||
config: { model: 'provider/model-name' },
|
||||
});
|
||||
const reka = ai({ name: 'reka', apiKey: 'your-key' });
|
||||
const grok = ai({ name: 'grok', apiKey: 'your-key' });
|
||||
const compatible = ai({ name: 'openai', apiKey: 'key', apiURL: 'https://api.example.com/v1', config: { model: 'provider/model' } });
|
||||
```
|
||||
|
||||
<!-- axir-nonportable:start webllm -->
|
||||
WebLLM is browser-only and requires a host-created WebLLM engine. The host
|
||||
loads or reloads models with WebLLM APIs such as `CreateMLCEngine(...)`; Ax
|
||||
only forwards chat requests to that loaded engine. Do not present WebLLM as a
|
||||
portable AxIR provider or a server-side default.
|
||||
|
||||
```typescript
|
||||
import { ai, AxAIWebLLMModel } from '@ax-llm/ax';
|
||||
|
||||
const engine = await CreateMLCEngine(AxAIWebLLMModel.Llama32_3B_Instruct);
|
||||
const llm = ai({
|
||||
name: 'webllm',
|
||||
engine,
|
||||
config: {
|
||||
model: AxAIWebLLMModel.Llama32_3B_Instruct,
|
||||
stream: false,
|
||||
supportsFunctions: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
<!-- axir-nonportable:end webllm -->
|
||||
|
||||
## Model Presets
|
||||
|
||||
```typescript
|
||||
import { ai, AxAIGoogleGeminiModel } from '@ax-llm/ax';
|
||||
|
||||
const gemini = ai({
|
||||
name: 'google-gemini',
|
||||
apiKey: process.env.GOOGLE_APIKEY!,
|
||||
config: { model: 'simple' },
|
||||
models: [
|
||||
{ key: 'tiny', model: AxAIGoogleGeminiModel.Gemini35FlashLite, description: 'Fast + cheap', config: { maxTokens: 1024 } },
|
||||
{ key: 'simple', model: AxAIGoogleGeminiModel.Gemini36Flash, description: 'Balanced' },
|
||||
],
|
||||
});
|
||||
|
||||
await gemini.chat({ model: 'tiny', chatPrompt: [{ role: 'user', content: 'Hi' }] });
|
||||
```
|
||||
|
||||
## Model Catalog
|
||||
|
||||
```typescript
|
||||
import { axGetSupportedAIModels } from '@ax-llm/ax';
|
||||
|
||||
const providers = axGetSupportedAIModels();
|
||||
const openai = providers.find((provider) => provider.name === 'openai');
|
||||
console.log(openai?.models[0]?.promptTokenCostPer1M);
|
||||
|
||||
const textProviders = axGetSupportedAIModels({ type: 'text' });
|
||||
const embeddingProviders = axGetSupportedAIModels({ type: 'embeddings' });
|
||||
```
|
||||
|
||||
Use `axGetSupportedAIModels()` to build provider/model selectors before creating an `ai(...)` instance. It returns bundled static metadata: provider names, display names, default models, raw `AxModelInfo` pricing/details, model type (`'text'`, `'embeddings'`, `'code'`, or `'audio'`), and normalized capability flags for thinking, thoughts, structured outputs, audio, temperature, and top-p support. Provider groups and models are sorted cheapest to most expensive based on bundled input + output token pricing; unpriced models sort last.
|
||||
|
||||
Filter with `{ type: 'all' | 'text' | 'embeddings' | 'code' | 'audio' }` or an array of those values. The `'text'` filter includes code-capable models; use `'code'` to show only code-first models.
|
||||
|
||||
Dynamic providers such as Azure OpenAI deployments are marked with `isDynamic: true` and may have an empty or static-limited model list.
|
||||
|
||||
## Routing And Balancing
|
||||
|
||||
Choose the primitive by responsibility:
|
||||
|
||||
- `AxMultiServiceRouter` combines model lists and dispatches the model key the caller already selected. It does not select a model.
|
||||
- `AxBalancer` without a strategy orders equivalent services once with a comparator, retries transient provider failures, and fails over in that order.
|
||||
- `AxBalancer` with `strategy.type: 'adaptive'` selects among services exposing the same logical model aliases using learned provider reliability, successful latency, and estimated cost.
|
||||
|
||||
Adaptive balancing is operational routing, not semantic prompt-to-model routing. Every provider model behind an alias must be an acceptable substitute for that application. Keep quality evaluation and content-aware model selection outside the balancer.
|
||||
|
||||
```typescript
|
||||
import { AxBalancer, AxInMemoryBalancerStatsStore } from '@ax-llm/ax';
|
||||
|
||||
const statsStore = new AxInMemoryBalancerStatsStore();
|
||||
const routeKeys = new Map<string, string>([
|
||||
[openai.getId(), 'openai-primary'],
|
||||
[anthropic.getId(), 'anthropic-primary'],
|
||||
]);
|
||||
|
||||
const llm = AxBalancer.create([openai, anthropic] as const, {
|
||||
strategy: {
|
||||
type: 'adaptive',
|
||||
deadlineMs: 6_000,
|
||||
badOutcomeCost: 0.02,
|
||||
expectedTokens: { promptTokens: 1_200, completionTokens: 300 },
|
||||
namespace: 'support-v1',
|
||||
routeKey: (service) => {
|
||||
const key = routeKeys.get(service.getId());
|
||||
if (!key) throw new Error('Missing stable route key.');
|
||||
return key;
|
||||
},
|
||||
slice: ({ options }) =>
|
||||
options?.customLabels?.workflow ?? 'default-workflow',
|
||||
statsStore,
|
||||
onRoutingEvent: (event) => telemetry.emit('llm.route', event),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The score is estimated request cost plus `badOutcomeCost` times the probability of provider failure or missing `deadlineMs`. `badOutcomeCost` and estimated cost must use the same currency or unit. By default, cost uses `expectedTokens`, the route's concrete model mapping, and `getEstimatedCost()`; missing catalog pricing contributes zero, while `estimateCost` can supply application pricing. Failures use an EWMA; successful latency is modeled in log space with a Normal-Inverse-Gamma posterior, and Thompson sampling supplies the deadline risk. Capability filtering still runs before ranking.
|
||||
|
||||
Rules:
|
||||
|
||||
- Reuse the in-memory store across balancers in one process. For multiple processes, implement `AxBalancerStatsStore` with Redis or an application database; its `observe()` operation must be atomic.
|
||||
- A custom store requires stable, unique `routeKey` values. Stats are partitioned by `namespace`, `slice`, logical model, and route.
|
||||
- `statsStore` is decision state. `onRoutingEvent` is best-effort telemetry and must not be used as the authoritative routing state.
|
||||
- Routing events contain scores, route metadata, and sanitized failure categories, never prompts, responses, or raw provider errors.
|
||||
- Adaptive mode attempts each candidate once. Provider-client retries remain controlled by `AxAIServiceOptions.retry`.
|
||||
- Streaming can fail over before the first emitted chunk. A mid-stream transient failure is learned but cannot be replayed after partial output; caller cancellation is not recorded.
|
||||
- Adaptive selection applies to chat. Embedding, transcription, and speech keep existing balancer behavior.
|
||||
|
||||
See the [adaptive balancer example](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/generation/adaptive-balancer.ts) for complete provider setup.
|
||||
|
||||
## Chat
|
||||
|
||||
```typescript
|
||||
const res = await llm.chat({
|
||||
chatPrompt: [
|
||||
{ role: 'system', content: 'You are concise.' },
|
||||
{ role: 'user', content: 'Write a haiku about the ocean.' },
|
||||
],
|
||||
});
|
||||
console.log(res.results[0]?.content);
|
||||
```
|
||||
|
||||
## Batch Audio
|
||||
|
||||
Use `ai.transcribe(...)` for batch speech-to-text and `ai.speak(...)` for batch text-to-speech. These are separate from conversational `.chat()` audio config.
|
||||
|
||||
```typescript
|
||||
const transcript = await llm.transcribe({
|
||||
audio: { data: base64Wav, format: 'wav' },
|
||||
model: 'gpt-4o-mini-transcribe',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const speech = await llm.speak({
|
||||
text: transcript.text,
|
||||
model: 'gpt-4o-mini-tts',
|
||||
voice: 'alloy',
|
||||
format: 'mp3',
|
||||
});
|
||||
|
||||
console.log(transcript.text);
|
||||
console.log(speech.data);
|
||||
```
|
||||
|
||||
Providers without the requested audio endpoint throw `AxMediaNotSupportedError`. Use `speech` forward options for signature audio artifacts and `modelConfig.audio` for conversational chat audio.
|
||||
|
||||
## Common Options
|
||||
|
||||
- `stream` (boolean): enable SSE; true by default
|
||||
- `thinkingTokenBudget`: `'minimal'` | `'low'` | `'medium'` | `'high'` | `'highest'` | `'none'`
|
||||
- `showThoughts`: include thoughts in output
|
||||
- `functionCallMode`: `'auto'` | `'native'` | `'prompt'`
|
||||
- `debug`, `logger`, `tracer`, `rateLimiter`, `timeout`
|
||||
|
||||
## Global Runtime Defaults
|
||||
|
||||
Use `axGlobals` when the app wants one live default for AI requests, generator runs, flows, or metrics:
|
||||
|
||||
```typescript
|
||||
import { ai, axGlobals, axCreateDefaultColorLogger } from '@ax-llm/ax';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
|
||||
axGlobals.tracer = trace.getTracer('my-app');
|
||||
axGlobals.debug = true;
|
||||
axGlobals.logger = axCreateDefaultColorLogger();
|
||||
axGlobals.customLabels = { service: 'api' };
|
||||
axGlobals.onUsage = (event) => usageQueue.enqueue(event);
|
||||
|
||||
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `axGlobals.tracer`, `meter`, `logger`, `debug`, `abortSignal`, and `customLabels` are live runtime defaults; future calls read the current value even if the AI instance already exists.
|
||||
- Precedence is: per-call options, then explicit AI/service options, then current `axGlobals`, then built-in defaults.
|
||||
- `customLabels` merge from globals to service to call options; later sources override earlier keys.
|
||||
- `abortSignal` values are merged, so either a global shutdown signal or a local request signal can cancel the request.
|
||||
- `axGlobals.onUsage` receives one immutable normalized event for each completed chat or embedding call that reports token usage. A fully consumed stream emits once.
|
||||
- Usage observers are best-effort and fail-open. Ax does not await them; synchronously enqueue events and persist or aggregate them out of band.
|
||||
|
||||
Use `usageContext` for multi-tenant and request attribution:
|
||||
|
||||
```typescript
|
||||
const llm = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
options: {
|
||||
usageContext: {
|
||||
tenantId: 'tenant-42',
|
||||
feature: 'support-chat',
|
||||
attributes: { environment: 'production' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await llm.chat(request, {
|
||||
usageContext: {
|
||||
userId: user.id,
|
||||
requestId: requestId,
|
||||
runId: runId,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Per-call context overrides service defaults, while `attributes` are shallow-merged. Events include normalized tokens, provider/model, available session and remote IDs, and a streaming flag. They do not estimate currency cost; calculate that downstream against a versioned pricing table.
|
||||
|
||||
## DeepSeek Notes
|
||||
|
||||
```typescript
|
||||
import { ai, AxAIDeepSeekModel } from '@ax-llm/ax';
|
||||
|
||||
const deepseek = ai({
|
||||
name: 'deepseek',
|
||||
apiKey: process.env.DEEPSEEK_APIKEY!,
|
||||
config: { model: AxAIDeepSeekModel.DeepSeekV4Flash },
|
||||
});
|
||||
```
|
||||
|
||||
DeepSeek's current API models are `deepseek-v4-flash` and `deepseek-v4-pro`.
|
||||
The deprecated `deepseek-chat` and `deepseek-reasoner` aliases are retained for
|
||||
compatibility until DeepSeek removes them on 2026-07-24.
|
||||
|
||||
DeepSeek V4 supports thinking mode. Ax sends `thinking: { type: "disabled" }`
|
||||
by default to preserve non-thinking behavior, and enables it when
|
||||
`thinkingTokenBudget` is set. Ax maps lower budget levels to DeepSeek's `high`
|
||||
effort and maps `highest` to `max`. DeepSeek V4 thinking models support tools,
|
||||
but reject the `tool_choice` request parameter, so Ax omits forced/auto tool
|
||||
choice for `deepseek-v4-pro`, `deepseek-v4-flash`, and `deepseek-reasoner`
|
||||
while still sending tool definitions.
|
||||
|
||||
## Extended Thinking
|
||||
|
||||
```typescript
|
||||
import { ai, AxAIAnthropicModel } from '@ax-llm/ax';
|
||||
|
||||
const claude = ai({
|
||||
name: 'anthropic',
|
||||
apiKey: process.env.ANTHROPIC_APIKEY!,
|
||||
config: { model: AxAIAnthropicModel.Claude48Opus },
|
||||
});
|
||||
|
||||
const res = await claude.chat(
|
||||
{ chatPrompt: [{ role: 'user', content: 'Solve step by step...' }] },
|
||||
{ thinkingTokenBudget: 'medium', showThoughts: true },
|
||||
);
|
||||
console.log(res.results[0]?.thought);
|
||||
console.log(res.results[0]?.content);
|
||||
```
|
||||
|
||||
### Budget Levels
|
||||
|
||||
| Level | Anthropic (tokens) | Gemini (tokens) |
|
||||
|---|---|---|
|
||||
| `'none'` | disabled | minimal |
|
||||
| `'minimal'` | 1,024 | 200 |
|
||||
| `'low'` | 5,000 | 800 |
|
||||
| `'medium'` | 10,000 | 5,000 |
|
||||
| `'high'` | 20,000 | 10,000 |
|
||||
| `'highest'` | 32,000 | 24,500 |
|
||||
|
||||
For GPT-5.6, these map to `none`, `low`, `low`, `medium`, `high`, and a top rung
|
||||
that depends on the API surface: `xhigh` on Chat Completions, which rejects
|
||||
`max`, and `max` on the Responses API, which is the only place it is served.
|
||||
Earlier OpenAI models retain their existing mapping.
|
||||
|
||||
### Anthropic Model-Specific Behavior
|
||||
|
||||
- Opus 4.8, 4.7, and 4.6 plus Sonnet 5: adaptive thinking, no manual
|
||||
`budget_tokens`, and no `temperature` / `topP` / `topK`. When thoughts are
|
||||
requested, Ax asks Anthropic for summarized display; when they are hidden,
|
||||
Ax explicitly requests `display: 'omitted'`.
|
||||
- Opus 4.5: budget_tokens + effort levels (capped at `'high'`)
|
||||
- Other thinking models: budget tokens only
|
||||
|
||||
Anthropic `modelConfig.effort` can be set directly on a request. Fast mode and
|
||||
task budgets are Anthropic-only opt-ins; `taskBudget.total` must be at least
|
||||
20,000 tokens.
|
||||
|
||||
```typescript
|
||||
const res = await claude.chat({
|
||||
chatPrompt: [{ role: 'user', content: 'Review this migration plan.' }],
|
||||
modelConfig: {
|
||||
effort: 'xhigh',
|
||||
speed: 'fast',
|
||||
taskBudget: { type: 'tokens', total: 64_000 },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Thinking Levels
|
||||
|
||||
```typescript
|
||||
const claude = ai({
|
||||
name: 'anthropic',
|
||||
apiKey: '...',
|
||||
config: {
|
||||
model: AxAIAnthropicModel.Claude48Opus,
|
||||
thinkingTokenBudgetLevels: {
|
||||
minimal: 2048,
|
||||
low: 8000,
|
||||
medium: 16000,
|
||||
high: 25000,
|
||||
highest: 40000,
|
||||
},
|
||||
effortLevelMapping: {
|
||||
minimal: 'low',
|
||||
low: 'medium',
|
||||
medium: 'high',
|
||||
high: 'high',
|
||||
highest: 'max',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Embeddings
|
||||
|
||||
```typescript
|
||||
const { embeddings } = await llm.embed({
|
||||
texts: ['hello', 'world'],
|
||||
embedModel: 'text-embedding-005',
|
||||
});
|
||||
```
|
||||
|
||||
## Context Caching
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, { code, language }, {
|
||||
mem,
|
||||
sessionId: 'code-review-session',
|
||||
contextCache: {
|
||||
ttlSeconds: 3600,
|
||||
cacheBreakpoint: 'after-examples',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Breakpoint values: `'system'` | `'after-functions'` | `'after-examples'`
|
||||
|
||||
Provider behavior:
|
||||
|
||||
- Google Gemini: explicit caching with cache resource ID and auto TTL refresh;
|
||||
failed refreshes recreate or fall back uncached, and rejected Ax-managed
|
||||
caches retry once without the cache
|
||||
- Anthropic: implicit via `cache_control` markers
|
||||
|
||||
### External Registry (serverless)
|
||||
|
||||
```typescript
|
||||
const accountId = getRequiredAccountId();
|
||||
const registry: AxContextCacheRegistry = {
|
||||
get: async (key) => {
|
||||
const value = await redis.get(`context-cache:${accountId}:${key}`);
|
||||
return value ? JSON.parse(value) : undefined;
|
||||
},
|
||||
set: async (key, entry) => {
|
||||
const ttl = Math.max(1, Math.ceil((entry.expiresAt - Date.now()) / 1000));
|
||||
await redis.set(
|
||||
`context-cache:${accountId}:${key}`,
|
||||
JSON.stringify(entry),
|
||||
{ ex: ttl }
|
||||
);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Ax registry keys are content-based and are not account-scoped. Require a stable
|
||||
tenant/account namespace when cross-account cache sharing is unsafe; do not
|
||||
silently fall back to a global namespace.
|
||||
|
||||
## AWS Bedrock
|
||||
|
||||
```typescript
|
||||
import { AxAIBedrock, AxAIBedrockModel } from '@ax-llm/ax-ai-aws-bedrock';
|
||||
|
||||
const bedrock = new AxAIBedrock({
|
||||
region: 'us-east-2',
|
||||
fallbackRegions: ['us-west-2'],
|
||||
config: { model: AxAIBedrockModel.ClaudeOpus45 },
|
||||
});
|
||||
```
|
||||
|
||||
## Vercel AI SDK Integration
|
||||
|
||||
```typescript
|
||||
import { generateText } from 'ai';
|
||||
import { ai } from '@ax-llm/ax';
|
||||
import { AxAIProvider } from '@ax-llm/ax-ai-sdk-provider';
|
||||
|
||||
const axAI = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY ?? '',
|
||||
});
|
||||
const model = new AxAIProvider(axAI);
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
prompt: 'Hello!',
|
||||
});
|
||||
```
|
||||
|
||||
## MCP + AxJSRuntime
|
||||
|
||||
```typescript
|
||||
import { AxMCPClient } from '@ax-llm/ax';
|
||||
import { axCreateMCPStdioTransport } from '@ax-llm/ax-tools';
|
||||
|
||||
const transport = axCreateMCPStdioTransport({
|
||||
command: 'npx',
|
||||
args: ['-y', '@anthropic/mcp-server-filesystem'],
|
||||
});
|
||||
const client = new AxMCPClient(transport);
|
||||
```
|
||||
|
||||
For server notifications, call `client.startListening({ signal, onError })` or
|
||||
attach the client through `AxMCPEventSource`. The event adapter is preferred
|
||||
for autonomous work because protocol callbacks only enqueue; explicit routes
|
||||
decide whether to observe, invalidate, resume, or wake.
|
||||
|
||||
For signed UCP lifecycle requests, mount
|
||||
`AxUCPWebhookEventSource.ingest(request)` in application-owned HTTP hosting.
|
||||
Signature, profile, digest, freshness, and replay verification completes before
|
||||
the event runtime sees the request.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- Use `ai()` factory for all providers.
|
||||
- Provider names: `'openai'`, `'openai-responses'`, `'anthropic'`, `'google-gemini'`, `'azure-openai'`, `'mistral'`, `'cohere'`, `'deepseek'`, `'reka'`, `'grok'`
|
||||
- Thinking constraints on Anthropic: every adaptive-thinking model omits
|
||||
`temperature`, `topP`, and `topK`; older thinking models ignore `temperature` and `topK`, with
|
||||
`topP` only sent if >= 0.95.
|
||||
- Bedrock uses `new AxAIBedrock()`, not `ai()`.
|
||||
- Vercel AI SDK uses `AxAIProvider` wrapper.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [Embeddings](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/embed.ts) — embedding generation
|
||||
- [Anthropic Thinking](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/anthropic-thinking-function.ts) — extended thinking with functions
|
||||
- [Anthropic Thinking Separation](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/anthropic-thinking-separation.ts) — thinking separation
|
||||
- [Anthropic Web Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/anthropic-web-search.ts) — Anthropic web search
|
||||
- [OpenAI Web Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/openai-web-search.ts) — OpenAI web search
|
||||
- [OpenAI Responses](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/openai-responses.ts) — OpenAI responses API
|
||||
- [o3 Reasoning](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/reasoning-o3-example.ts) — o3 reasoning
|
||||
- [Gemini Context Cache](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/gemini-context-cache.ts) — Gemini context caching
|
||||
- [Gemini Files](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/gemini-file-support.ts) — Gemini file handling
|
||||
- [Grok Live Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/grok-live-search.ts) — Grok live search
|
||||
- [OpenAI-Compatible](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/openai-compatible.ts) — custom OpenAI-compatible base URL
|
||||
- [Vertex AI Auth](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/vertex-auth-example.ts) — Vertex AI authentication
|
||||
- [MCP Stdio](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/mcp-client-memory.ts) — MCP stdio transport
|
||||
- [MCP HTTP](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/mcp-client-pipedream.ts) — MCP HTTP transport
|
||||
- [Telemetry](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/telemetry.ts) — OpenTelemetry tracing
|
||||
- [Multi-Modal](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/multi-modal.ts) — image handling
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not use `new AxAIOpenAI(...)` or similar class constructors for standard providers; use `ai()`.
|
||||
- Do not hardcode provider class names when `ai({ name: ... })` covers the provider.
|
||||
- Do not mix `thinkingTokenBudget` with explicit `temperature` on Anthropic thinking models.
|
||||
- Do not use `ai()` for AWS Bedrock; use `new AxAIBedrock()`.
|
||||
- Do not omit `resourceName` and `deploymentName` for Azure OpenAI.
|
||||
373
.agents/skills/ax-audio/SKILL.md
Normal file
373
.agents/skills/ax-audio/SKILL.md
Normal file
@@ -0,0 +1,373 @@
|
||||
---
|
||||
name: ax-audio
|
||||
description: This skill helps an LLM generate correct audio code with @ax-llm/ax. Use when the user asks about ai.transcribe(), ai.speak(), signature audio inputs or outputs, agent audio behavior, .chat() conversational audio, OpenAI audio or realtime models, Gemini Live native audio, Grok Voice Agent models, voices, formats, transcripts, or how audio fits with structured outputs.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# Audio I/O Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill for audio in Ax. Pick the smallest audio surface that matches the job:
|
||||
|
||||
- Use `ai.transcribe(...)` for batch speech-to-text.
|
||||
- Use `ai.speak(...)` for batch text-to-speech.
|
||||
- Use `speech:audio` signature outputs for structured programs that should return synthesized audio artifacts.
|
||||
- Use `.chat()` audio config for conversational or realtime audio turns.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Input `:audio` is an audio input value: `{ data, format?, mimeType?, sampleRate?, channels? }`.
|
||||
- Output `:audio` is a scripted audio artifact. The model returns plain text for that field; Ax synthesizes it after structured output parsing.
|
||||
- Output audio JSON schema is model-facing `string`, not a binary object.
|
||||
- Agents transcribe input audio fields before planner/executor/responder stages by default, so agent stages see text instead of base64 audio.
|
||||
- Realtime and conversational audio still use `.chat()` and `modelConfig.audio`.
|
||||
- Batch signature audio artifacts use forward-time `speech` options, not `modelConfig.audio`.
|
||||
|
||||
## Direct Batch APIs
|
||||
|
||||
```typescript
|
||||
import { ai } from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||
|
||||
const transcript = await llm.transcribe({
|
||||
audio: { data: base64Wav, format: 'wav' },
|
||||
model: 'gpt-4o-mini-transcribe',
|
||||
language: 'en',
|
||||
prompt: 'Product support call',
|
||||
});
|
||||
|
||||
const speech = await llm.speak({
|
||||
text: transcript.text,
|
||||
model: 'gpt-4o-mini-tts',
|
||||
voice: 'alloy',
|
||||
format: 'mp3',
|
||||
});
|
||||
|
||||
console.log(transcript.text);
|
||||
console.log(speech.data);
|
||||
console.log(speech.transcript);
|
||||
```
|
||||
|
||||
Providers without the requested batch audio capability throw `AxMediaNotSupportedError`.
|
||||
|
||||
## Signature Audio Artifacts
|
||||
|
||||
```typescript
|
||||
import { ai, ax } from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||
const say = ax('question:string -> speech:audio, summary:string');
|
||||
|
||||
const result = await say.forward(
|
||||
llm,
|
||||
{ question: 'Explain retries in one sentence.' },
|
||||
{
|
||||
speech: {
|
||||
speak: { voice: 'alloy', format: 'mp3' },
|
||||
fields: {
|
||||
speech: { voice: 'alloy' },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log(result.summary);
|
||||
console.log(result.speech.data);
|
||||
console.log(result.speech.mimeType);
|
||||
console.log(result.speech.transcript);
|
||||
```
|
||||
|
||||
The model emits a text script for `speech`; Ax replaces it with `AxChatAudioOutput` after result selection. If the field already contains an audio artifact with `{ data }` or `{ id }`, Ax leaves it alone.
|
||||
|
||||
## Agent Audio Inputs
|
||||
|
||||
```typescript
|
||||
import { agent, ai } from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||
|
||||
const voiceAgent = agent(
|
||||
'recording:audio, question:string -> speech:audio, summary:string',
|
||||
{
|
||||
agentIdentity: {
|
||||
name: 'Voice Assistant',
|
||||
description: 'Answers spoken requests with spoken and written output',
|
||||
},
|
||||
contextFields: [],
|
||||
}
|
||||
);
|
||||
|
||||
const result = await voiceAgent.forward(
|
||||
llm,
|
||||
{
|
||||
recording: { data: base64Wav, format: 'wav' },
|
||||
question: 'What should I do next?',
|
||||
},
|
||||
{
|
||||
speech: {
|
||||
transcribe: { model: 'gpt-4o-mini-transcribe' },
|
||||
speak: { voice: 'alloy', format: 'mp3' },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log(result.summary);
|
||||
console.log(result.speech.data);
|
||||
```
|
||||
|
||||
The agent runtime transcribes `recording` first and passes the transcript through the internal agent stages. Use direct `ax(...)` or `.chat()` when you specifically want native audio understanding in the model call.
|
||||
|
||||
## Conversational `.chat()` Audio
|
||||
|
||||
Use `modelConfig.audio` for conversational audio turns where audio is part of the chat response instead of a structured signature field.
|
||||
|
||||
```typescript
|
||||
const res = await llm.chat({
|
||||
chatPrompt: [{ role: 'user', content: 'Say hello out loud.' }],
|
||||
modelConfig: {
|
||||
audio: { output: { enabled: true, voice: 'alloy', format: 'wav' } },
|
||||
},
|
||||
});
|
||||
|
||||
console.log(res.results[0]?.content);
|
||||
console.log(res.results[0]?.audio?.data);
|
||||
console.log(res.results[0]?.audio?.transcript);
|
||||
```
|
||||
|
||||
## Config Shape
|
||||
|
||||
```typescript
|
||||
type AxAudioFormat =
|
||||
| 'wav'
|
||||
| 'mp3'
|
||||
| 'flac'
|
||||
| 'opus'
|
||||
| 'aac'
|
||||
| 'pcm16'
|
||||
| 'pcm'
|
||||
| 'ogg'
|
||||
| 'raw'
|
||||
| 'mulaw'
|
||||
| 'ulaw'
|
||||
| 'alaw';
|
||||
|
||||
type AxSpeechConfig = {
|
||||
transcribe?: {
|
||||
model?: string;
|
||||
language?: string;
|
||||
prompt?: string;
|
||||
};
|
||||
speak?: {
|
||||
model?: string;
|
||||
voice?: string;
|
||||
format?: AxAudioFormat;
|
||||
};
|
||||
fields?: Record<
|
||||
string,
|
||||
{
|
||||
model?: string;
|
||||
voice?: string;
|
||||
format?: AxAudioFormat;
|
||||
}
|
||||
>;
|
||||
};
|
||||
```
|
||||
|
||||
## OpenAI Defaults
|
||||
|
||||
Use `axAIOpenAIAudioDefaultConfig()` for OpenAI request-based audio chat:
|
||||
|
||||
- model: `gpt-audio-mini`
|
||||
- output enabled
|
||||
- voice: `alloy`
|
||||
- output format: `wav`
|
||||
- transcript enabled
|
||||
- streaming disabled by default
|
||||
- audio input formats: `wav`, `mp3`
|
||||
- audio output formats: `wav`, `mp3`, `flac`, `opus`, `aac`, `pcm16`
|
||||
|
||||
```typescript
|
||||
import { ai, axAIOpenAIAudioDefaultConfig } from '@ax-llm/ax';
|
||||
|
||||
const openai = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
config: axAIOpenAIAudioDefaultConfig(),
|
||||
});
|
||||
|
||||
const res = await openai.chat({
|
||||
chatPrompt: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'What is in this recording?' },
|
||||
{ type: 'audio', data: base64Wav, format: 'wav' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(res.results[0]?.content);
|
||||
console.log(res.results[0]?.audio?.data);
|
||||
```
|
||||
|
||||
Use `axAIOpenAIRealtimeDefaultConfig()` for OpenAI realtime speech-to-speech:
|
||||
|
||||
- model: `gpt-realtime-2`
|
||||
- output enabled
|
||||
- voice: `marin`
|
||||
- output format: `pcm16`
|
||||
- input default: `audio/pcm`, mono, 24000 Hz
|
||||
- turn timeout: `30000`
|
||||
- streaming disabled by default
|
||||
|
||||
Use `axAIOpenAIRealtimeTranscriptionDefaultConfig()` for realtime transcript deltas:
|
||||
|
||||
- model: `gpt-realtime-whisper`
|
||||
- input default: `audio/pcm`, mono, 24000 Hz
|
||||
- output audio disabled; transcript text is returned on `content`
|
||||
|
||||
Realtime models use a one-turn WebSocket call under `.chat()`. In Node, pass a WebSocket constructor through request options:
|
||||
|
||||
```typescript
|
||||
import WebSocket from 'ws';
|
||||
import { ai, axAIOpenAIRealtimeDefaultConfig } from '@ax-llm/ax';
|
||||
|
||||
const openai = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
config: axAIOpenAIRealtimeDefaultConfig(),
|
||||
});
|
||||
|
||||
const stream = await openai.chat(
|
||||
{
|
||||
chatPrompt: [{ role: 'user', content: 'Say hello out loud.' }],
|
||||
},
|
||||
{ stream: true, webSocket: WebSocket }
|
||||
);
|
||||
```
|
||||
|
||||
For follow-up turns, keep the assistant audio reference in history:
|
||||
|
||||
```typescript
|
||||
await openai.chat({
|
||||
chatPrompt: [
|
||||
{ role: 'assistant', audio: { id: previousAudioId } },
|
||||
{ role: 'user', content: 'Repeat that more slowly.' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Gemini Live Defaults
|
||||
|
||||
Use `axAIGoogleGeminiLiveAudioDefaultConfig()` for Gemini native audio:
|
||||
|
||||
- model: `gemini-2.5-flash-native-audio-preview-12-2025`
|
||||
- output enabled
|
||||
- voice: `Kore`
|
||||
- output format: `pcm16`
|
||||
- output sample rate: `24000`
|
||||
- input default: `audio/pcm;rate=16000`, mono
|
||||
- transcript enabled
|
||||
- turn timeout: `30000`
|
||||
- streaming disabled by default
|
||||
|
||||
```typescript
|
||||
import { ai, axAIGoogleGeminiLiveAudioDefaultConfig } from '@ax-llm/ax';
|
||||
|
||||
const gemini = ai({
|
||||
name: 'google-gemini',
|
||||
apiKey: process.env.GOOGLE_APIKEY!,
|
||||
config: axAIGoogleGeminiLiveAudioDefaultConfig(),
|
||||
});
|
||||
|
||||
const res = await gemini.chat({
|
||||
chatPrompt: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Answer this spoken question.' },
|
||||
{
|
||||
type: 'audio',
|
||||
data: base64Pcm16,
|
||||
format: 'pcm16',
|
||||
sampleRate: 16000,
|
||||
channels: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(res.results[0]?.content);
|
||||
console.log(res.results[0]?.audio?.data);
|
||||
```
|
||||
|
||||
Gemini Live uses a one-turn WebSocket call under `.chat()`. It expects PCM input for native audio turns; use `format: 'pcm16'` or `mimeType: 'audio/pcm;rate=16000'`.
|
||||
|
||||
## Grok Voice Defaults
|
||||
|
||||
Use `axAIGrokVoiceDefaultConfig()` for xAI Grok Voice Agent:
|
||||
|
||||
- model: `grok-voice-think-fast-1.0`
|
||||
- output enabled
|
||||
- voice: `eve`
|
||||
- output format: `pcm16`
|
||||
- output sample rate: `24000`
|
||||
- input default: `audio/pcm`, mono, 24000 Hz
|
||||
- transcript enabled
|
||||
- turn timeout: `30000`
|
||||
- streaming disabled by default
|
||||
|
||||
```typescript
|
||||
import WebSocket from 'ws';
|
||||
import { ai, axAIGrokVoiceDefaultConfig } from '@ax-llm/ax';
|
||||
|
||||
const grok = ai({
|
||||
name: 'grok',
|
||||
apiKey: process.env.GROK_API_KEY!,
|
||||
config: axAIGrokVoiceDefaultConfig(),
|
||||
});
|
||||
|
||||
const res = await grok.chat(
|
||||
{
|
||||
chatPrompt: [{ role: 'user', content: 'Say hello out loud.' }],
|
||||
},
|
||||
{ webSocket: WebSocket }
|
||||
);
|
||||
|
||||
console.log(res.results[0]?.content);
|
||||
console.log(res.results[0]?.audio?.data);
|
||||
```
|
||||
|
||||
Grok Voice uses a one-turn WebSocket call under `.chat()`. It expects PCM input for spoken input turns; use `format: 'pcm16'` or `mimeType: 'audio/pcm'`.
|
||||
|
||||
## Streaming Audio
|
||||
|
||||
OpenAI audio chat, OpenAI Realtime, Gemini Live, and Grok Voice all default to non-streaming, but each can stream deltas when you pass `{ stream: true }`.
|
||||
|
||||
```typescript
|
||||
const stream = await llm.chat(
|
||||
{
|
||||
chatPrompt: [{ role: 'user', content: 'Say hello.' }],
|
||||
},
|
||||
{ stream: true }
|
||||
);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const audio = chunk.results[0]?.audio;
|
||||
if (audio?.isDelta) {
|
||||
playAudioChunk(audio.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Structured Outputs
|
||||
|
||||
Use signature audio outputs for structured speech artifacts:
|
||||
|
||||
```typescript
|
||||
const gen = ax('question:string -> answer:string, speech:audio');
|
||||
```
|
||||
|
||||
Use `.chat()` audio when the response itself is a conversational audio turn. Do not combine `.chat()` audio output with provider-native structured response formats unless that provider explicitly supports the combination.
|
||||
145
.agents/skills/ax-event-runtime/SKILL.md
Normal file
145
.agents/skills/ax-event-runtime/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: ax-event-runtime
|
||||
description: Use AxEventRuntime to ingest events, explicitly wake or resume AxGen, AxAgent, and AxFlow, persist state and results, and route outputs safely.
|
||||
---
|
||||
|
||||
# Ax Event Runtime
|
||||
|
||||
Use this skill when an Ax program should react to notifications, webhooks,
|
||||
timers, queues, task completion, or application events.
|
||||
|
||||
## Mental Model
|
||||
|
||||
```text
|
||||
source -> inbox -> route -> target -> stored run -> sink
|
||||
```
|
||||
|
||||
Sources never call an Ax program directly. A route must explicitly choose
|
||||
`observe`, `invalidate`, `wake`, or `resume`. Only the last two invoke a model.
|
||||
|
||||
## Minimal Pattern
|
||||
|
||||
```ts
|
||||
const source = new AxPushEventSource('application');
|
||||
const target = eventTarget('triage')
|
||||
.program(triageAgent)
|
||||
.ai(llm)
|
||||
.input((input) => input.field('incident', eventPath.data()))
|
||||
.sink({ id: 'result', write: saveResult })
|
||||
.build();
|
||||
|
||||
const events = eventRuntime({
|
||||
sources: [source],
|
||||
routes: [
|
||||
eventRoute('incident-created')
|
||||
.types('incident.created')
|
||||
.wake(target)
|
||||
.build(),
|
||||
],
|
||||
});
|
||||
|
||||
await events.start();
|
||||
await source.publish({ event, identity, trust: 'authenticated' });
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Supply identity from authenticated adapter state, never from event data.
|
||||
- Treat events without verified identity as anonymous and untrusted.
|
||||
- Map event data into signature inputs; do not synthesize a user message.
|
||||
- Use `eventPath.data('field')` and other segment-safe selectors. Do not use
|
||||
dotted JSONPath strings or repurpose `s()` as a mapping language.
|
||||
- Use `.project(path)` only for same-name signature projection. Explicit
|
||||
`.field()` mappings override projection; missing or invalid signature inputs
|
||||
dead-letter before model invocation.
|
||||
- Use `eventInput().project(...).field(...)` when a declarative mapping should
|
||||
be callback-free and reusable, then pass that plan to `.input()`,
|
||||
`.wakeInput()`, or `.resumeInput()`.
|
||||
- Callback `mapInput` is an escape hatch, not a validation bypass: its result is
|
||||
normalized to the program signature and mapper failures dead-letter before
|
||||
invocation.
|
||||
- Use `.wakeInput()` and `.resumeInput()` when the two actions need different
|
||||
contracts. Neither action silently uses the other action's mapping.
|
||||
- Use `observe` for progress/logs and `invalidate` for catalog changes.
|
||||
- Use `resume` only with an owned continuation correlation key.
|
||||
- Use `createProgram(instance)` for stateful multi-tenant Agents.
|
||||
- Declare `retrySafety: 'idempotent'` only when stable delivery keys protect
|
||||
every possible side effect.
|
||||
- Persist outputs before final sink delivery; redrive sink failures separately.
|
||||
- Use `debounceMs` and `coalesce: 'latest'` only when replacing intermediate
|
||||
events is part of the route's declared policy.
|
||||
- Observe source failures with `onSourceError`.
|
||||
- The in-memory store is volatile and single-process.
|
||||
- For cooperating Node processes on one local disk, use
|
||||
`AxSQLiteEventStore` from `@ax-llm/ax-tools/event/sqlite` with explicit
|
||||
retention and `coordination: 'multi-worker'`. Never recommend SQLite on a
|
||||
network filesystem.
|
||||
- Close the runtime and caller-owned protocol clients explicitly.
|
||||
- Fan out to several Agents with several matching routes, not a multi-target
|
||||
route. This preserves independent authorization, ordering, retries, and runs.
|
||||
|
||||
## Continuation Pattern
|
||||
|
||||
```ts
|
||||
eventContext.registerContinuation({
|
||||
correlation: [{ kind: 'task', value: taskId }],
|
||||
expiresAt,
|
||||
});
|
||||
```
|
||||
|
||||
Route progress to `observe`. Route `input_required`, completed, failed, or
|
||||
cancelled task events to `resume` when the owning program must run again.
|
||||
|
||||
## MCP Adapter
|
||||
|
||||
Use `ax-mcp` for client construction, transports, authentication, catalogs,
|
||||
subscriptions, tasks, and MCP-specific security policy. This skill owns the
|
||||
generic inbox, routing, continuation, store, and sink behavior.
|
||||
|
||||
Use `client.inspectCatalog()` to discover server-owned tools, prompts, concrete
|
||||
resources, and URI templates from only the endpoint. Then use
|
||||
`AxMCPEventSource({ client, resourceSubscriptions, identity, trust })` with an
|
||||
explicit none/all/URI/selector policy. Omitted policy subscribes to no
|
||||
resources. Templates are never expanded automatically. Managed sources diff
|
||||
catalog changes, restore current logical ownership on reconnect, and release
|
||||
only their own subscriptions on close. Identity must come from the
|
||||
application's authenticated client or token mapping; a bare MCP session is
|
||||
anonymous. Add `...axMCPEventRoutes({ client })` for catalog invalidation,
|
||||
progress/log observation, and task resume. Resource notifications never get an
|
||||
implicit wake route. See `docs/MCP_SUBSCRIPTIONS.md`.
|
||||
|
||||
## UCP Adapter
|
||||
|
||||
Use `AxUCPWebhookEventSource({ client, identity })` inside an application-owned
|
||||
HTTP handler, then call `source.ingest(request)`. Verification of the signer
|
||||
profile, RFC 9421 signature, digest, freshness window, key rotation, and replay
|
||||
key completes before enqueue. Resolve tenant/account identity from application
|
||||
state after verification; do not copy identity from the business payload.
|
||||
|
||||
Generated Python, Java, C++, Go, and Rust packages expose the same Core-owned
|
||||
single-worker event state machine plus functioning inline lifecycle dispatch,
|
||||
continuations, state restoration, cancellation, persisted outputs, isolated
|
||||
sink redrive, signature-aware path/input/target/route builders, and host-owned
|
||||
source, sink, clock, and store boundaries. Generated targets use the host
|
||||
signature plus a typed invocation callback when no common object-safe program
|
||||
interface exists. Do not claim persistent multi-worker support from
|
||||
`axevent.single-worker` alone.
|
||||
|
||||
Generated runtimes do not create worker threads. `publish()` drains work due at
|
||||
`clock.now()`. Hosts use `nextDueAt()` to schedule `runDue()` for debounce,
|
||||
retry, and continuation expiry; `redrive()` is due immediately. Manual clocks
|
||||
make these transitions deterministic. Generated in-memory stores enforce
|
||||
10,000 pending deliveries, 64 MiB queued data, 1 MiB per envelope, and a
|
||||
five-second publication wait.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `AxManualEventClock`, `AxInMemoryEventStore`, deterministic event IDs, and
|
||||
an output-capturing sink. Assert that unmatched or observe-only events never
|
||||
invoke the program, tenant scopes do not collide, outputs exist before sinks,
|
||||
and uncertain side effects become `outcome_unknown`.
|
||||
|
||||
Persistent store implementations must pass
|
||||
`runAxEventStoreConformance(createStore, { clock })`. A store must not advertise
|
||||
multi-worker capability without the conformance marker checked by runtime
|
||||
startup.
|
||||
682
.agents/skills/ax-flow/SKILL.md
Normal file
682
.agents/skills/ax-flow/SKILL.md
Normal file
@@ -0,0 +1,682 @@
|
||||
---
|
||||
name: ax-flow
|
||||
description: This skill helps an LLM generate correct AxFlow workflow code using @ax-llm/ax. Use when the user asks about flow(), AxFlow, workflow orchestration, parallel execution, DAG workflows, conditional routing, map/reduce patterns, or multi-node AI pipelines.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxFlow Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill to generate `AxFlow` workflow code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use `flow()` factory, not `new AxFlow()`.
|
||||
- Import: `import { ai, flow, f } from '@ax-llm/ax';`
|
||||
- `autoParallel: true` is the default; independent executes and derives run in parallel when their metadata reads/writes are known and non-conflicting.
|
||||
- Node results are stored as `${nodeName}Result` in state.
|
||||
- Always define `.node()` before `.execute()` for that node.
|
||||
- Use `.returns()` (or `.r()`) as the last step to lock the output type.
|
||||
- Use descriptive node names: `documentSummarizer`, not `proc1`.
|
||||
- Use descriptive field names: `userInput`, `responseText`, not `text`, `result`.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- Use `flow()` factory syntax for new code.
|
||||
- Node results in state follow the pattern `state.${nodeName}Result.${fieldName}`.
|
||||
- `.execute()` maps current state to node inputs; `.map()` transforms state without AI calls.
|
||||
- `.returns()` maps final state to the flow output type.
|
||||
- Always define nodes before executing them; reversed order throws at runtime.
|
||||
- Keep state flat; avoid deep nesting in `.map()`.
|
||||
- Ensure loop conditions can change to avoid infinite loops.
|
||||
- Structure independent executes to maximize safe auto-parallelization.
|
||||
- Use `flow<InputType, OutputType>()` for typed flows.
|
||||
- Aliases: `.n()` = `.node()`, `.nx()` = `.nodeExtended()`, `.m()` = `.map()`, `.r()` = `.returns()`.
|
||||
|
||||
## Canonical Pattern
|
||||
|
||||
```typescript
|
||||
import { ai, flow } from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||
|
||||
const wf = flow<{ userInput: string }, { responseText: string }>()
|
||||
.node('testNode', 'userInput:string -> responseText:string')
|
||||
.execute('testNode', (state) => ({ userInput: state.userInput }))
|
||||
.returns((state) => ({ responseText: state.testNodeResult.responseText }));
|
||||
|
||||
const result = await wf.forward(llm, { userInput: 'Hello world' });
|
||||
console.log(result.responseText);
|
||||
```
|
||||
|
||||
## Factory Options
|
||||
|
||||
```typescript
|
||||
// Basic
|
||||
const wf = flow();
|
||||
|
||||
// With options
|
||||
const wf = flow({ autoParallel: false });
|
||||
|
||||
// Typed
|
||||
const wf = flow<InputType, OutputType>();
|
||||
|
||||
// Typed with options
|
||||
const wf = flow<InputType, OutputType>({ autoParallel: true, batchSize: 5 });
|
||||
```
|
||||
|
||||
## State Evolution
|
||||
|
||||
State grows with each executed node. Results are stored as `${nodeName}Result`:
|
||||
|
||||
```typescript
|
||||
// Initial state: { userInput: 'Hello' }
|
||||
flow.execute('processor', (state) => ({ input: state.userInput }));
|
||||
// State: { userInput: 'Hello', processorResult: { output: '...' } }
|
||||
|
||||
flow.execute('analyzer', (state) => ({ text: state.processorResult.output }));
|
||||
// State: { ..., analyzerResult: { sentiment: '...', confidence: 0.8 } }
|
||||
```
|
||||
|
||||
## Node Definition
|
||||
|
||||
```typescript
|
||||
// String signature (creates AxGen automatically)
|
||||
flow.node('processor', 'input:string -> output:string');
|
||||
|
||||
// Multiple outputs
|
||||
flow.node('analyzer', 'text:string -> sentiment:string, confidence:number');
|
||||
|
||||
// Array outputs
|
||||
flow.node('extractor', 'documentText:string -> entities:string[]');
|
||||
|
||||
// Short alias
|
||||
flow.n('processor', 'input:string -> output:string');
|
||||
```
|
||||
|
||||
### Rich Node Contracts (String Grammar)
|
||||
|
||||
Node signatures accept the full extended string grammar — constraint bags, class decisions, optional fields, and nested objects (full modifier table in the ax-signature skill):
|
||||
|
||||
```typescript
|
||||
flow
|
||||
.node('triage', 'ticketText:string -> ticketClass:class "bug, billing, question", severityScore:number(min 1, max 5)')
|
||||
.node('draft', 'ticketText:string, ticketClass:string, severityScore:number -> replyText:string(max 400)')
|
||||
.node('audit', 'replyText:string -> approved:boolean, flaggedSpans:object{ spanText:string, reasonNote:string }[]');
|
||||
```
|
||||
|
||||
- `class` is output-only: a downstream node consuming the decision declares it `:string`.
|
||||
- Optional marks go on the name (`note?:string`), never after the type.
|
||||
- `toString()` serializes these contracts losslessly into `%%ax` directives, so rich contracts survive the diagram round-trip.
|
||||
|
||||
## Extended Nodes (nx)
|
||||
|
||||
Add fields to a base signature without rewriting it:
|
||||
|
||||
```typescript
|
||||
import { f, flow } from '@ax-llm/ax';
|
||||
|
||||
// Chain-of-thought reasoning
|
||||
flow.nx('reasoner', 'question:string -> answer:string', {
|
||||
prependOutputs: [
|
||||
{ name: 'reasoning', type: f.internal(f.string('Step-by-step reasoning')) },
|
||||
],
|
||||
});
|
||||
|
||||
// Add confidence scoring
|
||||
flow.nx('analyzer', 'input:string -> result:string', {
|
||||
appendOutputs: [{ name: 'confidence', type: f.number('Confidence 0-1') }],
|
||||
});
|
||||
|
||||
// Add optional context input
|
||||
flow.nx('processor', 'query:string -> response:string', {
|
||||
appendInputs: [{ name: 'context', type: f.optional(f.string('Extra context')) }],
|
||||
});
|
||||
```
|
||||
|
||||
Extension options: `prependInputs`, `appendInputs`, `prependOutputs`, `appendOutputs`.
|
||||
|
||||
## Execute With Input Mapping
|
||||
|
||||
```typescript
|
||||
flow.execute('summarizer', (state) => ({ documentText: state.document }));
|
||||
|
||||
// With AI override (use a different model for this node)
|
||||
flow.execute('processor', (state) => ({ input: state.data }), { ai: alternativeAI });
|
||||
```
|
||||
|
||||
## Map (State Transformation)
|
||||
|
||||
Use `map()` for data shaping without AI calls:
|
||||
|
||||
```typescript
|
||||
// Sync
|
||||
flow.map((state) => ({ ...state, upperText: state.rawText.toUpperCase() }));
|
||||
|
||||
// Async
|
||||
flow.map(async (state) => {
|
||||
const data = await fetchFromAPI(state.query);
|
||||
return { ...state, enrichedData: data };
|
||||
});
|
||||
|
||||
// Parallel async transforms
|
||||
flow.map([
|
||||
async (state) => ({ ...state, result1: await api1(state.data) }),
|
||||
async (state) => ({ ...state, result2: await api2(state.data) }),
|
||||
], { parallel: true });
|
||||
```
|
||||
|
||||
## Returns (Final Output)
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ input: string }>()
|
||||
.map((state) => ({ ...state, upper: state.input.toUpperCase(), len: state.input.length }))
|
||||
.returns((state) => ({ upper: state.upper, isLong: state.len > 20 }));
|
||||
|
||||
// Result is typed as { upper: string; isLong: boolean }
|
||||
const result = await wf.forward(llm, { input: 'test' });
|
||||
```
|
||||
|
||||
## Sequential Processing
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ input: string }, { finalResult: string }>()
|
||||
.node('step1', 'input:string -> intermediate:string')
|
||||
.node('step2', 'intermediate:string -> output:string')
|
||||
.execute('step1', (state) => ({ input: state.input }))
|
||||
.execute('step2', (state) => ({ intermediate: state.step1Result.intermediate }))
|
||||
.returns((state) => ({ finalResult: state.step2Result.output }));
|
||||
```
|
||||
|
||||
## Auto-Parallel Execution
|
||||
|
||||
Independent execute steps run in parallel automatically (`autoParallel: true` by default) when their metadata reads/writes are known and non-conflicting:
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ text: string }, { combined: string }>()
|
||||
.node('sentimentAnalyzer', 'text:string -> sentiment:string')
|
||||
.node('topicExtractor', 'text:string -> topics:string[]')
|
||||
.node('entityRecognizer', 'text:string -> entities:string[]')
|
||||
// These three run in parallel (all depend only on state.text)
|
||||
.execute('sentimentAnalyzer', (state) => ({ text: state.text }))
|
||||
.execute('topicExtractor', (state) => ({ text: state.text }))
|
||||
.execute('entityRecognizer', (state) => ({ text: state.text }))
|
||||
// This waits for all three
|
||||
.returns((state) => ({
|
||||
combined: JSON.stringify({
|
||||
sentiment: state.sentimentAnalyzerResult.sentiment,
|
||||
topics: state.topicExtractorResult.topics,
|
||||
entities: state.entityRecognizerResult.entities,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Inspect execution plan
|
||||
const plan = wf.getExecutionPlan();
|
||||
console.log(plan.parallelGroups, plan.maxParallelism);
|
||||
```
|
||||
|
||||
Planner rules:
|
||||
- Independent `.execute()` and `.derive()` steps may parallelize.
|
||||
- `.map()`, `.returns()`, `.branch()`, `.while()`, `.feedback()`, and explicit `.parallel()` are barriers.
|
||||
- Branch, while, and feedback bodies still use the same planner internally.
|
||||
- Use `autoParallel: false` when you need strict sequential execution.
|
||||
|
||||
Disable auto-parallel:
|
||||
|
||||
```typescript
|
||||
const wf = flow({ autoParallel: false });
|
||||
// or per execution:
|
||||
await wf.forward(llm, input, { autoParallel: false });
|
||||
```
|
||||
|
||||
## Conditional Branching
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ query: string; expertMode: boolean }, { response: string }>()
|
||||
.node('simple', 'query:string -> response:string')
|
||||
.node('expert', 'query:string -> response:string')
|
||||
.branch((state) => state.expertMode)
|
||||
.when(true)
|
||||
.execute('expert', (state) => ({ query: state.query }))
|
||||
.when(false)
|
||||
.execute('simple', (state) => ({ query: state.query }))
|
||||
.merge()
|
||||
.returns((state) => ({
|
||||
response: state.expertResult?.response ?? state.simpleResult?.response,
|
||||
}));
|
||||
```
|
||||
|
||||
After `.merge()`, only the taken branch's result exists; use optional chaining (`?.`) on untaken branch results.
|
||||
|
||||
## While Loops
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ content: string }, { finalContent: string }>()
|
||||
.node('processor', 'content:string -> processedContent:string')
|
||||
.node('qualityChecker', 'content:string -> qualityScore:number')
|
||||
.map((state) => ({ currentContent: state.content, iteration: 0, qualityScore: 0 }))
|
||||
.while((state) => state.iteration < 3 && state.qualityScore < 0.8)
|
||||
.map((state) => ({ ...state, iteration: state.iteration + 1 }))
|
||||
.execute('processor', (state) => ({ content: state.currentContent }))
|
||||
.execute('qualityChecker', (state) => ({
|
||||
content: state.processorResult.processedContent,
|
||||
}))
|
||||
.map((state) => ({
|
||||
...state,
|
||||
currentContent: state.processorResult.processedContent,
|
||||
qualityScore: state.qualityCheckerResult.qualityScore,
|
||||
}))
|
||||
.endWhile()
|
||||
.returns((state) => ({ finalContent: state.currentContent }));
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Every `.while()` needs a matching `.endWhile()`.
|
||||
- Ensure the loop condition can change to avoid infinite loops.
|
||||
|
||||
## Feedback Loops (label/feedback)
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ prompt: string }, { result: string }>()
|
||||
.node('gen', 'prompt:string -> result:string, quality:number')
|
||||
.map((state) => ({ ...state, tries: 0 }))
|
||||
.label('retry')
|
||||
.map((state) => ({ ...state, tries: state.tries + 1 }))
|
||||
.execute('gen', (state) => ({ prompt: state.prompt }))
|
||||
.feedback((state) => state.genResult.quality < 0.9 && state.tries < 3, 'retry')
|
||||
.returns((state) => ({ result: state.genResult.result }));
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Define the label before referencing it in `.feedback()`.
|
||||
- Always include a max-iteration guard to avoid infinite loops.
|
||||
|
||||
## Explicit Parallel Sub-Flows
|
||||
|
||||
```typescript
|
||||
flow
|
||||
.parallel([
|
||||
(sub) => sub.execute('analyzer1', (state) => ({ text: state.input })),
|
||||
(sub) => sub.execute('analyzer2', (state) => ({ text: state.input })),
|
||||
(sub) => sub.execute('analyzer3', (state) => ({ text: state.input })),
|
||||
])
|
||||
.merge('combinedResults', (r1, r2, r3) => ({
|
||||
a1: r1.analyzer1Result.analysis,
|
||||
a2: r2.analyzer2Result.analysis,
|
||||
a3: r3.analyzer3Result.analysis,
|
||||
}));
|
||||
```
|
||||
|
||||
## Derive (Batch/Array Processing)
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ items: string[] }, { processed: string[] }>({ batchSize: 3 })
|
||||
.derive('processed', 'items', (item, index) => `processed-${item}-${index}`, {
|
||||
batchSize: 2,
|
||||
});
|
||||
```
|
||||
|
||||
## Dynamic AI Context (Multi-Model)
|
||||
|
||||
Route nodes to different AI providers:
|
||||
|
||||
```typescript
|
||||
const fast = ai({ name: 'openai', apiKey: '...', config: { model: 'gpt-5.4-mini' } });
|
||||
const smart = ai({ name: 'anthropic', apiKey: '...' });
|
||||
|
||||
const wf = flow<{ text: string }, { out: string }>()
|
||||
.node('draft', 'text:string -> out:string')
|
||||
.node('refine', 'text:string -> out:string')
|
||||
.execute('draft', (state) => ({ text: state.text }), { ai: fast })
|
||||
.execute('refine', (state) => ({ text: state.draftResult.out }), { ai: smart })
|
||||
.returns((state) => ({ out: state.refineResult.out }));
|
||||
```
|
||||
|
||||
## Description and toFunction
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ userQuestion: string }, { responseText: string }>()
|
||||
.node('qa', 'userQuestion:string -> responseText:string')
|
||||
.execute('qa', (state) => ({ userQuestion: state.userQuestion }))
|
||||
.returns((state) => ({ responseText: state.qaResult.responseText }))
|
||||
.description('Question Answerer', 'Answers user questions concisely.');
|
||||
|
||||
const fn = wf.toFunction();
|
||||
// fn.name, fn.parameters (JSON Schema), fn.func
|
||||
```
|
||||
|
||||
## Instrumentation (Tracing)
|
||||
|
||||
```typescript
|
||||
import { ai, flow } from '@ax-llm/ax';
|
||||
import { context, trace } from '@opentelemetry/api';
|
||||
|
||||
const tracer = trace.getTracer('axflow');
|
||||
const llm = ai({ name: 'openai', apiKey: '...' });
|
||||
|
||||
const wf = flow<{ userQuestion: string }>()
|
||||
.node('summarizer', 'documentText:string -> summaryText:string')
|
||||
.execute('summarizer', (s) => ({ documentText: s.userQuestion }))
|
||||
.returns((s) => ({ answer: s.summarizerResult.summaryText }));
|
||||
|
||||
const result = await wf.forward(llm, { userQuestion: 'hi' }, {
|
||||
tracer,
|
||||
traceContext: context.active(),
|
||||
});
|
||||
```
|
||||
|
||||
Flow tracing also respects live app-wide defaults:
|
||||
|
||||
```typescript
|
||||
import { axGlobals } from '@ax-llm/ax';
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
|
||||
axGlobals.tracer = tracer;
|
||||
axGlobals.meter = metrics.getMeter('axflow');
|
||||
|
||||
const result = await wf.forward(llm, { userQuestion: 'hi' });
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `wf.forward(..., { tracer, meter })` overrides flow defaults and `axGlobals`.
|
||||
- Constructor/factory flow defaults override `axGlobals`.
|
||||
- If no local tracer or meter is provided, `AxFlow` reads current `axGlobals.tracer` and `axGlobals.meter`, creates a parent flow span, and propagates tracer/meter plus trace context to node forwards.
|
||||
- `axGlobals.abortSignal` is merged with flow-level abort signals.
|
||||
|
||||
## Program IDs and Demos
|
||||
|
||||
```typescript
|
||||
const wf = flow<{ input: string }>()
|
||||
.node('summarizer', 'text:string -> summary:string')
|
||||
.node('classifier', 'text:string -> category:string');
|
||||
|
||||
// Discover program IDs
|
||||
console.log(wf.namedPrograms());
|
||||
// [{ id: 'root.summarizer', ... }, { id: 'root.classifier', ... }]
|
||||
|
||||
// Set demos (TypeScript catches typos)
|
||||
wf.setDemos([{ programId: 'root.summarizer', traces: [] }]);
|
||||
|
||||
// Apply optimization
|
||||
wf.applyOptimization(optimizedProgram);
|
||||
```
|
||||
|
||||
For tuning a flow, use top-level `optimize(wf, train, metric, options)` from the
|
||||
`ax-gepa` skill. There is no separate `flow.optimize(...)` helper.
|
||||
|
||||
## Chat Logs
|
||||
|
||||
`AxFlow.getChatLog()` returns a flat `readonly AxChatLogEntry[]` after `forward()`. Each child-node entry is tagged with `entry.name` so callers can filter by node:
|
||||
|
||||
```typescript
|
||||
const log = wf.getChatLog();
|
||||
for (const entry of log) {
|
||||
console.log(entry.name, entry.model);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await wf.forward(llm, input);
|
||||
} catch (error) {
|
||||
console.error('Flow execution failed:', error);
|
||||
}
|
||||
```
|
||||
|
||||
Common errors:
|
||||
- `"Node 'x' not found"` -- define `.node()` before `.execute()`.
|
||||
- `"endWhile() without matching while()"` -- every `.while()` needs `.endWhile()`.
|
||||
- `"when() without matching branch()"` -- `.when()` must be inside `.branch()`/`.merge()`.
|
||||
- `"merge() without matching branch()"` -- every `.branch()` needs `.merge()`.
|
||||
- `"Label 'x' not found"` -- define `.label()` before `.feedback()` references it.
|
||||
|
||||
## Native MCP/UCP
|
||||
|
||||
Use `ax-mcp` for MCP client construction, transport/authentication policy,
|
||||
subscriptions, tasks, event routing, and replay. This section covers how Flow
|
||||
inherits and coordinates the resulting live execution context.
|
||||
|
||||
Set `mcp`/`ucp` on the flow or a node. Sequential nodes reuse sessions; parallel nodes multiplex through each client's concurrency policy. Branch cancellation and flow aborts propagate to outstanding requests and newly created remote tasks. Structured protocol values stay structured in flow state.
|
||||
|
||||
```typescript
|
||||
const wf = flow({ mcp: [inventory], ucp: [merchant] })
|
||||
.node('lookup', lookupProgram)
|
||||
.node('checkout', checkoutProgram, { mcpInheritance: ['merchant'] });
|
||||
```
|
||||
|
||||
## Mermaid Source (Author or Serialize Flows)
|
||||
|
||||
A whole flow can be written as (or exported to) a mermaid flowchart. Pass the
|
||||
diagram string straight to `flow()` — a string argument compiles the AxFlow
|
||||
mermaid dialect into a runnable flow (an options object still constructs an
|
||||
empty builder). `String(wf)` / `wf.toString()` renders any flow back, so
|
||||
`flow(String(wf))` round-trips.
|
||||
|
||||
```typescript
|
||||
import { flow } from '@ax-llm/ax';
|
||||
|
||||
const wf = flow<{ documentText: string }, { finalReport: string }>(`
|
||||
flowchart TD
|
||||
%%ax summarize: documentText:string -> summaryText:string(max 500)
|
||||
%%ax check: summaryText:string -> verdict:class "pass, fail", note?:string
|
||||
%%ax format: summaryText:string, note?:string -> finalReport:string
|
||||
|
||||
summarize[Summarize document] --> check{verdict}
|
||||
check -->|pass| format
|
||||
check -->|fail, max 3| summarize
|
||||
`);
|
||||
|
||||
const { finalReport } = await wf.forward(llm, { documentText });
|
||||
console.log(String(wf)); // render back to the same dialect
|
||||
```
|
||||
|
||||
Dialect:
|
||||
- `%%ax nodeId: <signature>` comment directives carry node contracts (mermaid renderers ignore them); the full string-signature grammar applies (`?` optional on the name, constraint bags, `object{ ... }`).
|
||||
- Data auto-wires by field name: each node input binds to the nearest upstream node that outputs that field; a field no node produces becomes a flow input.
|
||||
- A diamond `nodeId{field}` names a `class` decision; its labeled out-edges (`-->|pass|`) become branches. A back-edge is a loop: `-->|label, max N|` is feedback, `-->|while cond, max N|` is a while loop.
|
||||
|
||||
Render options and bindings:
|
||||
- `wf.toString({ direction: 'LR' })` when you need render options; bare `String(wf)` uses defaults (`flowchart TD`).
|
||||
- `bindings` supplies closures the dialect can't inline: `{ nodes: { normalize: (s) => ({...}) }, conditions: { keepGoing: (s) => ... } }` for map steps and `while` conditions.
|
||||
|
||||
### Flow Gallery
|
||||
|
||||
Every diagram below compiles with `flow(text)` as written (the while loop additionally needs its `conditions` binding).
|
||||
|
||||
Linear pipeline — three nodes auto-wired by field name:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax extract: contractText:string -> parties:string[], effectiveDate?:string(format date)
|
||||
%%ax summarize: contractText:string, parties:string[] -> summaryText:string(max 300)
|
||||
%%ax redline: summaryText:string -> riskNotes:string(item "one risk")[]
|
||||
|
||||
extract --> summarize --> redline
|
||||
```
|
||||
|
||||
Decision branch — a class diamond routes to per-branch responders, then re-joins:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax classify: requestText:string -> routeClass:class "support, sales"
|
||||
%%ax supportReply: requestText:string -> replyText:string(max 300)
|
||||
%%ax salesReply: requestText:string -> replyText:string(max 300)
|
||||
%%ax send: replyText:string -> deliveredReply:string
|
||||
|
||||
classify{routeClass}
|
||||
classify -->|support| supportReply
|
||||
classify -->|sales| salesReply
|
||||
supportReply --> send
|
||||
salesReply --> send
|
||||
```
|
||||
|
||||
Retry loop — a reviewer sends drafts back with a capped revise edge:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax draft: briefText:string -> articleText:string(max 800)
|
||||
%%ax review: articleText:string -> verdict:class "publish, revise", editorNote?:string
|
||||
%%ax publish: articleText:string, editorNote?:string -> finalPost:string
|
||||
|
||||
draft --> review{verdict}
|
||||
review -->|publish| publish
|
||||
review -->|revise, max 2| draft
|
||||
```
|
||||
|
||||
Fan-out / fan-in — two perspectives run in parallel, then a judge joins them:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax outline: topicText:string -> questionText:string
|
||||
%%ax proponent: questionText:string -> proArgument:string
|
||||
%%ax skeptic: questionText:string -> conArgument:string
|
||||
%%ax judge: proArgument:string, conArgument:string -> verdictSummary:string
|
||||
|
||||
outline --> proponent & skeptic
|
||||
proponent & skeptic --> judge
|
||||
```
|
||||
|
||||
While loop — repeat until a host-owned condition says stop (`flow(text, { conditions: { keepPolishing } })`):
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax polish: draftText:string -> polishedText:string
|
||||
%%ax grade: polishedText:string -> qualityScore:number(min 0, max 1)
|
||||
|
||||
polish --> grade
|
||||
grade -->|while keepPolishing, max 5| polish
|
||||
```
|
||||
|
||||
Three-way branch and re-join — triage routes to one of three handlers before delivery:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax triage: ticketText:string -> ticketClass:class "bug, billing, question"
|
||||
%%ax bugHandler: ticketText:string -> replyText:string(max 300)
|
||||
%%ax billingHandler: ticketText:string -> replyText:string(max 300)
|
||||
%%ax questionHandler: ticketText:string -> replyText:string(max 300)
|
||||
%%ax send: replyText:string -> deliveredReply:string
|
||||
|
||||
triage{ticketClass}
|
||||
triage -->|bug| bugHandler
|
||||
triage -->|billing| billingHandler
|
||||
triage -->|question| questionHandler
|
||||
bugHandler --> send
|
||||
billingHandler --> send
|
||||
questionHandler --> send
|
||||
```
|
||||
|
||||
Judge panel — three independent drafts fan out, then converge on one verdict:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax outline: topicText:string -> outlineText:string
|
||||
%%ax draftA: outlineText:string -> draftAText:string
|
||||
%%ax draftB: outlineText:string -> draftBText:string
|
||||
%%ax draftC: outlineText:string -> draftCText:string
|
||||
%%ax judge: draftAText:string, draftBText:string, draftCText:string -> verdictText:string
|
||||
|
||||
outline --> draftA & draftB & draftC
|
||||
draftA & draftB & draftC --> judge
|
||||
```
|
||||
|
||||
Escalation ladder — a quality gate either sends the first answer or falls back to level two:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax l1Answer: ticketText:string -> answerText:string
|
||||
%%ax qualityGate: answerText:string -> verdict:class "pass, escalate"
|
||||
%%ax l2Answer: ticketText:string -> answerText:string
|
||||
%%ax send: answerText:string -> deliveredAnswer:string
|
||||
|
||||
l1Answer --> qualityGate{verdict}
|
||||
qualityGate -->|pass| send
|
||||
qualityGate -->|escalate| l2Answer --> send
|
||||
```
|
||||
|
||||
Itinerary planner — rich contracts stay attached to a simple linear graph:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax parse: requestText:string -> destinationName:string, stayWindow:dateRange, travelerCount:number(min 1, max 12), budgetUsd?:number(min 0)
|
||||
%%ax plan: destinationName:string, stayWindow:dateRange, travelerCount:number, budgetUsd?:number -> itineraryItems:object{ dayNumber:number(min 1), activityText:string }[]
|
||||
%%ax price: itineraryItems:object{ dayNumber:number, activityText:string }[], travelerCount:number -> estimatedTotalUsd:number(min 0), bookingNotes?:string(max 300)
|
||||
|
||||
parse --> plan --> price
|
||||
```
|
||||
|
||||
Fan-out with capped revision — two sections join, then review can send the assembly back twice:
|
||||
|
||||
```text
|
||||
flowchart TD
|
||||
%%ax outline: briefText:string -> outlineText:string
|
||||
%%ax sectionA: outlineText:string -> sectionAText:string
|
||||
%%ax sectionB: outlineText:string -> sectionBText:string
|
||||
%%ax assemble: sectionAText:string, sectionBText:string -> articleText:string
|
||||
%%ax review: articleText:string -> verdict:class "approve, revise", reviewNote?:string
|
||||
%%ax publish: articleText:string, reviewNote?:string -> publishedArticle:string
|
||||
|
||||
outline --> sectionA & sectionB
|
||||
sectionA & sectionB --> assemble --> review{verdict}
|
||||
review -->|approve| publish
|
||||
review -->|revise, max 2| assemble
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [Flow](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow.ts) — complete flow usage
|
||||
- [Mermaid Flow](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-mermaid.ts) — author/serialize a flow as a mermaid diagram
|
||||
- [Auto-Parallel](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-auto-parallel.ts) — auto-parallelization
|
||||
- [Async Map](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-async-map.ts) — async map transforms
|
||||
- [Enhanced Demo](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-enhanced-demo.ts) — instance-based nodes
|
||||
- [Flow as Function](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-to-function.ts) — flow as callable function
|
||||
- [Fluent Builder](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fluent-flow-example.ts) — fluent builder pattern
|
||||
- [Adaptive Provider Balancing](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/typescript/generation/adaptive-balancer.ts) — cost, deadline, reliability, and failover routing
|
||||
|
||||
## Event-Triggered Flows
|
||||
|
||||
An AxFlow is an `AxProgrammable` event target. The runtime maps an event into
|
||||
the Flow's typed initial state and propagates `eventContext`, cancellation, and
|
||||
idempotency metadata to every node. Abandoned branches still use normal Flow
|
||||
cancellation semantics.
|
||||
|
||||
Task-backed MCP tools called by a Flow node register a continuation on the
|
||||
shared event context. `axMCPEventRoutes` observes progress and resumes the Flow
|
||||
on input-required or terminal task notifications.
|
||||
|
||||
For resource-driven wake, discover the endpoint with `inspectCatalog()` and
|
||||
give `AxMCPEventSource` an explicit `resourceSubscriptions` policy. Managed
|
||||
subscriptions reconcile list changes and reconnect separately from the Flow;
|
||||
subscription alone never starts or resumes a Flow.
|
||||
|
||||
UCP lifecycle webhooks use the same continuation boundary through
|
||||
`AxUCPWebhookEventSource`. Correlate on `ucp.checkout` or `ucp.order` only after
|
||||
the signed request has been verified and mapped to application identity.
|
||||
|
||||
Use `eventTarget('id').program(flow).wakeInput(...).resumeInput(...)` when wake
|
||||
and resume events have different shapes. Segment-safe `eventPath` mappings are
|
||||
validated against the Flow signature before any node executes; a declarative
|
||||
`.waitFor(kind, path)` creates the owned continuation consumed by the resume
|
||||
route.
|
||||
|
||||
Reusable `eventInput()` plans are the preferred callback-free boundary.
|
||||
Callback `mapInput` is normalized against the Flow signature before any node
|
||||
runs. In generated hosts, immediate publications dispatch inline; the host uses
|
||||
`nextDueAt()` and `runDue()` for delayed retries, debounce, and continuation
|
||||
expiry.
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not use `new AxFlow(...)` for new code.
|
||||
- Do not execute a node before defining it with `.node()`.
|
||||
- Do not use removed terminal shapers like `.mapOutput()` or `.mo()`.
|
||||
- Do not rely on broad signature inference from arbitrary transform source. Use explicit input/output generics and `.returns()` for the final output contract.
|
||||
- Do not use generic field names like `text`, `result`, `data`, `input`, `output`.
|
||||
- Do not create deep-nested state objects in `.map()`.
|
||||
- Do not create loop conditions that can never change.
|
||||
- Do not add unnecessary dependencies between executes (kills auto-parallelism).
|
||||
- Do not forget to use optional chaining on branch results after `.merge()`.
|
||||
543
.agents/skills/ax-gen/SKILL.md
Normal file
543
.agents/skills/ax-gen/SKILL.md
Normal file
@@ -0,0 +1,543 @@
|
||||
---
|
||||
name: ax-gen
|
||||
description: This skill helps an LLM generate correct AxGen code using @ax-llm/ax. Use when the user asks about ax(), AxGen, generators, forward(), streamingForward(), validation, assertions, streaming assertions, field processors, step hooks, self-tuning, or structured outputs. For MCP clients, transports, prompts, resources, tasks, subscriptions, or authentication use ax-mcp alongside this skill.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# AxGen Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill to generate `AxGen` code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||
|
||||
Use the `ax-mcp` skill when AxGen attaches native MCP clients or consumes MCP
|
||||
prompts, resources, tools, tasks, subscriptions, authentication, or events.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use `ax(...)` factory, not `new AxGen(...)`.
|
||||
- Always pass an AI instance from `ai(...)` as the first argument to `forward()`.
|
||||
- Streaming uses `streamingForward()`, not `forward()` with a stream option.
|
||||
- Use schema validation for field shape and constraints.
|
||||
- Use `addAssert(...)` for whole-output hard invariants with correction retries.
|
||||
- Use `addStreamingAssert(...)` for partial streaming hard invariants with fail-fast per-attempt correction retries.
|
||||
- Use `bestOfN(...)` / `refine(...)` for reward-scored complete outputs.
|
||||
- Step hook mutations are applied at the next step boundary (pending pattern).
|
||||
- `stopFunction` accepts a string or string[] for multiple stop functions.
|
||||
- Multi-step continues until: all outputs filled, stop function called, or `maxSteps` reached.
|
||||
|
||||
## Canonical Pattern
|
||||
|
||||
```typescript
|
||||
import { ai, ax, s } from '@ax-llm/ax';
|
||||
|
||||
const llm = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
});
|
||||
|
||||
// Inline signature
|
||||
const gen = ax('input:string -> output:string, reasoning:string');
|
||||
|
||||
// Reusable signature
|
||||
const sig = s('question:string, context:string[] -> answer:string');
|
||||
const gen2 = ax(sig);
|
||||
|
||||
// With options
|
||||
const gen3 = ax('input -> output', {
|
||||
description: 'A helpful assistant',
|
||||
maxRetries: 3,
|
||||
maxSteps: 10,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const result = await gen.forward(llm, { input: 'Hello world' });
|
||||
console.log(result.output);
|
||||
```
|
||||
|
||||
### Signatures from zod / valibot / arktype
|
||||
|
||||
`ax()` accepts any signature built with `f()`, and `f().input()` / `.output()` accept [Standard Schema v1](https://standardschema.dev) validators directly — per-field or a whole `z.object({...})`:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { ax, f } from '@ax-llm/ax';
|
||||
|
||||
const gen = ax(
|
||||
f()
|
||||
.input(z.object({
|
||||
productName: z.string(),
|
||||
buyerProfile: z.string(),
|
||||
}))
|
||||
.output(z.object({
|
||||
headline: z.string(),
|
||||
recommendation: z.enum(['buy', 'wait', 'skip']),
|
||||
}))
|
||||
.build()
|
||||
);
|
||||
```
|
||||
|
||||
Constraints (`.min()`, `.email()`, `.regex()`) and custom logic (`.refine()`, `.transform()`, `.superRefine()`) execute in the normal validation/retry pipeline — at parse time on complete field values, including at field boundaries during streaming. For cache/internal hints pass companion options: `.input('ctx', z.string(), { cache: true })` or `.output('reasoning', z.string(), { internal: true })`.
|
||||
|
||||
Define tool functions with zod the same way — `fn().arg()` / `.returns()` accept per-argument or whole-object schemas and infer the handler's argument type:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { ax, fn } from '@ax-llm/ax';
|
||||
|
||||
const lookupProduct = fn('lookupProduct')
|
||||
.description('Look up a product by name')
|
||||
.arg(z.object({
|
||||
productName: z.string().min(1),
|
||||
includeSpecs: z.boolean().optional(),
|
||||
}))
|
||||
.returns(z.object({
|
||||
price: z.number(),
|
||||
inStock: z.boolean(),
|
||||
rating: z.number().min(1).max(5),
|
||||
}))
|
||||
.handler(async ({ productName, includeSpecs }) => ({
|
||||
price: 79.99,
|
||||
inStock: true,
|
||||
rating: 4.3,
|
||||
}))
|
||||
.build();
|
||||
|
||||
const result = await gen.forward(llm, { ... }, { functions: [lookupProduct] });
|
||||
```
|
||||
|
||||
## Running AxGen
|
||||
|
||||
### `forward()`
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, { input: '...' });
|
||||
|
||||
// With options
|
||||
const result = await gen.forward(llm, { input: '...' }, {
|
||||
maxRetries: 5,
|
||||
model: 'gpt-5.4-mini',
|
||||
modelConfig: { temperature: 0.9, maxTokens: 1000 },
|
||||
debug: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Live Global Defaults
|
||||
|
||||
`AxGen` respects `axGlobals` for app-wide runtime defaults:
|
||||
|
||||
```typescript
|
||||
import { axGlobals } from '@ax-llm/ax';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
|
||||
const responseCache = new Map<string, any>();
|
||||
|
||||
axGlobals.tracer = trace.getTracer('my-app');
|
||||
axGlobals.debug = true;
|
||||
axGlobals.cachingFunction = async (key, value?) => {
|
||||
if (value !== undefined) {
|
||||
responseCache.set(key, value);
|
||||
return;
|
||||
}
|
||||
return responseCache.get(key);
|
||||
};
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Tracing/logging precedence is: forward options, then generator options, then AI service options, then current `axGlobals`, then built-in defaults.
|
||||
- `abortSignal` from `axGlobals` is merged with local forward signals.
|
||||
- `customLabels` merge from globals to AI service to forward options.
|
||||
- `cachingFunction` and `functionResultFormatter` also fall back to current `axGlobals` when local options do not provide them.
|
||||
|
||||
### `streamingForward()`
|
||||
|
||||
```typescript
|
||||
const stream = gen.streamingForward(llm, { input: 'Write a long story' });
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.delta.output) process.stdout.write(chunk.delta.output);
|
||||
}
|
||||
```
|
||||
|
||||
## Stopping And Cancellation
|
||||
|
||||
```typescript
|
||||
import { AxAIServiceAbortedError } from '@ax-llm/ax';
|
||||
|
||||
const timer = setTimeout(() => gen.stop(), 3_000);
|
||||
|
||||
try {
|
||||
const result = await gen.forward(llm, { topic: 'Long document' }, {
|
||||
abortSignal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof AxAIServiceAbortedError) console.log('Aborted');
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `gen.stop()` gracefully stops multi-step execution at the next step boundary.
|
||||
- `abortSignal` cancels the underlying AI service call immediately.
|
||||
- Catch `AxAIServiceAbortedError` when using either mechanism.
|
||||
|
||||
## Validation, Selection, And Guards
|
||||
|
||||
```typescript
|
||||
import { ax, bestOfN, f } from '@ax-llm/ax';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema validation: output shape and field validity.
|
||||
const gen = ax(
|
||||
f()
|
||||
.input('topic', z.string().min(1))
|
||||
.output('summary', z.string().min(50))
|
||||
.build()
|
||||
);
|
||||
|
||||
// bestOfN: choose the best complete candidate.
|
||||
const selected = bestOfN(gen, {
|
||||
n: 4,
|
||||
rewardFn: ({ prediction }) => prediction.summary.length,
|
||||
});
|
||||
|
||||
// Whole-output assertion: retries with correction feedback.
|
||||
gen.addAssert(
|
||||
(output) => output.summary.includes(topic) || 'Summary must mention the topic.'
|
||||
);
|
||||
|
||||
// Streaming assertion: fail fast on unsafe partial output.
|
||||
gen.addStreamingAssert(
|
||||
'summary',
|
||||
(text) => !text.includes('forbidden'),
|
||||
'Output contains forbidden text'
|
||||
);
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Schema validation retries with parser/constraint feedback.
|
||||
- `addAssert(...)` checks the complete parsed output after validation/processors and retries with correction feedback on failure.
|
||||
- `bestOfN(...)` scores complete candidates and returns the highest reward or first threshold hit.
|
||||
- `refine(...)` runs rounds and can feed reward-derived advice into instruction components between rounds.
|
||||
- `addStreamingAssert(...)` targets a string/code output field and receives partial text so far.
|
||||
- Streaming assertions abort the current stream attempt by throwing `AxStreamingAssertionError`, then feed correction feedback into AxGen retries.
|
||||
|
||||
## Field Processors
|
||||
|
||||
```typescript
|
||||
// Post-processing after generation
|
||||
gen.addFieldProcessor('summary', (value, context) => value.toUpperCase());
|
||||
|
||||
// Streaming field processor (called on each chunk)
|
||||
gen.addStreamingFieldProcessor('content', (partialValue, context) => {
|
||||
console.log(`Received ${partialValue.length} chars`);
|
||||
return partialValue;
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `addFieldProcessor` runs once after the field is fully generated.
|
||||
- `addStreamingFieldProcessor` runs on each streaming chunk for the target field.
|
||||
- Both must return the (possibly transformed) value.
|
||||
|
||||
## Function Calling
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, { question: '...' }, {
|
||||
functions: tools,
|
||||
functionCallMode: 'auto',
|
||||
stopFunction: 'finalAnswer',
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `functionCallMode` can be `'auto'`, `'none'`, or a specific function name to force.
|
||||
- `stopFunction` accepts a string or string[] to halt multi-step on specific function calls.
|
||||
- Multi-step continues until all outputs filled, stop function called, or `maxSteps` reached.
|
||||
|
||||
## Caching
|
||||
|
||||
### Response Caching
|
||||
|
||||
```typescript
|
||||
const gen = ax('question:string -> answer:string', {
|
||||
cachingFunction: async (key, value?) => {
|
||||
if (value !== undefined) {
|
||||
await cache.set(key, value);
|
||||
return;
|
||||
}
|
||||
return await cache.get(key);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Context Caching
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, { question: '...' }, {
|
||||
contextCache: { cacheBreakpoint: 'after-examples' },
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `cachingFunction` acts as a get/set: called with `(key)` to read, `(key, value)` to write.
|
||||
- `contextCache` enables AI provider-level prompt caching for long context.
|
||||
|
||||
## Sampling And Result Picker
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, { question: '...' }, {
|
||||
sampleCount: 3,
|
||||
resultPicker: async (samples) => {
|
||||
// Evaluate each sample and return the index of the best one
|
||||
return bestIndex;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `sampleCount` generates multiple completions in parallel.
|
||||
- `resultPicker` receives all samples and must return the index of the chosen result.
|
||||
|
||||
## Extended Thinking
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, { question: '...' }, {
|
||||
thinkingTokenBudget: 'medium',
|
||||
showThoughts: true,
|
||||
});
|
||||
console.log(result.thought);
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `thinkingTokenBudget` can be `'low'`, `'medium'`, `'high'`, or a number.
|
||||
- Set `showThoughts: true` to include the model's reasoning in `result.thought`.
|
||||
|
||||
## Structured Outputs
|
||||
|
||||
```typescript
|
||||
const sig = f()
|
||||
.input('text', f.string())
|
||||
.output('summary', f.string())
|
||||
.output('metadata', f.json().optional())
|
||||
.useStructured()
|
||||
.build();
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `.useStructured()` asks providers with native support, including OpenAI, Anthropic, and Gemini, for schema-constrained JSON.
|
||||
- Native structured-output schemas list every object property in `required`, set `additionalProperties: false` on objects, and express optional fields as nullable types.
|
||||
- Flexible `json` fields and unshaped `object` fields are sent as JSON-encoded strings for native structured outputs, then parsed back into normal JavaScript values.
|
||||
|
||||
## Step Hooks
|
||||
|
||||
```typescript
|
||||
const result = await gen.forward(llm, values, {
|
||||
stepHooks: {
|
||||
beforeStep: (ctx) => {
|
||||
if (ctx.functionsExecuted.has('complexanalysis')) {
|
||||
ctx.setModel('smart');
|
||||
ctx.setThinkingBudget('high');
|
||||
}
|
||||
},
|
||||
afterStep: (ctx) => {
|
||||
console.log(`Usage: ${ctx.usage.totalTokens} tokens`);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### AxStepContext Read-Only Properties
|
||||
|
||||
- `stepIndex` - current step number
|
||||
- `maxSteps` - configured maximum steps
|
||||
- `isFirstStep` - whether this is the first step
|
||||
- `functionsExecuted` - `Set<string>` of function names called so far
|
||||
- `lastFunctionCalls` - array of the most recent function call results
|
||||
- `usage` - token usage statistics
|
||||
- `state` - current step state
|
||||
|
||||
### AxStepContext Mutators
|
||||
|
||||
- `setModel(model)` - change the model for the next step
|
||||
- `setThinkingBudget(budget)` - adjust thinking budget
|
||||
- `setTemperature(temp)` - adjust temperature
|
||||
- `setMaxTokens(max)` - adjust max output tokens
|
||||
- `setOptions(opts)` - set arbitrary forward options
|
||||
- `addFunctions(fns)` - add functions for the next step
|
||||
- `removeFunctions(names)` - remove functions by name
|
||||
- `stop()` - stop multi-step execution
|
||||
|
||||
Rules:
|
||||
|
||||
- All mutations are pending and applied at the next step boundary.
|
||||
- `beforeStep` runs before each LLM call; `afterStep` runs after.
|
||||
- Use `afterFunctionExecution` to react to specific function results.
|
||||
|
||||
## Self-Tuning
|
||||
|
||||
```typescript
|
||||
// Simple: enable all self-tuning
|
||||
const result = await gen.forward(llm, values, { selfTuning: true });
|
||||
|
||||
// Granular: pick what to tune
|
||||
const result = await gen.forward(llm, values, {
|
||||
selfTuning: {
|
||||
model: true,
|
||||
thinkingBudget: true,
|
||||
functions: [searchWeb, calculate],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `selfTuning: true` enables automatic model and parameter selection.
|
||||
- Granular config allows tuning specific aspects independently.
|
||||
- `selfTuning.functions` provides a pool of functions the tuner may add or remove per step.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { AxGenerateError } from '@ax-llm/ax';
|
||||
|
||||
try {
|
||||
const result = await gen.forward(llm, { input: '...' });
|
||||
} catch (error) {
|
||||
if (error instanceof AxGenerateError) {
|
||||
console.log(error.details.model, error.details.signature);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `AxGenerateError` includes `details` with `model` and `signature` for debugging.
|
||||
- `AxAIServiceAbortedError` is thrown on cancellation via `stop()` or `abortSignal`.
|
||||
|
||||
## Chat Log and Usage
|
||||
|
||||
### getChatLog()
|
||||
|
||||
After any `.forward()` or `streamingForward()` call, `gen.getChatLog()` returns the full normalized chat history — every `ai.chat()` round-trip, including the system prompt, all messages, and the model response. The log is reset at the start of each `.forward()` call. Multi-step generators (with function calls) produce one entry per step.
|
||||
|
||||
```typescript
|
||||
await gen.forward(llm, { question: 'What is 2+2?' });
|
||||
|
||||
for (const entry of gen.getChatLog()) {
|
||||
console.log('model:', entry.model);
|
||||
for (const msg of entry.messages) {
|
||||
console.log(`[${msg.role}]`, msg.content);
|
||||
}
|
||||
console.log('tokens:', entry.modelUsage?.tokens);
|
||||
}
|
||||
```
|
||||
|
||||
Message roles: `system`, `user`, `assistant`, `tool`. Assistant content uses inline XML:
|
||||
- `<think>...</think>` — reasoning/thinking tokens
|
||||
- `<tool_call>\n{...}\n</tool_call>` — tool invocations
|
||||
|
||||
The system message includes a `<tools>` JSON block when functions are present.
|
||||
|
||||
```typescript
|
||||
type AxChatLogMessage =
|
||||
| { role: 'system'; content: string }
|
||||
| { role: 'user'; content: string }
|
||||
| { role: 'assistant'; content: string }
|
||||
| { role: 'tool'; name: string; content: string };
|
||||
|
||||
type AxChatLogEntry = {
|
||||
name?: string;
|
||||
model: string;
|
||||
messages: AxChatLogMessage[];
|
||||
modelUsage?: AxProgramUsage;
|
||||
};
|
||||
|
||||
gen.getChatLog(): readonly AxChatLogEntry[]
|
||||
```
|
||||
|
||||
### getUsage()
|
||||
|
||||
Returns token usage aggregated by `(ai, model)` across all steps. When a provider reports prompt-cache usage, `promptTokens` is the uncached input portion and `cacheReadTokens` / `cacheCreationTokens` carry the cache counters. Reset with `resetUsage()`.
|
||||
|
||||
```typescript
|
||||
const usage = gen.getUsage(); // AxProgramUsage[]
|
||||
console.log(usage[0]?.tokens?.promptTokens);
|
||||
gen.resetUsage();
|
||||
```
|
||||
|
||||
`AxAgent` and `AxFlow` also return flat `AxChatLogEntry[]` logs; composite programs set `entry.name` so callers can filter by node/stage.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [Streaming](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/streaming.ts) — field-by-field streaming
|
||||
- [Best Of N](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/best-of-n.ts) — reward-scored sample selection
|
||||
- [Refine](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/refine.ts) — retry rounds with generated feedback
|
||||
- [Streaming Assert](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/streaming-asserts.ts) — fail-fast partial-output correction
|
||||
- [Structured Output](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/structured_output.ts) — fluent API with validation
|
||||
- [Debug Logging](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/debug-logging.ts) — debug mode and step hooks
|
||||
- [Stop Function](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/stop-function.ts) — stop functions
|
||||
- [Fibonacci](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fibonacci.ts) — streaming with thinking
|
||||
- [Extraction](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/extract.ts) — information extraction
|
||||
- [Multi-Sampling](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/sample-count.ts) — sample count usage
|
||||
|
||||
## Native MCP/UCP
|
||||
|
||||
Use `ax-mcp` for client construction, transports, authentication, catalog and
|
||||
task APIs, subscriptions, event routing, and recording/replay. This section
|
||||
only covers the AxGen attachment boundary.
|
||||
|
||||
Pass live clients directly to constructor or forward options:
|
||||
|
||||
```typescript
|
||||
const gen = ax('question:string -> answer:string', { mcp: [docs, search] });
|
||||
const result = await gen.forward(llm, { question }, {
|
||||
mcpContext: [
|
||||
{ client: 'docs', resource: { uri: 'docs://guide' } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
The model receives native tool definitions. Structured, image, audio, resource-link, embedded-resource, metadata, task, and error results are preserved until the provider adapter maps supported content. Streaming keeps MCP progress/task events separate from Ax output. Never call `toFunction()` for native integration.
|
||||
|
||||
Use `client.inspectCatalog()` when an endpoint is the only configuration. It
|
||||
discovers server-owned tool/prompt names, concrete resource URIs, and URI
|
||||
templates. Event sources require an explicit none/all/URI/selector resource
|
||||
subscription policy and never create a wake route implicitly.
|
||||
|
||||
Under an event target, a required task-backed MCP tool registers the owning
|
||||
`namespace:taskId` continuation automatically. Use `AxMCPEventSource` plus
|
||||
`axMCPEventRoutes` to observe progress and resume the target on
|
||||
`input_required` or a terminal state.
|
||||
|
||||
## Event Targets
|
||||
|
||||
Wrap an AxGen with
|
||||
`eventTarget('id').program(gen).ai(ai).input(...).build()` to invoke it from an
|
||||
explicit `wake` or `resume` route. Use segment-safe `eventPath` selectors;
|
||||
projection and explicit fields are validated against the AxGen signature before
|
||||
invocation. Use `.wakeInput()` and `.resumeInput()` for different action
|
||||
contracts. Streaming targets persist each chunk before optional chunk sinks and
|
||||
persist the final result before final sinks.
|
||||
|
||||
Use a reusable `eventInput().project(...).field(...)` plan when mapping should
|
||||
be callback-free. Callback `mapInput` remains available, but its result is
|
||||
cloned, stripped to declared AxGen inputs, and signature-validated before the
|
||||
first model call; mapper exceptions become non-retryable
|
||||
`event_input_invalid` deliveries.
|
||||
|
||||
## Do Not Generate
|
||||
|
||||
- Do not use `new AxGen(...)` for new code unless explicitly required.
|
||||
- Do not pass raw API keys or config objects where an `ai(...)` instance is expected.
|
||||
- Do not use `forward()` for streaming; use `streamingForward()`.
|
||||
- Do not use streaming assertions as reward/refine mechanisms; they enforce hard partial-output invariants and retry with correction.
|
||||
- Do not mutate step hook context expecting immediate effect; mutations are pending until the next step.
|
||||
- Do not assume multi-step stops after one LLM call; it continues until outputs are filled, a stop function fires, or `maxSteps` is reached.
|
||||
264
.agents/skills/ax-gepa/SKILL.md
Normal file
264
.agents/skills/ax-gepa/SKILL.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
name: ax-gepa
|
||||
description: This skill helps an LLM generate correct AxGEPA optimization code using @ax-llm/ax. Use when the user asks about AxGEPA, GEPA, Pareto optimization, multi-objective prompt tuning, reflective prompt evolution, validationExamples, maxMetricCalls, or optimizing a generator, flow, or agent tree.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# GEPA Optimization Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill to generate GEPA optimization code. Prefer the top-level `optimize(...)` helper for normal code, and use direct `AxGEPA` / `AxBootstrapFewShot` only when the user needs low-level optimizer control.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Use `optimize(program, train, metric, { studentAI, teacherAI, ... })` for normal generator and flow tuning.
|
||||
- Prefer `ai()`, `ax()`, and `flow()` for new code.
|
||||
- Use a strong `teacherAI` and a cheaper `studentAI`.
|
||||
- Pass `validationExamples` when you have a holdout set.
|
||||
- Set `maxMetricCalls` to bound optimizer cost; `optimize(...)` defaults it to `100`.
|
||||
- Use scalar metrics for one objective and object metrics for Pareto optimization.
|
||||
- Apply results with `program.applyOptimization(result.optimizedProgram!)`.
|
||||
- For tree-wide runs, expect `optimizedProgram.componentMap`.
|
||||
- Persist artifacts with `axSerializeOptimizedProgram(...)` and restore them with `axDeserializeOptimizedProgram(...)` so the same flow works in browsers and Node.
|
||||
- `optimize(...)` runs `AxBootstrapFewShot -> AxGEPA` for small starter sets by default, preserving the demos in `result.optimizedProgram.demos`.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- `optimize(...)` and `AxGEPA.compile()` work for a single generator and for tree-aware roots such as flows or agents with registered optimizable descendants.
|
||||
- There is no separate flow-only GEPA optimizer. Use `AxGEPA` for flows too.
|
||||
- The metric may return either `number` or `Record<string, number>`.
|
||||
- Keep metrics deterministic and cheap by default.
|
||||
- Avoid extra LLM calls inside the metric unless the user explicitly wants judge-based evaluation.
|
||||
- If the user needs LLM-as-judge scoring for a non-agent GEPA run, prefer a plain typed `AxGen` evaluator instead of writing a custom judge abstraction.
|
||||
- `maxMetricCalls` must be large enough to cover the initial validation pass over `validationExamples`.
|
||||
- GEPA optimizes generic string components exposed by `getOptimizableComponents()`. If a tree exposes no components, optimization will fail.
|
||||
- Use held-out validation examples for selection. Do not reuse the training set as `validationExamples`.
|
||||
- `result.optimizedProgram` is the easy-to-apply best candidate. `result.paretoFront` is the full trade-off set for multi-objective runs.
|
||||
- Direct `AxGEPA` still has its own `bootstrap` option, but top-level `optimize(...)` composes the existing `AxBootstrapFewShot` optimizer before GEPA instead.
|
||||
|
||||
## Metric Selection
|
||||
|
||||
Choose the evaluation path deliberately:
|
||||
|
||||
- Prefer a deterministic metric when correctness can be read directly from `prediction` and `example`.
|
||||
- Prefer a deterministic metric when cost, latency, recursion depth, or tool count matters.
|
||||
- Use a plain typed `AxGen` evaluator only when the task is genuinely qualitative and hard to score exactly.
|
||||
- For `agent.optimize(...)`, prefer the built-in judge path instead of manually wrapping a judge metric. Normal agent users usually do not need to set `target` or `metric` at all.
|
||||
|
||||
Rule of thumb:
|
||||
|
||||
- `optimize(...)` on `AxGen` or flow: use a metric first, optionally a plain typed `AxGen` evaluator if needed.
|
||||
- `agent.optimize(...)`: use custom `metric` for crisp scoring, otherwise let the built-in judge handle scoring. Add `judgeAI` plus `judgeOptions` only when you want a stronger or separate judge model.
|
||||
|
||||
## Canonical Scalar Pattern
|
||||
|
||||
```typescript
|
||||
import { ai, ax, optimize, AxAIOpenAIModel } from '@ax-llm/ax';
|
||||
|
||||
const student = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
config: { model: AxAIOpenAIModel.GPT54Mini },
|
||||
});
|
||||
|
||||
const teacher = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
config: { model: AxAIOpenAIModel.GPT54 },
|
||||
});
|
||||
|
||||
const classifier = ax(
|
||||
'emailText:string -> priority:class "high, normal, low", rationale:string'
|
||||
);
|
||||
|
||||
const train = [
|
||||
{ emailText: 'URGENT: Server down!', priority: 'high' },
|
||||
{ emailText: 'Weekly newsletter', priority: 'low' },
|
||||
];
|
||||
|
||||
const validation = [
|
||||
{ emailText: 'Invoice overdue', priority: 'high' },
|
||||
{ emailText: 'Lunch plans?', priority: 'low' },
|
||||
];
|
||||
|
||||
const metric = ({ prediction, example }: { prediction: any; example: any }) =>
|
||||
prediction?.priority === example?.priority ? 1 : 0;
|
||||
|
||||
const result = await optimize(classifier, train, metric, {
|
||||
studentAI: student,
|
||||
teacherAI: teacher,
|
||||
numTrials: 12,
|
||||
minibatch: true,
|
||||
minibatchSize: 4,
|
||||
earlyStoppingTrials: 4,
|
||||
sampleCount: 1,
|
||||
validationExamples: validation,
|
||||
maxMetricCalls: 120,
|
||||
});
|
||||
|
||||
classifier.applyOptimization(result.optimizedProgram!);
|
||||
console.log(result.bestScore);
|
||||
```
|
||||
|
||||
## Canonical Pareto Pattern
|
||||
|
||||
```typescript
|
||||
import { ai, flow, optimize, AxAIOpenAIModel } from '@ax-llm/ax';
|
||||
|
||||
const student = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
config: { model: AxAIOpenAIModel.GPT54Mini },
|
||||
});
|
||||
|
||||
const teacher = ai({
|
||||
name: 'openai',
|
||||
apiKey: process.env.OPENAI_APIKEY!,
|
||||
config: { model: AxAIOpenAIModel.GPT54 },
|
||||
});
|
||||
|
||||
const wf = flow<{ emailText: string }>()
|
||||
.n('classifier', 'emailText:string -> priority:class "high, normal, low"')
|
||||
.n(
|
||||
'rationale',
|
||||
'emailText:string, priority:string -> rationale:string "One concise sentence"'
|
||||
)
|
||||
.e('classifier', (state) => ({ emailText: state.emailText }))
|
||||
.e('rationale', (state) => ({
|
||||
emailText: state.emailText,
|
||||
priority: state.classifierResult.priority,
|
||||
}))
|
||||
.r((state) => ({
|
||||
priority: state.classifierResult.priority,
|
||||
rationale: state.rationaleResult.rationale,
|
||||
}));
|
||||
|
||||
const train = [
|
||||
{ emailText: 'URGENT: Server down!', priority: 'high' },
|
||||
{ emailText: 'Weekly newsletter', priority: 'low' },
|
||||
];
|
||||
|
||||
const validation = [
|
||||
{ emailText: 'Invoice overdue', priority: 'high' },
|
||||
{ emailText: 'Lunch plans?', priority: 'low' },
|
||||
];
|
||||
|
||||
const metric = ({ prediction, example }: { prediction: any; example: any }) => {
|
||||
const accuracy = prediction?.priority === example?.priority ? 1 : 0;
|
||||
const rationale = typeof prediction?.rationale === 'string'
|
||||
? prediction.rationale
|
||||
: '';
|
||||
const brevity = rationale.length <= 40 ? 1 : rationale.length <= 80 ? 0.5 : 0.1;
|
||||
return { accuracy, brevity };
|
||||
};
|
||||
|
||||
const result = await optimize(wf, train, metric, {
|
||||
studentAI: student,
|
||||
teacherAI: teacher,
|
||||
numTrials: 16,
|
||||
minibatch: true,
|
||||
minibatchSize: 6,
|
||||
earlyStoppingTrials: 5,
|
||||
sampleCount: 1,
|
||||
validationExamples: validation,
|
||||
maxMetricCalls: 240,
|
||||
});
|
||||
|
||||
for (const point of result.paretoFront) {
|
||||
console.log(point.scores, point.configuration);
|
||||
}
|
||||
|
||||
wf.applyOptimization(result.optimizedProgram!);
|
||||
console.log(result.optimizedProgram?.componentMap);
|
||||
```
|
||||
|
||||
## Metric Patterns
|
||||
|
||||
```typescript
|
||||
// Scalar objective
|
||||
const scalarMetric = ({ prediction, example }) =>
|
||||
prediction.answer === example.answer ? 1 : 0;
|
||||
|
||||
// Multi-objective
|
||||
const multiMetric = ({ prediction, example }) => ({
|
||||
accuracy: prediction.answer === example.answer ? 1 : 0,
|
||||
brevity:
|
||||
typeof prediction?.reasoning === 'string' &&
|
||||
prediction.reasoning.length < 120
|
||||
? 1
|
||||
: 0.2,
|
||||
});
|
||||
```
|
||||
|
||||
- Return plain numbers or plain object literals.
|
||||
- Keep objective names stable across calls.
|
||||
- Prefer normalized scores such as `0..1` so trade-offs are easy to reason about.
|
||||
|
||||
## Result Handling
|
||||
|
||||
```typescript
|
||||
const { optimizedProgram, paretoFront } = result;
|
||||
|
||||
program.applyOptimization(optimizedProgram!);
|
||||
|
||||
// Save for later
|
||||
const saved = JSON.stringify(optimizedProgram);
|
||||
|
||||
// Load later and re-apply
|
||||
const loaded = JSON.parse(saved);
|
||||
program.applyOptimization(loaded);
|
||||
```
|
||||
|
||||
- Single-target runs usually populate both `optimizedProgram.instruction` and `optimizedProgram.componentMap`.
|
||||
- Tree-wide runs rely on `componentMap`, keyed by full component key.
|
||||
- Pareto points expose candidate configs under `point.configuration.componentMap`.
|
||||
|
||||
## Useful Options
|
||||
|
||||
```typescript
|
||||
const optimizer = new AxGEPA({
|
||||
studentAI,
|
||||
teacherAI,
|
||||
numTrials: 20,
|
||||
minibatch: true,
|
||||
minibatchSize: 5,
|
||||
minibatchFullEvalSteps: 5,
|
||||
earlyStoppingTrials: 5,
|
||||
minImprovementThreshold: 0,
|
||||
sampleCount: 1,
|
||||
seed: 42,
|
||||
verbose: true,
|
||||
});
|
||||
```
|
||||
|
||||
- `numTrials`: number of reflection/evolution rounds.
|
||||
- `minibatch`: reduce per-round evaluation cost.
|
||||
- `minibatchSize`: examples per minibatch.
|
||||
- `earlyStoppingTrials`: stop after repeated non-improvement.
|
||||
- `minImprovementThreshold`: reject tiny gains below this threshold.
|
||||
- `seed`: stabilize sampling during demos and tests.
|
||||
|
||||
## Budgeting and Validation
|
||||
|
||||
- Always create distinct `train` and `validationExamples` arrays.
|
||||
- Size `maxMetricCalls` for at least one full validation pass plus several rounds.
|
||||
- If the user wants a strict budget, say so explicitly and set `maxMetricCalls`.
|
||||
- For expensive trees, start with `auto: 'light'` or fewer `numTrials`, then scale up.
|
||||
- GEPA selects among exposed components using measured accept/reject history, not LLM-generated numeric scores. The LLM proposes component text; metrics decide whether to keep it.
|
||||
- Function/tool trace reflection is keyed by stable component IDs where available, so function renames do not break saved candidate maps.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Error about `maxMetricCalls` being too small: increase it until the initial validation pass fits.
|
||||
- Empty or poor Pareto front: verify the metric returns numbers for every example.
|
||||
- No tree optimization effect: ensure child programs are registered under the root and expose optimizable components.
|
||||
- Saved optimization applies only partly: use `program.applyOptimization(...)`, not just `setInstruction(...)`, so `componentMap` reaches the full tree.
|
||||
- Agent target seems too broad: when using `agent.optimize(...)`, set `target: 'actor'`, `'responder'`, `'all'`, or explicit program IDs. The wrapper filters GEPA components to the selected target.
|
||||
|
||||
## Good Example Targets
|
||||
|
||||
- `/Users/vr/src/ax/src/examples/optimize.ts`
|
||||
- `/Users/vr/src/ax/src/examples/gepa.ts`
|
||||
- `/Users/vr/src/ax/src/examples/gepa-flow.ts`
|
||||
- `/Users/vr/src/ax/src/examples/gepa-train-inference.ts`
|
||||
- `/Users/vr/src/ax/src/examples/gepa-quality-vs-speed-optimization.ts`
|
||||
- `/Users/vr/src/ax/src/examples/axagent-gepa-optimization.ts`
|
||||
351
.agents/skills/ax-llm/SKILL.md
Normal file
351
.agents/skills/ax-llm/SKILL.md
Normal file
@@ -0,0 +1,351 @@
|
||||
---
|
||||
name: ax-llm
|
||||
description: This skill helps with using the @ax-llm/ax TypeScript library for building LLM applications. Use when the user asks about ax(), ai(), f(), s(), agent(), flow(), AxGen, AxAgent, AxFlow, signatures, streaming, or mentions @ax-llm/ax.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# Ax Library (@ax-llm/ax) Quick Reference
|
||||
|
||||
Ax is a TypeScript library for building LLM-powered applications with type-safe signatures, streaming support, and multi-provider compatibility.
|
||||
|
||||
> **Detailed skills available:** ax-ai (providers, routing, adaptive balancing), ax-signature (signatures/types), ax-gen (generators), ax-agent (core agents/tools), ax-agent-rlm (agent runtime/RLM/delegation), ax-agent-observability (callbacks/logs/usage), ax-agent-memory-skills (recall and dynamic skill loading), ax-agent-optimize (agent tuning/eval), ax-flow (workflows), ax-gepa (top-level `optimize(...)`, BootstrapFewShot -> GEPA, Pareto optimization).
|
||||
|
||||
## Imports & Factories
|
||||
|
||||
```typescript
|
||||
// Prefer factory functions: ax(), ai(), agent(), flow(); avoid class constructors.
|
||||
import { ax, ai, f, s, fn, agent, flow, AxMemory, AxMCPClient } from '@ax-llm/ax';
|
||||
import { z } from 'zod'; // optional — any Standard Schema v1 library works
|
||||
|
||||
// AI provider
|
||||
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY });
|
||||
|
||||
// Generator (from string signature)
|
||||
const gen = ax('question:string -> answer:string');
|
||||
|
||||
// Generator (from fluent signature)
|
||||
const gen = ax(
|
||||
f()
|
||||
.input('question', f.string('User question'))
|
||||
.output('answer', f.string('AI response'))
|
||||
.build()
|
||||
);
|
||||
|
||||
// Generator (from zod — Standard Schema v1, also works with valibot/arktype)
|
||||
const zodGen = ax(
|
||||
f()
|
||||
.input(z.object({ question: z.string().describe('User question') }))
|
||||
.output(z.object({ answer: z.string().describe('AI response') }))
|
||||
.build()
|
||||
);
|
||||
|
||||
// Reusable signature
|
||||
const sig = s('question:string, context:string[] -> answer:string');
|
||||
|
||||
// Agent
|
||||
const myAgent = agent('userInput:string -> response:string', {
|
||||
name: 'helper',
|
||||
description: 'A helpful assistant',
|
||||
});
|
||||
|
||||
// Flow
|
||||
const wf = flow<{ input: string }, { output: string }>()
|
||||
.node('step1', 'input:string -> output:string')
|
||||
.execute('step1', (state) => ({ input: state.input }))
|
||||
.returns((state) => ({ output: state.step1Result.output }));
|
||||
|
||||
// Function tool — native fluent
|
||||
const tool = fn('search')
|
||||
.description('Search the web')
|
||||
.arg('query', f.string('Search query'))
|
||||
.returns(f.string('Search results'))
|
||||
.handler(({ query }) => searchWeb(query))
|
||||
.build();
|
||||
|
||||
// Function tool — zod schema (Standard Schema v1: also works with valibot, arktype)
|
||||
const zodTool = fn('calculateTax')
|
||||
.description('Calculate tax for an amount')
|
||||
.arg(z.object({
|
||||
amount: z.number().positive().describe('Pre-tax amount in USD'),
|
||||
region: z.enum(['US', 'EU', 'UK']).describe('Tax region'),
|
||||
}))
|
||||
.returns(z.object({ tax: z.number(), total: z.number() }))
|
||||
.handler(async ({ amount }) => ({ tax: amount * 0.1, total: amount * 1.1 }))
|
||||
.build();
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```typescript
|
||||
// Forward (blocking)
|
||||
const result = await gen.forward(llm, { question: 'What is 2+2?' });
|
||||
|
||||
// Streaming
|
||||
for await (const chunk of gen.streamingForward(llm, { question: 'Tell a story' })) {
|
||||
if (chunk.delta.answer) process.stdout.write(chunk.delta.answer);
|
||||
}
|
||||
```
|
||||
|
||||
## Forward Options Quick Reference
|
||||
|
||||
| Goal | Option | Example |
|
||||
|------|--------|---------|
|
||||
| Model override | `model` | `{ model: 'gpt-5.4-mini' }` |
|
||||
| Temperature | `modelConfig.temperature` | `{ modelConfig: { temperature: 0.8 } }` |
|
||||
| Max tokens | `modelConfig.maxTokens` | `{ modelConfig: { maxTokens: 500 } }` |
|
||||
| Retry on failure | `maxRetries` | `{ maxRetries: 3 }` |
|
||||
| Max agent steps | `maxSteps` | `{ maxSteps: 10 }` |
|
||||
| Fail fast | `fastFail` | `{ fastFail: true }` |
|
||||
| Thinking budget | `thinkingTokenBudget` | `{ thinkingTokenBudget: 'medium' }` |
|
||||
| Show thoughts | `showThoughts` | `{ showThoughts: true }` |
|
||||
| Context caching | `contextCache` | `{ contextCache: { cacheBreakpoint: 'after-examples' } }` |
|
||||
| Multi-sampling | `sampleCount` | `{ sampleCount: 5 }` |
|
||||
| Debug logging | `debug` | `{ debug: true }` |
|
||||
| Abort signal | `abortSignal` | `{ abortSignal: controller.signal }` |
|
||||
| Memory | `mem` | `{ mem: new AxMemory() }` |
|
||||
| Stop function | `stopFunction` | `{ stopFunction: 'finalAnswer' }` |
|
||||
| Function mode | `functionCallMode` | `{ functionCallMode: 'auto' }` |
|
||||
|
||||
Global runtime defaults can be set with `axGlobals` and are read live by future AI, AxGen, and AxFlow calls:
|
||||
|
||||
```typescript
|
||||
import { axGlobals, axCreateDefaultColorLogger } from '@ax-llm/ax';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
|
||||
axGlobals.tracer = trace.getTracer('my-app');
|
||||
axGlobals.debug = true;
|
||||
axGlobals.logger = axCreateDefaultColorLogger();
|
||||
```
|
||||
|
||||
Precedence is: per-call options, then explicit instance/program options, then current `axGlobals`, then built-in defaults. `customLabels` merge in that order, and `abortSignal` values are combined so either global or local cancellation works.
|
||||
|
||||
## Memory and Context
|
||||
|
||||
```typescript
|
||||
import { AxMemory } from '@ax-llm/ax';
|
||||
|
||||
const memory = new AxMemory();
|
||||
|
||||
// Multi-turn conversation
|
||||
await gen.forward(llm, { userMessage: 'My name is Alice' }, { mem: memory });
|
||||
const r = await gen.forward(llm, { userMessage: 'What is my name?' }, { mem: memory });
|
||||
```
|
||||
|
||||
## Few-Shot Examples
|
||||
|
||||
```typescript
|
||||
const classifier = ax('reviewText:string -> sentiment:class "positive, negative, neutral"');
|
||||
|
||||
classifier.setExamples([
|
||||
{ reviewText: 'I love this!', sentiment: 'positive' },
|
||||
{ reviewText: 'Terrible.', sentiment: 'negative' },
|
||||
{ reviewText: 'It works.', sentiment: 'neutral' },
|
||||
]);
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Classification
|
||||
|
||||
```typescript
|
||||
const classifier = ax(
|
||||
f()
|
||||
.input('text', f.string())
|
||||
.output('category', f.class(['spam', 'ham', 'uncertain']))
|
||||
.output('confidence', f.number().min(0).max(1))
|
||||
.build()
|
||||
);
|
||||
```
|
||||
|
||||
### Extraction
|
||||
|
||||
```typescript
|
||||
const extractor = ax(
|
||||
f()
|
||||
.input('text', f.string())
|
||||
.output('entities', f.object({
|
||||
people: f.string().array(),
|
||||
organizations: f.string().array(),
|
||||
locations: f.string().array()
|
||||
}))
|
||||
.build()
|
||||
);
|
||||
```
|
||||
|
||||
### Multi-modal (Images)
|
||||
|
||||
```typescript
|
||||
const analyzer = ax(
|
||||
f()
|
||||
.input('image', f.image('Image to analyze'))
|
||||
.input('question', f.string('Question').optional())
|
||||
.output('description', f.string())
|
||||
.output('objects', f.string().array())
|
||||
.build()
|
||||
);
|
||||
|
||||
const result = await analyzer.forward(llm, {
|
||||
image: { mimeType: 'image/jpeg', data: base64Data },
|
||||
question: 'What objects are in this image?'
|
||||
});
|
||||
```
|
||||
|
||||
### Chaining Generators
|
||||
|
||||
```typescript
|
||||
const researcher = ax('topic:string -> research:string, keyFacts:string[]');
|
||||
const writer = ax('research:string, keyFacts:string[] -> article:string');
|
||||
|
||||
const research = await researcher.forward(llm, { topic: 'AGI' });
|
||||
const draft = await writer.forward(llm, { research: research.research, keyFacts: research.keyFacts });
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { AxGenerateError, AxAIServiceError, AxAIServiceAbortedError } from '@ax-llm/ax';
|
||||
|
||||
try {
|
||||
const result = await gen.forward(llm, { input: 'test' });
|
||||
} catch (error) {
|
||||
if (error instanceof AxGenerateError) {
|
||||
console.error('Generation failed:', error.details.model, error.details.signature);
|
||||
} else if (error instanceof AxAIServiceAbortedError) {
|
||||
console.log('Request was aborted');
|
||||
} else if (error instanceof AxAIServiceError) {
|
||||
console.error('AI service error:', error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
import { axCreateDefaultColorLogger, axGlobals } from '@ax-llm/ax';
|
||||
|
||||
const result = await gen.forward(llm, { input: 'test' }, {
|
||||
debug: true,
|
||||
logger: axCreateDefaultColorLogger(),
|
||||
// OpenTelemetry
|
||||
tracer: openTelemetryTracer,
|
||||
meter: openTelemetryMeter,
|
||||
});
|
||||
|
||||
// Or set live app-wide defaults for future calls:
|
||||
axGlobals.tracer = openTelemetryTracer;
|
||||
axGlobals.meter = openTelemetryMeter;
|
||||
```
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Use the `ax-mcp` skill for the complete native client, transport,
|
||||
authentication, catalog, task, subscription, event, and replay workflow.
|
||||
|
||||
```typescript
|
||||
import { AxMCPClient, agent } from '@ax-llm/ax';
|
||||
import { AxMCPStdioTransport } from '@ax-llm/ax-tools';
|
||||
|
||||
// Stdio transport (local MCP server)
|
||||
const transport = new AxMCPStdioTransport({
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-memory'],
|
||||
});
|
||||
|
||||
const mcpClient = new AxMCPClient(transport, { namespace: 'memory' });
|
||||
|
||||
// Native MCP context is initialized once and inherited by all agent stages.
|
||||
const myAgent = agent('userMessage:string -> response:string', {
|
||||
mcp: mcpClient,
|
||||
functionDiscovery: true,
|
||||
contextFields: [],
|
||||
});
|
||||
|
||||
const result = await myAgent.forward(llm, { userMessage: 'Remember this.' });
|
||||
await mcpClient.close(); // caller-owned clients remain caller-owned
|
||||
```
|
||||
|
||||
### HTTP Transport (Remote MCP)
|
||||
|
||||
```typescript
|
||||
import { AxMCPStreamableHTTPTransport } from '@ax-llm/ax';
|
||||
|
||||
const transport = new AxMCPStreamableHTTPTransport('https://remote.example/mcp', {
|
||||
headers: { 'x-pd-project-id': projectId },
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
});
|
||||
```
|
||||
|
||||
### Native MCP and UCP behavior
|
||||
|
||||
- Pass `mcp` and `ucp` to AxGen, streaming AxGen, chat, AxAgent, AxFlow, optimization, or evaluation options.
|
||||
- Use `mcpContext` to inject attributed prompts/resources before the first model call.
|
||||
- Use `mcpInheritance: 'all' | 'none' | string[]` to restrict child programs.
|
||||
- Tool calls retain raw MCP content, metadata, errors, tasks, and protocol provenance in memory.
|
||||
- AxAgent exposes native modules as `mcp.<namespace>` and `ucp.<namespace>`.
|
||||
- `inspectCatalog()` discovers tool/prompt names, concrete resources, and URI
|
||||
templates from only an endpoint. Resource event sources default to no
|
||||
subscriptions and require an explicit all/URI/selector policy.
|
||||
- `toFunction()` remains a compatibility adapter only; native Ax execution never uses it.
|
||||
- Live optimization is rejected by default. Use recording/replay or explicitly opt into live MCP evaluation.
|
||||
|
||||
```typescript
|
||||
const catalog = await mcpClient.inspectCatalog();
|
||||
const tools = catalog.tools;
|
||||
const prompts = await mcpClient.listPrompts();
|
||||
const resource = await mcpClient.readResource('docs://guide');
|
||||
const tasks = await mcpClient.listTasks();
|
||||
```
|
||||
|
||||
### Function Overrides
|
||||
|
||||
```typescript
|
||||
const mcpClient = new AxMCPClient(transport, {
|
||||
functionOverrides: [
|
||||
{ name: 'search_documents', updates: { name: 'findDocs', description: 'Search docs' } }
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Type Reference
|
||||
|
||||
```typescript
|
||||
class AxGen<IN, OUT> {
|
||||
forward(ai: AxAIService, values: IN, options?: AxProgramForwardOptions): Promise<OUT>;
|
||||
streamingForward(ai: AxAIService, values: IN, options?: AxProgramStreamingForwardOptions): AsyncGenerator<{ delta: Partial<OUT> }>;
|
||||
setExamples(examples: Array<Partial<IN & OUT>>): void;
|
||||
addAssert(fn: (output: OUT) => boolean | string | undefined | Promise<boolean | string | undefined>, message?: string): void;
|
||||
addStreamingAssert(field: keyof OUT, fn: (chunk: string, done?: boolean) => boolean | string | undefined | Promise<boolean | string | undefined>, message?: string): void;
|
||||
addFieldProcessor(field: keyof OUT, fn: (value: any) => any): void;
|
||||
addStreamingFieldProcessor(field: keyof OUT, fn: (chunk: string, ctx: any) => void): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
class AxAgent<IN, OUT> {
|
||||
forward(ai: AxAIService, values: IN, options?: AxAgentOptions): Promise<OUT>;
|
||||
streamingForward(ai: AxAIService, values: IN, options?: AxAgentOptions): AsyncGenerator<{ delta: Partial<OUT> }>;
|
||||
getFunction(): AxFunction;
|
||||
}
|
||||
|
||||
class AxFlow<IN, OUT> {
|
||||
node(name: string, signature: string | AxSignature): AxFlow;
|
||||
execute(name: string, mapper: (state) => any): AxFlow;
|
||||
returns(mapper: (state) => OUT): AxFlow;
|
||||
forward(ai: AxAIService, values: IN): Promise<OUT>;
|
||||
}
|
||||
```
|
||||
|
||||
## Event-Driven Programs
|
||||
|
||||
Use `eventRuntime()` when notifications, webhooks, timers, or remote tasks
|
||||
should wake or resume an Ax program. Sources publish into an inbox; explicit
|
||||
routes choose `observe`, `invalidate`, `wake`, or `resume`. Event payloads are
|
||||
never inserted as user messages automatically. See `ax-event-runtime.md`.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [Standard Schema (zod)](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/standard-schema.ts) — zod with f() and fn()
|
||||
- [Chat](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/chat.ts) — multi-turn conversation
|
||||
- [Marketing](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/marketing.ts) — product use case
|
||||
- [MCP Integration](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/mcp-client-memory.ts) — MCP integration
|
||||
425
.agents/skills/ax-mcp/SKILL.md
Normal file
425
.agents/skills/ax-mcp/SKILL.md
Normal file
@@ -0,0 +1,425 @@
|
||||
---
|
||||
name: ax-mcp
|
||||
description: This skill helps an LLM build correct native Model Context Protocol integrations with @ax-llm/ax. Use when the user asks about AxMCPClient, MCP transports, tools, prompts, resources, subscriptions, tasks, sampling, elicitation, roots, authentication, OAuth, MCP Apps, recording/replay, or MCP integration with AxGen, AxAgent, AxFlow, chat, optimization, and AxEventRuntime.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# Native MCP With Ax
|
||||
|
||||
Use MCP as a live protocol client, not as a function-conversion utility. Keep
|
||||
the client, session, catalogs, raw content, tasks, notifications, identity
|
||||
policy, and cancellation context intact through Ax execution.
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
- Pass clients through `mcp`; do not put them in `functions`.
|
||||
- Never use `toFunction()` for native integration. It is a lossy compatibility
|
||||
adapter for old applications only.
|
||||
- Give every client a stable, unique `namespace`.
|
||||
- Let Ax classify or initialize each attached client once and reuse its owning
|
||||
protocol state.
|
||||
- Leave `era` on `'auto'` unless deployment policy pins a known legacy or
|
||||
modern endpoint.
|
||||
- Close caller-owned clients explicitly.
|
||||
- Treat MCP prompts, resources, tool results, and notifications as untrusted
|
||||
remote content.
|
||||
- Apply `authorizeToolCall` before side-effecting tools execute.
|
||||
- Do not infer tenant or account identity from an MCP session. Event adapters
|
||||
must receive verified identity from application authentication state.
|
||||
- Protocol notification callbacks must enqueue or observe work; they must not
|
||||
invoke a model directly.
|
||||
- Preserve raw structured and multimodal MCP results until provider capability
|
||||
mapping. Do not pre-flatten results to text.
|
||||
|
||||
## Choose A Transport
|
||||
|
||||
- Use `AxMCPStreamableHTTPTransport` for current remote MCP servers.
|
||||
- Use `AxMCPHTTPSSETransport` only for legacy HTTP/SSE servers.
|
||||
- Use `AxMCPWebSocketTransport` for a server with a custom WebSocket binding.
|
||||
- Use `AxMCPStdioTransport` from `@ax-llm/ax-tools` for local Node processes.
|
||||
- Use a caller-defined `AxMCPTransport` for application-owned bindings.
|
||||
|
||||
```ts
|
||||
import {
|
||||
AxMCPClient,
|
||||
AxMCPStreamableHTTPTransport,
|
||||
axMCPBearerAuthentication,
|
||||
} from '@ax-llm/ax';
|
||||
|
||||
const transport = new AxMCPStreamableHTTPTransport(
|
||||
'https://mcp.example.com/mcp',
|
||||
{
|
||||
authentication: axMCPBearerAuthentication(
|
||||
() => process.env.MCP_ACCESS_TOKEN!
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const docs = new AxMCPClient(transport, {
|
||||
namespace: 'docs',
|
||||
maxConcurrency: 4,
|
||||
authorizeToolCall: async ({ tool }) =>
|
||||
tool.annotations?.destructiveHint !== true,
|
||||
});
|
||||
```
|
||||
|
||||
## Protocol Eras
|
||||
|
||||
The TypeScript client supports two wire models through one API:
|
||||
|
||||
- **Legacy (`2025-11-25`)** initializes a stateful session, sends the
|
||||
initialized notification, uses resource subscribe/unsubscribe requests, and
|
||||
may resume GET/SSE with `Last-Event-ID`.
|
||||
- **Modern (`2026-07-28`)** is stateless. It probes `server/discover`, sends
|
||||
protocol metadata and routing headers on every request, listens through a
|
||||
long-running `subscriptions/listen` POST, and uses Tasks v2.
|
||||
|
||||
Automatic classification is the default and can be persisted with `eraStore`.
|
||||
Pin `era: 'legacy'` or `era: 'modern'` only when the endpoint contract is
|
||||
known. `getEra()` reports the classified era after initialization.
|
||||
|
||||
```ts
|
||||
const modern = new AxMCPClient(transport, {
|
||||
namespace: 'inventory',
|
||||
era: 'auto',
|
||||
readCache: true,
|
||||
logLevel: 'info',
|
||||
});
|
||||
|
||||
const discovery = await modern.discover();
|
||||
console.log(modern.getEra(), discovery.supportedVersions);
|
||||
```
|
||||
|
||||
`discover()` performs initialization/classification but returns only for a
|
||||
modern endpoint. For era-neutral application code, use `inspectCatalog()`; it
|
||||
works in both eras.
|
||||
|
||||
For local stdio:
|
||||
|
||||
```ts
|
||||
import { AxMCPClient } from '@ax-llm/ax';
|
||||
import { AxMCPStdioTransport } from '@ax-llm/ax-tools';
|
||||
|
||||
const stdio = new AxMCPStdioTransport({
|
||||
command: 'node',
|
||||
args: ['./server.mjs'],
|
||||
});
|
||||
const local = new AxMCPClient(stdio, { namespace: 'local' });
|
||||
```
|
||||
|
||||
`AxMCPStdioTransport` owns its child process. After `local.close()`, also call
|
||||
`stdio.terminate()` until the stdio transport exposes the common `close()`
|
||||
lifecycle directly.
|
||||
|
||||
## Attach MCP To AxGen
|
||||
|
||||
Attach clients in constructor or forward options. Per-call options override
|
||||
instance defaults.
|
||||
|
||||
```ts
|
||||
const gen = ax('question:string -> answer:string', { mcp: docs });
|
||||
|
||||
const result = await gen.forward(llm, { question }, {
|
||||
mcpContext: [
|
||||
{ client: 'docs', resource: { uri: 'docs://guide' } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
`mcpContext` resolves selected prompts or resources before the first model
|
||||
call and adds attributed, untrusted context. Native tool calls retain client
|
||||
identity and raw MCP results in memory. `streamingForward()` keeps Ax output
|
||||
streaming separate from MCP progress and task events.
|
||||
|
||||
## Attach MCP To AxAgent
|
||||
|
||||
```ts
|
||||
const assistant = agent('query:string -> answer:string', {
|
||||
mcp: [docs, search],
|
||||
mcpInheritance: 'all',
|
||||
functionDiscovery: true,
|
||||
contextFields: [],
|
||||
});
|
||||
```
|
||||
|
||||
Agents expose native modules under `mcp.<namespace>`:
|
||||
|
||||
```text
|
||||
mcp.docs.tools.<tool>
|
||||
mcp.docs.prompts.list()
|
||||
mcp.docs.prompts.get(name, args)
|
||||
mcp.docs.resources.list()
|
||||
mcp.docs.resources.templates()
|
||||
mcp.docs.resources.read(uri)
|
||||
mcp.docs.resources.subscribe(uri)
|
||||
mcp.docs.resources.unsubscribe(uri)
|
||||
mcp.docs.tasks.get(taskId)
|
||||
mcp.docs.tasks.cancel(taskId)
|
||||
mcp.docs.complete(...)
|
||||
```
|
||||
|
||||
Modern modules also expose task input/update behavior through the client.
|
||||
`tasks.list()` and `tasks.result()` are legacy task-draft compatibility APIs and
|
||||
reject modern servers.
|
||||
|
||||
Use `mcpInheritance: 'all'`, `'none'`, or a namespace allowlist. The resulting
|
||||
live execution context propagates through Agent stages, `llmQuery`, RLM, and
|
||||
child programs. Large catalogs participate in Agent discovery; do not copy
|
||||
their tools into an inline `functions` array.
|
||||
|
||||
## AxFlow And High-Level Chat
|
||||
|
||||
Pass `mcp` in Flow defaults or forward options. Nested nodes inherit the same
|
||||
execution context unless `mcpInheritance` restricts it. Parallel nodes share
|
||||
the client while respecting its concurrency limit and abort signal.
|
||||
|
||||
Use `axMCPChat(ai, request, { mcp })` for a high-level non-streaming native MCP
|
||||
tool loop. Do not build a second ad-hoc tool dispatcher around `ai.chat()`.
|
||||
|
||||
## Catalogs And Raw Operations
|
||||
|
||||
An endpoint is only the server address. The server owns tool names, prompt
|
||||
names, resource names, resource URIs, and URI templates. Discover one cloned
|
||||
snapshot before asking users to configure identifiers:
|
||||
|
||||
```ts
|
||||
const catalog = await docs.inspectCatalog();
|
||||
|
||||
console.log(catalog.tools);
|
||||
console.log(catalog.prompts);
|
||||
console.log(catalog.resources);
|
||||
console.log(catalog.resourceTemplates);
|
||||
|
||||
const prompt = await docs.getPrompt('review', { topic: 'MCP' });
|
||||
const resource = await docs.readResource('docs://guide');
|
||||
const completion = await docs.complete(reference, argument);
|
||||
```
|
||||
|
||||
`inspectCatalog({ refresh: true })` forces fresh bounded pagination. Snapshot
|
||||
mutation cannot change the live client. List-change notifications refresh the
|
||||
catalog revision, and native Ax model steps rebuild tool definitions when that
|
||||
revision changes. Concrete resources can be selected immediately. URI
|
||||
templates are discoverable but never expanded automatically; applications
|
||||
construct an authorized concrete URI and may use MCP completion to suggest
|
||||
argument values.
|
||||
|
||||
## Multi Round-Trip Requests
|
||||
|
||||
Modern tool calls, prompt reads, and resource reads may return
|
||||
`resultType: 'input_required'`. Ax fulfills the embedded roots, sampling, or
|
||||
elicitation requests through the same host handlers used by legacy server
|
||||
requests, then repeats the original operation with the latest input responses
|
||||
and byte-exact `requestState`.
|
||||
|
||||
The generated Python, Java, C++, Go, and Rust clients fulfill roots and
|
||||
host-callback elicitation in modern MRTR rounds. They advertise elicitation only
|
||||
when a real handler is installed, never advertise sampling, and reject a truthy
|
||||
sampling option during initialization. Legacy inbound elicitation and MRTR
|
||||
sampling remain TypeScript-only.
|
||||
|
||||
Configure only handlers the host can enforce. Ax limits the loop to five input
|
||||
rounds by default; use `maxInputRounds` for a stricter policy. A missing handler
|
||||
or exhausted round limit is a protocol error. The tool concurrency slot stays
|
||||
held throughout all rounds.
|
||||
|
||||
## Tasks, Progress, And Cancellation
|
||||
|
||||
Modern Tasks v2 are server-directed. `callTool()` auto-awaits an unsolicited
|
||||
task by default, so Ax tool bindings keep returning the final tool result. Use
|
||||
`callToolOutcome()` or `taskHandling: 'expose'` when the application needs the
|
||||
task handle, and answer `input_required` work with `provideTaskInput()`:
|
||||
|
||||
```ts
|
||||
const outcome = await docs.callToolOutcome('reindex', { scope: 'all' });
|
||||
if (outcome.kind === 'task') {
|
||||
const task = await docs.getTask(outcome.task.taskId);
|
||||
if (task.status === 'input_required') {
|
||||
await docs.provideTaskInput(task.taskId, {
|
||||
approval: { action: 'accept', content: { approved: true } },
|
||||
});
|
||||
}
|
||||
if (task.status === 'working') await docs.cancelTask(task.taskId);
|
||||
}
|
||||
```
|
||||
|
||||
Use `subscribeTaskStatus` or `subscribeEvents` for observation. Keep polling
|
||||
available because task notifications are optional. Pass Ax abort signals
|
||||
through program execution; never blindly replay a tool call after an uncertain
|
||||
post-side-effect failure.
|
||||
|
||||
`callToolTask()`, `listTasks()`, and `getTaskResult()` remain functional only
|
||||
for legacy task-draft servers and are deprecated. Modern completed results and
|
||||
errors are embedded in `tasks/get`.
|
||||
|
||||
## Subscriptions And Event-Driven Agents
|
||||
|
||||
Use `AxMCPEventSource` with `AxEventRuntime`. A subscription callback only
|
||||
publishes an event into the inbox. Explicit routes decide whether to observe,
|
||||
invalidate, wake, or resume.
|
||||
|
||||
```ts
|
||||
const source = new AxMCPEventSource({
|
||||
client: docs,
|
||||
resourceSubscriptions: {
|
||||
select: (resource) =>
|
||||
resource.mimeType === 'text/markdown' &&
|
||||
resource.name === 'Engineering guide',
|
||||
},
|
||||
identity: { tenantId: 'tenant-1' },
|
||||
trust: 'authenticated',
|
||||
});
|
||||
|
||||
const runtime = eventRuntime({
|
||||
allowVolatile: true,
|
||||
sources: [source],
|
||||
routes: [
|
||||
...axMCPEventRoutes({ client: docs }),
|
||||
eventRoute('guide-updated')
|
||||
.types('mcp.resource.updated')
|
||||
.authenticated()
|
||||
.wake(
|
||||
eventTarget('reviewer')
|
||||
.program(reviewer)
|
||||
.ai(llm)
|
||||
.input((input) =>
|
||||
input.field('uri', eventPath.data('uri'))
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build(),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Safe defaults are:
|
||||
|
||||
- omitted resource policy -> subscribe to no resources
|
||||
- `'all'` -> explicitly subscribe to all discovered concrete resources
|
||||
- URI array -> explicitly subscribe to application-constructed concrete URIs
|
||||
- selector -> choose concrete resources by name, URI, description, MIME type,
|
||||
annotations, or the surrounding catalog
|
||||
- catalog changes -> `invalidate`
|
||||
- progress and logging -> `observe`
|
||||
- resource updates -> no implicit wake
|
||||
- `input_required` and terminal task states -> resume the owning continuation
|
||||
|
||||
The signature-aware input plan is the data boundary. Raw event data remains in
|
||||
`eventContext`; only fields selected with segment-safe `eventPath` descriptors
|
||||
become program inputs. Use multiple matching routes to fan one notification out
|
||||
to multiple Agents with independent authorization and run records.
|
||||
|
||||
Managed sources refresh and diff their selection after
|
||||
`notifications/resources/list_changed`. They keep the prior selection if a
|
||||
selector throws, retain successful wire transitions after a partial failure,
|
||||
and retry incomplete work on the next change or reconnect. The client tracks a
|
||||
separate logical owner for manual subscriptions, every source, and restored
|
||||
intent: only the first owner sends `resources/subscribe`, and only the last
|
||||
release sends `resources/unsubscribe`. Closing a source cannot break another
|
||||
owner. Closing the client terminates all ownership and transport state.
|
||||
|
||||
Listening is era-aware. Legacy clients maintain resource subscriptions with
|
||||
`resources/subscribe` and resume a GET/SSE stream with `Last-Event-ID` when the
|
||||
server supports it. Modern clients place catalog interests, concrete resource
|
||||
URIs, and known task IDs in `subscriptions/listen`; changes restart that POST
|
||||
stream with a fresh request ID and no resume header. In both eras,
|
||||
`startListening()` is nonblocking and returns a handle with `ready`, `done`,
|
||||
and `close()`.
|
||||
|
||||
For the detailed lifecycle and troubleshooting guide, read
|
||||
`docs/MCP_SUBSCRIPTIONS.md` and use the checked-in six-language MCP examples.
|
||||
|
||||
## Server-Initiated Requests
|
||||
|
||||
Configure handlers on `AxMCPClient` when advertising the corresponding client
|
||||
capability:
|
||||
|
||||
- `sampling` for `sampling/createMessage`
|
||||
- `elicitation` for form or URL elicitation
|
||||
- `roots` for `roots/list`
|
||||
- `onProgress`, `onLoggingMessage`, and `onTaskStatus` for observation
|
||||
|
||||
Do not advertise a client capability without a working host handler and policy.
|
||||
|
||||
## Authentication And OAuth
|
||||
|
||||
For simple authentication, compose `axMCPBearerAuthentication`,
|
||||
`axMCPBasicAuthentication`, `axMCPAPIKeyAuthentication`,
|
||||
`axMCPHMACAuthentication`, or a caller-defined strategy in the HTTP transport.
|
||||
|
||||
Use the transport `oauth` option for protected-resource discovery, PKCE,
|
||||
client metadata or dynamic registration, refresh, challenge-driven scope
|
||||
step-up, DPoP, PAR/JAR/RAR, mTLS, revocation, introspection, client credentials,
|
||||
or enterprise-managed authorization. Supply persistent token and registration
|
||||
stores in distributed deployments. Never serialize tokens into Ax program or
|
||||
event state.
|
||||
|
||||
Generated Python, Java, C++, Go, and Rust transports implement the portable
|
||||
OAuth middle tier: RFC 9728 well-known and challenge discovery, RFC 8414/OIDC
|
||||
authorization-server metadata, PKCE S256 authorization-code exchange,
|
||||
RFC 8707 resource binding, refresh with a 60-second skew, client credentials,
|
||||
and RFC 9207 `iss`. Configure the language's `AxMCPOAuthOptions` with
|
||||
`clientId`, an endpoint-keyed `tokenStore`, `onAuthCode`, and `requireIss`.
|
||||
The host callback receives an authorization URL and returns `code`, `state`,
|
||||
and `iss`; it owns browser or headless interaction. `none` and
|
||||
`client_secret_post` are the only generated-port client authentication modes.
|
||||
|
||||
Do not suggest TypeScript-only DPoP, CIMD/DCR, JAR, PAR, RAR, mTLS,
|
||||
revocation/introspection, JWT validation, or enterprise-managed authorization
|
||||
for a generated-language client. Use `npm run test:mcp-oauth` as the
|
||||
credential-free cross-language gate. See `docs/MCP_UCP.md` for compact
|
||||
per-language configuration snippets.
|
||||
|
||||
Keep SSRF protection enabled for remote discovery and redirect handling. Relax
|
||||
loopback or HTTP restrictions only for controlled local development.
|
||||
|
||||
For checked-in Streamable HTTP examples, set `AX_MCP_ENDPOINT`. A localhost
|
||||
TypeScript demo must opt in explicitly with
|
||||
`ssrfProtection: { allowHTTP: true, allowLoopback: true }`; never copy that
|
||||
configuration to a remote endpoint. Generated examples use their equivalent
|
||||
`requireHttps` / `allowLocalhost` / `allowPrivateNetworks` fields only for
|
||||
`127.0.0.1`.
|
||||
|
||||
For a real local check, run `src/examples/mcp-event-demo-server.ts`, set
|
||||
`AX_MCP_ENDPOINT=http://127.0.0.1:3001/mcp`, and trigger
|
||||
`/control/resource` or `/control/task/complete`. The mandatory credential-free
|
||||
matrix is `npm run test:mcp-events:generated`; provider-backed examples are
|
||||
advisory and require their documented API key.
|
||||
|
||||
## MCP Apps And Extensions
|
||||
|
||||
Negotiate official extensions through client capabilities. Use
|
||||
`AxMCPAppBridge` for MCP App resources and host messages; enforce CSP,
|
||||
permissions, visibility, allowed tools, and untrusted model-context policy.
|
||||
Do not render arbitrary HTML returned from a normal tool result as an MCP App.
|
||||
|
||||
## Recording, Replay, And Evaluation
|
||||
|
||||
Wrap a real transport with `AxMCPRecordingTransport` to capture deterministic
|
||||
protocol interactions. Use `AxMCPReplayTransport` for tests, optimization, and
|
||||
evaluation. Live MCP evaluation is rejected by default because repeated model
|
||||
runs could repeat external side effects; opt in only deliberately.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- Use a local deterministic protocol server or replay transport.
|
||||
- Assert namespace and tool collisions fail before model execution.
|
||||
- Test raw text, image, audio, resource-link, embedded-resource, metadata,
|
||||
task, and error results.
|
||||
- Test catalog changes during a multi-step run.
|
||||
- Test authorization denial before transport execution.
|
||||
- Test subscription reconnect and logical resubscription.
|
||||
- Test anonymous events cannot match authenticated routes.
|
||||
- Test terminal task events resume only the owning identity and correlation.
|
||||
- Test cancellation and uncertain outcomes without duplicate side effects.
|
||||
- Close clients, listening handles, runtimes, and local servers in `finally`.
|
||||
|
||||
Generated transports currently supervise legacy long-lived GET/SSE connections
|
||||
and resume with `Last-Event-ID` when available. Generated event runtimes remain
|
||||
host-driven: schedule delayed work with `nextDueAt()` and `runDue()`. Rust hosts
|
||||
also call `AxMCPEventSource.poll()` to drain protocol callbacks on the host
|
||||
thread. Close the source/runtime before the caller-owned client so unsubscribe
|
||||
and cancellation messages can still be sent.
|
||||
|
||||
For generic inbox, continuation, store, and sink behavior, use the
|
||||
`ax-event-runtime` skill. For program-specific behavior, combine this skill with
|
||||
`ax-gen`, `ax-agent`, or `ax-flow`.
|
||||
125
.agents/skills/ax-playbook/SKILL.md
Normal file
125
.agents/skills/ax-playbook/SKILL.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: ax-playbook
|
||||
description: This skill helps an LLM generate correct playbook code using @ax-llm/ax. Use when the user asks about playbook(), AxPlaybook, context playbooks, evolving context, ACE / Agentic Context Engineering, agent.playbook(), or growing/applying task knowledge offline and online with evolve() and update().
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# Playbook Codegen Rules (@ax-llm/ax)
|
||||
|
||||
Use this skill to generate context-playbook code. A playbook grows an evolving body of task knowledge and renders it into a program's context. The evolution engine (ACE — Agentic Context Engineering) is hidden behind `playbook(...)`, exactly as `optimize(...)` hides its optimizer. Prefer the `playbook(...)` concept; only reach for `AxACE` directly when the user explicitly wants the low-level engine.
|
||||
|
||||
## Use These Defaults
|
||||
|
||||
- Create with `playbook(program, { studentAI, teacherAI? })`; it returns an `AxPlaybook` handle.
|
||||
- Grow offline with `await pb.evolve(examples, metric)` — returns `{ bestScore, playbook }`.
|
||||
- Grow online with `await pb.update({ example, prediction, feedback })` — no metric needed.
|
||||
- Apply with `pb.applyTo(program)` (defaults to the bound program).
|
||||
- Persist with `pb.toJSON()` and restore with `playbook(program, opts).load(snapshot)`.
|
||||
- Inspect with `pb.render()` (markdown) and `pb.getState()` (`{ playbook, artifact }`).
|
||||
- For agents use `agent.playbook({ target: 'actor' | 'responder' })`; default target is `'actor'`.
|
||||
- Use a cheaper `studentAI` to run the program and an optional stronger `teacherAI` to reflect/curate.
|
||||
- Prefer `ai()`, `ax()`, and `agent()` for new code.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- `playbook(...)` binds to an `AxGen` program; `evolve`/`update` need that program's signature.
|
||||
- `evolve()` returns only `{ bestScore, playbook }`. There is no Pareto front and no `optimizedProgram` — that is `optimize(...)`'s shape, not a playbook's.
|
||||
- `update({ example, prediction, feedback })` requires the full `{ example, prediction }`; `example` must match the program's input fields (plus any expected output). Do not pass bare input fields at the top level.
|
||||
- `update()` works without a prior `evolve()`/`load()` — the handle hydrates lazily on first use.
|
||||
- `applyTo()` injects a `## Context Playbook` block into the program description; calling it repeatedly recomposes from the original base (no stacking).
|
||||
- Keep the offline `metric` deterministic and cheap, like a GEPA metric.
|
||||
- A playbook is plain JSON. Persist `pb.toJSON()` and `load(...)` it into a fresh program for production.
|
||||
- The playbook engine, construction-time agent attachment, failure harvesting,
|
||||
and verified agent evolution are available in TypeScript and the generated
|
||||
Python, Java, C++, Go, and Rust packages. Use each package's native casing and
|
||||
callback types.
|
||||
|
||||
## Offline Pattern (evolve)
|
||||
|
||||
```typescript
|
||||
import { type AxMetricFn, ai, ax, playbook } from '@ax-llm/ax';
|
||||
|
||||
const program = ax('review:string -> sentiment:class "positive, negative"');
|
||||
const studentAI = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||
const metric: AxMetricFn = ({ prediction, example }) =>
|
||||
(prediction as any).sentiment === (example as any).sentiment ? 1 : 0;
|
||||
|
||||
const pb = playbook(program, { studentAI, maxEpochs: 2 });
|
||||
const { bestScore } = await pb.evolve(train, metric);
|
||||
pb.applyTo(program);
|
||||
```
|
||||
|
||||
## Online Pattern (update)
|
||||
|
||||
```typescript
|
||||
// After a real run, feed the outcome back so the playbook keeps learning.
|
||||
await pb.update({
|
||||
example: { review: 'Five stars, would buy again.' },
|
||||
prediction: { sentiment: 'negative' },
|
||||
feedback: 'WRONG: enthusiastic praise is positive.',
|
||||
});
|
||||
pb.applyTo(program);
|
||||
```
|
||||
|
||||
## Persist And Restore
|
||||
|
||||
```typescript
|
||||
const snapshot = pb.toJSON(); // { playbook, artifact } — plain JSON
|
||||
// later, in another process / a production program instance:
|
||||
playbook(prodProgram, { studentAI }).load(snapshot).applyTo(prodProgram);
|
||||
```
|
||||
|
||||
## Agents
|
||||
|
||||
`a.playbook({ target })` returns an agent-aware `AxAgentPlaybook` (the stage `AxPlaybook` handle plus an agent-level `evolve`). The one playbook the agent renders into its prompt grows three ways:
|
||||
|
||||
- Continuous (trust): the construction-time `playbook` option (see `ax-agent`) harvests each run's failures automatically — no dataset.
|
||||
- On-demand (trust): `apb.update({ example, prediction, feedback })`.
|
||||
- Batch verified (proof): `apb.evolve(dataset, options)` runs the full agent over a task set, mines failure clusters, and proposes one playbook bullet per weakness; with `verify` (default on) it keeps a bullet only if held-in improves AND the `validation` held-out set does not regress, else exact rollback. `verify: false` = trust-batch. Bullets-only.
|
||||
|
||||
```typescript
|
||||
const a = agent('ticket:string -> reply:string', { ai });
|
||||
const apb = a.playbook({ target: 'actor' }); // agent-aware handle; 'actor' (default) or 'responder'
|
||||
await apb.update({ example, prediction, feedback }); // online: injected into the live stage prompt
|
||||
const result = await apb.evolve(
|
||||
{ train, validation }, // AxAgentEvalDataset
|
||||
{ metric, runsPerTask: 2 }, // verify:true by default
|
||||
);
|
||||
```
|
||||
|
||||
The agent-level `evolve(dataset, options)` is distinct from the program-level `pb.evolve(examples, metric)` above: it takes an `AxAgentEvalDataset` plus options, runs the whole pipeline, and returns baseline/final held-in & held-out with per-bullet outcomes (no `{ bestScore }`). For full-pipeline tuning of agent instructions and demos (not the playbook) use `agent.optimize(...)` (GEPA).
|
||||
|
||||
Generated packages expose that same agent-bound loop with language-shaped APIs:
|
||||
|
||||
| Language | Agent-bound evolve call |
|
||||
|---|---|
|
||||
| Python | `agent.playbook().evolve(dataset, options)` |
|
||||
| Java | `agent.playbook(null).evolve(dataset, options)` |
|
||||
| C++ | `agent.get_playbook()->evolve(dataset, options)` |
|
||||
| Go | `agent.GetPlaybook().EvolveAgent(ctx, dataset, options)` |
|
||||
| Rust | `playbook.evolve_agent(&mut agent, client, dataset, options)` |
|
||||
|
||||
All five generated packages thread structured `failureSignals` through agent
|
||||
evaluation predictions. The default verify gate accepts a proposed bullet only
|
||||
when held-in score improves and held-out score stays within `epsilon`; rejection
|
||||
restores the exact prior snapshot. Scoring is host-shaped: TypeScript uses its
|
||||
metric, Python/Java/Go can accept a metric callback, and all generated ports can
|
||||
use task `score`/`scores` values plus the agent evaluation result.
|
||||
|
||||
## Playbook vs optimize()
|
||||
|
||||
- `playbook(...)` — accumulate reusable, evolving task knowledge; the only path that also learns online via `update(...)`.
|
||||
- `optimize(...)` / `agent.optimize(...)` — tune instruction text and few-shot demos offline to a best/Pareto result.
|
||||
- They are complementary; a project can use both.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- "Cannot convert undefined or null to object" from `update()` → you passed input fields at the top level; wrap them in `example: { ... }`.
|
||||
- Empty playbook after `evolve()` → the model already scored well, so nothing was curated; use harder/ambiguous examples or a weaker `studentAI` to surface lessons.
|
||||
- Playbook not affecting an agent's behavior → ensure `apply` is not `false` and you used `agent.playbook(...)` (not a bare `playbook()` on an internal program).
|
||||
|
||||
## See Also
|
||||
|
||||
- `ax-gepa` - `optimize(...)` and `AxGEPA` for instruction/demo tuning.
|
||||
- `ax-agent-context` - choosing between contextMap, contextPolicy, `agent.playbook(...)`, and recall.
|
||||
- `ax-agent-optimize` - `agent.optimize(...)` GEPA tuning for agents.
|
||||
81
.agents/skills/ax-refine/SKILL.md
Normal file
81
.agents/skills/ax-refine/SKILL.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: ax-refine
|
||||
description: Use this skill when writing or reviewing Ax bestOfN/refine code, reward functions, thresholds, native sample selection, serial attempts, generated advice, and attempt diagnostics.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# Ax Refine And BestOfN
|
||||
|
||||
Use `bestOfN(...)` when you can score complete outputs independently. Use `refine(...)` when failed rounds should produce feedback that changes the next attempt.
|
||||
|
||||
## Validation And Assertions
|
||||
|
||||
Keep reward scoring, whole-output assertions, and streaming assertions separate:
|
||||
|
||||
- Use schema validation for shape, types, and field-level constraints.
|
||||
- Use `addAssert(...)` for whole-output hard invariants. Failed assertions feed correction text into the normal retry loop.
|
||||
- Use `addStreamingAssert(...)` for partial streaming hard invariants. It aborts the current stream attempt as soon as the partial field fails, then feeds correction text into the normal retry loop.
|
||||
- Use `bestOfN(...)` for complete-candidate selection.
|
||||
- Use `refine(...)` for reward-scored retry rounds with generated feedback.
|
||||
|
||||
## APIs
|
||||
|
||||
```typescript
|
||||
import { bestOfN, refine } from '@ax-llm/ax';
|
||||
|
||||
const selected = bestOfN(program, {
|
||||
n: 4,
|
||||
threshold: 0.8,
|
||||
rewardFn: ({ input, prediction, traces, chatLog }) => score(prediction),
|
||||
});
|
||||
|
||||
const improved = refine(program, {
|
||||
rounds: 3,
|
||||
samplesPerRound: 2,
|
||||
threshold: 0.85,
|
||||
rewardDescription: 'Prefer complete, grounded, concise answers.',
|
||||
rewardFn: ({ prediction }) => score(prediction),
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `forward(...)` returns the selected prediction.
|
||||
- `streamingForward(...)` is unsupported; score complete outputs instead.
|
||||
- `getUsage()` aggregates usage across attempts.
|
||||
- `getTraces()` and `getChatLog()` return the selected attempt's diagnostics.
|
||||
- `getAttempts()` returns all attempt metadata, including reward, errors, and advice application.
|
||||
|
||||
## Reward Functions
|
||||
|
||||
Reward functions return a number. Higher is better. A `threshold` marks a good-enough candidate and can stop serial attempts early.
|
||||
|
||||
```typescript
|
||||
const rewardFn = ({ prediction }) => {
|
||||
const exact = prediction.answer === 'Paris' ? 1 : 0;
|
||||
const concise = prediction.answer.length < 80 ? 0.2 : 0;
|
||||
return exact + concise;
|
||||
};
|
||||
```
|
||||
|
||||
Use serial strategy when the reward needs traces, chat logs, tools, or full flow behavior.
|
||||
|
||||
## Strategies
|
||||
|
||||
- `strategy: "auto"` uses native samples for `AxGen` and serial attempts for composite programs.
|
||||
- `strategy: "native-samples"` uses `sampleCount` and a reward-backed `resultPicker`; candidate context includes outputs, not full per-candidate traces.
|
||||
- `strategy: "serial"` runs isolated full-program attempts with fresh memory/session IDs.
|
||||
|
||||
## Refine Advice
|
||||
|
||||
`refine(...)` generates advice after a below-threshold round. Advice is appended temporarily to matching `kind: "instruction"` components exposed by `getOptimizableComponents()` and applied through `applyOptimizedComponents()`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Original instruction values are restored in `finally`, on success and error.
|
||||
- Programs without instruction components continue as best-of-N rounds and mark `adviceApplied: false`.
|
||||
- Do not add DSPy-style `hint_` signature fields; Ax uses instruction-component advice.
|
||||
|
||||
## Streaming
|
||||
|
||||
Do not use `refine(...)` for streaming. For partial-output safety, use `addStreamingAssert(fieldName, fn, message?)` on `AxGen`. Streaming assertions fail fast within the current attempt with `AxStreamingAssertionError`, then retry with correction feedback when retries remain.
|
||||
421
.agents/skills/ax-signature/SKILL.md
Normal file
421
.agents/skills/ax-signature/SKILL.md
Normal file
@@ -0,0 +1,421 @@
|
||||
---
|
||||
name: ax-signature
|
||||
description: This skill helps an LLM generate correct DSPy signature code using @ax-llm/ax. Use when the user asks about signatures, s(), f(), field types, string syntax, fluent builder API, validation constraints, or type-safe inputs/outputs.
|
||||
version: "23.0.9"
|
||||
---
|
||||
|
||||
# Ax Signature Reference
|
||||
|
||||
## Signature Syntax
|
||||
|
||||
```
|
||||
[description] input1:type, input2:type -> output1:type, output2:type
|
||||
```
|
||||
|
||||
## Field Types
|
||||
|
||||
| Type | Syntax | TypeScript | Example |
|
||||
|------|--------|-----------|---------|
|
||||
| String | `:string` | `string` | `userName:string` |
|
||||
| Number | `:number` | `number` | `score:number` |
|
||||
| Boolean | `:boolean` | `boolean` | `isValid:boolean` |
|
||||
| JSON | `:json` | `any` | `metadata:json` |
|
||||
| Date | `:date` | `Date` | `birthDate:date` |
|
||||
| DateTime | `:datetime` | `Date` | `timestamp:datetime` |
|
||||
| DateRange | `:dateRange` | `{ start: Date; end: Date }` | `travelDates:dateRange` |
|
||||
| DateTimeRange | `:datetimeRange` | `{ start: Date; end: Date }` | `meetingWindow:datetimeRange` |
|
||||
| Image | `:image` | `{mimeType, data}` | `photo:image` (input only) |
|
||||
| Audio | `:audio` | input: `AxAudioInput`; output: `AxChatAudioOutput` | `recording:audio`, `speech:audio` |
|
||||
| File | `:file` | `{mimeType, data}` | `document:file` (input only) |
|
||||
| URL | `:url` | `string` | `website:url` |
|
||||
| Code | `:code` | `string` | `pythonScript:code` |
|
||||
| Class | `:class "a, b, c"` | `"a" \| "b" \| "c"` | `mood:class "happy, sad"` |
|
||||
|
||||
Date, datetime, and range fields are AI-friendly but strict. They accept ISO-style values, trim minor whitespace/casing issues, and parse ranges as `{ "start": "...", "end": "..." }`, `[start, end]`, `start/end`, or natural delimiters like `start to end`; invalid values and reversed ranges should fail validation rather than being silently autocorrected.
|
||||
|
||||
## Arrays, Optional, and Internal Fields
|
||||
|
||||
```typescript
|
||||
'tags:string[] -> processedTags:string[]' // arrays
|
||||
'query:string, context?:string -> response:string' // optional with ?
|
||||
'problem:string -> reasoning!:string, solution:string' // internal with !
|
||||
```
|
||||
|
||||
## Extended String Grammar (Modifier Bags + Nested Objects)
|
||||
|
||||
The string form is constraint-complete: everything the fluent API expresses
|
||||
(except Standard Schema fields) can be written in the string. A type takes an
|
||||
optional comma-separated, order-free **modifier bag** in parentheses, and
|
||||
objects declare structured fields inline.
|
||||
|
||||
```typescript
|
||||
`userAge:number(min 0, max 120), contactEmail:string(format email, cache), codeSnippet:code(python)
|
||||
-> userName:string(pattern "^[a-z_]+$" "lowercase name"), tagList:string(item "a short tag")[] "all tags",
|
||||
profileList:object{ fullName:string, userAge?:number(min 0) }[] "matched profiles"`
|
||||
```
|
||||
|
||||
| Modifier | Applies to | Effect |
|
||||
|----------|-----------|--------|
|
||||
| `min N` / `max N` | `string`, `number` | String length bounds / numeric value bounds |
|
||||
| `format email\|uri\|date\|date-time` | `string` | Format validation |
|
||||
| `pattern "regex" ["desc"]` | `string` | Regex validation with optional description |
|
||||
| `cache` | top-level input | Prefix-cache breakpoint |
|
||||
| `item "desc"` | arrays | Per-item description: `tags:string(item "a tag")[]` |
|
||||
| `<language>` | `code` | Language of the snippet: `snippet:code(python)` |
|
||||
|
||||
- `object{ field:type, opt?:type }` nests recursively; append `[]` for an array of objects.
|
||||
- Optional goes on the **name** (`userAge?:number`), never after the type.
|
||||
- The string API is **strict**: a modifier that does not apply to its type (e.g. `min` on a boolean) is a parse error, where the fluent API silently ignores it.
|
||||
- Inside `object{ ... }`, the `!` internal marker, media types, `cache`, and `item` are rejected (they only apply at the top level).
|
||||
- In quoted values, backslashes are doubled — a regex `\d` is written `pattern "\\d+"`.
|
||||
- `AxSignature.toString()` renders every construct back to this grammar losslessly, so a signature round-trips — this is what lets a whole flow serialize its node contracts into mermaid `%%ax` directives (see the ax-flow skill).
|
||||
|
||||
## Signature Gallery
|
||||
|
||||
Real-world contracts, one line each — every entry below parses with `s()` as written (`#` lines are captions, not part of the signature):
|
||||
|
||||
```text
|
||||
# Support triage: several class outputs plus a capped reply draft
|
||||
ticketText:string -> priorityClass:class "p0, p1, p2", sentimentClass:class "angry, neutral, happy", replyDraft:string(max 500)
|
||||
|
||||
# Invoice extraction: regex-validated id, bounded totals, structured line items
|
||||
invoiceText:string -> invoiceNumber:string(pattern "^INV-\\d+$" "INV- then digits"), totalAmount:number(min 0), lineItems:object{ description:string, quantity:number(min 1), unitPrice:number }[]
|
||||
|
||||
# Contact enrichment: optional format-validated outputs
|
||||
bioText:string -> contactEmail?:string(format email), websiteUrl?:string(format uri), birthDate?:string(format date)
|
||||
|
||||
# RAG: cached corpus input plus per-item described citations
|
||||
corpusText:string(cache), userQuestion:string -> answerText:string, citedChunks:string(item "verbatim quote")[]
|
||||
|
||||
# Code generation: language-tagged code outputs
|
||||
taskBrief:string -> pythonScript:code(python), testCases:code(python), riskNotes?:string
|
||||
|
||||
# Chain of thought: internal reasoning stripped from the result
|
||||
problemText:string -> reasoning!:string, solutionText:string
|
||||
|
||||
# Resume parsing: nested objects inside nested arrays
|
||||
resumeText:string -> candidateProfile:object{ fullName:string, yearsExperience:number(min 0), skillList:string[], education:object{ schoolName:string, degreeName?:string }[] }
|
||||
|
||||
# Lead scoring: signature-level description, bounded score, class next step
|
||||
"Score sales leads" leadNotes:string -> fitScore:number(min 0, max 100) "0-100 fit", nextStep:class "call, email, drop"
|
||||
|
||||
# Multimodal: top-level image input with an optional question
|
||||
productPhoto:image, question?:string -> productDescription:string, detectedObjects:string[]
|
||||
|
||||
# Meeting audio: audio input, capped summary, per-item action list
|
||||
meetingAudio:audio -> meetingSummary:string(max 1000), actionItems:string(item "one action item")[]
|
||||
|
||||
# Moderation: class verdict plus structured flagged spans
|
||||
postText:string -> moderationVerdict:class "allow, review, block", flaggedSpans:object{ spanText:string, reasonNote:string }[]
|
||||
|
||||
# Translation: optional locale input
|
||||
sourceText:string, targetLocale?:string -> translatedText:string, glossaryHits:string[]
|
||||
|
||||
# Text-to-SQL: cached schema plus SQL-tagged output
|
||||
schemaText:string(cache), questionText:string -> sqlQuery:code(sql), queryNotes?:string(max 200)
|
||||
|
||||
# Calendar extraction: datetime fields and an optional end
|
||||
emailText:string -> eventTitle:string, startsAt:datetime, endsAt?:datetime, attendeeNames:string[]
|
||||
|
||||
# Booking window: date range, bounded party size, and flexibility flag
|
||||
requestText:string -> stayWindow:dateRange, partySize:number(min 1, max 12), flexibleDates:boolean
|
||||
|
||||
# Contract dates: date fields plus bounded notice period
|
||||
contractText:string -> effectiveDate:date, expiryDate?:date, autoRenews:boolean, noticeDays?:number(min 0)
|
||||
|
||||
# Link audit: URL arrays and an optional primary URL
|
||||
pageText:string -> referencedUrls:url[], primaryUrl?:url
|
||||
|
||||
# Config generation: JSON output plus per-item warnings
|
||||
requirementsText:string -> serviceConfig:json, setupWarnings:string(item "one warning")[]
|
||||
|
||||
# Claims gate: cached policy, bounded confidence, and optional citation
|
||||
claimText:string, policyText:string(cache) -> isCovered:boolean, confidenceScore:number(min 0, max 1), citedClause?:string
|
||||
|
||||
# Earnings extraction: structured period data plus a class outlook
|
||||
filingText:string(cache) -> revenueByPeriod:object{ periodLabel:string, amountUsd:number }[], guidanceTone:class "raise, hold, cut"
|
||||
|
||||
# Pull request review: diff code, cached guide, structured comments, and verdict
|
||||
diffText:code(diff), styleGuide?:string(cache) -> reviewComments:object{ filePath:string, lineNumber:number(min 1), commentText:string(max 300) }[], overallVerdict:class "approve, revise"
|
||||
|
||||
# Incident triage: severity class, optional service, and per-item runbook steps
|
||||
alertLog:string -> incidentSeverity:class "sev1, sev2, sev3", suspectedService?:string, runbookSteps:string(item "one step")[]
|
||||
|
||||
# Product listing: image and file inputs with constrained listing outputs
|
||||
productPhoto:image, priceSheet?:file -> listingTitle:string(max 80), bulletPoints:string(item "one selling point")[], priceUsd?:number(min 0)
|
||||
|
||||
# Study cards: nested object array with an optional difficulty tag
|
||||
chapterText:string -> flashCards:object{ questionText:string, answerText:string, difficultyTag?:string }[]
|
||||
```
|
||||
|
||||
## Four Ways to Create Signatures
|
||||
|
||||
### 1. String-Based (Recommended for simple cases)
|
||||
|
||||
```typescript
|
||||
import { ax, s } from '@ax-llm/ax';
|
||||
const gen = ax('input:string -> output:string');
|
||||
const sig = s('query:string -> response:string');
|
||||
```
|
||||
|
||||
### 2. Pure Fluent Builder API
|
||||
|
||||
```typescript
|
||||
import { f } from '@ax-llm/ax';
|
||||
const sig = f()
|
||||
.input('userMessage', f.string('User input'))
|
||||
.input('contextData', f.string('Additional context').optional())
|
||||
.input('tags', f.string('Keywords').array())
|
||||
.output('responseText', f.string('AI response'))
|
||||
.output('confidenceScore', f.number('Confidence 0-1'))
|
||||
.output('debugInfo', f.string('Debug info').internal())
|
||||
.build();
|
||||
```
|
||||
|
||||
### 3. Standard Schema (zod / valibot / arktype)
|
||||
|
||||
`.input()` and `.output()` accept any [Standard Schema v1](https://standardschema.dev) compatible library — no wrapper, no adapter. Three shapes work everywhere:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { f } from '@ax-llm/ax';
|
||||
|
||||
// Shape A: per-field schema — name first, then the schema, then optional ax hints
|
||||
const sig = f()
|
||||
.input('contextData', z.string().describe('Background context'), { cache: true })
|
||||
.input('userQuestion', z.string().describe('Question to answer'))
|
||||
.output('reasoning', z.string().describe('Step-by-step thinking'), { internal: true })
|
||||
.output('answer', z.string().describe('Final answer'))
|
||||
.build();
|
||||
|
||||
// Shape B: whole-object schema — decomposed into fields in declaration order
|
||||
const sig2 = f()
|
||||
.description('Answer questions from retrieved context')
|
||||
.input(
|
||||
z.object({
|
||||
contextData: z.string().describe('Background context'),
|
||||
userQuestion: z.string().describe('Question to answer'),
|
||||
}),
|
||||
{ fields: { contextData: { cache: true } } } // companion options map
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
reasoning: z.string().describe('Step-by-step thinking'),
|
||||
answer: z.string().describe('Final answer'),
|
||||
}),
|
||||
{ fields: { reasoning: { internal: true } } }
|
||||
)
|
||||
.build();
|
||||
```
|
||||
|
||||
Validation constraints from zod flow into ax's prompt validation:
|
||||
|
||||
```typescript
|
||||
// String constraints: .email(), .url(), .min(), .max(), .regex()
|
||||
// Number constraints: .min(), .max()
|
||||
// Arrays: z.array(z.string())
|
||||
// Enums: z.enum([...]) — NOTE: enum maps to ax class type, output fields only
|
||||
const sig3 = f()
|
||||
.input(z.object({
|
||||
emailAddress: z.string().email().describe('Contact email'),
|
||||
username: z.string().min(3).max(20).describe('Handle'),
|
||||
score: z.number().min(0).max(100).describe('Numeric score'),
|
||||
}))
|
||||
.output(z.object({
|
||||
priority: z.enum(['low', 'medium', 'high']).describe('Priority'),
|
||||
summary: z.string().describe('Result'),
|
||||
}))
|
||||
.build();
|
||||
```
|
||||
|
||||
**Companion options** (`AxFieldOptions`) carry ax-specific hints that schema libraries don't represent:
|
||||
|
||||
| Option | Effect |
|
||||
|--------|--------|
|
||||
| `{ cache: true }` | Mark input field as a prefix-cache breakpoint |
|
||||
| `{ internal: true }` | Mark output field as internal scratchpad (stripped from result) |
|
||||
|
||||
The same Standard Schema shapes work on `fn()` tools via `.arg()`, `.returns()`, and `.returnsField()` — argument types are inferred from the schema:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { fn } from '@ax-llm/ax';
|
||||
|
||||
// Whole-object zod on a tool — AI-SDK-style
|
||||
const lookupProduct = fn('lookupProduct')
|
||||
.description('Look up a product by name and return its current details')
|
||||
.arg(
|
||||
z.object({
|
||||
productName: z.string().min(1).describe('Exact product name'),
|
||||
includeSpecs: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.returns(
|
||||
z.object({
|
||||
price: z.number(),
|
||||
inStock: z.boolean(),
|
||||
rating: z.number().min(1).max(5),
|
||||
})
|
||||
)
|
||||
.handler(async ({ productName, includeSpecs }) => ({
|
||||
price: 79.99,
|
||||
inStock: true,
|
||||
rating: 4.3,
|
||||
}))
|
||||
.build();
|
||||
|
||||
// Per-argument form — mix with f.*() args, attach ax hints
|
||||
const searchDocs = fn('searchDocs')
|
||||
.description('Search indexed docs')
|
||||
.arg('query', z.string().min(1), { cache: true })
|
||||
.arg('limit', z.number().int().positive().optional())
|
||||
.returnsField('results', z.array(z.string()))
|
||||
.handler(async ({ query }) => [])
|
||||
.build();
|
||||
```
|
||||
|
||||
### 4. Hybrid
|
||||
|
||||
```typescript
|
||||
import { s, f } from '@ax-llm/ax';
|
||||
const sig = s('base:string -> result:string')
|
||||
.appendInputField('extra', f.json('Metadata').optional())
|
||||
.appendOutputField('score', f.number('Quality score'));
|
||||
```
|
||||
|
||||
## Fluent API Reference
|
||||
|
||||
Type creators:
|
||||
- `f.string(desc)`, `f.number(desc)`, `f.boolean(desc)`, `f.json(desc)`
|
||||
- `f.image(desc)`, `f.audio(desc)`, `f.file(desc)`, `f.url(desc)`
|
||||
- `f.email(desc)`, `f.date(desc)`, `f.datetime(desc)`, `f.dateRange(desc)`, `f.datetimeRange(desc)`
|
||||
- `f.class(['a','b','c'], desc)`, `f.code(desc)`
|
||||
- `f.object({ field: f.string() }, desc)`
|
||||
|
||||
Chainable modifiers (method chaining only, no nesting):
|
||||
- `.optional()` - make field optional
|
||||
- `.array()` / `.array('list description')` - make field an array
|
||||
- `.internal()` - output only, hidden from final output
|
||||
- `.cache()` - input only, mark for prompt caching
|
||||
|
||||
```typescript
|
||||
// Correct: pure fluent chaining
|
||||
f.string('description').optional().array()
|
||||
f.string('context').cache().optional()
|
||||
f.object({ field: f.string() }, 'item desc').array('list desc')
|
||||
|
||||
// Wrong: nested function calls (removed)
|
||||
f.array(f.string('description')) // REMOVED
|
||||
f.optional(f.string('description')) // REMOVED
|
||||
f.internal(f.string('description')) // REMOVED
|
||||
```
|
||||
|
||||
## Validation Constraints
|
||||
|
||||
### String Constraints
|
||||
|
||||
```typescript
|
||||
f.string('username').min(3).max(20)
|
||||
f.string('email').email()
|
||||
f.string('website').url()
|
||||
f.string('birthDate').date()
|
||||
f.string('timestamp').datetime()
|
||||
f.string('pattern').regex('^[A-Z0-9]')
|
||||
```
|
||||
|
||||
### Number Constraints
|
||||
|
||||
```typescript
|
||||
f.number('age').min(18).max(120)
|
||||
f.number('score').min(0).max(100)
|
||||
```
|
||||
|
||||
### Complete Validation Example
|
||||
|
||||
```typescript
|
||||
const sig = f()
|
||||
.input('formData', f.string('Raw form data'))
|
||||
.output('user', f.object({
|
||||
username: f.string('Username').min(3).max(20),
|
||||
email: f.string('Email').email(),
|
||||
age: f.number('Age').min(18).max(120),
|
||||
bio: f.string('Bio').max(500).optional(),
|
||||
website: f.string('Website').url().optional(),
|
||||
tags: f.string('Tag').min(2).max(30).array()
|
||||
}, 'User profile'))
|
||||
.build();
|
||||
```
|
||||
|
||||
## Cached Input Fields
|
||||
|
||||
```typescript
|
||||
const sig = f()
|
||||
.input('staticContext', f.string('Context').cache())
|
||||
.input('userQuery', f.string('Dynamic query'))
|
||||
.output('answer', f.string('Response'))
|
||||
.build();
|
||||
```
|
||||
|
||||
## Field Naming Rules
|
||||
|
||||
Good: `userQuestion`, `customerEmail`, `analysisResult`, `confidenceScore`
|
||||
Bad: `text`, `data`, `input`, `output`, `a`, `x`, `val` (too generic), `1field` (starts with number)
|
||||
|
||||
## Media Type Restrictions
|
||||
|
||||
- Image and file fields are top-level input fields only.
|
||||
- Audio fields can be top-level inputs or single top-level outputs.
|
||||
- Audio output fields are scripted speech artifacts: the model returns plain text, then Ax synthesizes `AxChatAudioOutput`.
|
||||
- Media fields cannot be nested in objects.
|
||||
- Media arrays are supported for inputs only; output `audio[]` is not supported.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
```typescript
|
||||
// Chain of Thought
|
||||
'problem:string -> reasoning!:string, solution:string'
|
||||
|
||||
// Classification
|
||||
'email:string -> priority:class "urgent, normal, low"'
|
||||
|
||||
// Multi-modal input
|
||||
'imageData:image, question?:string -> description:string, objects:string[]'
|
||||
|
||||
// Scripted speech output
|
||||
'question:string -> speech:audio, summary:string'
|
||||
|
||||
// Data Extraction
|
||||
'invoiceText:string -> invoiceNumber:string, totalAmount:number, lineItems:json[]'
|
||||
|
||||
// Constrained string form (no fluent builder needed)
|
||||
'reviewText:string(max 2000) -> rating:number(min 1, max 5), themes:string(item "a theme")[]'
|
||||
|
||||
// Nested object output in the string form
|
||||
'profileText:string -> profile:object{ fullName:string, age?:number(min 0) }'
|
||||
|
||||
// With description
|
||||
'"Answer TypeScript questions" question:string -> answer:string, confidence:number'
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- The string form is constraint-complete: reach for modifier bags (`string(max 500)`, `number(min 0, max 10)`, `string(format email)`) and inline `object{ ... }` before switching to fluent/zod just for constraints. Reserve fluent/Standard Schema for zod/valibot-backed fields.
|
||||
- The string API is strict — a modifier that does not apply to its type is a parse error (the fluent API silently ignores it).
|
||||
- Use `f()` fluent builder, NOT nested `f.array(f.string())` -- those are removed.
|
||||
- Field names must be descriptive (not generic like `text`, `data`, `input`).
|
||||
- Image/file media types are input-only, top-level only; audio may also be a single top-level output.
|
||||
- `.internal()` / `{ internal: true }` is output-only (for chain-of-thought reasoning).
|
||||
- `.cache()` / `{ cache: true }` is input-only (for prompt caching).
|
||||
- Validation errors trigger auto-retry with correction feedback.
|
||||
- `f.email()`, `f.url()`, `f.date()`, `f.datetime()` are shorthand for `f.string().email()` etc.; `f.dateRange()` and `f.datetimeRange()` return `{ start: Date; end: Date }`.
|
||||
- `z.enum()` maps to ax's `class` type — only valid on **output** fields.
|
||||
- For multimodal inputs (images, audio, files) and scripted audio outputs, use `f.image()` / `f.audio()` / `f.file()` — zod has no equivalent.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetch these for full working code:
|
||||
|
||||
- [Standard Schema (zod)](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/standard-schema.ts) — zod with f() and fn(), all three shapes
|
||||
- [Fluent Signature](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fluent-signature-example.ts) — native fluent f() API
|
||||
- [Structured Output](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/structured_output.ts) — structured output with validation
|
||||
- [Debug Schema](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/debug_schema.ts) — JSON schema validation
|
||||
537
.agents/skills/live-cursors/BASE_SKILL.md
Normal file
537
.agents/skills/live-cursors/BASE_SKILL.md
Normal file
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: "rivetkit"
|
||||
description: "RivetKit backend and Rivet Actor runtime guidance. Use for building, modifying, debugging, or testing Rivet Actors, registries, serverless/runner modes, deployment, or actor-based workflows."
|
||||
---
|
||||
|
||||
# Rivet Actors
|
||||
|
||||
Build high-performance, long-lived, in-memory processes on Rivet's actor runtime. Use this skill whenever HTTP endpoints, databases, or queues no longer meet the requirements for AI agents, realtime collaboration, workflow orchestration, or geo-distributed data.
|
||||
|
||||
## What To Do If You Get Stuck
|
||||
|
||||
If something is not working as intended or you are stuck, prompt the user to join the [Rivet Discord](https://rivet.dev/discord) or file an issue on the [Rivet GitHub](https://github.com/rivet-dev/rivet) to report an issue and get help.
|
||||
|
||||
Provide the user with a pre-generated report with:
|
||||
|
||||
- Symptoms
|
||||
- If this is happening in local dev, deployed, or both
|
||||
- The error you're seeing
|
||||
- Relevant source code related to this
|
||||
- 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, etc)
|
||||
- If applicable, HTTP router in use (e.g. Hono, Express, Elysia)
|
||||
|
||||
## Debugging Actors
|
||||
|
||||
Use the inspector HTTP API to examine running actors. These endpoints are accessible through the gateway at `/gateway/{actor_id}/inspector/*`. Key endpoints:
|
||||
|
||||
- `GET /inspector/summary` - full actor snapshot (state, connections, RPCs, queue)
|
||||
- `GET /inspector/state` / `PATCH /inspector/state` - read/write actor state
|
||||
- `GET /inspector/connections` - active connections
|
||||
- `GET /inspector/rpcs` - available actions
|
||||
- `POST /inspector/action/{name}` - execute an action with `{"args": [...]}`
|
||||
- `POST /inspector/database/execute` - run SQL with `{"sql": "...", "args": [...]}` or `{"sql": "...", "properties": {...}}` for reads or mutations
|
||||
- `GET /inspector/queue?limit=50` - queue status
|
||||
- `GET /inspector/traces?startMs=0&endMs=...&limit=1000` - trace spans (OTLP JSON)
|
||||
- `GET /inspector/workflow-history` - workflow history and status as JSON (`nameRegistry`, `entries`, `entryMetadata`)
|
||||
- `POST /inspector/workflow/replay` - replay a workflow from a specific step or from the beginning; returns `409 actor/workflow_in_flight` if the workflow is still running
|
||||
- `GET /inspector/database/schema` - SQLite tables and views exposed by `c.db`
|
||||
- `GET /inspector/database/rows?table=...&limit=100&offset=0` - paged SQLite rows for a table or view
|
||||
|
||||
In local dev, no auth token is needed. In production, pass `Authorization: Bearer <inspector-token>`, where the inspector token is the actor-specific token auto-generated on first start and persisted in the actor's internal KV at key `0x03`. The Rivet dashboard retrieves this token automatically; for direct API access, fetch it through the management KV endpoint. See the [debugging docs](https://rivet.dev/docs/actors/debugging) for details.
|
||||
|
||||
## Citing Sources
|
||||
|
||||
When providing information from Rivet documentation, cite the canonical URL so users can learn more. Each reference file includes its canonical URL in the header metadata.
|
||||
|
||||
**How to cite:**
|
||||
|
||||
- Use inline links for key concepts: "Use [actor keys](https://rivet.dev/docs/actors/keys) to uniquely identify instances."
|
||||
- Add a "Learn more" link after explanations for complex topics
|
||||
|
||||
**Finding canonical URLs:**
|
||||
|
||||
The Reference Map below links to reference files. Each file's header contains:
|
||||
|
||||
```
|
||||
> Canonical URL: https://rivet.dev/docs/actors/actions
|
||||
```
|
||||
|
||||
Use that canonical URL when citing, not the reference file path.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Actions → `https://rivet.dev/docs/actors/actions`
|
||||
- React client → `https://rivet.dev/docs/clients/react`
|
||||
- Self-hosting on Kubernetes → `https://rivet.dev/docs/self-hosting/kubernetes`
|
||||
|
||||
## Version Check
|
||||
|
||||
Before starting any work, check if the user's project is on the latest version of RivetKit (latest: 2.3.5-rc.1). Look at the `rivetkit` version in the user's `package.json` (check both `dependencies` and `devDependencies`). If the installed version is older than 2.3.5-rc.1, inform the user and suggest upgrading:
|
||||
|
||||
```bash
|
||||
npm install rivetkit@2.3.5-rc.1
|
||||
```
|
||||
|
||||
If the user also uses `@rivetkit/react`, `@rivetkit/next-js`, or other `@rivetkit/*` client packages, suggest upgrading those too. Outdated versions may have known bugs or missing features that cause issues.
|
||||
|
||||
## First Steps
|
||||
|
||||
1. Install RivetKit (latest: 2.3.5-rc.1)
|
||||
```bash
|
||||
npm install rivetkit@2.3.5-rc.1
|
||||
```
|
||||
2. Define a registry with `setup({ use: { /* actors */ } })`.
|
||||
3. Call `registry.start()` to start the server. For custom HTTP server integration, use `registry.handler()` with a router like Hono. For serverless deployments, use `registry.serve()`. For runner-only mode, use `registry.startEnvoy()`.
|
||||
4. Verify `/api/rivet/metadata` returns 200 before deploying.
|
||||
5. Configure Rivet Cloud or self-hosted engine
|
||||
- You must configure versioning for production builds. This is not needed for local development. See [Versions & Upgrades](https://rivet.dev/docs/actors/versions).
|
||||
6. Integrate clients (see client guides below for JavaScript, React, or Swift)
|
||||
7. Prompt the user if they want to deploy. If so, go to Deploying Rivet Backends.
|
||||
|
||||
For more information, read the quickstart guide relevant to the user's project.
|
||||
|
||||
## Project Setup
|
||||
|
||||
### .gitignore
|
||||
|
||||
Every RivetKit project should have a `.gitignore`. Include at minimum:
|
||||
|
||||
```
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
```
|
||||
|
||||
### .dockerignore
|
||||
|
||||
Every project with a Dockerfile should have a `.dockerignore` to keep the image small and avoid leaking secrets:
|
||||
|
||||
```
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.git/
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
|
||||
Use this as a base Dockerfile for deploying a RivetKit project. The `RIVET_RUNNER_VERSION` build arg is only needed when self-hosting or using a custom runner (not needed for Rivet Compute). It lets Rivet track which version of the actor is running and drain old actors on deploy. See https://rivet.dev/docs/actors/versions for details.
|
||||
|
||||
```dockerfile
|
||||
FROM node:24-alpine
|
||||
|
||||
ARG RIVET_RUNNER_VERSION
|
||||
ENV RIVET_RUNNER_VERSION=$RIVET_RUNNER_VERSION
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build --if-present
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
Build with:
|
||||
|
||||
```bash
|
||||
docker build --build-arg RIVET_RUNNER_VERSION=$(date +%s) .
|
||||
```
|
||||
|
||||
Adjust the `CMD` to match the project's entry point. If the project uses a different output directory or start command, update accordingly.
|
||||
|
||||
## Error Handling Policy
|
||||
|
||||
- Prefer fail-fast behavior by default.
|
||||
- Avoid `try/catch` unless it is required for a real recovery path, cleanup boundary, or to add actionable context.
|
||||
- Never swallow errors. If you add a `catch`, you must handle the error explicitly, at minimum by logging it.
|
||||
- When you cannot recover, log context and rethrow.
|
||||
|
||||
## State vs Vars: Persistence Rules
|
||||
|
||||
**`c.vars` is ephemeral.** Data in `c.vars` is lost on every restart, crash, upgrade, or sleep/wake cycle. Only use `c.vars` for non-serializable objects (e.g., physics engines, WebSocket references, event emitters, caches) or truly transient runtime data (e.g., current input direction that doesn't matter after disconnect).
|
||||
|
||||
**Persistent storage options.** Any data that must survive restarts belongs in one of these, NOT in `c.vars`:
|
||||
|
||||
- **`c.state`** — CBOR-serializable data for small, bounded datasets. Ideal for configuration, counters, small player lists, phase flags, etc. Keep under 128 KB. Do not store unbounded or growing data here (e.g., chat logs, event histories, spawned entity lists that grow without limit). State is read/written as a single blob on every persistence cycle.
|
||||
- **`c.kv`** — Key-value store for unbounded data. This is what `c.state` uses under the hood. Supports binary values. Use for larger or variable-size data like user inventories, world chunks, file blobs, or any collection that may grow over time. Keys are scoped to the actor instance.
|
||||
- **`c.db`** — SQLite database for structured or complex data. Use when you need queries, indexes, joins, aggregations, or relational modeling. Ideal for leaderboards, match histories, player pools, or any data that benefits from SQL.
|
||||
|
||||
**Common mistake:** Storing meaningful game/application data in `c.vars` instead of persisting it. For example, if users can spawn objects in a physics simulation, the spawn definitions (position, size, type) must be persisted in `c.state` (or `c.kv` if unbounded), even though the physics engine handles (non-serializable) live in `c.vars`. On restart, `run()` should recreate the runtime objects from the persisted data.
|
||||
|
||||
## Deploying Rivet Backends
|
||||
|
||||
Assume the user is deploying to Rivet Cloud, unless otherwise specified. If user is self-hosting, read the self-hosting guides below.
|
||||
|
||||
1. Verify that Rivet Actors are working in local dev
|
||||
2. Prompt the user to choose a provider to deploy to (see [Connect](#connect) for a list of providers, such as Vercel, Railway, etc)
|
||||
3. Follow the deploy guide for that given provider. You will need to instruct the user when you need manual intervention.
|
||||
|
||||
## API Reference
|
||||
|
||||
The RivetKit OpenAPI specification is available in the skill directory at `openapi.json`. This file documents all HTTP endpoints for managing actors.
|
||||
|
||||
## Misc Notes
|
||||
|
||||
- The Rivet domain is rivet.dev, not rivet.gg
|
||||
|
||||
## TypeScript Caveat: Actor Client Inference
|
||||
|
||||
- In multi-file TypeScript projects, bidirectional actor calls can create a circular type dependency when both actors use `c.client<typeof registry>()`.
|
||||
- Symptoms usually include `c.state` becoming `unknown`, actor methods becoming possibly `undefined`, or `TS2322` / `TS2722` errors after the first cross-actor call.
|
||||
- If an action returns the result of another actor call, prefer an explicit return type annotation on that action instead of relying on inference through `c.client<typeof registry>()`.
|
||||
- If explicit return types are not enough, use a narrower client or registry type for only the actors that action needs.
|
||||
- As a last resort, pass `unknown` for the registry type and be explicit that this gives up type safety at that call site.
|
||||
|
||||
## Features
|
||||
|
||||
- **Long-Lived, Stateful Compute**: Each unit of compute is like a tiny server that remembers things between requests – no need to re-fetch data from a database or worry about timeouts. Like AWS Lambda, but with memory and no timeouts.
|
||||
- **Blazing-Fast Reads & Writes**: State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes. State is persisted to Rivet for long term storage, so it survives server restarts.
|
||||
- **Realtime**: Update state and broadcast changes in realtime with WebSockets. No external pub/sub systems, no polling – just built-in low-latency events.
|
||||
- **Infinitely Scalable**: Automatically scale from zero to millions of concurrent actors. Pay only for what you use with instant scaling and no cold starts.
|
||||
- **Fault Tolerant**: Built-in error handling and recovery. Actors automatically restart on failure while preserving state integrity and continuing operations.
|
||||
|
||||
## When to Use Rivet Actors
|
||||
|
||||
- **AI agents & sandboxes**: multi-step toolchains, conversation memory, sandbox orchestration.
|
||||
- **Multiplayer or collaborative apps**: CRDT docs, shared cursors, realtime dashboards, chat.
|
||||
- **Workflow automation**: background jobs, cron, rate limiters, durable queues, backpressure control.
|
||||
- **Data-intensive backends**: geo-distributed or per-tenant databases, in-memory caches, sharded SQL.
|
||||
- **Networking workloads**: WebSocket servers, custom protocols, local-first sync, edge fanout.
|
||||
|
||||
## Minimal Project
|
||||
|
||||
### Backend
|
||||
|
||||
**index.ts**
|
||||
|
||||
### Client Docs
|
||||
|
||||
Use the client SDK that matches your app:
|
||||
|
||||
- [JavaScript Client](/docs/clients/javascript)
|
||||
- [React Client](/docs/clients/react)
|
||||
- [Swift Client](/docs/clients/swift)
|
||||
|
||||
## Actor Quick Reference
|
||||
|
||||
### In-Memory State
|
||||
|
||||
Persistent data that survives restarts, crashes, and deployments. State is persisted on Rivet Cloud or Rivet self-hosted, so it survives restarts if the current process crashes or exits.
|
||||
|
||||
### Static Initial State
|
||||
|
||||
### Dynamic Initial State
|
||||
|
||||
[Documentation](/docs/actors/state)
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
Don't build keys with string interpolation like `"org:${userId}"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
|
||||
|
||||
[Documentation](/docs/actors/keys)
|
||||
|
||||
### Input
|
||||
|
||||
Pass initialization data when creating actors. Input is only available in `createState` and `onCreate`, so store it in state if you need it later.
|
||||
|
||||
[Documentation](/docs/actors/input)
|
||||
|
||||
### Temporary Variables
|
||||
|
||||
Temporary data that doesn't survive restarts. Use for non-serializable objects (event emitters, connections, etc).
|
||||
|
||||
### Static Initial Vars
|
||||
|
||||
### Dynamic Initial Vars
|
||||
|
||||
[Documentation](/docs/actors/state)
|
||||
|
||||
### Actions
|
||||
|
||||
Actions are the primary way clients and other actors communicate with an actor.
|
||||
|
||||
[Documentation](/docs/actors/actions)
|
||||
|
||||
### Events & Broadcasts
|
||||
|
||||
Events enable real-time communication from actors to connected clients.
|
||||
|
||||
[Documentation](/docs/actors/events)
|
||||
|
||||
### Connections
|
||||
|
||||
Access the current connection via `c.conn` or all connected clients via `c.conns`. Use `c.conn.id` or `c.conn.state` to securely identify who is calling an action. `c.conn` is only available for actions invoked through a connected client; stateless actor-handle calls run without a connection, so guard against that. Connection state is initialized via `connState` or `createConnState`, which receives parameters passed by the client on connect.
|
||||
|
||||
### Static Connection Initial State
|
||||
|
||||
### Dynamic Connection Initial State
|
||||
|
||||
[Documentation](/docs/actors/connections)
|
||||
|
||||
### Queues
|
||||
|
||||
Use queues to process durable messages in order inside a `run` loop.
|
||||
|
||||
[Documentation](/docs/actors/queues)
|
||||
|
||||
### Workflows
|
||||
|
||||
Use workflows when your `run` logic needs durable, replayable multi-step execution.
|
||||
|
||||
[Documentation](/docs/actors/workflows)
|
||||
|
||||
### Actor-to-Actor Communication
|
||||
|
||||
Actors can call other actors using `c.client()`.
|
||||
|
||||
[Documentation](/docs/actors/communicating-between-actors)
|
||||
|
||||
### Schedule & Cron
|
||||
|
||||
Run one-shot actions after a delay or at a specific time. Use named cron jobs for calendar schedules and fixed intervals. Both persist across sleep, restarts, upgrades, and crashes.
|
||||
|
||||
[Documentation](/docs/actors/schedule)
|
||||
|
||||
### Destroying Actors
|
||||
|
||||
Permanently delete an actor and its state using `c.destroy()`.
|
||||
|
||||
[Documentation](/docs/actors/destroy)
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
Actors support hooks for initialization, background processing, connections, networking, and state changes. Use `run` for long-lived background loops, and use `c.aborted` or `c.abortSignal` for graceful shutdown.
|
||||
|
||||
[Documentation](/docs/actors/lifecycle)
|
||||
|
||||
### Context Types
|
||||
|
||||
When writing helper functions outside the actor definition, use `*ContextOf<typeof myActor>` to extract the correct context type. Helpers like `ActionContextOf`, `CreateContextOf`, `ConnContextOf`, and `ConnInitContextOf` are exported from `"rivetkit"`. Do not manually define your own context interface. Always derive it from the actor definition.
|
||||
|
||||
[Documentation](/docs/actors/types)
|
||||
|
||||
### Errors
|
||||
|
||||
Use `UserError` to throw errors that are safely returned to clients. Pass `metadata` to include structured data. Other errors are converted to generic "internal error" for security.
|
||||
|
||||
### Actor
|
||||
|
||||
### Client
|
||||
|
||||
[Documentation](/docs/actors/errors)
|
||||
|
||||
### Low-Level HTTP & WebSocket Handlers
|
||||
|
||||
For custom protocols or integrating libraries that need direct access to HTTP `Request`/`Response` or WebSocket connections, use `onRequest` and `onWebSocket`.
|
||||
|
||||
[HTTP Handler Documentation](/docs/actors/request-handler) · [WebSocket Handler Documentation](/docs/actors/websocket-handler)
|
||||
|
||||
### Icons & Names
|
||||
|
||||
Customize how actors appear in the UI with display names and icons. It's recommended to always provide a name and icon to actors in order to make them easier to distinguish in the dashboard.
|
||||
|
||||
```typescript
|
||||
import { actor } from "rivetkit";
|
||||
|
||||
const chatRoom = actor({
|
||||
options: {
|
||||
name: "Chat Room",
|
||||
icon: "💬", // or FontAwesome: "comments", "chart-line", etc.
|
||||
},
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
[Documentation](/docs/actors/appearance)
|
||||
|
||||
## Client Documentation
|
||||
|
||||
Find the full client guides here:
|
||||
|
||||
- [JavaScript Client](/docs/clients/javascript)
|
||||
- [React Client](/docs/clients/react)
|
||||
- [Swift Client](/docs/clients/swift)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
Actors scale naturally through isolated state and message-passing. Structure your applications with these patterns:
|
||||
|
||||
[Documentation](/docs/actors/design-patterns)
|
||||
|
||||
### Actor Per Entity
|
||||
|
||||
Create one actor per user, document, or room. Use compound keys to scope entities:
|
||||
|
||||
### Coordinator & Data Actors
|
||||
|
||||
**Data actors** handle core logic (chat rooms, game sessions, user data). **Coordinator actors** track and manage collections of data actors—think of them as an index.
|
||||
|
||||
### Run Loop
|
||||
|
||||
Use a `run` loop for continuous background work inside an actor. Process queue messages in order, run logic on intervals, stream AI responses, or coordinate long-running tasks.
|
||||
|
||||
### Workflow Loop
|
||||
|
||||
Use this pattern for long-lived, durable workflows that initialize resources, process commands in a loop, then clean up.
|
||||
|
||||
[Documentation](/docs/actors/workflows)
|
||||
|
||||
### Actions vs Queues
|
||||
|
||||
- **Actions** are not durable. Use them for realtime reads, ephemeral data, and low-latency communication like player input.
|
||||
- **Queues** are durable. Use them to serialize mutations through the run loop, avoiding race conditions with SQLite and other local state. Callers can still wait for a response from queued work.
|
||||
|
||||
### Authentication, Security, & CORS
|
||||
|
||||
- Validate credentials in `onBeforeConnect` or `createConnState` and throw an error to reject unauthorized connections.
|
||||
- Use `c.conn.state` to securely identify users in actions rather than trusting action parameters.
|
||||
- For cross-origin access, validate the request origin in `onBeforeConnect`.
|
||||
|
||||
[Authentication Documentation](/docs/actors/authentication) · [CORS Documentation](/docs/general/cors)
|
||||
|
||||
### Versions & Upgrades
|
||||
|
||||
When deploying new code, set a version number so Rivet can route new actors to the latest runner and optionally drain old ones. Use a build timestamp, git commit count, or CI build number as the version. It is very important to [configure versioning](/docs/actors/versions) before deploying to production. Without versioning, actors can regress by running on older runner versions, and existing actors will never be forced to migrate to new runners. They will continue running indefinitely on the old runners until they exit.
|
||||
|
||||
[Documentation](/docs/actors/versions)
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
#### Never build a "god" actor
|
||||
|
||||
Do not put all your logic in a single actor. A god actor serializes every operation through one bottleneck, kills parallelism, and makes the entire system fail as a unit. Split into focused actors per entity.
|
||||
|
||||
#### Never create an actor per request
|
||||
|
||||
Actors are long-lived and maintain state across requests. Creating a new actor for every incoming request throws away the core benefit of the model and wastes resources on actor creation and teardown. Use actors for persistent entities and regular functions for stateless work.
|
||||
|
||||
## Reference Map
|
||||
|
||||
### Actors
|
||||
|
||||
- [Access Control](reference/actors/access-control.md)
|
||||
- [Actions](reference/actors/actions.md)
|
||||
- [Actor Keys](reference/actors/keys.md)
|
||||
- [Actor Runtime Socket](reference/actors/actor-runtime-socket.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)
|
||||
- [Schedule & Cron](reference/actors/schedule.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)
|
||||
|
||||
### Cli
|
||||
|
||||
- [CLI](reference/cli.md)
|
||||
|
||||
### Clients
|
||||
|
||||
- [Node.js & Bun](reference/clients/javascript.md)
|
||||
- [React](reference/clients/react.md)
|
||||
- [Rust (Beta)](reference/clients/rust.md)
|
||||
- [Swift](reference/clients/swift.md)
|
||||
- [SwiftUI](reference/clients/swiftui.md)
|
||||
|
||||
### Cookbook
|
||||
|
||||
- [AI Agent](reference/cookbook/ai-agent.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
|
||||
|
||||
- [Container Runner](reference/deploy/container-runner.md)
|
||||
- [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)
|
||||
|
||||
282
.agents/skills/live-cursors/SKILL.md
Normal file
282
.agents/skills/live-cursors/SKILL.md
Normal file
@@ -0,0 +1,282 @@
|
||||
---
|
||||
name: "live-cursors"
|
||||
description: "Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling."
|
||||
---
|
||||
|
||||
# Live Cursors and Presence
|
||||
|
||||
**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:
|
||||
|
||||
- [cursors](https://github.com/rivet-dev/rivet/tree/main/examples/cursors)
|
||||
- [cursors-raw-websocket](https://github.com/rivet-dev/rivet/tree/main/examples/cursors-raw-websocket)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
## Reference Map
|
||||
|
||||
### Actors
|
||||
|
||||
- [Access Control](reference/actors/access-control.md)
|
||||
- [Actions](reference/actors/actions.md)
|
||||
- [Actor Keys](reference/actors/keys.md)
|
||||
- [Actor Runtime Socket](reference/actors/actor-runtime-socket.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)
|
||||
- [Schedule & Cron](reference/actors/schedule.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)
|
||||
|
||||
### Cli
|
||||
|
||||
- [CLI](reference/cli.md)
|
||||
|
||||
### Clients
|
||||
|
||||
- [Node.js & Bun](reference/clients/javascript.md)
|
||||
- [React](reference/clients/react.md)
|
||||
- [Rust (Beta)](reference/clients/rust.md)
|
||||
- [Swift](reference/clients/swift.md)
|
||||
- [SwiftUI](reference/clients/swiftui.md)
|
||||
|
||||
### Cookbook
|
||||
|
||||
- [AI Agent](reference/cookbook/ai-agent.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
|
||||
|
||||
- [Container Runner](reference/deploy/container-runner.md)
|
||||
- [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)
|
||||
|
||||
694
.agents/skills/live-cursors/index.json
Normal file
694
.agents/skills/live-cursors/index.json
Normal file
@@ -0,0 +1,694 @@
|
||||
{
|
||||
"name": "live-cursors",
|
||||
"description": "Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling.",
|
||||
"skill_url": "/metadata/skills/live-cursors/SKILL.md",
|
||||
"generated_at": "2026-07-20T21:43:04.687Z",
|
||||
"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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/actors/keys.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/actor-runtime-socket",
|
||||
"title": "Actor Runtime Socket",
|
||||
"description": "Access an actor's SQLite database from another local process.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/actor-runtime-socket",
|
||||
"reference_url": "/metadata/skills/live-cursors/reference/actors/actor-runtime-socket.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/live-cursors/reference/actors/statuses.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/live-cursors/reference/cookbook/ai-agent.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 horizontally. read omre about runners below.",
|
||||
"canonical_url": "https://rivet.dev/docs/general/architecture",
|
||||
"reference_url": "/metadata/skills/live-cursors/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/live-cursors/reference/actors/authentication.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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/actors/communicating-between-actors.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/live-cursors/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/live-cursors/reference/actors/connections.md"
|
||||
},
|
||||
{
|
||||
"name": "deploy/container-runner",
|
||||
"title": "Container Runner",
|
||||
"description": "Run any containerized server as a Rivet Actor.",
|
||||
"canonical_url": "https://rivet.dev/docs/deploy/container-runner",
|
||||
"reference_url": "/metadata/skills/live-cursors/reference/deploy/container-runner.md"
|
||||
},
|
||||
{
|
||||
"name": "cookbook/cron-jobs",
|
||||
"title": "Cron Jobs and Scheduled Tasks",
|
||||
"description": "Patterns for durable one-shot, calendar, and fixed-interval work on Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/cookbook/cron-jobs",
|
||||
"reference_url": "/metadata/skills/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/deploy/vm-and-bare-metal.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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/actors/quickstart/effect.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/live-cursors/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/live-cursors/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/live-cursors/reference/actors/errors.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/live-cursors/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/live-cursors/reference/self-hosting/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/actors/lifecycle.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/live-cursors/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/live-cursors/reference/cookbook/live-cursors.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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/self-hosting/multi-region.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/live-cursors/reference/cookbook/multiplayer-game.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/live-cursors/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/live-cursors/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/live-cursors/reference/actors/quickstart/backend.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/live-cursors/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/live-cursors/reference/self-hosting/postgres.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/live-cursors/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/live-cursors/reference/self-hosting/production-checklist.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/live-cursors/reference/actors/queues.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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/general/runtime-modes.md"
|
||||
},
|
||||
{
|
||||
"name": "clients/rust",
|
||||
"title": "Rust (Beta)",
|
||||
"description": "Connect Rust apps to Rivet Actors.",
|
||||
"canonical_url": "https://rivet.dev/docs/clients/rust",
|
||||
"reference_url": "/metadata/skills/live-cursors/reference/clients/rust.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/live-cursors/reference/actors/quickstart/rust.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/live-cursors/reference/actors/scaling.md"
|
||||
},
|
||||
{
|
||||
"name": "actors/schedule",
|
||||
"title": "Schedule & Cron",
|
||||
"description": "Run durable one-shot and recurring actor actions on a schedule.",
|
||||
"canonical_url": "https://rivet.dev/docs/actors/schedule",
|
||||
"reference_url": "/metadata/skills/live-cursors/reference/actors/schedule.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/live-cursors/reference/actors/sharing-and-joining-state.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/live-cursors/reference/actors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/clients/swiftui.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/live-cursors/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/live-cursors/reference/self-hosting/tls.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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/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/live-cursors/reference/general/wasm-vs-native-sdk.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/live-cursors/reference/actors/workflows.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
1311
.agents/skills/live-cursors/openapi.json
Normal file
1311
.agents/skills/live-cursors/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
# 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`.
|
||||
|
||||
## 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_
|
||||
98
.agents/skills/live-cursors/reference/actors/actions.md
Normal file
98
.agents/skills/live-cursors/reference/actors/actions.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 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:
|
||||
|
||||
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)
|
||||
|
||||
Learn more about [communicating with actors from the frontend](/docs/actors/communicating-between-actors).
|
||||
|
||||
### Backend (registry.handler)
|
||||
|
||||
Learn more about [communicating with actors from the backend](/docs/actors/communicating-between-actors).
|
||||
|
||||
### Actor-to-Actor (c.client())
|
||||
|
||||
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:
|
||||
|
||||
## 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:
|
||||
|
||||
### 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:
|
||||
|
||||
## 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.
|
||||
|
||||
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:
|
||||
|
||||
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_
|
||||
@@ -0,0 +1,62 @@
|
||||
# Actor Runtime Socket
|
||||
|
||||
> Source: `src/content/docs/actors/actor-runtime-socket.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/actor-runtime-socket
|
||||
> Description: Access an actor's SQLite database from another local process.
|
||||
|
||||
---
|
||||
The Actor Runtime Socket is an advanced Beta API for integrating Rivet Actors with external software without sacrificing performance. It currently exposes SQLite over Unix domain sockets in the native runtime.
|
||||
|
||||
For example, an agentOS agent can use the socket to work with its actor's SQLite database. Software orchestrated by an actor, such as another process or container, can use it to access the actor's runtime resources. Like the Chrome DevTools Protocol exposes a browser to external tools, the Actor Runtime Socket exposes selected actor capabilities to colocated software.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Enable the socket, call `c.actorRuntimeSocket()` to get its path, and pass that path to your process:
|
||||
|
||||
```ts actor.ts
|
||||
import { spawn } from "node:child_process";
|
||||
import { actor } from "rivetkit";
|
||||
import { db } from "rivetkit/db";
|
||||
|
||||
export const example = actor({
|
||||
options: { enableActorRuntimeSocket: true },
|
||||
db: db({
|
||||
onMigrate: (db) =>
|
||||
db.execute("CREATE TABLE IF NOT EXISTS items (value TEXT)"),
|
||||
}),
|
||||
actions: {
|
||||
startWorker: async (c) => {
|
||||
// Get the Unix socket path external processes use
|
||||
// to communicate with the actor.
|
||||
// (Example: /tmp/rivet-actor-runtime.abc/def.sock)
|
||||
const { path } = await c.actorRuntimeSocket();
|
||||
|
||||
// Spawn a process that can access the actor's
|
||||
// SQLite database over the socket.
|
||||
spawn(process.execPath, ["worker.js"], {
|
||||
env: { ...process.env, ACTOR_RUNTIME_SOCKET_PATH: path },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts worker.ts
|
||||
import { connectRuntimeSocket } from "./generated/runtime-socket-client";
|
||||
|
||||
const db = await connectRuntimeSocket(process.env.ACTOR_RUNTIME_SOCKET_PATH!);
|
||||
const rows = await db.query("SELECT value FROM items", []);
|
||||
console.log(rows);
|
||||
```
|
||||
|
||||
The path is valid only for the current actor generation, so get it again after the actor restarts or wakes. Only share it with trusted local processes.
|
||||
|
||||
## Protocol
|
||||
|
||||
The [protocol schema](https://github.com/rivet-dev/rivet/blob/main/engine/sdks/rust/actor-runtime-socket-protocol/schemas/v1.bare) defines the handshake, SQLite requests, values, responses, and errors. Generate codecs using vbare's [TypeScript](https://github.com/rivet-dev/vbare/tree/main/typescript) or [Rust](https://github.com/rivet-dev/vbare/tree/main/rust) tooling, then add a small Unix socket wrapper for its length-prefixed frames. `connectRuntimeSocket` above represents that application-specific wrapper.
|
||||
|
||||
## SQLite transaction interleaving
|
||||
|
||||
Separate socket requests can interleave with SQLite work from the main actor. For multi-request atomic work, begin a transaction with a client-generated `leaseKey`, include that key with each query, and commit or roll back with the same key. Other actor and socket SQL waits until that transaction finishes.
|
||||
|
||||
_Source doc path: /docs/actors/actor-runtime-socket_
|
||||
50
.agents/skills/live-cursors/reference/actors/appearance.md
Normal file
50
.agents/skills/live-cursors/reference/actors/appearance.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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`:
|
||||
|
||||
## Icon Formats
|
||||
|
||||
The `icon` property accepts two formats:
|
||||
|
||||
### Emoji
|
||||
|
||||
Use any emoji character directly:
|
||||
|
||||
### FontAwesome Icons
|
||||
|
||||
Use [FontAwesome](https://fontawesome.com/search) icon names without the "fa" prefix:
|
||||
|
||||
## 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:
|
||||
|
||||
## 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`:
|
||||
|
||||
Users can then use this directly:
|
||||
|
||||
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:
|
||||
|
||||
The built-in `workflow()` helper uses this pattern to automatically display the workflow icon for workflow-based actors.
|
||||
|
||||
_Source doc path: /docs/actors/appearance_
|
||||
105
.agents/skills/live-cursors/reference/actors/authentication.md
Normal file
105
.agents/skills/live-cursors/reference/actors/authentication.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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.
|
||||
|
||||
### `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.
|
||||
|
||||
## 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:
|
||||
|
||||
### Handling Errors
|
||||
|
||||
Authentication errors use the same system as regular errors. See [errors](/docs/actors/errors) for more details.
|
||||
|
||||
## Examples
|
||||
|
||||
### JWT
|
||||
|
||||
Validate JSON Web Tokens and extract user claims:
|
||||
|
||||
### External Auth Provider
|
||||
|
||||
Validate credentials against an external authentication service:
|
||||
|
||||
### Using `c.state` In Authorization
|
||||
|
||||
Access actor state via `c.state` and the actor's key via `c.key` to make authorization decisions:
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
Create helper functions for common authorization patterns:
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Use `c.vars` to track connection attempts and rate limit by user:
|
||||
|
||||
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.
|
||||
|
||||
## 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,42 @@
|
||||
# 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.
|
||||
|
||||
## Use Cases and Patterns
|
||||
|
||||
### Actor Orchestration
|
||||
|
||||
Use a coordinator actor to manage complex workflows:
|
||||
|
||||
### Data Aggregation
|
||||
|
||||
Collect data from multiple actors:
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
Use connections to listen for events from other actors:
|
||||
|
||||
### Batch Operations
|
||||
|
||||
Process multiple items in parallel:
|
||||
|
||||
## 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_
|
||||
119
.agents/skills/live-cursors/reference/actors/connections.md
Normal file
119
.agents/skills/live-cursors/reference/actors/connections.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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).
|
||||
|
||||
## 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:
|
||||
|
||||
## Connection State
|
||||
|
||||
There are two ways to define an actor's connection state:
|
||||
|
||||
|
||||
### connState
|
||||
|
||||
Define connection state as a constant value:
|
||||
|
||||
|
||||
|
||||
This value will be cloned for every new connection using `structuredClone`.
|
||||
|
||||
|
||||
|
||||
### createConnState
|
||||
|
||||
Create connection state dynamically with a function called for each connection:
|
||||
|
||||
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
`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:
|
||||
|
||||
If you need to wait for the disconnection to complete, you can use `await`:
|
||||
|
||||
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/live-cursors/reference/actors/debugging.md
Normal file
644
.agents/skills/live-cursors/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 in the actor KV compatibility snapshot. After an actor has migrated to SQLite-backed runtime storage, this endpoint is stale for user KV and internal runtime records; it continues to expose the frozen pre-migration KV data. The inspector token key is the exception and remains mirrored for dashboard compatibility.
|
||||
|
||||
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 internal SQLite storage. It is also mirrored to legacy KV key `0x03` (base64 `Aw==`) for dashboard compatibility. 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 internal SQLite storage. The token is also mirrored to the legacy KV key below so the Rivet dashboard can continue to retrieve it through 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_
|
||||
163
.agents/skills/live-cursors/reference/actors/design-patterns.md
Normal file
163
.agents/skills/live-cursors/reference/actors/design-patterns.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 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
|
||||
|
||||
### Client
|
||||
|
||||
## 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
|
||||
|
||||
### Client
|
||||
|
||||
**Example: Random Sharding**
|
||||
|
||||
### Actor
|
||||
|
||||
### Client
|
||||
|
||||
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
|
||||
|
||||
### Client
|
||||
|
||||
## 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
|
||||
|
||||
### Client
|
||||
|
||||
### 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
|
||||
|
||||
### Client
|
||||
|
||||
`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:**
|
||||
|
||||
**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_
|
||||
71
.agents/skills/live-cursors/reference/actors/destroy.md
Normal file
71
.agents/skills/live-cursors/reference/actors/destroy.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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:
|
||||
|
||||
### 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:
|
||||
|
||||
## 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_
|
||||
81
.agents/skills/live-cursors/reference/actors/errors.md
Normal file
81
.agents/skills/live-cursors/reference/actors/errors.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
## Error Codes
|
||||
|
||||
Use error codes for explicit error matching in try-catch blocks:
|
||||
|
||||
### Actor
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
## Errors With Metadata
|
||||
|
||||
Include metadata to provide additional context for rich error handling:
|
||||
|
||||
### Actor
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
## 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
|
||||
|
||||
### Client (Connection)
|
||||
|
||||
### Client (Stateless)
|
||||
|
||||
### 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:
|
||||
|
||||
## 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_
|
||||
143
.agents/skills/live-cursors/reference/actors/events.md
Normal file
143
.agents/skills/live-cursors/reference/actors/events.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 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).
|
||||
|
||||
## 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:
|
||||
|
||||
### Sending to Specific Connections
|
||||
|
||||
Send events to individual connections using `conn.send(eventName, ...args)`:
|
||||
|
||||
Send events to all connections except the sender:
|
||||
|
||||
## 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:
|
||||
|
||||
```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:
|
||||
|
||||
```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:
|
||||
|
||||
```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_
|
||||
10
.agents/skills/live-cursors/reference/actors/helper-types.md
Normal file
10
.agents/skills/live-cursors/reference/actors/helper-types.md
Normal file
@@ -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_
|
||||
10
.agents/skills/live-cursors/reference/actors/http-api.md
Normal file
10
.agents/skills/live-cursors/reference/actors/http-api.md
Normal file
@@ -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_
|
||||
51
.agents/skills/live-cursors/reference/actors/input.md
Normal file
51
.agents/skills/live-cursors/reference/actors/input.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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:
|
||||
|
||||
## Accessing Input in Lifecycle Hooks
|
||||
|
||||
Input is available as the second argument to the `createState` and `onCreate` lifecycle hooks:
|
||||
|
||||
## Input Validation
|
||||
|
||||
You can validate input parameters in the `createState` or `onCreate` hooks:
|
||||
|
||||
## 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
|
||||
|
||||
## Input Best Practices
|
||||
|
||||
### Use Type Safety
|
||||
|
||||
Define input types to ensure type safety:
|
||||
|
||||
### 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:
|
||||
|
||||
## 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_
|
||||
251
.agents/skills/live-cursors/reference/actors/inspector-tabs.md
Normal file
251
.agents/skills/live-cursors/reference/actors/inspector-tabs.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 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:
|
||||
|
||||
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:
|
||||
|
||||
## 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_
|
||||
51
.agents/skills/live-cursors/reference/actors/keys.md
Normal file
51
.agents/skills/live-cursors/reference/actors/keys.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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:
|
||||
|
||||
### 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:
|
||||
|
||||
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:
|
||||
|
||||
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:
|
||||
|
||||
## Accessing Keys in Metadata
|
||||
|
||||
Access the actor's key within the actor using the [metadata](/docs/actors/metadata) API:
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Simple Configuration with Keys
|
||||
|
||||
Use keys to provide basic actor configuration:
|
||||
|
||||
### Complex Configuration with Input
|
||||
|
||||
For more complex configuration, use [input parameters](/docs/actors/input):
|
||||
|
||||
## 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_
|
||||
44
.agents/skills/live-cursors/reference/actors/kv.md
Normal file
44
.agents/skills/live-cursors/reference/actors/kv.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
|
||||
## 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 returns a concrete type based on the option you pass in:
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## API Reference
|
||||
|
||||
- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - `c.kv` is available on the context
|
||||
|
||||
_Source doc path: /docs/actors/kv_
|
||||
388
.agents/skills/live-cursors/reference/actors/lifecycle.md
Normal file
388
.agents/skills/live-cursors/reference/actors/lifecycle.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# Lifecycle
|
||||
|
||||
> Source: `src/content/docs/actors/lifecycle.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/lifecycle
|
||||
> Description: Learn about actor lifecycle hooks for initialization, state management, and cleanup.
|
||||
|
||||
---
|
||||
# Lifecycle
|
||||
|
||||
Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Actors transition through several states during their lifetime. Each transition triggers specific hooks that let you initialize resources, manage connections, and clean up state.
|
||||
|
||||
```
|
||||
Loading ──Start──▶ Ready ──spawn driver──▶ Started
|
||||
│
|
||||
┌────────────────────────────────────────┤
|
||||
│ │
|
||||
│ idle timer + can_sleep │ Destroy command
|
||||
▼ ▼
|
||||
SleepGrace ─── grace window closes ──▶ Destroying
|
||||
│ │
|
||||
▼ │
|
||||
SleepFinalize ──── stop sequence ───────────┤
|
||||
▼
|
||||
Terminated
|
||||
```
|
||||
|
||||
**On Create** (runs once per actor)
|
||||
|
||||
1. `onMigrate`
|
||||
2. `createState`
|
||||
3. `onCreate`
|
||||
4. `createVars`
|
||||
5. `onWake`
|
||||
6. `run` (background, does not block)
|
||||
|
||||
**On Destroy**
|
||||
|
||||
1. `onDestroy`
|
||||
|
||||
**On Wake** (after sleep, restart, or crash)
|
||||
|
||||
1. `onMigrate`
|
||||
2. `createVars`
|
||||
3. `onWake`
|
||||
4. `run` (background, does not block)
|
||||
|
||||
**On Sleep** (after idle period)
|
||||
|
||||
1. Wait for `run` to complete (with timeout)
|
||||
2. `onSleep`
|
||||
|
||||
**On Connect** (per client)
|
||||
|
||||
1. `onBeforeConnect`
|
||||
2. `createConnState`
|
||||
3. `onConnect`
|
||||
|
||||
**On Disconnect** (per client)
|
||||
|
||||
1. `onDisconnect`
|
||||
|
||||
**On Inbound Action Invoke** (per action call)
|
||||
|
||||
1. Action handler executes
|
||||
|
||||
**On Inbound Queue Publish** (per `handle.send(...)`)
|
||||
|
||||
1. `queues.<name>.canPublish` (if defined)
|
||||
2. Queue message is enqueued
|
||||
|
||||
**On Event Subscription Request** (per subscribe request)
|
||||
|
||||
1. `events.<name>.canSubscribe` (if defined)
|
||||
2. Subscription is applied
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
Actor lifecycle hooks are defined as functions in the actor configuration.
|
||||
|
||||
### `state`
|
||||
|
||||
The `state` constant defines the initial state of the actor. See [state documentation](/docs/actors/state) for more information.
|
||||
|
||||
### `onMigrate`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `onMigrate` hook runs on every actor start, before `createState`, `onCreate`, `createVars`, and `onWake`. Can be async. It runs early so that database migrations are applied before any other lifecycle hook accesses the database. The second parameter is `true` when the actor is being created for the first time.
|
||||
|
||||
### `createState`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `createState` function dynamically initializes state based on input. Called only once when the actor is first created. Can be async. See [state documentation](/docs/actors/state) for more information.
|
||||
|
||||
### `vars`
|
||||
|
||||
The `vars` constant defines ephemeral variables for the actor. These variables are not persisted and are useful for storing runtime-only data. The value for `vars` must be clonable via `structuredClone`. See [ephemeral variables documentation](/docs/actors/state#ephemeral-variables) for more information.
|
||||
|
||||
### `createVars`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `createVars` function dynamically initializes ephemeral variables. Can be async. Use this when you need to initialize values at runtime. See [ephemeral variables documentation](/docs/actors/state#ephemeral-variables) for more information.
|
||||
|
||||
### `onCreate`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `onCreate` hook is called when the actor is first created. Can be async. Use this hook for initialization logic that doesn't affect the initial state.
|
||||
|
||||
### `onDestroy`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `onDestroy` hook is called when the actor is being permanently destroyed. Can be async. Use this for final cleanup operations like closing external connections, releasing resources, or performing any last-minute state persistence.
|
||||
|
||||
The actor is still fully functional when `onDestroy` runs. You can access the database, broadcast events, call `waitUntil`, send queue messages, and use `schedule.after`. State mutations made during `onDestroy` are persisted before the actor is torn down.
|
||||
|
||||
### `onWake`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
This hook is called any time the actor is started (e.g. after restarting, upgrading code, or crashing). Can be async.
|
||||
|
||||
This is called after the actor has been initialized but before any connections are accepted.
|
||||
|
||||
Use this hook to set up any resources or start any background tasks, such as `setInterval`.
|
||||
|
||||
### `onSleep`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
This hook is called when the actor is going to sleep. Can be async. Use this to clean up resources, close connections, or perform any shutdown operations.
|
||||
|
||||
The actor is still fully functional when `onSleep` runs. You can access the database, broadcast events, call `waitUntil`, send queue messages, and use `schedule.after`. State mutations made during `onSleep` are persisted before the actor finishes sleeping.
|
||||
|
||||
This hook may not always be called in situations like crashes or forced terminations. Don't rely on it for critical cleanup operations.
|
||||
|
||||
Not supported on Cloudflare Workers.
|
||||
|
||||
### `run`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `run` hook is called after the actor starts and runs in the background without blocking actor startup. This is ideal for long-running background tasks like:
|
||||
|
||||
- Reading from message queues in a loop
|
||||
- Tick loops for periodic work
|
||||
- Custom workflow logic
|
||||
- Background processing
|
||||
|
||||
The handler exposes `c.aborted` for loop checks and `c.abortSignal` for canceling operations when the actor is stopping. You should always check or listen for shutdown to exit gracefully.
|
||||
|
||||
**Important behavior:**
|
||||
- The actor may go to sleep at any time during the `run` handler. Wrap work that must keep the actor awake with `c.keepAwake(promise)` to block idle sleep until the promise settles.
|
||||
- If the `run` handler exits (returns), the actor follows its normal idle sleep timeout once it becomes idle
|
||||
- If the `run` handler throws an error, the actor logs the error and then follows its normal idle sleep timeout once it becomes idle
|
||||
- On shutdown, `c.abortSignal` fires so the `run` handler can exit within the graceful shutdown window.
|
||||
|
||||
Finite `run` handlers leave the actor alive after they finish. If you want a one shot task to clean itself up when it is done, call `c.destroy()` before returning.
|
||||
|
||||
### `onStateChange`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
Called whenever the actor's state changes. Cannot be async. This is often used to broadcast state updates.
|
||||
|
||||
Do not mutate `c.state` inside `onStateChange`; re-entrant state mutation is rejected.
|
||||
|
||||
### `createConnState` and `connState`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.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.
|
||||
|
||||
### `onBeforeConnect`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.BeforeConnectContext.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 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.ConnectContext.html)
|
||||
|
||||
Executed after the client has successfully connected. Can be async. Receives the connection object as a second parameter.
|
||||
|
||||
Messages will not be processed for this actor until this hook succeeds. Errors thrown from this hook will cause the client to disconnect.
|
||||
|
||||
### `canPublish` and `canSubscribe`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
Use schema-level hooks to authorize queue publishes and event subscriptions. Both hooks can be async and must return booleans:
|
||||
|
||||
- `queues.<name>.canPublish` runs before inbound queue publishes.
|
||||
- `events.<name>.canSubscribe` runs before inbound event subscription requests.
|
||||
|
||||
For actions, enforce authorization directly inside each action handler.
|
||||
|
||||
Use deny-by-default rules for each hook and return `false` unless explicitly allowed. See [Access Control](/docs/actors/access-control) for full guidance.
|
||||
|
||||
### `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.
|
||||
|
||||
### `onRequest`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.RequestContext.html)
|
||||
|
||||
The `onRequest` hook handles HTTP requests sent to your actor at `/actors/{actorName}/http/*` endpoints. Can be async. It receives the request context and a standard `Request` object, and should return a `Response` object.
|
||||
|
||||
See [Request Handler](/docs/actors/request-handler) for more details.
|
||||
|
||||
### `onWebSocket`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.WebSocketContext.html)
|
||||
|
||||
The `onWebSocket` hook handles WebSocket connections to your actor. Can be async. It receives the actor context and a `WebSocket` object. Use this to set up WebSocket event listeners and handle real-time communication.
|
||||
|
||||
See [WebSocket Handler](/docs/actors/websocket-handler) for more details.
|
||||
|
||||
### `onBeforeActionResponse`
|
||||
|
||||
[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html)
|
||||
|
||||
The `onBeforeActionResponse` hook is called before sending an action response to the client. Can be async. Use this hook to modify or transform the output of an action before it's sent to the client. This is useful for formatting responses, adding metadata, or applying transformations to the output.
|
||||
|
||||
## Sleeping
|
||||
|
||||
Actors automatically sleep after a period of inactivity to free up resources. When a request arrives for a sleeping actor, it wakes up, restores its state, and handles the request.
|
||||
|
||||
### When Actors Sleep
|
||||
|
||||
#### Idle Timeout
|
||||
|
||||
An actor is considered idle and eligible to sleep when **all** of the following are true:
|
||||
|
||||
- No active HTTP requests
|
||||
- No active connections (unless they are hibernatable WebSockets)
|
||||
- No active `run` handler (unless it is waiting on a queue)
|
||||
- No outstanding `c.keepAwake(promise)` promises
|
||||
- No pending disconnect callbacks
|
||||
- No async `onWebSocket` event handlers (eg `open`, `message`, `close`) still running
|
||||
|
||||
Once the actor becomes idle, the sleep timer starts. After `sleepTimeout` (default 30 seconds) of continuous inactivity, the actor begins the sleep process. Any activity resets the timer.
|
||||
|
||||
Outbound requests (e.g. `fetch` calls) do not count as activity and will not keep the actor awake. Wrap them with `c.waitUntil()` if they must complete before the actor sleeps.
|
||||
|
||||
#### Upgrades & Eviction
|
||||
|
||||
The platform may force an actor to migrate to a new machine during version upgrades or when a serverless request is about to timeout. The same [shutdown sequence](#shutdown-sequence) runs, then the actor is rescheduled on a new machine and wakes up with its persisted state.
|
||||
|
||||
Use `onSleep`, `waitUntil`, or `keepAwake` to control the length of the grace period before the actor moves to another machine.
|
||||
|
||||
### Manual Lifecycle Controls
|
||||
|
||||
You can also trigger lifecycle transitions from the Rivet Cloud API. These endpoints are useful for operational workflows, debugging, and forcing an actor to move through the same sleep or reschedule path that the platform would normally trigger.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
"https://cloud-api.rivet.dev/actors/$ACTOR_ID/sleep?namespace=$NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
"https://cloud-api.rivet.dev/actors/$ACTOR_ID/reschedule?namespace=$NAMESPACE" \
|
||||
-H "Authorization: Bearer $RIVET_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
`/sleep` asks the actor to enter the normal sleep shutdown sequence. `/reschedule` asks the platform to allocate the actor again, which is useful after crashes or when you need to force a fresh placement. Both endpoints require the actor ID and namespace.
|
||||
|
||||
### Skip Ready Wait
|
||||
|
||||
The gateway normally holds requests until the actor is ready. The actor is not ready during startup (before `onWake` finishes) or during the sleep grace period (while `onSleep` and `waitUntil` are running). Probes and readiness checks can opt out with `skipReadyWait` to reach the actor's `onRequest` or `onWebSocket` handler in either window.
|
||||
|
||||
See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) on the JavaScript client page for usage.
|
||||
|
||||
### Keeping the Actor Awake
|
||||
|
||||
RivetKit gives you two primitives for holding the actor awake across background work. Both take a `Promise` and differ in how they interact with idle sleep and the grace period.
|
||||
|
||||
| Method | Accepts | Blocks idle sleep | Blocks grace finalize | Use case |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `c.keepAwake(promise)` | `Promise` (returns same promise) | Yes | Yes | Critical work that must keep the actor running end to end (for example a turn in a game, an ongoing tool call). |
|
||||
| `c.waitUntil(promise)` | `Promise<unknown>` (returns void) | No | Yes | Best-effort finalization work that may complete during the grace window (for example analytics flushes, cleanup writes). |
|
||||
|
||||
`c.keepAwake(promise)` is the preferred primitive for long-running work the actor should not sleep through. It holds a keep-awake counter until the promise settles, which blocks both idle sleep and the grace finalize step. The promise is returned unchanged, so you can `await` it if you need the value.
|
||||
|
||||
`setPreventSleep(enabled)` is deprecated and now a no-op. Wrap the work you want to keep alive with `c.keepAwake(promise)` instead.
|
||||
|
||||
### On Sleep Hook
|
||||
|
||||
The [`onSleep`](#onsleep) hook runs during shutdown for cleanup like clearing intervals or closing connections. It is best-effort and will not run if the actor crashes.
|
||||
|
||||
### Wait Before Sleep
|
||||
|
||||
`c.waitUntil(promise)` registers a background promise that must resolve before the actor finishes sleeping. Use this to flush data or finish in-flight work during shutdown without blocking the main execution flow.
|
||||
|
||||
The actor waits up to `sleepGracePeriod` for graceful sleep work during the [shutdown sequence](#shutdown-sequence). That single budget covers `onSleep`, `waitUntil`, `keepAwake`, async raw WebSocket handlers such as `message` and `close`. By default, this graceful sleep window is 15 seconds total. If the timeout is exceeded, the actor proceeds with sleep anyway.
|
||||
|
||||
### Sleep Timeouts
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `sleepTimeout` | 30 seconds | Time of inactivity before the actor begins sleeping. |
|
||||
| `sleepGracePeriod` | 15 seconds | Total graceful shutdown window for hooks, `waitUntil`, `keepAwake`, async raw WebSocket handlers, and disconnects. |
|
||||
|
||||
Rivet enforces a hard limit of **30 minutes** for the entire stop process. These can be configured in the [options](#options).
|
||||
|
||||
### WebSocket Hibernation
|
||||
|
||||
WebSocket connections are preserved across sleep cycles by default and transparently migrated to the new actor instance. Client stays connected and sees no interruption. Actor migration is very fast, realtime workloads are not interrupted.
|
||||
|
||||
### Shutdown Sequence
|
||||
|
||||
When an actor sleeps or is destroyed, it enters the graceful shutdown window:
|
||||
|
||||
1. `c.abortSignal` fires and `c.aborted` becomes `true`. New connections and dispatch are rejected. Alarm timeouts are cancelled. On sleep, scheduled events are persisted and will be re-armed when the actor wakes.
|
||||
2. `onSleep` or `onDestroy` and `onDisconnect` for each closing connection run during the same window. User `waitUntil` promises and async raw WebSocket handlers are drained. Hibernatable WebSocket connections are preserved on sleep and closed on destroy.
|
||||
3. Once graceful work has completed, state is saved and final cleanup runs.
|
||||
|
||||
The entire window is bounded by `sleepGracePeriod` on both sleep and destroy. Defaults to 15 seconds. If the window is exceeded, the actor proceeds to state save anyway.
|
||||
|
||||
#### Graceful shutdown window
|
||||
|
||||
During steps 1 through 6, the actor is still fully functional. Database access, `broadcast`, `waitUntil`, `queue.send`, and `schedule.after` all work. State mutations are persisted at step 7. Actions invoked by pre-existing connections or lifecycle hooks continue to execute normally.
|
||||
|
||||
New connections and raw WebSocket upgrades are rejected as soon as the shutdown sequence begins. New requests that arrive during shutdown are held until the actor wakes up again. The caller does not need to retry.
|
||||
|
||||
If `schedule.after` is called during shutdown, the event is persisted so it survives the sleep/wake cycle, but no local timeout is scheduled. The event will fire after the actor wakes.
|
||||
|
||||
In-flight actions are **not** waited on during shutdown. If an action must complete before the actor stops, wrap the critical work with `c.waitUntil()`.
|
||||
|
||||
## Options
|
||||
|
||||
The `options` object allows you to configure various timeouts and behaviors for your actor.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `createVarsTimeout` | 5000ms | Timeout for `createVars` function |
|
||||
| `createConnStateTimeout` | 5000ms | Timeout for `createConnState` function |
|
||||
| `onConnectTimeout` | 5000ms | Timeout for `onConnect` hook |
|
||||
| `sleepGracePeriod` | 15000ms | Total graceful shutdown window for both sleep and destroy |
|
||||
| `stateSaveInterval` | 1000ms | Interval for persisting state |
|
||||
| `actionTimeout` | 60000ms | Timeout for action execution |
|
||||
| `connectionLivenessTimeout` | 2500ms | Timeout for connection liveness check |
|
||||
| `connectionLivenessInterval` | 5000ms | Interval for connection liveness check |
|
||||
| `sleepTimeout` | 30000ms | Time before actor sleeps due to inactivity |
|
||||
| `canHibernateWebSocket` | false | Whether WebSockets can hibernate (experimental) |
|
||||
|
||||
## Advanced
|
||||
|
||||
### Actor Shutdown Abort Signal
|
||||
|
||||
The `c.abortSignal` provides an `AbortSignal` that fires when the actor is stopping, and `c.aborted` is the shorthand boolean for loop checks. Use these to cancel ongoing operations when the actor sleeps or is destroyed.
|
||||
|
||||
The abort signal fires at the very start of the [shutdown sequence](#shutdown-sequence), before `onSleep` or `onDestroy` runs. This means `c.aborted` is already `true` inside those lifecycle hooks. The signal fires early so that the `run` handler can exit promptly, but the actor remains fully functional throughout the graceful shutdown window.
|
||||
|
||||
See [Canceling Long-Running Actions](/docs/actors/actions#canceling-long-running-actions) for manually canceling operations on-demand.
|
||||
|
||||
### Using `ActorContext` Type Externally
|
||||
|
||||
When extracting logic from lifecycle hooks or actions into external functions, you'll often need to define the type of the context parameter. Rivet provides helper types that make it easy to extract and pass these context types to external functions.
|
||||
|
||||
See [Types](/docs/actors/types) for more details on using `ActorContextOf`.
|
||||
|
||||
## Full Example
|
||||
|
||||
_Source doc path: /docs/actors/lifecycle_
|
||||
169
.agents/skills/live-cursors/reference/actors/limits.md
Normal file
169
.agents/skills/live-cursors/reference/actors/limits.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 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`:
|
||||
|
||||
Per-actor limits such as queue sizes and lifecycle timeouts are passed to `actor(...)` via `options`:
|
||||
|
||||
## 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`; hard limit configurable in self-hosted engine config via `guard.websocket_max_message_size`. |
|
||||
| Max outgoing message size | 1 MiB | 32 MiB | Maximum size of outgoing WebSocket messages. Soft limit configurable via `maxOutgoingMessageSize`; hard limit configurable in self-hosted engine config via `guard.websocket_max_message_size`. |
|
||||
| Max WebSocket frame size | — | 32 MiB | Maximum size of a single WebSocket frame accepted by Rivet's transport edge. Configurable in self-hosted engine config via `guard.websocket_max_frame_size`. This is not configurable by browser clients and is not negotiated during the WebSocket handshake. |
|
||||
| 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 | 60 seconds | — | Maximum time for an `onRequest` handler to complete. Defaults to `actionTimeout`; configure with `actionTimeout`. |
|
||||
|
||||
### 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. |
|
||||
| Transaction deadline | 60 seconds | — | Safety backstop for `db.transaction()`. Configure per transaction with `{ timeout: milliseconds }`; there is no product-level maximum. |
|
||||
| Transaction coordinator queue | — | 128 operations | Maximum admitted SQLite operations waiting behind an active coordinated transaction. Additional operations fail with `sqlite.transaction_queue_full`. |
|
||||
|
||||
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.
|
||||
|
||||
Avoid `VACUUM` inside actor databases. It rewrites the database in one transaction and can exceed the dirty-page commit limit even when ordinary incremental writes fit.
|
||||
|
||||
### Actor Runtime Socket
|
||||
|
||||
The Beta [Actor Runtime Socket](/docs/actors/actor-runtime-socket) negotiates its active frame limit during the handshake.
|
||||
|
||||
| Name | Soft Limit | Hard Limit | Description |
|
||||
|------|------------|------------|-------------|
|
||||
| Frame payload size | 32 MiB | — | Default maximum request or response payload. Self-hosted native runtimes can configure `RIVET_ACTOR_RUNTIME_SOCKET_MAX_FRAME_BYTES`; oversized responses return `ResponseTooLarge`. |
|
||||
|
||||
### 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`. |
|
||||
| On request timeout | 60 seconds | — | Timeout for raw `onRequest` handlers. Defaults to `actionTimeout`; configure with `actionTimeout`. |
|
||||
| On before connect timeout | 5 seconds | — | Timeout for the `onBeforeConnect` hook. Configurable via `onBeforeConnectTimeout`. |
|
||||
| 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`. |
|
||||
| On migrate timeout | 30 seconds | — | Timeout for `onMigrate` hook. Configurable via `onMigrateTimeout`. |
|
||||
| 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`. |
|
||||
| Connection liveness timeout | 2.5 seconds | — | Time a stateful connection can miss liveness checks before RivetKit treats it as unhealthy. Configurable via `connectionLivenessTimeout`. |
|
||||
| Connection liveness interval | 5 seconds | — | Interval between stateful connection liveness checks. Configurable via `connectionLivenessInterval`. |
|
||||
|
||||
### 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_
|
||||
39
.agents/skills/live-cursors/reference/actors/metadata.md
Normal file
39
.agents/skills/live-cursors/reference/actors/metadata.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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:
|
||||
|
||||
## Actor Name
|
||||
|
||||
Get the actor type name:
|
||||
|
||||
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:
|
||||
|
||||
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`.
|
||||
|
||||
`c.region` is only supported on Rivet at the moment.
|
||||
|
||||
## Example Usage
|
||||
|
||||
## 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_
|
||||
120
.agents/skills/live-cursors/reference/actors/queues.md
Normal file
120
.agents/skills/live-cursors/reference/actors/queues.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
|
||||
## 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(...)`.
|
||||
|
||||
## 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(...)`.
|
||||
|
||||
## Request/reply pattern
|
||||
|
||||
Use this when the sender needs data back from queued work.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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/).
|
||||
|
||||
## 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.
|
||||
|
||||
## Poll messages
|
||||
|
||||
Use `tryNext` when you need one non-blocking read.
|
||||
Use `tryNextBatch` for non-blocking batch reads.
|
||||
|
||||
- Returns immediately and never waits.
|
||||
|
||||
## Abort signals
|
||||
|
||||
Use `signal` when your receive loop needs external cancellation semantics in addition to actor shutdown behavior.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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:
|
||||
|
||||
### Delayed delivery
|
||||
|
||||
Use [`c.schedule`](/docs/actors/schedule) to enqueue messages at a future time instead of processing them immediately:
|
||||
|
||||
See [Schedule](/docs/actors/schedule) for the full scheduling API.
|
||||
|
||||
_Source doc path: /docs/actors/queues_
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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:
|
||||
|
||||
### 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
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### React
|
||||
|
||||
See the [React documentation](/docs/clients/react) for more information.
|
||||
|
||||
### Deploy
|
||||
|
||||
## Configuration Options
|
||||
|
||||
_Source doc path: /docs/actors/quickstart/backend_
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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
|
||||
|
||||
See the [JavaScript client documentation](/docs/clients/javascript) for more information.
|
||||
|
||||
### React
|
||||
|
||||
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 | `RivetLogger` | 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,117 @@
|
||||
# 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:
|
||||
|
||||
### 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,82 @@
|
||||
# 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:
|
||||
|
||||
### Create React Frontend
|
||||
|
||||
Set up your React application:
|
||||
|
||||
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_
|
||||
249
.agents/skills/live-cursors/reference/actors/quickstart/rust.md
Normal file
249
.agents/skills/live-cursors/reference/actors/quickstart/rust.md
Normal file
@@ -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,109 @@
|
||||
# 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);
|
||||
```
|
||||
|
||||
Add a `deno.json` next to the function so the deploy bundles only the WebAssembly runtime. It points `rivetkit` at the pre-bundled `@rivetkit/supabase`, keeping the deploy small. Without it, the deploy pulls Rivet's native engine and 413s.
|
||||
|
||||
```json supabase/functions/rivet/deno.json
|
||||
{
|
||||
"imports": {
|
||||
"rivetkit": "npm:@rivetkit/supabase",
|
||||
"@rivetkit/supabase": "npm:@rivetkit/supabase"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your function code keeps importing from `rivetkit` as usual. The import map only changes how Deno resolves it at bundle time.
|
||||
|
||||
### 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,79 @@
|
||||
# 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
|
||||
|
||||
### Hono
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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:
|
||||
|
||||
```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.
|
||||
|
||||
## 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_
|
||||
10
.agents/skills/live-cursors/reference/actors/scaling.md
Normal file
10
.agents/skills/live-cursors/reference/actors/scaling.md
Normal file
@@ -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_
|
||||
190
.agents/skills/live-cursors/reference/actors/schedule.md
Normal file
190
.agents/skills/live-cursors/reference/actors/schedule.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Schedule & Cron
|
||||
|
||||
> Source: `src/content/docs/actors/schedule.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/schedule
|
||||
> Description: Run durable one-shot and recurring actor actions on a schedule.
|
||||
|
||||
---
|
||||
Rivet Actor schedules invoke actions on the same actor and survive sleep, restarts, upgrades, and crashes. Use one-shot schedules for delayed work, cron jobs for calendar-based work, and fixed intervals for frequent jobs.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts After
|
||||
const reminders = actor({
|
||||
onCreate: async (c) => {
|
||||
await c.schedule.after(30_000, "sendReminder", "reminder-123");
|
||||
},
|
||||
actions: {
|
||||
sendReminder: (_c, reminderId: string) => {
|
||||
console.log("Sending reminder", reminderId);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts Cron
|
||||
const reports = actor({
|
||||
onCreate: async (c) => {
|
||||
await c.cron.set({
|
||||
name: "daily-report",
|
||||
expression: "0 9 * * *",
|
||||
action: "runReport",
|
||||
args: ["sales"], // Optional.
|
||||
timezone: "America/Los_Angeles", // Optional; defaults to UTC.
|
||||
maxHistory: 100, // Optional; defaults to 100. Set to 0 to disable.
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
runReport: (_c, report: string) => {
|
||||
console.log("Running report", report);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts Every
|
||||
const cache = actor({
|
||||
onCreate: async (c) => {
|
||||
await c.cron.every({
|
||||
name: "refresh-cache",
|
||||
interval: 60_000, // Minimum 5 seconds.
|
||||
action: "refreshCache",
|
||||
args: ["products"], // Optional.
|
||||
maxHistory: 100, // Optional; defaults to 100. Set to 0 to disable.
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
refreshCache: (_c, cache: string) => {
|
||||
console.log("Refreshing cache", cache);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Sleep Between Runs
|
||||
|
||||
[Actors do not need to stay awake](/docs/actors/lifecycle#sleeping) while waiting for a scheduled action. Once an actor becomes idle, it can sleep normally. Sleeping does not pause or remove its schedules. Rivet wakes the actor when the next action is due, whether that is seconds or arbitrarily far in the future.
|
||||
|
||||
Recurring Cron and fixed-interval schedules continue until they are updated or deleted. One-shot schedules remain pending until their target time.
|
||||
|
||||
## One-shot schedules
|
||||
|
||||
### Run after a delay
|
||||
|
||||
Runs once after the given delay in milliseconds and returns the generated schedule ID.
|
||||
|
||||
```ts
|
||||
const id = await c.schedule.after(5_000, "sendReminder", reminderId);
|
||||
```
|
||||
|
||||
### Run at a specific time
|
||||
|
||||
Runs once at a Unix timestamp in milliseconds.
|
||||
|
||||
```ts
|
||||
const id = await c.schedule.at(Date.parse("2026-08-01T09:00:00Z"), "openSale", saleId);
|
||||
```
|
||||
|
||||
### Inspect and cancel schedules
|
||||
|
||||
```ts
|
||||
const schedule = await c.schedule.get(id);
|
||||
const pending = await c.schedule.list();
|
||||
const cancelled = await c.schedule.cancel(id);
|
||||
```
|
||||
|
||||
## Recurring jobs
|
||||
|
||||
### Run on a cron schedule
|
||||
|
||||
Cron jobs use standard five-field expressions. Names are unique per actor, and reusing a name updates the existing job. The timezone defaults to UTC.
|
||||
|
||||
```ts
|
||||
await c.cron.set({
|
||||
name: "daily-report",
|
||||
expression: "0 9 * * *",
|
||||
action: "runReport",
|
||||
args: ["sales"], // Optional.
|
||||
timezone: "America/Los_Angeles", // Optional; defaults to UTC.
|
||||
maxHistory: 100, // Optional; defaults to 100. Set to 0 to disable.
|
||||
});
|
||||
```
|
||||
|
||||
### Run at a fixed interval
|
||||
|
||||
Fixed-interval jobs keep their cadence anchored to scheduled deadlines, so action runtime does not introduce drift. Names are unique per actor, and reusing a name updates the existing job. The minimum interval is 5 seconds.
|
||||
|
||||
```ts
|
||||
await c.cron.every({
|
||||
name: "presence-sweep",
|
||||
interval: 15_000, // Minimum 5 seconds.
|
||||
action: "sweepPresence",
|
||||
args: [], // Optional.
|
||||
maxHistory: 25, // Optional; defaults to 100. Set to 0 to disable.
|
||||
});
|
||||
```
|
||||
|
||||
### Update and delete jobs
|
||||
|
||||
Submitting a recurring job with an existing name updates it. Changing its cadence calculates a new next run, while changing only its action, arguments, or history settings preserves the existing cadence.
|
||||
|
||||
```ts
|
||||
const job = await c.cron.get("daily-report");
|
||||
const jobs = await c.cron.list();
|
||||
const deleted = await c.cron.delete("daily-report");
|
||||
```
|
||||
|
||||
### View run history
|
||||
|
||||
```ts
|
||||
const recent = await c.cron.history("daily-report", {
|
||||
limit: 20, // Optional.
|
||||
});
|
||||
```
|
||||
|
||||
Each entry's `result` is `running`, `ok`, `error`, or `skipped`. Jobs retain 100 entries by default. Set maximum history to zero to disable and clear history, or choose up to 1,000 entries. An actor retains at most 10,000 history entries across all jobs.
|
||||
|
||||
## Run information
|
||||
|
||||
When a schedule invokes an action, it appends a ScheduledFireInfo object after the configured action arguments.
|
||||
|
||||
```ts
|
||||
import type { ScheduledFireInfo } from "rivetkit";
|
||||
|
||||
const reports = actor({
|
||||
onCreate: async (c) => {
|
||||
await c.cron.set({
|
||||
name: "daily-report",
|
||||
expression: "0 9 * * *",
|
||||
action: "runReport",
|
||||
args: ["sales"], // Optional.
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
runReport: (
|
||||
_c,
|
||||
report: string,
|
||||
fire: ScheduledFireInfo,
|
||||
) => {
|
||||
console.log(
|
||||
report,
|
||||
fire.kind,
|
||||
fire.id,
|
||||
fire.name,
|
||||
fire.scheduledAt,
|
||||
fire.firedAt,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Execution behavior
|
||||
|
||||
- **Durability**: Schedules survive actor sleep, restarts, upgrades, and crashes. See [Sleep Between Runs](#sleep-between-runs).
|
||||
- **Failures**: Failed runs are not retried immediately. A failed recurring run continues at the next normal cadence; a failed one-shot is complete after its attempted invocation. Cron failures are available in [run history](#view-run-history), so you can build a custom retry mechanism.
|
||||
- **Idempotency**: Scheduled actions should be idempotent so manually retrying a failed or interrupted run is safe.
|
||||
- **Overlaps**: Overlapping recurring runs are skipped.
|
||||
- **Missing actions**: Recurring jobs are deleted when their action no longer exists.
|
||||
|
||||
_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_
|
||||
248
.agents/skills/live-cursors/reference/actors/sqlite-drizzle.md
Normal file
248
.agents/skills/live-cursors/reference/actors/sqlite-drizzle.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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).
|
||||
|
||||
## 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 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_
|
||||
127
.agents/skills/live-cursors/reference/actors/sqlite.md
Normal file
127
.agents/skills/live-cursors/reference/actors/sqlite.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
```ts @nocheck
|
||||
await c.db.transaction(async (tx) => {
|
||||
await tx.execute("INSERT INTO todos (title) VALUES (?)", title);
|
||||
await tx.execute(
|
||||
"INSERT INTO comments (todo_id, body) VALUES (last_insert_rowid(), ?)",
|
||||
body,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
RivetKit commits when the callback resolves and rolls back when it throws. Other transactions and ordinary actor SQL queue in FIFO order until the callback finishes. Transactions have a 60-second safety timeout by default; increase it for legitimately long work with `{ timeout: 120_000 }`.
|
||||
|
||||
Always use the callback's `tx` value inside the transaction. Starting another transaction or using the outer `c.db` from the callback waits behind the active transaction and eventually reaches the safety timeout; the resulting error points to this possible deadlock.
|
||||
|
||||
Manual `BEGIN`/`COMMIT` calls remain supported for compatibility, but cannot protect against interleaving callers. RivetKit logs a warning recommending `db.transaction()`. Set `warnOnManualTransactions: false` in `db(...)` to disable the warning; the warning itself mentions this flag.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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_
|
||||
270
.agents/skills/live-cursors/reference/actors/state.md
Normal file
270
.agents/skills/live-cursors/reference/actors/state.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# 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.
|
||||
|
||||
### Ephemeral
|
||||
|
||||
Live objects on `c.vars` like database connections, API clients, and event emitters, or data loaded from an external source. Never persisted.
|
||||
|
||||
```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:
|
||||
|
||||
### 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:
|
||||
|
||||
### 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_
|
||||
36
.agents/skills/live-cursors/reference/actors/statuses.md
Normal file
36
.agents/skills/live-cursors/reference/actors/statuses.md
Normal file
@@ -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_
|
||||
51
.agents/skills/live-cursors/reference/actors/testing.md
Normal file
51
.agents/skills/live-cursors/reference/actors/testing.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
|
||||
## Testing Actor State
|
||||
|
||||
State persists within each test, allowing you to verify that your actor correctly maintains state between operations.
|
||||
|
||||
## Testing Events
|
||||
|
||||
For actors that emit events, you can verify events are correctly triggered by subscribing to them:
|
||||
|
||||
## Testing Schedules
|
||||
|
||||
Rivet's schedule functionality can be tested by scheduling work and waiting for it to run:
|
||||
|
||||
Use a short schedule and `expect.poll` the action's observable result. Vitest's fake date and fake JavaScript timers do not advance RivetKit's scheduler, which runs outside the test's JavaScript timer queue.
|
||||
|
||||
## 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**: Poll observable state or output with a bounded timeout instead of sleeping for an exact duration.
|
||||
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_
|
||||
154
.agents/skills/live-cursors/reference/actors/troubleshooting.md
Normal file
154
.agents/skills/live-cursors/reference/actors/troubleshooting.md
Normal file
@@ -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_
|
||||
46
.agents/skills/live-cursors/reference/actors/types.md
Normal file
46
.agents/skills/live-cursors/reference/actors/types.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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` orchestration helpers (root, `loop`, `try`, `race`, `join` branches) | `WorkflowContextOf` |
|
||||
| `workflow` `step` / `tryStep` run + rollback 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_
|
||||
241
.agents/skills/live-cursors/reference/actors/versions.md
Normal file
241
.agents/skills/live-cursors/reference/actors/versions.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
### 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:
|
||||
|
||||
**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:
|
||||
|
||||
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,201 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### 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:
|
||||
|
||||
## 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_
|
||||
228
.agents/skills/live-cursors/reference/actors/workflows.md
Normal file
228
.agents/skills/live-cursors/reference/actors/workflows.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Workflows
|
||||
|
||||
> Source: `src/content/docs/actors/workflows.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/actors/workflows
|
||||
> Description: Build durable, replayable run loops in Rivet Actors with steps, queue waits, timers, and rollback.
|
||||
|
||||
---
|
||||
Use workflows for durable, multi-step execution with replay safety.
|
||||
|
||||
## What are workflows?
|
||||
|
||||
A workflow is a durable, replayable run handler for a Rivet Actor.
|
||||
|
||||
- Survives restarts: workflow progress is saved automatically.
|
||||
- Re-runs safely: replay follows the same recorded steps.
|
||||
- Event-driven: workflows can pause for queue messages, then continue.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Simple workflow
|
||||
|
||||
Use this when you need a short multi-step sequence.
|
||||
|
||||
### Loops
|
||||
|
||||
This is the recommended workflow shape for most actor workloads.
|
||||
|
||||
- Use a queue wait inside the loop to receive the next unit of work.
|
||||
- Keep actor state changes in a single workflow loop.
|
||||
- This gives you one durable workflow that manages all actor progress.
|
||||
|
||||
### Setup & teardown
|
||||
|
||||
Use this when the workflow should initialize resources, process queued commands, then clean up.
|
||||
|
||||
## Features
|
||||
|
||||
### Queue
|
||||
|
||||
Use this for fire-and-forget commands where the client does not need a reply.
|
||||
|
||||
Use the `Loops` example above as the baseline pattern.
|
||||
|
||||
### Request/response (using queue)
|
||||
|
||||
Use this when the caller needs a response from queued processing.
|
||||
|
||||
### Timers
|
||||
|
||||
Use queue messages as the trigger source, then sleep durably inside the workflow.
|
||||
|
||||
### Join
|
||||
|
||||
Use `join` when several independent tasks can run in parallel.
|
||||
|
||||
### Race
|
||||
|
||||
Use `race` when you need first-winner behavior.
|
||||
|
||||
### Timeouts
|
||||
|
||||
Use step timeouts and retries for slow or flaky dependencies.
|
||||
|
||||
Step timeouts are critical by default and fail immediately. Set `retryOnTimeout: true` if a timeout should retry like any other error using `maxRetries`.
|
||||
|
||||
Workflows use roll-forward semantics everywhere. When a step throws, any `state` or `vars` mutations it made before failing are never rolled back, whether the step retries or the failure is caught by `tryStep` or `try`. The next attempt observes whatever the failed attempt already wrote, so write steps idempotently: check before you increment, or move the mutation after the fallible work.
|
||||
|
||||
### Handling terminal failures as data
|
||||
|
||||
Use `tryStep` when a step failure should produce data instead of failing the whole workflow.
|
||||
|
||||
Use `try` when you want to recover from terminal `step`, `join`, or `race` failures inside a named block.
|
||||
|
||||
```ts
|
||||
async function runPaymentFlow(ctx: any) {
|
||||
return await ctx.try("payment-flow", async (blockCtx: any) => {
|
||||
const auth = await blockCtx.step("authorize", async (blockCtx) =>
|
||||
authorizeOrder("order-123"),
|
||||
);
|
||||
const capture = await blockCtx.step("capture", async (blockCtx) =>
|
||||
captureOrder("order-123"),
|
||||
);
|
||||
return { auth, capture };
|
||||
});
|
||||
}
|
||||
|
||||
async function authorizeOrder(orderId: string): Promise<string> {
|
||||
return `auth-${orderId}`;
|
||||
}
|
||||
|
||||
async function captureOrder(orderId: string): Promise<string> {
|
||||
return `capture-${orderId}`;
|
||||
}
|
||||
```
|
||||
|
||||
- `tryStep` and `try` only catch terminal failures. Retry backoff, sleeps, queue waits, eviction, and history divergence still rethrow.
|
||||
- Catching a failure does not undo it. `state` and `vars` mutations made before the failure remain visible after `tryStep` or `try` returns, so use explicit compensating steps when a caught failure needs cleanup.
|
||||
- `RollbackError` is not caught by default. Pass `catch: ["rollback"]` when you want rollback failures returned as data.
|
||||
|
||||
### Error hooks
|
||||
|
||||
Use `onError` when you want a best-effort notification for workflow failures.
|
||||
|
||||
- Step failures include the attempt number, retry counts, whether the step will retry, and the next retry delay.
|
||||
- Workflow failures also include terminal errors outside steps, such as rollback failures or code/history mismatches.
|
||||
- The hook is observational. It is not part of workflow replay, so use it for logging, metrics, or updating non-critical actor state.
|
||||
- This is also a good place to forward workflow failures to Sentry or another error reporting pipeline.
|
||||
|
||||
### Rollback
|
||||
|
||||
Use rollback checkpoints before steps that have compensating actions.
|
||||
|
||||
## Patterns
|
||||
|
||||
### Store workflow progress in state + broadcast
|
||||
|
||||
Store progress in `state` so replay and recovery always restore it. Broadcast state changes so clients can render progress in realtime.
|
||||
|
||||
### Cron (queue-driven)
|
||||
|
||||
Rivet scheduling triggers actions. For cron-like workflows, use a small scheduled action as a bridge that enqueues work, then process that work in the workflow loop.
|
||||
|
||||
These are common workflow shapes used in production systems.
|
||||
|
||||
### Queue-driven worker
|
||||
|
||||
Use this when external systems enqueue work and the actor should process each item durably.
|
||||
|
||||
### Setup & teardown
|
||||
|
||||
Use this when you need one-time initialization before a long-lived loop, plus cleanup when the actor stops sleeping or is destroyed.
|
||||
|
||||
### Human approval gate
|
||||
|
||||
Use this when an operation must pause for a user or system decision before continuing.
|
||||
|
||||
### Fan-out / fan-in (join)
|
||||
|
||||
Use this when independent work items can run in parallel and you need a single merged result.
|
||||
|
||||
### Batch drainer
|
||||
|
||||
Use this when throughput matters and handling one message at a time is too expensive.
|
||||
|
||||
### Coordinator -> worker RPC
|
||||
|
||||
Use this when one actor orchestrates work by calling actions on other actors.
|
||||
|
||||
### Request/response over queue (async RPC)
|
||||
|
||||
Use this when you want decoupled actor-to-actor communication with durable waits and explicit completion.
|
||||
|
||||
### Scatter-gather across actors
|
||||
|
||||
Use this when multiple actors can process independent parts of a request in parallel, then return a merged response.
|
||||
|
||||
### Timeout + fallback actor
|
||||
|
||||
Use this when a primary actor call might be slow or unavailable and you need a deterministic fallback path.
|
||||
|
||||
### Cross-actor saga (compensating actions)
|
||||
|
||||
Use this when a workflow spans multiple actors and each side effect may need compensation.
|
||||
|
||||
### Signal-driven control loop
|
||||
|
||||
Use this when workflow progress should be triggered by commands/events instead of fixed polling intervals.
|
||||
|
||||
### Poll + backoff loop
|
||||
|
||||
Use this when an external dependency has variable availability and retries should slow down after failures.
|
||||
|
||||
### Child worker orchestration
|
||||
|
||||
Use this when one workflow coordinates many child workers (actors or worker workflows) and manages their lifecycle.
|
||||
|
||||
### Bounded drain + concurrency cap
|
||||
|
||||
Use this when inbound work can spike and you need predictable per-iteration limits.
|
||||
|
||||
### Versioned workflow evolution
|
||||
|
||||
Use this when workflow structure changes across deployments and old histories must still replay.
|
||||
|
||||
### Version gates with `getVersion`
|
||||
|
||||
Use `ctx.getVersion(name, latest)` to branch behavior when you change a workflow's logic while old instances are still in flight. It returns the version this instance is pinned to at that point:
|
||||
|
||||
- A fresh instance resolves to `latest`.
|
||||
- An instance that already executed past this point under older code resolves to `1` (the implicit floor).
|
||||
|
||||
The resolved version is recorded in history, so replays are deterministic and each instance stays on the branch it started on. Inside a loop, every iteration resolves independently, so in-flight iterations finish on the old branch while new iterations pick up `latest`.
|
||||
|
||||
`latest` must be an integer `>= 1`, and the gate name must be unique within its scope like any other entry. Once every old instance has drained, retire the gate by replacing the call with `ctx.removed(name, "version_check")`.
|
||||
|
||||
### Checkpoint-friendly loop design
|
||||
|
||||
Use this when you need reliable replay and resume semantics across crashes and restarts.
|
||||
|
||||
## Migrations
|
||||
|
||||
- Keep workflow entry names stable once deployed.
|
||||
- If an old entry was removed or renamed, call `ctx.removed(name, originalType)`.
|
||||
- To change behavior at a point while old instances are still running, gate it with `ctx.getVersion(name, latest)` (see [Version gates](#version-gates-with-getversion)).
|
||||
- This keeps replay compatible across deployments.
|
||||
|
||||
## Step-only access to actor APIs
|
||||
|
||||
`state`, `vars`, `db`, `client()`, and connection/event APIs are only valid inside `ctx.step(...)` callbacks.
|
||||
|
||||
Use non-step workflow code for orchestration only: queue waits, sleeps, loops, joins, races, and rollback boundaries. Keep actor-local side effects in steps.
|
||||
|
||||
## Debugging
|
||||
|
||||
- `GET /inspector/workflow-history` returns workflow history status for an actor.
|
||||
- Response includes `isWorkflowEnabled` and `history`.
|
||||
- In non-dev mode, inspector endpoints require authorization.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Prefer queue-driven loops for long-lived workflows.
|
||||
- Structure long-lived workflows with setup and teardown around the main loop.
|
||||
- Keep actor state changes and side effects inside steps.
|
||||
- Store workflow progress in `state` and broadcast updates as progress changes.
|
||||
- Use timeouts and rollback for external side effects.
|
||||
- Write step bodies idempotently. `state` and `vars` mutations from a failed attempt are never rolled back, whether the step retries or `tryStep`/`try` catches the failure.
|
||||
|
||||
_Source doc path: /docs/actors/workflows_
|
||||
103
.agents/skills/live-cursors/reference/cli.md
Normal file
103
.agents/skills/live-cursors/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_
|
||||
120
.agents/skills/live-cursors/reference/clients/javascript.md
Normal file
120
.agents/skills/live-cursors/reference/clients/javascript.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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
|
||||
|
||||
## Stateless vs Stateful
|
||||
|
||||
## Getting Actors
|
||||
|
||||
## Connection Parameters
|
||||
|
||||
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
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
## 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
|
||||
|
||||
## Error Handling
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
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_
|
||||
88
.agents/skills/live-cursors/reference/clients/react.md
Normal file
88
.agents/skills/live-cursors/reference/clients/react.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
|
||||
## Stateless vs Stateful
|
||||
|
||||
## Getting Actors
|
||||
|
||||
## Connection Parameters
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
## 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
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
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_
|
||||
367
.agents/skills/live-cursors/reference/clients/rust.md
Normal file
367
.agents/skills/live-cursors/reference/clients/rust.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Rust (Beta)
|
||||
|
||||
> Source: `src/content/docs/clients/rust.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/clients/rust
|
||||
> Description: Connect Rust apps to Rivet Actors.
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Rust quickstart guide](/docs/actors/quickstart/rust) for getting started.
|
||||
|
||||
## Install
|
||||
|
||||
Add the `rivetkit` crate and its companions:
|
||||
|
||||
```sh
|
||||
cargo add rivetkit anyhow async-trait
|
||||
cargo add serde --features derive
|
||||
cargo add tokio --features full
|
||||
```
|
||||
|
||||
The Rust client is strongly typed. It shares the same action and event types as your actor, so define your actor in `src/lib.rs` and import those types from both your server and your client. There is no need to redefine the actor on the client. See [Define Your Actor](/docs/actors/quickstart/rust#define-your-actor) in the quickstart for the actor definition this page builds on.
|
||||
|
||||
## Minimal Client
|
||||
|
||||
```rust @nocheck
|
||||
use counter::{Counter, Increment};
|
||||
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: 1 }).await?;
|
||||
println!("New count: {count}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`counter` here is your crate name (the package `name` in `Cargo.toml`, with dashes as underscores). `Counter` and `Increment` are the types you defined alongside your actor.
|
||||
|
||||
## Stateless vs Stateful
|
||||
|
||||
```rust @nocheck
|
||||
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"])?;
|
||||
|
||||
// Stateless: each call is independent
|
||||
counter.send(Increment { amount: 1 }).await?;
|
||||
|
||||
// Stateful: keep a connection open for realtime events
|
||||
let connection = counter.connect();
|
||||
connection
|
||||
.on::<NewCount>(|event| println!("count: {}", event.count))
|
||||
.await;
|
||||
connection.send(Increment { amount: 1 }).await?;
|
||||
|
||||
connection.disconnect().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
A stateless call on the handle opens a short-lived request per action. A connection keeps a WebSocket open so you can receive events and reuse it across calls.
|
||||
|
||||
## Getting Actors
|
||||
|
||||
```rust @nocheck
|
||||
use counter::Counter;
|
||||
use rivetkit::{
|
||||
client::{Client, ClientConfig, GetOrCreateOptions},
|
||||
prelude::*,
|
||||
TypedClientExt,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
||||
|
||||
// Get or create an actor
|
||||
let room = client.get_or_create_typed_default::<Counter>("counter", ["room-42"])?;
|
||||
|
||||
// Get an existing actor handle (fails when used if the actor does not exist)
|
||||
let existing = client.get_typed_default::<Counter>("counter", ["room-42"])?;
|
||||
|
||||
// Create a new actor with input
|
||||
let created = client.get_or_create_typed::<Counter>(
|
||||
"counter",
|
||||
["game-1"],
|
||||
GetOrCreateOptions {
|
||||
create_with_input: Some(json!({ "mode": "ranked" })),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
// Get an actor handle by ID
|
||||
let by_id = client.get_for_id("counter", "actor-id", Default::default())?;
|
||||
|
||||
// Resolve the actor ID
|
||||
let resolved_id = room.inner().resolve().await?;
|
||||
println!("Resolved ID: {resolved_id}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`get_typed_default` / `get_or_create_typed_default` use default options. The non-default variants (`get_typed` / `get_or_create_typed`) take `GetOptions` / `GetOrCreateOptions` for connection parameters, input, and region.
|
||||
|
||||
## Connection Parameters
|
||||
|
||||
Pass connection parameters through the handle options. They are delivered to the actor's `create_conn_state` callback:
|
||||
|
||||
```rust @nocheck
|
||||
use counter::Counter;
|
||||
use rivetkit::{
|
||||
client::{Client, ClientConfig, GetOrCreateOptions},
|
||||
prelude::*,
|
||||
TypedClientExt,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
||||
|
||||
let chat = client.get_or_create_typed::<Counter>(
|
||||
"counter",
|
||||
["general"],
|
||||
GetOrCreateOptions {
|
||||
params: Some(json!({ "authToken": "jwt-token-here" })),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let connection = chat.connect();
|
||||
connection.disconnect().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
`on` registers a typed callback for an event and returns once the subscription is registered:
|
||||
|
||||
```rust @nocheck
|
||||
use counter::{Counter, 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 connection = client
|
||||
.get_or_create_typed_default::<Counter>("counter", ["general"])?
|
||||
.connect();
|
||||
|
||||
connection
|
||||
.on::<NewCount>(|event| println!("count changed: {}", event.count))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Event callbacks are synchronous and run for every matching event. The actor's emitted event type (here `NewCount`) is decoded into the typed value for you.
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
The lower-level connection exposes lifecycle callbacks and the current status. Reach it with `connection.inner()`:
|
||||
|
||||
```rust @nocheck
|
||||
use counter::Counter;
|
||||
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 connection = client
|
||||
.get_or_create_typed_default::<Counter>("counter", ["general"])?
|
||||
.connect();
|
||||
let inner = connection.inner().clone();
|
||||
|
||||
inner.on_open(|| println!("connected")).await;
|
||||
inner.on_close(|| println!("disconnected")).await;
|
||||
inner.on_error(|message| eprintln!("error: {message}")).await;
|
||||
inner
|
||||
.on_status_change(|status| println!("status: {status:?}"))
|
||||
.await;
|
||||
|
||||
println!("current status: {:?}", inner.conn_status());
|
||||
|
||||
connection.disconnect().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`ConnectionStatus` is one of `Idle`, `Connecting`, `Connected`, or `Disconnected`. Connections reconnect automatically with backoff until you call `disconnect`.
|
||||
|
||||
## Low-Level HTTP & WebSocket
|
||||
|
||||
For actors that implement `on_request` or `on_websocket`, call them directly on the untyped handle (`handle.inner()`). `fetch` returns a `reqwest::Response`, and `web_socket` returns a `tokio_tungstenite` stream. This example also needs a few extra crates:
|
||||
|
||||
```sh
|
||||
cargo add futures-util tokio-tungstenite
|
||||
cargo add reqwest --features json
|
||||
```
|
||||
|
||||
```rust @nocheck
|
||||
use counter::Counter;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use reqwest::{header::HeaderMap, Method};
|
||||
use rivetkit::{
|
||||
client::{Client, ClientConfig},
|
||||
prelude::*,
|
||||
TypedClientExt,
|
||||
};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
||||
let handle = client.get_or_create_typed_default::<Counter>("counter", ["general"])?;
|
||||
|
||||
// Raw HTTP request
|
||||
let response = handle
|
||||
.inner()
|
||||
.fetch("history", Method::GET, HeaderMap::new(), None)
|
||||
.await?;
|
||||
let history: Vec<String> = response.json().await?;
|
||||
println!("history: {history:?}");
|
||||
|
||||
// Raw WebSocket connection
|
||||
let mut ws = handle.inner().web_socket("stream", None).await?;
|
||||
ws.send(Message::text("hello")).await?;
|
||||
if let Some(message) = ws.next().await {
|
||||
println!("received: {:?}", message?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Calling from Backend
|
||||
|
||||
The client is a normal Tokio type, so you can hold it in your backend (Axum, Actix, etc.) and call actors from request handlers. The client is `Clone` and cheap to share:
|
||||
|
||||
```rust @nocheck
|
||||
use counter::{Counter, Increment};
|
||||
use rivetkit::{
|
||||
client::{Client, ClientConfig},
|
||||
prelude::*,
|
||||
TypedClientExt,
|
||||
};
|
||||
|
||||
async fn increment(client: Client) -> Result<i64> {
|
||||
let counter = client.get_or_create_typed_default::<Counter>("counter", ["server-counter"])?;
|
||||
let count = counter.send(Increment { amount: 1 }).await?;
|
||||
Ok(count)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Action and connection calls return `anyhow::Result`. Actor-side errors surface as an `anyhow::Error` carrying the error group, code, message, and metadata:
|
||||
|
||||
```rust @nocheck
|
||||
use counter::{Counter, Increment};
|
||||
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"])?;
|
||||
|
||||
match counter.send(Increment { amount: 1 }).await {
|
||||
Ok(count) => println!("count: {count}"),
|
||||
Err(error) => eprintln!("action failed: {error:#}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Keys
|
||||
|
||||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||||
|
||||
```rust @nocheck
|
||||
use counter::Counter;
|
||||
use rivetkit::{
|
||||
client::{Client, ClientConfig},
|
||||
prelude::*,
|
||||
TypedClientExt,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
||||
|
||||
// Compound key: [org, room]
|
||||
let room = client.get_or_create_typed_default::<Counter>("counter", ["org-acme", "general"])?;
|
||||
let actor_id = room.inner().resolve().await?;
|
||||
println!("Actor ID: {actor_id}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Keys accept arrays of `&str` or `String` (`["org-acme", "general"]`). Don't build keys with string interpolation like `format!("org:{user_id}")` when `user_id` contains user data. Use arrays instead to prevent key injection attacks.
|
||||
|
||||
### Configuration
|
||||
|
||||
`ClientConfig::new(endpoint)` is a builder. The endpoint is always required; there is no default. Common options:
|
||||
|
||||
```rust @nocheck
|
||||
use rivetkit::client::ClientConfig;
|
||||
|
||||
let config = ClientConfig::new("http://localhost:6420")
|
||||
.namespace("default")
|
||||
.token("pk_...")
|
||||
.pool_name("my-pool")
|
||||
.header("x-custom", "value");
|
||||
```
|
||||
|
||||
- `namespace` - target namespace (defaults to the engine's configured namespace).
|
||||
- `token` - authentication token for the engine.
|
||||
- `pool_name` - runner pool to target.
|
||||
- `header` / `headers` - extra HTTP headers sent with each request.
|
||||
- `max_input_size` - cap on encoded action input size.
|
||||
|
||||
For serverless deployments, set the endpoint to your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints) for details.
|
||||
|
||||
## API Reference
|
||||
|
||||
See the full client API documentation on [docs.rs/rivetkit-client](https://docs.rs/rivetkit-client/latest/rivetkit_client/).
|
||||
|
||||
_Source doc path: /docs/clients/rust_
|
||||
454
.agents/skills/live-cursors/reference/clients/swift.md
Normal file
454
.agents/skills/live-cursors/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/live-cursors/reference/clients/swiftui.md
Normal file
359
.agents/skills/live-cursors/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_
|
||||
127
.agents/skills/live-cursors/reference/cookbook/ai-agent.md
Normal file
127
.agents/skills/live-cursors/reference/cookbook/ai-agent.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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. |
|
||||
|
||||
## 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_
|
||||
114
.agents/skills/live-cursors/reference/cookbook/chat-room.md
Normal file
114
.agents/skills/live-cursors/reference/cookbook/chat-room.md
Normal file
@@ -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_
|
||||
69
.agents/skills/live-cursors/reference/cookbook/cron-jobs.md
Normal file
69
.agents/skills/live-cursors/reference/cookbook/cron-jobs.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Cron Jobs and Scheduled Tasks
|
||||
|
||||
> Source: `src/content/cookbook/cron-jobs.mdx`
|
||||
> Canonical URL: https://rivet.dev/cookbook/cron-jobs
|
||||
> Description: Patterns for durable one-shot, calendar, and fixed-interval work on Rivet Actors.
|
||||
|
||||
---
|
||||
Rivet Actor schedules are durable actor-local timers. They survive actor sleep, restarts, upgrades, deploys, and crashes without a separate cron service.
|
||||
|
||||
## Choose a schedule type
|
||||
|
||||
| API | Use it for |
|
||||
| --- | --- |
|
||||
| `c.schedule.after(delayMs, action, ...args)` | One-time work after a relative delay. |
|
||||
| `c.schedule.at(timestamp, action, ...args)` | One-time work at an exact Unix timestamp in milliseconds. |
|
||||
| `c.cron.set({ ... })` | Named calendar recurrence in an IANA timezone. |
|
||||
| `c.cron.every({ ... })` | Named fixed intervals of at least 5 seconds. |
|
||||
|
||||
All callbacks are ordinary actions on the same actor. Keep the action name fixed in your code rather than accepting an arbitrary action name from a client.
|
||||
|
||||
See [Schedule & Cron](/docs/actors/schedule) for the full API, history, cancellation, failure behavior, and limits.
|
||||
|
||||
## Calendar job
|
||||
|
||||
Use `cron.set` instead of manually re-arming a one-shot action:
|
||||
|
||||
Install fixed background jobs in `onCreate` so setup runs once per actor. The job name remains an upsert key, so a later `cron.set` call updates the existing job rather than creating a duplicate. `cron.set` also handles timezone and daylight-saving transitions.
|
||||
|
||||
## Fixed-interval job
|
||||
|
||||
Use `cron.every` for frequent work such as presence sweeps or cache refreshes:
|
||||
|
||||
```ts
|
||||
await c.cron.every({
|
||||
name: "presence-sweep",
|
||||
interval: 15_000, // Minimum 5 seconds.
|
||||
action: "sweepPresence",
|
||||
maxHistory: 25,
|
||||
});
|
||||
```
|
||||
|
||||
Intervals remain anchored to scheduled deadlines rather than drifting by the action's runtime. If a previous run is still active, the overlapping occurrence is skipped.
|
||||
|
||||
## Cancellation and updates
|
||||
|
||||
Keep the ID returned by a one-shot schedule when it may need cancellation:
|
||||
|
||||
```ts
|
||||
const id = await c.schedule.after(60_000, "expireSession", sessionId);
|
||||
await c.schedule.cancel(id);
|
||||
```
|
||||
|
||||
Recurring jobs are managed by name:
|
||||
|
||||
```ts
|
||||
await c.cron.delete("presence-sweep");
|
||||
```
|
||||
|
||||
Calling `cron.set` or `cron.every` again with the same name replaces its configuration.
|
||||
|
||||
## Failure and idempotency
|
||||
|
||||
Keep scheduled actions idempotent when duplicate work would be harmful. See [Execution behavior](/docs/actors/schedule#execution-behavior) for retry behavior and workflow guidance.
|
||||
|
||||
## Topology
|
||||
|
||||
Use a singleton actor key for one global job, such as `jobs["daily-report"]`. Use an actor per user or resource for isolated reminders, trials, billing periods, or other per-entity schedules.
|
||||
|
||||
_Source doc path: /cookbook/cron-jobs_
|
||||
152
.agents/skills/live-cursors/reference/cookbook/live-cursors.md
Normal file
152
.agents/skills/live-cursors/reference/cookbook/live-cursors.md
Normal file
@@ -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_
|
||||
136
.agents/skills/live-cursors/reference/cookbook/vpc-air-gapped.md
Normal file
136
.agents/skills/live-cursors/reference/cookbook/vpc-air-gapped.md
Normal file
@@ -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_
|
||||
13
.agents/skills/live-cursors/reference/deploy/aws-ecs.md
Normal file
13
.agents/skills/live-cursors/reference/deploy/aws-ecs.md
Normal file
@@ -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_
|
||||
10
.agents/skills/live-cursors/reference/deploy/aws-lambda.md
Normal file
10
.agents/skills/live-cursors/reference/deploy/aws-lambda.md
Normal file
@@ -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_
|
||||
53
.agents/skills/live-cursors/reference/deploy/cloudflare.md
Normal file
53
.agents/skills/live-cursors/reference/deploy/cloudflare.md
Normal file
@@ -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_
|
||||
@@ -0,0 +1,95 @@
|
||||
# Container Runner
|
||||
|
||||
> Source: `src/content/docs/deploy/container-runner.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/container-runner
|
||||
> Description: Run any containerized server as a Rivet Actor.
|
||||
|
||||
---
|
||||
The container runner (`rivet-container-runner`) is an adapter for running arbitrary containers as Rivet Actors. Use it for non-RivetKit workloads such as Unity or Godot dedicated game servers and batch jobs like FFmpeg transcoding.
|
||||
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A containerized server (a Unity or Godot dedicated server, a plain Node process, or any HTTP/WebSocket server)
|
||||
- Access to the [Rivet Cloud](https://dashboard.rivet.dev/) or a [self-hosted Rivet Engine](/docs/general/self-hosting)
|
||||
- Docker running locally
|
||||
|
||||
### Install in Your Container
|
||||
|
||||
Download the static binary from Rivet's release artifacts in your Dockerfile and set it as the entrypoint, passing your server's launch command after `--`:
|
||||
|
||||
```dockerfile @nocheck
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the Rivet container runner.
|
||||
RUN curl -fsSL https://releases.rivet.dev/rivet/latest/container-runner/rivet-container-runner-x86_64-unknown-linux-musl \
|
||||
-o /usr/local/bin/rivet-container-runner \
|
||||
&& chmod +x /usr/local/bin/rivet-container-runner
|
||||
|
||||
# Your server binary and assets.
|
||||
COPY build/ /game/
|
||||
WORKDIR /game
|
||||
|
||||
ENTRYPOINT ["rivet-container-runner", "--", "./GameServer", "-batchmode", "-nographics", "-logFile", "-"]
|
||||
```
|
||||
|
||||
Artifacts are published for `x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl`. The binaries are fully static, so they run in any Linux base image, including `scratch`. Pin a version by replacing `latest` with a release version, for example `https://releases.rivet.dev/rivet/2.3.3/container-runner/rivet-container-runner-x86_64-unknown-linux-musl`.
|
||||
|
||||
### Deploy
|
||||
|
||||
Deploy the image to [Rivet Compute](/docs/deploy/rivet-compute) with the CLI. For game servers, configure the pool with one actor per instance:
|
||||
|
||||
```bash
|
||||
npx @rivetkit/cli deploy \
|
||||
--token "$RIVET_CLOUD_TOKEN" \
|
||||
--instance-request-concurrency 1 \
|
||||
--dockerfile Dockerfile
|
||||
```
|
||||
|
||||
### Create Actors and Connect Clients
|
||||
|
||||
Create actors against the pool's runner (`default`) and connect clients through the gateway URL shown in the dashboard. WebSocket clients connect at the bare gateway URL with the `rivet` WebSocket subprotocol.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The engine cold-starts your container and calls `POST /api/rivet/start` on the port it injects as `RIVET_PORT`.
|
||||
2. The runner spawns your server as a child process with `PORT` set to the child port, waits for the port to open, and reports the actor as running.
|
||||
3. Gateway traffic for the actor arrives over Rivet's tunnel and is proxied to `127.0.0.1:<child port>`. WebSocket clients connect at the bare gateway URL with the `rivet` WebSocket subprotocol. Raw HTTP reaches the child under the `/request/*` prefix on the actor surface (the prefix is stripped before proxying); other paths are reserved for the runtime's own endpoints.
|
||||
4. Child stdout and stderr are re-emitted with an `[actorId=... key=...]` prefix so actor logs are attributed in the dashboard.
|
||||
5. When an actor stops, the runner sends its child `SIGTERM`, escalates to `SIGKILL` after a grace period, and exits the process once no actors remain.
|
||||
|
||||
## Configuration
|
||||
|
||||
All flags can also be set through environment variables:
|
||||
|
||||
| Flag | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `--port` | `RIVET_PORT` / `PORT` | `8080` | Serverless front-door HTTP port. Rivet Compute injects `RIVET_PORT` automatically. |
|
||||
| `--child-port` | `CHILD_PORT` | `7770` | First local child port; each actor's child gets the next free port at or above this, exported to the child as `PORT`. |
|
||||
| `--actor-name` | `RIVET_ACTOR_NAME` | `game` | Actor name this runner serves. |
|
||||
| `--runner-version` | `RIVET_RUNNER_VERSION` | `1` | Version reported to the engine, used to drain old runners on deploy. |
|
||||
| `--base-path` | `RIVET_SERVERLESS_BASE_PATH` | `/api/rivet` | Base path the engine calls for serverless start. |
|
||||
| `--stop-grace-secs` | `RIVET_STOP_GRACE_SECS` | `25` | `SIGTERM` to `SIGKILL` grace period when stopping the child. Capped to a few seconds when the platform itself is reclaiming the instance, so shutdown fits inside the platform's own kill window. |
|
||||
| `--readiness-timeout-secs` | `RIVET_READINESS_TIMEOUT_SECS` | `30` | How long to wait for the child's port to open before failing the start. |
|
||||
|
||||
### Per-Actor Input
|
||||
|
||||
The actor's `input` payload can override the launch spec per actor. All fields are optional and fall back to the entrypoint command. RivetKit clients pass this object directly; when creating actors through the raw engine API, encode it as CBOR before base64-encoding the `input` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": ["./GameServer", "-batchmode"],
|
||||
"args": ["-extra-flag"],
|
||||
"env": { "MATCH_MODE": "ranked" }
|
||||
}
|
||||
```
|
||||
|
||||
`command` replaces the entrypoint command template, `args` are appended to it, and `env` adds environment variables for the child.
|
||||
|
||||
## Source and Examples
|
||||
|
||||
The runner and a full end-to-end example, including a Unity FishNet demo project and a local test harness, live in the Rivet repository under [`container-runner/`](https://github.com/rivet-dev/rivet/tree/main/container-runner).
|
||||
|
||||
_Source doc path: /docs/deploy/container-runner_
|
||||
136
.agents/skills/live-cursors/reference/deploy/freestyle.md
Normal file
136
.agents/skills/live-cursors/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_
|
||||
10
.agents/skills/live-cursors/reference/deploy/hetzner.md
Normal file
10
.agents/skills/live-cursors/reference/deploy/hetzner.md
Normal file
@@ -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/live-cursors/reference/deploy/kubernetes.md
Normal file
121
.agents/skills/live-cursors/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_
|
||||
45
.agents/skills/live-cursors/reference/deploy/railway.md
Normal file
45
.agents/skills/live-cursors/reference/deploy/railway.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Deploying to Railway
|
||||
|
||||
> Source: `src/content/docs/deploy/railway.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/railway
|
||||
> Description: Deploy your RivetKit app to Railway.
|
||||
|
||||
---
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Railway account](https://railway.app)
|
||||
- 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)
|
||||
|
||||
### Deploy to Railway
|
||||
|
||||
1. Connect your GitHub account to Railway
|
||||
2. Select your repository containing your RivetKit app
|
||||
3. Railway will automatically detect and deploy your app
|
||||
|
||||
See [Railway's deployment docs](https://docs.railway.com/quick-start) for more details.
|
||||
|
||||
### Set Environment Variables
|
||||
|
||||
After creating your project on the Rivet dashboard, select Railway as your provider. You'll be provided `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` environment variables to add to your Railway service.
|
||||
|
||||
See [Railway's environment variables docs](https://docs.railway.com/guides/variables#service-variables) for more details.
|
||||
|
||||
### Connect to Rivet
|
||||
|
||||
1. In your Railway project, go to **Settings > Networking**
|
||||
2. Click **Create Custom Domain** then **Create Domain** to generate a Railway domain (e.g. `my-app.railway.app`)
|
||||
3. On the Rivet dashboard, paste your domain with the `/api/rivet` path into the connect form (e.g. `https://my-app.railway.app/api/rivet`)
|
||||
4. Click "Done"
|
||||
|
||||
Rivet envoys connect to your app over long-lived WebSockets, and your app's clients (browsers, SDKs) do the same. Railway's HTTP proxy supports WebSockets, but if you front your app with your own reverse proxy (NGINX, Caddy, etc.) inside the Railway service, raise its idle / read timeout to at least 1 hour (`3600` seconds). Default proxy timeouts (typically 30 to 60 seconds) drop these connections and cause reconnect storms.
|
||||
|
||||
### Configure Sleeping & Multi-Region (Optional)
|
||||
|
||||
- [Enable App Sleeping](https://docs.railway.com/reference/app-sleeping) to reduce costs when idle
|
||||
- [Configure Multi-Region](https://docs.railway.com/reference/deployment-regions) to deploy closer to your users
|
||||
|
||||
_Source doc path: /docs/deploy/railway_
|
||||
144
.agents/skills/live-cursors/reference/deploy/rivet-compute.md
Normal file
144
.agents/skills/live-cursors/reference/deploy/rivet-compute.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Deploying to Rivet Compute
|
||||
|
||||
> Source: `src/content/docs/deploy/rivet-compute.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/rivet-compute
|
||||
> Description: Run your backend on Rivet Compute.
|
||||
|
||||
---
|
||||
Using an AI coding agent? Open **Connect** on the [Rivet dashboard](https://dashboard.rivet.dev), select **Rivet Cloud**, and paste the one-shot prompt into your agent and have it connect with Rivet Compute for you.
|
||||
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- 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)
|
||||
- A [Rivet Cloud](https://dashboard.rivet.dev) account and project
|
||||
- Docker running locally
|
||||
|
||||
### Create a Dockerfile
|
||||
|
||||
Add a `Dockerfile` to your project root that builds and runs your RivetKit server:
|
||||
|
||||
```dockerfile @nocheck
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
CMD ["node", "src/server.js"]
|
||||
```
|
||||
|
||||
### Get Your Cloud Token
|
||||
|
||||
1. Open the [Rivet dashboard](https://dashboard.rivet.dev) and navigate to your project
|
||||
2. Click **Connect** and select **Rivet Cloud**
|
||||
3. Copy the **`RIVET_CLOUD_TOKEN`** value shown
|
||||
|
||||
### Deploy
|
||||
|
||||
Run the deploy command from your project root. The token is saved to `~/.rivet/credentials`, so later deploys can omit it.
|
||||
|
||||
```bash
|
||||
npx @rivetkit/cli deploy --token cloud_api_xxxxx
|
||||
```
|
||||
|
||||
The CLI resolves your project from the token, builds and pushes your Docker image to Rivet's built-in registry, upserts the managed pool, and prints the deployment URL on stdout when the pool is ready.
|
||||
|
||||
### Optionally Add CI
|
||||
|
||||
After local deploys work, install the GitHub Actions workflow that deploys on every push and pull request:
|
||||
|
||||
```bash
|
||||
npx @rivetkit/cli setup-ci
|
||||
```
|
||||
|
||||
This writes `.github/workflows/rivet-deploy.yml`. Add your token as a repository secret to enable it:
|
||||
|
||||
```bash
|
||||
gh secret set RIVET_CLOUD_TOKEN
|
||||
```
|
||||
|
||||
The workflow creates production and pull-request namespaces, posts preview links, and cleans up PR namespaces when pull requests close. See the [CLI reference](/docs/cli) for all commands.
|
||||
|
||||
### Monitor Deployment
|
||||
|
||||
The dashboard shows live status as Rivet Compute provisions your backend:
|
||||
|
||||
| Status | Description |
|
||||
| --- | --- |
|
||||
| Initializing | Starting the runtime environment |
|
||||
| Deploying | Pulling and launching your container |
|
||||
| Binding | Connecting the runner to the network |
|
||||
| Ready | Deployment complete |
|
||||
|
||||
Once the status reaches **Ready**, your backend is live and actors are available for connections.
|
||||
|
||||
If you are an agent monitoring the deployment via API rather than the dashboard, poll the managed-pool endpoint on the Cloud API.
|
||||
|
||||
The `RIVET_CLOUD_TOKEN` secret is a `cloud_api_*` management token scoped to the Cloud API at `cloud-api.rivet.dev`. Use it for `Authorization: Bearer ...` against the Cloud API. Do not confuse it with a `pk_*` publishable key, which is scoped to the Rivet Engine API at `api.rivet.dev` and will 401 against this endpoint.
|
||||
|
||||
Substitute `$CLOUD_API_URL` (typically `https://cloud-api.rivet.dev`), `$PROJECT`, `$ORG`, `$CLOUD_NAMESPACE`, and `$CLOUD_TOKEN`.
|
||||
|
||||
Poll every 5 seconds until `status` is `ready`. Stop and investigate if `status` is `error`.
|
||||
|
||||
```bash
|
||||
curl -s "$CLOUD_API_URL/projects/$PROJECT/namespaces/$CLOUD_NAMESPACE/managed-pools/default?org=$ORG" -H "Authorization: Bearer $CLOUD_TOKEN"
|
||||
```
|
||||
|
||||
## Checking Logs
|
||||
|
||||
Use the CLI to read your deployment's logs. By default `rivet logs` prints the last 100 lines from the `production` namespace, oldest to newest, then exits.
|
||||
|
||||
```bash
|
||||
npx @rivetkit/cli logs
|
||||
```
|
||||
|
||||
The CLI resolves your token the same way `deploy` does (the `--token` flag, then the `RIVET_CLOUD_TOKEN` environment variable, then `~/.rivet/credentials`).
|
||||
|
||||
### Follow logs live
|
||||
|
||||
Pass `--follow` (`-f`) to stream new logs as they arrive instead of fetching history:
|
||||
|
||||
```bash
|
||||
npx @rivetkit/cli logs --follow
|
||||
```
|
||||
|
||||
Each formatted line is printed as `<timestamp> [] <region> <message>`:
|
||||
|
||||
```
|
||||
2026-06-16T18:26:51.160Z [INFO] eu-central-1 server listening on port 3000
|
||||
2026-06-17T11:24:20.425Z [ERROR] us-east-1 failed to connect to upstream
|
||||
```
|
||||
|
||||
A few examples:
|
||||
|
||||
```bash
|
||||
# Last 200 lines from a specific namespace
|
||||
npx @rivetkit/cli logs --namespace production -n 200
|
||||
|
||||
# Live tail, only lines containing "error"
|
||||
npx @rivetkit/cli logs --follow --contains error
|
||||
|
||||
# JSON output piped to jq
|
||||
npx @rivetkit/cli logs -n 50 --json | jq .
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the status stays in **Initializing** for more than a few minutes, verify that:
|
||||
|
||||
- The `RIVET_CLOUD_TOKEN` secret is correctly set in your GitHub repository
|
||||
- The GitHub Actions workflow completed without errors — check the run logs
|
||||
|
||||
If the status shows **Error**, check that your container starts successfully and does not exit immediately (you can check this with container logs). Common causes:
|
||||
|
||||
- The server file is not calling `registry.start()`
|
||||
- A runtime crash on startup — test the image locally with `docker run`
|
||||
- The server is not listening on the `RIVET_PORT` environment variable (RivetKit reads `RIVET_PORT`, defaulting to `3000`)
|
||||
|
||||
## Pricing
|
||||
|
||||
You are billed for the compute resources your deployment uses while it is running.
|
||||
|
||||
_Source doc path: /docs/deploy/rivet-compute_
|
||||
50
.agents/skills/live-cursors/reference/deploy/supabase.md
Normal file
50
.agents/skills/live-cursors/reference/deploy/supabase.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Deploying to Supabase Functions
|
||||
|
||||
> Source: `src/content/docs/deploy/supabase.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/supabase
|
||||
> Description: Deploy an existing Rivet project to Supabase Edge Functions.
|
||||
|
||||
---
|
||||
This guide covers deploying an existing Rivet project to Supabase Edge Functions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Supabase project](https://supabase.com/)
|
||||
- [Supabase CLI](https://supabase.com/docs/guides/cli) configured for your project
|
||||
- A Rivet namespace from the [Rivet Dashboard](https://dashboard.rivet.dev/) or a self-hosted Rivet Engine
|
||||
|
||||
## Steps
|
||||
|
||||
### Set up your project
|
||||
|
||||
Follow the [Supabase Functions Quickstart](/docs/actors/quickstart/supabase) to set up your project locally.
|
||||
|
||||
### Set Secrets
|
||||
|
||||
Set your Rivet connection values as Supabase secrets. 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.
|
||||
|
||||
```sh
|
||||
npx supabase secrets set \
|
||||
RIVET_ENDPOINT=https://your-namespace:sk_...@api.rivet.dev \
|
||||
RIVET_PUBLIC_ENDPOINT=https://your-namespace@api.rivet.dev
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
Make sure the function has the `deno.json` import map from the [quickstart](/docs/actors/quickstart/supabase) that points `rivetkit` at `@rivetkit/supabase`. It keeps the deploy to the WebAssembly runtime; without it the deploy pulls Rivet's native engine and fails with a `413` (request too large).
|
||||
|
||||
```sh
|
||||
npx supabase functions deploy rivet
|
||||
```
|
||||
|
||||
### Register the Serverless Runner URL
|
||||
|
||||
After deploy, set the function URL with the `/api/rivet` path as the serverless runner URL in Rivet. For a function named `rivet`, this is usually `https://your-project.functions.supabase.co/functions/v1/rivet/api/rivet`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Supabase Functions Quickstart](/docs/actors/quickstart/supabase)
|
||||
- [Deploying to Cloudflare Workers](/docs/deploy/cloudflare)
|
||||
- [SQLite](/docs/actors/sqlite)
|
||||
|
||||
_Source doc path: /docs/deploy/supabase_
|
||||
138
.agents/skills/live-cursors/reference/deploy/vercel.md
Normal file
138
.agents/skills/live-cursors/reference/deploy/vercel.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Deploying to Vercel
|
||||
|
||||
> Source: `src/content/docs/deploy/vercel.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/vercel
|
||||
> Description: Deploy your Next.js Rivet app to Vercel.
|
||||
|
||||
---
|
||||
This guide assumes a Next.js app.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Vercel account](https://vercel.com/)
|
||||
- A Next.js Rivet app
|
||||
- Access to the [Rivet Cloud](https://dashboard.rivet.dev/) or a [self-hosted Rivet Engine](/docs/general/self-hosting)
|
||||
|
||||
## Steps
|
||||
|
||||
### Set up your project
|
||||
|
||||
Follow the [Next.js Quickstart](/docs/actors/quickstart/next-js) to set up your project.
|
||||
|
||||
### Verify Your Project Structure
|
||||
|
||||
Your Next.js project should have the following structure:
|
||||
|
||||
- `src/app/api/rivet/[...all]/route.ts`: RivetKit route handler
|
||||
- `src/rivet/registry.ts`: Actor definitions and registry
|
||||
|
||||
The route handler sets `maxDuration` to extend the serverless function timeout so long-lived actor requests are not cut short:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
### Set Environment Variables
|
||||
|
||||
Set `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` in your Vercel project settings using the URL auth format:
|
||||
|
||||
```
|
||||
RIVET_ENDPOINT=https://my-namespace:sk_****@api.rivet.dev
|
||||
RIVET_PUBLIC_ENDPOINT=https://my-namespace:pk_****@api.rivet.dev
|
||||
```
|
||||
|
||||
`RIVET_ENDPOINT` uses the secret token for server-side access. `RIVET_PUBLIC_ENDPOINT` uses the publishable token and tells the metadata endpoint what connection info to provide to clients.
|
||||
|
||||
### Deploy to Vercel
|
||||
|
||||
1. Connect your GitHub repository to Vercel
|
||||
2. Vercel will deploy your app
|
||||
|
||||
### Configure Preview Deployments (Recommended)
|
||||
|
||||
Add a GitHub action to automatically create isolated Rivet namespaces for each PR:
|
||||
|
||||
1. Add these secrets to your GitHub repository:
|
||||
- `RIVET_CLOUD_TOKEN`: Get from [Rivet Dashboard](https://dashboard.rivet.dev) → Settings → Advanced → Cloud API Tokens
|
||||
- `VERCEL_TOKEN`: Get from [Vercel Account Settings](https://vercel.com/account/tokens)
|
||||
|
||||
2. Create `.github/workflows/rivet-preview.yml`:
|
||||
|
||||
```yaml
|
||||
name: Rivet Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: rivet-preview-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
rivet-preview:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: rivet-dev/preview-namespace-action@v1
|
||||
with:
|
||||
platform: vercel
|
||||
rivet-token: ${{ secrets.RIVET_CLOUD_TOKEN }}
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```
|
||||
Error: ENOENT: no such file or directory, mkdir '.../rivetkit/.../state'
|
||||
```
|
||||
|
||||
**Cause:** The `RIVET_ENDPOINT` environment variable is not configured. RivetKit falls back to the file system driver, which fails in Vercel's read-only serverless environment.
|
||||
|
||||
**Solution:** Ensure `RIVET_ENDPOINT` is set with your Rivet endpoint using the URL auth format:
|
||||
|
||||
```
|
||||
RIVET_ENDPOINT=https://my-namespace:sk_****@api.rivet.dev
|
||||
```
|
||||
|
||||
If using the [preview-namespace-action](https://github.com/rivet-dev/preview-namespace-action), this is configured automatically.
|
||||
|
||||
The `/api/rivet/metadata` endpoint returns data but `clientEndpoint`, `clientNamespace`, and `clientToken` are missing.
|
||||
|
||||
**Cause:** The `RIVET_PUBLIC_ENDPOINT` environment variable is not configured. This tells the metadata endpoint what connection info to provide to clients.
|
||||
|
||||
**Solution:** Set `RIVET_PUBLIC_ENDPOINT` with the publishable token (for client access):
|
||||
|
||||
```
|
||||
RIVET_PUBLIC_ENDPOINT=https://my-namespace:pk_****@api.rivet.dev
|
||||
```
|
||||
|
||||
If using the [preview-namespace-action](https://github.com/rivet-dev/preview-namespace-action), this is configured automatically.
|
||||
|
||||
Rivet fails to connect to your Vercel deployment with a 401 error mentioning "Authentication Required".
|
||||
|
||||
**Cause:** [Vercel Deployment Protection](https://vercel.com/docs/security/deployment-protection) is blocking requests from Rivet.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Create a bypass secret in your Vercel project settings
|
||||
2. In Rivet, go to **Settings > Providers**
|
||||
3. Click the three dots on your provider and select **Edit**
|
||||
4. Click **Add Header** and add `x-vercel-protection-bypass` with your bypass secret
|
||||
|
||||
If using the [preview-namespace-action](https://github.com/rivet-dev/preview-namespace-action), this is configured automatically.
|
||||
|
||||
## Related
|
||||
|
||||
- [Next.js Quickstart](/docs/actors/quickstart/next-js)
|
||||
- [Self-Hosting](/docs/general/self-hosting)
|
||||
|
||||
_Source doc path: /docs/deploy/vercel_
|
||||
@@ -0,0 +1,96 @@
|
||||
# Deploying to VMs & Bare Metal
|
||||
|
||||
> Source: `src/content/docs/deploy/vm-and-bare-metal.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/deploy/vm-and-bare-metal
|
||||
> Description: Deploy your RivetKit app to any Linux VM or bare metal host.
|
||||
|
||||
---
|
||||
## Steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A Linux VM or bare metal server with SSH access
|
||||
- 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)
|
||||
|
||||
### Upload Your App
|
||||
|
||||
- Build your RivetKit app locally
|
||||
- Copy the build output to your server (example):
|
||||
|
||||
```bash
|
||||
scp -r ./dist user@server:/opt/rivetkit-app
|
||||
```
|
||||
|
||||
Place the files somewhere readable by the service user, such as `/opt/rivetkit-app`.
|
||||
|
||||
### Set Environment Variables
|
||||
|
||||
After creating your project on the Rivet dashboard, select VM & Bare Metal as your provider. You'll be provided `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` environment variables to use in the next step.
|
||||
|
||||
### Create the systemd Service
|
||||
|
||||
Create `/etc/systemd/system/rivetkit-app.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=RivetKit App
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/rivetkit-app
|
||||
ExecStart=/usr/bin/node server.js
|
||||
Restart=on-failure
|
||||
Environment=RIVET_ENDPOINT=<your-rivet-endpoint>
|
||||
Environment=RIVET_PUBLIC_ENDPOINT=<your-rivet-public-endpoint>
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Replace the environment values with those from the Rivet dashboard and adjust paths to match your deployment.
|
||||
|
||||
### Start the Service
|
||||
|
||||
Reload systemd units and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now rivetkit-app.service
|
||||
```
|
||||
|
||||
### Connect to Rivet
|
||||
|
||||
1. Ensure your server is accessible via a public URL (e.g. `https://my-app.example.com`)
|
||||
2. On the Rivet dashboard, paste your URL with the `/api/rivet` path into the connect form (e.g. `https://my-app.example.com/api/rivet`)
|
||||
3. Click "Done"
|
||||
|
||||
If you front your app with a reverse proxy or load balancer (NGINX, HAProxy, Caddy, AWS ALB/NLB, etc.), raise its idle / read timeout to at least 1 hour (`3600` seconds). Rivet envoys connect to your app over long-lived WebSockets, and clients (browsers, SDKs) do the same. Default proxy timeouts (typically 30 to 60 seconds) drop these connections and cause reconnect storms.
|
||||
|
||||
Examples:
|
||||
|
||||
- **NGINX**: `proxy_read_timeout 3600s;` and `proxy_send_timeout 3600s;` in the relevant `location` block.
|
||||
- **HAProxy**: `timeout client 1h` and `timeout server 1h` in the matching frontend / backend.
|
||||
- **AWS ALB / NLB**: set the load balancer attribute `idle_timeout.timeout_seconds = 3600`.
|
||||
|
||||
## Operating
|
||||
|
||||
### Restart
|
||||
|
||||
Restart the service after deploying new builds or environment changes:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart rivetkit-app.service
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
Follow realtime logs when debugging:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u rivetkit-app.service -f
|
||||
```
|
||||
|
||||
_Source doc path: /docs/deploy/vm-and-bare-metal_
|
||||
@@ -0,0 +1,19 @@
|
||||
# Actor Configuration
|
||||
|
||||
> Source: `src/content/docs/general/actor-configuration.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/actor-configuration
|
||||
> Description: This page documents the configuration options available when defining a RivetKit actor. The actor configuration is passed to the `actor()` function.
|
||||
|
||||
---
|
||||
## Basic Example
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
## Related
|
||||
|
||||
- [Registry Configuration](/docs/general/registry-configuration): Configure the RivetKit registry
|
||||
- [State](/docs/actors/state): Managing actor state
|
||||
- [Actions](/docs/actors/actions): Defining actor actions
|
||||
- [Lifecycle](/docs/actors/lifecycle): Actor lifecycle hooks
|
||||
|
||||
_Source doc path: /docs/general/actor-configuration_
|
||||
452
.agents/skills/live-cursors/reference/general/architecture.md
Normal file
452
.agents/skills/live-cursors/reference/general/architecture.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# Architecture
|
||||
|
||||
> Source: `src/content/docs/general/architecture.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/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 horizontally. read omre about runners below.
|
||||
|
||||
---
|
||||
## 3 ways of running
|
||||
|
||||
### rivetkit
|
||||
|
||||
- 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 horizontally. read more about runners below.
|
||||
|
||||
#### local development
|
||||
|
||||
- in local development, rivetkit provides a full actor environment for single-node deployments
|
||||
|
||||
#### drivers
|
||||
|
||||
- rivetkit supports multiple drivers. currently supports: file system (default in local dev), memory, rivet engine (used for rivet cloud & self-hosting), cloudflare durable objects (does not rely on rivet engine)
|
||||
- drivers are very flexible to enable you to write your actors once and plug in to any system that fits your architecture adequately
|
||||
- see the driver interface
|
||||
- actordriver https://github.com/rivet-dev/rivet/blob/eeb01fc4d9ca0e06f2e740d267bd53280ca7330e/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts
|
||||
- managerdriver https://github.com/rivet-dev/rivet/blob/eeb01fc4d9ca0e06f2e740d267bd53280ca7330e/rivetkit-typescript/packages/rivetkit/src/manager/driver.ts
|
||||
|
||||
### rivet cloud
|
||||
|
||||
- provides multi-region and highest performance out of the box
|
||||
- accessible at dashboard.rivet.dev and the api is avialble at api.rivet.dev
|
||||
|
||||
### rivet self-hosted
|
||||
|
||||
- available as a standalone rust binary or a docker contianer
|
||||
- can be configured to persist to postgres or rocksdb
|
||||
- can scale horiziontally across multipe nodes and can scale across multiple regions
|
||||
- see [self-hosting docs](/docs/self-hosting/)
|
||||
|
||||
## actors
|
||||
|
||||
- Actors for long-lived processes with durable state, realtime, and hibernate when not in use. read more about actors at a high level at (link to actors/index)
|
||||
|
||||
### actor-per-entity
|
||||
|
||||
- actors are designed to have an actor-per-entity
|
||||
- you can think about actors a bit like objects in object-oriented programming where each is responsible for their own state and expose methods (ie actions in our case)
|
||||
- examples include
|
||||
- actor per user
|
||||
- actor per user session
|
||||
- actor per document
|
||||
- actor per game room
|
||||
- actor per tenant
|
||||
- actor per rate limit topic
|
||||
|
||||
### architecting for scale
|
||||
|
||||
- actors scale by:
|
||||
- having isolated state to each actor that combines compute and storage for in-memory reads and writes
|
||||
- communication is standardized based on actions & events
|
||||
- scale horizontally
|
||||
- read more about scaling at [Design Patterns](/docs/actors/design-patterns)
|
||||
|
||||
### horizontal scaling
|
||||
|
||||
- actors can run across multiple rivetkit runners. this is orchestrated by rivetkit itself.
|
||||
|
||||
### lifecycle
|
||||
|
||||
actors have create, destroy, wake, and sleep lifecycle hooks that you can implement to modify behavior. see the lifecycle docs for reference on actor lifecycel hook sequences
|
||||
|
||||
### actor sleeping
|
||||
|
||||
- actors sleep when not in use
|
||||
- an actor is considered not in use when there are no active network connections to the actor (or the network connections are hibernatable websockets, see below) and there are no actions in flight
|
||||
- actors have a sleep timeout (configured in `options.sleepTimeout`) that decides how long to keep the actor in memory with no recent activity
|
||||
- sleep can be held off for the lifetime of a promise with `c.keepAwake(promise)`
|
||||
- see the [sleeping docs](/docs/actors/lifecycle#sleeping) for full details
|
||||
|
||||
### wake events
|
||||
|
||||
- actors can wake to any of the follwoing events:
|
||||
- network requests
|
||||
- websocket messages
|
||||
- alarms (see scheduling docs)
|
||||
|
||||
### live actor migration
|
||||
|
||||
- live actor migrations lets your application ugprade, crash, or hot reload cahnges without interruption to your user or application (including websockets)
|
||||
- this is powered by hibernating websockets for live websocket migraiton & our fault tolerance mechanism (read more below)
|
||||
|
||||
### coldstart performance
|
||||
|
||||
- actors have negligible coldstart performance. the code to run the actor is already started (ie the runner), so creating/starting an actor is incredibly cheap.
|
||||
- creating new actors with a key requires some overhead to communicate with other regions in order to reserve the actor's key (see below). actors can be created without keys with near-0 latency.
|
||||
|
||||
### multi-region, globally unique actor keys
|
||||
|
||||
- actors can optionally have a globally unique "key"
|
||||
- when creating an actor with a key
|
||||
- this system is highly optimized to reduce wan round trips using per-key Paxos with a custom database called Epoxy (https://github.com/rivet-dev/rivet/tree/main/engine/packages/epoxy)
|
||||
- limitation: when creating an actor with a given key, that key will always be pinned to that region even if the actor is destroyed. creating a new actor with the same key will always live in the same region.
|
||||
- see the actors keys document
|
||||
|
||||
### input
|
||||
|
||||
- actors have input data that can be passed to them when constructed
|
||||
- this is similar to apssing data to a constructor in an object
|
||||
|
||||
### generic parameters
|
||||
|
||||
actor definitions include the following generic parameters that you'll see frequently in the code:
|
||||
|
||||
- state
|
||||
- conn state
|
||||
- conn params
|
||||
- ephemeral variables
|
||||
- input data
|
||||
- (experimental) database connector
|
||||
|
||||
### persistence
|
||||
|
||||
- state automatically flushes to storage intelligently
|
||||
- to force a state flush and wait for it to finish, call (TODO: look this up in state document)
|
||||
- read more about state persistence in the state document (link to document)
|
||||
- state is stored in the same place as where the actor lives. loading an actor in to memory has comparable performance to network attached storage, and once in memory, has performance of any standard in-memory read/write like a variable.
|
||||
|
||||
### scheduling & alarms
|
||||
|
||||
- actors have a scheduling api to be able to wake up at any time in the indefinite future
|
||||
- think of this like setTimeout but without a max timeout
|
||||
- rivet is responsible for waking the actor when this timeout wakes
|
||||
|
||||
### ephemeral variables
|
||||
|
||||
- actors have the ability to create ephemrla variables for things that you do not want to be persisted with the actor's state
|
||||
- this is useful for non-serializable data like a utility class like a pubsubs erver or something (TODO extra info)
|
||||
- link to ephemeral variables docs
|
||||
|
||||
### actions
|
||||
|
||||
- for stateless clients, actions are sent as http requests via `POST /gateway/{actor id}/actions/{action name}`
|
||||
- for stateful clients, actions are sent as websocket messages
|
||||
|
||||
### events & subscriptions
|
||||
|
||||
- events are sent as websocket messages
|
||||
|
||||
### error handling
|
||||
|
||||
- this is different than fault tolerance:
|
||||
- error handling is a user error
|
||||
- fault tolerance is something goes wrong that your applciation was not built to handle (ie hard crash, oom, network error)
|
||||
- rivet provdies a special UserError class to throw custom errors that will be returned to the client
|
||||
- all other errors are returned as a generic "internal error"
|
||||
- this is becuase leaking error deatils is a common security hole, so we default to expose-nothing errors
|
||||
|
||||
### logging
|
||||
|
||||
- rivet uses pino for logging
|
||||
- we expose a scoped child logger for each actor at `c.log` that automatically logs the actor id + key
|
||||
- this allows you to search lgos easily by actor id without having to log the actor id frequently
|
||||
- logs can be configured via the `RIVET_LOG_LEVEL` env var
|
||||
|
||||
### fault tolerance
|
||||
|
||||
- actors are fault tolerant, meaning that the host machine can crash and the actors will proceed to operate as if nothing happened
|
||||
- runners maintain a socket with rivet engine. when this socket closes or takes to long to ping, actors will reschedule
|
||||
- hibernating websockets (enabled by default) will live-migrate to the new actor as if nothing happened
|
||||
|
||||
### crash policy
|
||||
|
||||
- there are 3 crash policies: sleep, restart, and destroyed
|
||||
- sleep (default, usually the option you want):
|
||||
- when to use: actors that need high-performance in-memory logic.
|
||||
- when not to use: you need this actor running at all times no matter what, even if idle
|
||||
- examples: (list commone xamples)
|
||||
- destroy:
|
||||
- when to use: actors that need to run once until completion. on crash, do not try to reschedule.
|
||||
- when not to use: if you want your actor to have fault tolerance and be able to run transaprenlty to the underlying runner
|
||||
- examples: batch jobs, image conversions, ephemeral jobs, (TODO come up with better eaxmples)
|
||||
- restart:
|
||||
- when to use: actors that should be running at all times
|
||||
- when not to use: if you don't absolutely need something running at all times, since this consumes needless compute resources. considure using the scheduling api instead.
|
||||
- examples: maintain outbound sockets, daemons, always-running jobs, (TODO come up with better examples)
|
||||
|
||||
the behavior for each is described below:
|
||||
|
||||
| Event | Restart | Sleep | Destroy |
|
||||
|------------------------------|--------------|--------------|--------------|
|
||||
| Graceful exit (StopCode::Ok) | Destroy | Destroy | Destroy |
|
||||
| Crash (non-Ok exit) | Reschedule | Sleep | Destroy |
|
||||
| Lost (runner disappeared) | Reschedule | Sleep | Destroy |
|
||||
| Lost + force_reschedule | Reschedule | Reschedule | Reschedule |
|
||||
| GoingAway (runner draining) | Reschedule | Sleep | Destroy |
|
||||
| No capacity (allocation) | Queue (wait) | Sleep | Queue (wait) |
|
||||
| No capacity + serverless | Queue (wait) | Queue (wait) | Queue (wait) |
|
||||
| Wake signal (while sleeping) | Reschedule | Reschedule | Reschedule |
|
||||
|
||||
### inspector
|
||||
|
||||
- actors provide an inspector api to implement the:
|
||||
- repl
|
||||
- state read/write
|
||||
- network inspector
|
||||
- event log
|
||||
- this is impelmented over a websocket over bare
|
||||
|
||||
### http api
|
||||
|
||||
- see the http api document on actors
|
||||
|
||||
### multi-region
|
||||
|
||||
- actors can be scheduled across multiple regions
|
||||
- each actor has an actor id which embeds which region it lives in
|
||||
- networking is automatically routed to the region that an actor lives in
|
||||
- limitation: actors curretnly cannot migrate across regions
|
||||
|
||||
### backpressure
|
||||
|
||||
#### no runner capacity
|
||||
|
||||
- this is how actors with different crash policies behave when when there's backpressure:
|
||||
- sleep = sleeps (sheds load by not rescheduling)
|
||||
- restart = queues
|
||||
- destroy = queues
|
||||
- see the above matrix for more details on actor crash policy on how it handles no capacity.
|
||||
|
||||
- the actor queue is built to withstand high amounts of backpressure on rivet, so queueing actors is fine here
|
||||
- a large queue means it'll take more time for your application to process the queue to catch up with demand when it comes online.
|
||||
|
||||
#### per-actor cpu & networking exhaustion
|
||||
|
||||
- actors are isolated, so they each have their own individual bottleneck. you can think of this like a process thread where each thread can only do so much.
|
||||
- there is no durable message queue/"mailbox" for actors. if the actor cannot respond in time, then the request is dropped.
|
||||
- if an actor exhauses its cpu or networking, then the runner
|
||||
- returns service unavailble (503) if the actor fails to respond to a request in time
|
||||
- there is no hard cap on the networking or cpu usage for each actor at the moment
|
||||
- if your actor is resource intensive, it's common to use a separate mailbox actor to act as a queue
|
||||
|
||||
## runners
|
||||
|
||||
### regular vs serverless runners
|
||||
|
||||
there are 2 types of runners:
|
||||
|
||||
- regular: these are standard nodejs processes connected to rivet that rivet can orchestrate actors to and send network requests to at any time
|
||||
- serverless: rivet works with serverless platforms. when an actor is created, it has a request-per-actor model where it opens a long-running request on the serverless platofrm to run a given actor.
|
||||
|
||||
### runner pool
|
||||
|
||||
- runners are pooled together by sharing a common name (ie "default")
|
||||
- when an actor is created, it chooses the pool by selecting the runner name to run on
|
||||
- rivet will automatically load balance actors across these runners
|
||||
|
||||
### runner key
|
||||
|
||||
- not relevnat for serverless runners
|
||||
- each runner has a unique key that it provides when connecting. keys are unique to the instace the runner is running on and should be the same if the runner is restarted.
|
||||
- this can be the: machine's ip, k8s pod name, etc
|
||||
- if there is an existing runner connected with a given key, the runner will disconnect the old runner and replace it
|
||||
- rivet is designed to handle network partitions by waiting for runners to miss a ping, indicating it's no longer alive. however, often times runners restart immediately after a hard crash and reconnect. in this case, the runner will reconnect on restart and terminate the old runner in order to prevent further actors from scheduling to the crashed runner.
|
||||
|
||||
### capacity
|
||||
|
||||
- not relevnat for serverless runners
|
||||
- each runner can be assigned a capacity of how many actors it can run
|
||||
- rivet will schedule with spread (not binpacking) in order to spread load over actors
|
||||
|
||||
#### usefulness of capacity = 1
|
||||
|
||||
- setting a capacity of 1 is helpful for situations where you have cpu-intensive apps that should not run with any other actors
|
||||
- examples include game servers, ffmpeg jobs, etc
|
||||
|
||||
### versions & upgrading code
|
||||
|
||||
- each runner has a version index
|
||||
- actors are always scheduled to the highest verison index (see runner priority below)
|
||||
- this means that when a new runner is deployed:
|
||||
1. runners with higher index come online
|
||||
2. actors schedule to the highest index, stop scheduling to the older index
|
||||
3. old index runners start draining and migrating actors to new index
|
||||
4. all old runners are now shut down
|
||||
- websocekts are live migrated to the new version when upgrading using hibernating websockets to users see no hiccup in their applications
|
||||
- this is important because actors should never downgrade their runner. they should always move to a newer version of code in order to prevent corruption.
|
||||
|
||||
### runner scheduling prioroty
|
||||
|
||||
- actors are scheduled to runners sorted by priority of (version DESC, remaining capacity ASC)
|
||||
|
||||
### multi-region
|
||||
|
||||
TODO
|
||||
|
||||
### shutdown sequence
|
||||
|
||||
- runner shutdown is important to ensure that actors do not get unexpectedly terminated when either:
|
||||
- upgrading your applciation and taking down old pods
|
||||
- scaling down your runners horizontally (ie from an hpa)
|
||||
- pressing ctrl-c when in development
|
||||
- on shutdown:
|
||||
1. tell rivet the runner is stopping
|
||||
2. rivet tells all the actors on this runner to migrate
|
||||
3. runner waits for all actors to finish migrating
|
||||
4. runner exits process
|
||||
|
||||
### reconnection
|
||||
|
||||
- runners can handle temporary network partitions
|
||||
- they'll automatically reconnect and replay missed commands/events between rivet and the runner
|
||||
- this happens transparenlty to the user
|
||||
- if disconnected for too long (indicating a network partition), the runner will shut itself down and exit
|
||||
|
||||
### autoscaling
|
||||
|
||||
- not relevant to serverless
|
||||
- runners currently autoscale on cpu. more intelligent scaling is coming soon.
|
||||
- tune your runner total slots capacity accordingly
|
||||
- it's up to you to configure your hpa/etc to work like this. see the Connect guides (link to index page) for reference on hwo to configure this.
|
||||
|
||||
### serverless timeouts
|
||||
|
||||
- serverless runners take in to account the maximum run duration of the serverless platform
|
||||
- the runners will mgirate actors to a new request before the request times out
|
||||
- this is completely transparent to you and the user because of the fault tolerance and websocket migraiton characteristics
|
||||
- it's common for actors to go sleep before hitting the serverless timeout
|
||||
|
||||
## networking
|
||||
|
||||
### web standards
|
||||
|
||||
- everything in rivet is built on webstandards by default
|
||||
- nothing in rivet requires you to use our sdk, our sdks are meant to be a convenience. it's built to be as easy to use raw http/websocket endpoints from scratch.
|
||||
- actions, events, etc are all built on simple, well-documented http/websocket under the hood (link to openapi & asyncapi docs).
|
||||
- you can use low-level request handlers (lnk to dock) and low-level weboscket handlers (link to doc) to handle low-level primtivies yourself
|
||||
|
||||
### encoding
|
||||
|
||||
- rivetkit's action/events api supports communicating via [VBARE](link to github repo, see the blog post for the link), CBOR, or JSON
|
||||
- VBARE: high-perofrmance & compact, optimal use case
|
||||
- CBOR: descent encoding/decoding perf + portable libraries, good for implemnting high-ish performance on other platforms
|
||||
- JSON: good for fast implementations & debugging (easy to read)
|
||||
|
||||
### tunneling
|
||||
|
||||
- when a runner connects it opens a tunnel to rivet to allow incoming traffic
|
||||
- this is simila to systems like tailscale, ngrok, or other vpns
|
||||
- we do this for security & configuraiton simplicity since it means that you don't have to manage exposing your rivetkit applications' networkig to rivet. instead, anything that can open a socket to rivet can accept inbound traffic to actors.
|
||||
|
||||
### gateway
|
||||
|
||||
- incoming traffic to actors come to the Rivet gateway and are routed to the appropriate runner
|
||||
- the rivet gateway automatically handles:
|
||||
- multi-region routing to route traffic to the correct reigon for an actor
|
||||
- automatically waking the actor if needed
|
||||
- sending traffic over the runner
|
||||
|
||||
### hibernating websockets
|
||||
|
||||
- hibernating web sockets are a core component of live actor migration & fault tolerance. it allows us to maintain an open websocket while the actor crashes, upgrades, or moves to another runner.
|
||||
TODO: copy the rest of this from low-level webosckets document and rephrase
|
||||
|
||||
### actor health endpoin
|
||||
|
||||
- actors provide a simple, utility health endpoint at `/health` that lets you check if your actor is reachable (e.g. `curl https://api.rivet.dev/gateway/{actor id}/health`)
|
||||
|
||||
## multi-region
|
||||
|
||||
### networking
|
||||
|
||||
- actors may live in different regions than inbound requests
|
||||
- Rivet uses the Epoxy (link again) system to handle global routing to route traffic to the correct region with high performance
|
||||
- this is completely transparent to you. your app sends traffic to https://api.rivet.dev/gateway/* and it automatically routes to the correct actor in the appropriate region
|
||||
|
||||
### globally unique actor keys
|
||||
|
||||
- actor keys are globally unique to be able to benefit from multi-region capabilities without any extra work
|
||||
- see more about globally uniuqe actor keys above
|
||||
- see the actor keys document
|
||||
|
||||
### regional endpoints
|
||||
|
||||
- each reigon has a regional endpoint
|
||||
- this endpoint is used specifically for connecting runners (for example https://api-us-east-1.rivet.dev), opt to use api.rivet.dev for all other traffic
|
||||
- runners are required to connect to the regional endpoints
|
||||
- this is because runners are sensitive to latency to the rivet regional datacenter
|
||||
- we add datacenters regularly so each runner needs to be pinned to a single datacenter in order to ensure your availble datacneter list doesn't change sporadically without your consent
|
||||
|
||||
### persistence
|
||||
|
||||
- data is always persisted in the same region that is written
|
||||
- this is important for minimal coldstarts & data locality laws
|
||||
|
||||
## namespaces
|
||||
|
||||
- rivet provides namespaces to run multiple actor systems in isolation
|
||||
- this makes it really easy to have prod/staging environments or completely different applications running on the same rivet instance
|
||||
- when you connect to rivet, you can specify which namespace you're connecting to
|
||||
- self-hotsed rivet defaults to namespace "default"
|
||||
- rivet cloud provdies isolated tokens for each namespace
|
||||
|
||||
## manager api
|
||||
|
||||
- rivet provides a standard rest api for managing actors
|
||||
- useful endoints include:
|
||||
- get /actors
|
||||
- delete /actors/{}
|
||||
- get /actors/names -> get all actor types available
|
||||
|
||||
## comparison to prior art for actors
|
||||
|
||||
### runtime
|
||||
|
||||
- there are very few serious actor implementation targeted at the javascirpt eocsystem. rivet is arguably the most serious open-source actor implementation for typescript out there.
|
||||
|
||||
### library vs orchestrator
|
||||
|
||||
- some actor systems opt to be purely a library while rivet opts to have an orchestrator (i.e. the single rust binary)
|
||||
- this lets us to a lot of things other actor systems can't:
|
||||
- separating orchestration, persistence, and proxy lets us isoalte the core to be incredibly reliable while the fast-changing applications that ocnnect to rivet can be more error-prone safely. with a library, the blast radius of your application also affects the entire actor system.
|
||||
- support for serverless platforms to benefit from cost, multi-region, blitz scaling, and relibaiblity benefits
|
||||
- optimize fault tolerance since we can make more assumtions about application state when the rivet core does not crash and your app does
|
||||
|
||||
### scheduling
|
||||
|
||||
actors is a loose term, but there are generally 2 types of schedulign in practice:
|
||||
|
||||
- ephemeral actors
|
||||
- examples: erlang/otp, akka, swift
|
||||
- provides no persistence or sleeping mechanism by defualt
|
||||
- relies on supervisors for managing persistence
|
||||
- [virtual actors](https://www.microsoft.com/en-us/research/project/orleans-virtual-actors/)
|
||||
- an extension of the actor pattern that provides actors that can hibernate ("sleep") when not in use
|
||||
- examples: orleans, dapr, durable objects
|
||||
|
||||
rivet has similarities with both to provide more flexibility:
|
||||
|
||||
- crash policies provdie for 3 types of actors:
|
||||
- sleep -> most similar to virutal actors
|
||||
- restart -> most similar to ephemeral actors but with a supervisor to auto-restart, however still has a durable queue ot handle backpressure
|
||||
- crash -> most similar to traditional actors but with no supervisor to restart, however still has a durable queue to handle backpressure
|
||||
|
||||
### communication
|
||||
|
||||
- many actor frameworks use inbox patterns (think: queue-per-actor) to handle sending messages between actors
|
||||
- there is no callback mechanims, instead you need to send messages back to the actual actor
|
||||
- rivet opts to behave like web standards instead of using the message pattern
|
||||
- actors can impelment the inbox pattern optionally
|
||||
- but we provide lower-level networking to be able to be compatible with more techniologies
|
||||
- rivet assumes the same serial concurerntly that other actors do (by the nature of javascript being single-threaded) but we allow you to run promises in parallel or handl eyour own concurrency control (which some other actor frameworks might require a spawning new actor to do)
|
||||
|
||||
_Source doc path: /docs/general/architecture_
|
||||
18
.agents/skills/live-cursors/reference/general/cors.md
Normal file
18
.agents/skills/live-cursors/reference/general/cors.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Cross-Origin Resource Sharing
|
||||
|
||||
> Source: `src/content/docs/general/cors.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/cors
|
||||
> 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.
|
||||
|
||||
---
|
||||
Unlike stateless HTTP APIs that use CORS headers, Rivet Actors are stateful and support persistent WebSocket connections. Since WebSockets don't natively support CORS, we validate origins manually in the `onBeforeConnect` hook before connections may open.
|
||||
|
||||
## Implementing Origin Restrictions
|
||||
|
||||
To implement origin restrictions on Rivet Actors, use the `onBeforeConnect` hook to verify the request.
|
||||
|
||||
To catch the error on the client, use the following code:
|
||||
|
||||
See tracking issue for [configuring CORS per-actor on the gateway](https://github.com/rivet-dev/rivet/issues/3539) that will remove the need to implement origin restrictions in `onBforeRequest`.
|
||||
|
||||
_Source doc path: /docs/general/cors_
|
||||
@@ -0,0 +1,51 @@
|
||||
# Documentation for LLMs & AI
|
||||
|
||||
> Source: `src/content/docs/general/docs-for-llms.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/docs-for-llms
|
||||
> Description: Rivet provides optimized documentation formats specifically designed for Large Language Models (LLMs) and AI integration tools.
|
||||
|
||||
---
|
||||
## Skills (Recommended)
|
||||
|
||||
For AI coding assistants like Claude Code, Cursor, or Windsurf, install Rivet skills for the best development experience:
|
||||
|
||||
```sh
|
||||
npx skills add rivet-dev/skills
|
||||
```
|
||||
|
||||
Skills provide your AI assistant with Rivet-specific knowledge, best practices, and code patterns directly in your project context.
|
||||
|
||||
## Available Formats
|
||||
|
||||
### `llms.txt` (Condensed)
|
||||
A condensed version of the documentation perfect for quick reference and context-aware AI assistance.
|
||||
|
||||
**Access:** <a href="/llms.txt" target="_blank">/llms.txt</a>
|
||||
|
||||
This format includes:
|
||||
- Key concepts and features
|
||||
- Essential getting started information
|
||||
- Summaries of main functionality
|
||||
- Optimized for token efficiency
|
||||
|
||||
### `llms-full.txt` (Complete)
|
||||
The complete documentation in a single file, ideal for comprehensive AI assistance and in-depth analysis.
|
||||
|
||||
**Access:** <a href="/llms-full.txt" target="_blank">/llms-full.txt</a>
|
||||
|
||||
This format includes:
|
||||
- Complete documentation content
|
||||
- All examples and detailed explanations
|
||||
- Full API references and guides
|
||||
- Suitable for complex queries and comprehensive understanding
|
||||
|
||||
## Access Pages As Markdown
|
||||
|
||||
Each documentation page is also available as clean markdown by appending `.md` to any documentation URL path.
|
||||
|
||||
For example:
|
||||
|
||||
- Original URL: `https://rivet.dev/docs/actors`
|
||||
- Markdown URL: `https://rivet.dev/docs/actors.md`
|
||||
|
||||
_Source doc path: /docs/general/docs-for-llms_
|
||||
24
.agents/skills/live-cursors/reference/general/edge.md
Normal file
24
.agents/skills/live-cursors/reference/general/edge.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Edge Networking
|
||||
|
||||
> Source: `src/content/docs/general/edge.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/edge
|
||||
> Description: Actors automatically run near your users on your provider's global network.
|
||||
|
||||
---
|
||||
At the moment, edge networking is only supported on Rivet Cloud & Cloudflare Workers. More self-hosted platforms are on the roadmap.
|
||||
|
||||
## Region selection
|
||||
|
||||
### Automatic region selection
|
||||
|
||||
By default, actors will choose the nearest region based on the client's location.
|
||||
|
||||
Under the hood, Rivet and Cloudflare use [Anycast routing](https://en.wikipedia.org/wiki/Anycast) to automatically find the best location for the client to connect to without relying on a slow manual pinging process.
|
||||
|
||||
### Manual region selection
|
||||
|
||||
The region an actor is created in can be overridden using region options:
|
||||
|
||||
See [Create Manage Actors](/docs/actors/communicating-between-actors) for more information.
|
||||
|
||||
_Source doc path: /docs/general/edge_
|
||||
98
.agents/skills/live-cursors/reference/general/endpoints.md
Normal file
98
.agents/skills/live-cursors/reference/general/endpoints.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Endpoints
|
||||
|
||||
> Source: `src/content/docs/general/endpoints.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/endpoints
|
||||
> Description: Configure how your backend connects to Rivet and how clients reach your actors.
|
||||
|
||||
---
|
||||
## Local Development
|
||||
|
||||
No configuration is needed for local development. RivetKit runs entirely on your local machine without any extra configuration to run Rivet Actors.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
When deploying to production, you need to configure endpoints so your backend can communicate with Rivet Engine and clients can reach your actors.
|
||||
|
||||
<img src={imgEndpointEnvVars.src} alt="Diagram showing Client connecting to Rivet, which connects to Your Backend" />
|
||||
|
||||
### Private Endpoint
|
||||
|
||||
The private endpoint tells your backend where to find the Rivet Engine.
|
||||
|
||||
### Environment Variable
|
||||
|
||||
```bash
|
||||
RIVET_ENDPOINT=https://my-namespace:sk_xxxxx@api.rivet.dev
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
### Public Endpoint
|
||||
|
||||
The public endpoint tells clients where to connect to reach your actors.
|
||||
|
||||
This endpoint and token will be exposed to the internet. Use a public token (`pk_`), not your secret token (`sk_`).
|
||||
|
||||
The public endpoint is only required if using the [serverless runtime mode](/docs/general/runtime-modes#runners) and if you have a frontend using RivetKit.
|
||||
|
||||
### Environment Variable
|
||||
|
||||
```bash
|
||||
RIVET_PUBLIC_ENDPOINT=https://my-namespace:pk_xxxxx@api.rivet.dev
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
## Advanced
|
||||
|
||||
### URL Auth Syntax
|
||||
|
||||
Endpoint URLs support embedding namespace and token directly in the URL:
|
||||
|
||||
```
|
||||
https://namespace:token@host/path
|
||||
```
|
||||
|
||||
This is the recommended approach for simplicity. Alternatively, you can use separate environment variables:
|
||||
|
||||
```bash
|
||||
RIVET_ENDPOINT=https://api.rivet.dev
|
||||
RIVET_NAMESPACE=my-namespace
|
||||
RIVET_TOKEN=sk_xxxxx
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
In serverless mode, the private endpoint is used to validate that requests to `GET /api/rivet/start` are coming from your trusted Rivet endpoint. If the private endpoint is not configured, anyone can run a self-hosted instance of Rivet and connect to your backend from any endpoint.
|
||||
|
||||
### How Clients Connect
|
||||
|
||||
This flow applies to [serverless runtime mode](/docs/general/runtime-modes#serverless). For [runner runtime mode](/docs/general/runtime-modes#runners) or [clients configured to connect directly to Rivet](/docs/clients/javascript), clients connect directly to Rivet and this metadata flow is not needed.
|
||||
|
||||
When a client connects to your serverless application, it follows this flow:
|
||||
|
||||
1. Client makes a request to `https://my-app.example.com/api/rivet/metadata`
|
||||
2. Your app returns the public endpoint configuration:
|
||||
```json
|
||||
{
|
||||
"clientEndpoint": "https://api.rivet.dev",
|
||||
"clientNamespace": "my-namespace",
|
||||
"clientToken": "pk_xxxxx"
|
||||
}
|
||||
```
|
||||
3. Client caches these values and uses them for subsequent requests
|
||||
4. Client connects to `https://api.rivet.dev/gateway/{actor}`, which routes requests to your actors
|
||||
|
||||
This indirection exists because Rivet acts as a gateway between clients and your actors. This is because Rivet handles routing, load balancing, and actor lifecycle management of actors.
|
||||
|
||||
## Reference
|
||||
|
||||
| Environment Variable | Config Option | Description |
|
||||
|---------------------|---------------|-------------|
|
||||
| `RIVET_ENDPOINT` | `endpoint` | Rivet Engine URL for your backend |
|
||||
| `RIVET_NAMESPACE` | `namespace` | Namespace for actor isolation |
|
||||
| `RIVET_TOKEN` | `token` | Authentication token for engine connection |
|
||||
| `RIVET_PUBLIC_ENDPOINT` | `serverless.publicEndpoint` | Client-facing endpoint |
|
||||
| `RIVET_PUBLIC_TOKEN` | `serverless.publicToken` | Client-facing token |
|
||||
|
||||
_Source doc path: /docs/general/endpoints_
|
||||
@@ -0,0 +1,87 @@
|
||||
# Environment Variables
|
||||
|
||||
> Source: `src/content/docs/general/environment-variables.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/environment-variables
|
||||
> Description: This page documents all environment variables that configure RivetKit behavior.
|
||||
|
||||
---
|
||||
## Connection
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_ENDPOINT` | Endpoint URL to connect to Rivet Engine. Supports [URL auth syntax](/docs/general/endpoints#url-auth-syntax). |
|
||||
| `RIVET_TOKEN` | Authentication token for Rivet Engine |
|
||||
| `RIVET_NAMESPACE` | Namespace to use (default: "default") |
|
||||
|
||||
## Public Endpoint
|
||||
|
||||
These variables configure how clients connect to your actors.
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_PUBLIC_ENDPOINT` | Public endpoint for client connections. Supports [URL auth syntax](/docs/general/endpoints#url-auth-syntax). |
|
||||
| `RIVET_PUBLIC_TOKEN` | Public token for client authentication |
|
||||
|
||||
## Runner Configuration
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_RUNNER` | Runner name (default: "default") |
|
||||
| `RIVET_RUNNER_VERSION` | Version number for the runner. See [Versions & Upgrades](/docs/actors/versions). |
|
||||
| `RIVET_RUNNER_KIND` | Type of runner |
|
||||
| `RIVET_TOTAL_SLOTS` | Total actor slots available (default: 100000) |
|
||||
|
||||
## Engine
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_RUN_ENGINE` | Set to `1` to spawn the engine process |
|
||||
| `RIVET_RUN_ENGINE_HOST` | Host to bind the spawned local engine process to. Defaults to `127.0.0.1`. |
|
||||
| `RIVET_RUN_ENGINE_PORT` | Port to bind the spawned local engine process to. Defaults to `6420`. |
|
||||
| `RIVET_RUN_ENGINE_VERSION` | Version of engine to download |
|
||||
|
||||
## Inspector
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_INSPECTOR_DISABLE` | Set to `1` to disable the inspector |
|
||||
|
||||
## Metrics
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `_RIVET_METRICS_TOKEN` | Bearer token that gates the per-actor Prometheus `/metrics` endpoint. When unset, `/metrics` is disabled. |
|
||||
|
||||
## Experimental
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_EXPERIMENTAL_OTEL` | Set to `1` to enable experimental OTel tracing in Rivet Actors |
|
||||
|
||||
## Storage
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVETKIT_RUNTIME` | Runtime binding to use for RivetKit core: `auto`, `native`, or `wasm`. Defaults to `auto`. |
|
||||
| `RIVETKIT_STORAGE_PATH` | Overrides the default file-system storage path used by RivetKit when using the default driver. |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVETKIT_RUNTIME_MODE` | Controls how `registry.start()` runs. Accepted values are `envoy` and `serverless`; any other explicit value errors. Defaults to `envoy`: opens a long-lived WebSocket to the engine (Mode A). Set to `serverless` to bind an HTTP listener via `registry.listen()` (Mode B). |
|
||||
| `RIVETKIT_PUBLIC_DIR` | Directory of static assets to serve alongside the framework routes when calling `registry.listen()`. Used as a fallback when `opts.publicDir` is not passed. On auto-listen via `registry.start()`, defaults to `/public` when this env var is unset. |
|
||||
| `RIVET_PORT` | Port the listener binds when calling `registry.listen()` without an explicit `opts.port`. Must be an integer between 1 and 65535. Defaults to `3000`. |
|
||||
|
||||
## Logging
|
||||
|
||||
| Environment Variable | Description |
|
||||
|---------------------|-------------|
|
||||
| `RIVET_LOG_LEVEL` | Log level: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent` |
|
||||
| `RIVET_LOG_TARGET` | Set to `1` to include log target |
|
||||
| `RIVET_LOG_TIMESTAMP` | Set to `1` to include timestamps |
|
||||
| `RIVET_LOG_MESSAGE` | Set to `1` to include message formatting |
|
||||
| `RIVET_LOG_ERROR_STACK` | Set to `1` to include error stack traces |
|
||||
| `RIVET_LOG_HEADERS` | Set to `1` to log request headers |
|
||||
|
||||
_Source doc path: /docs/general/environment-variables_
|
||||
96
.agents/skills/live-cursors/reference/general/http-server.md
Normal file
96
.agents/skills/live-cursors/reference/general/http-server.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# HTTP Server
|
||||
|
||||
> Source: `src/content/docs/general/http-server.mdx`
|
||||
> Canonical URL: https://rivet.dev/docs/general/http-server
|
||||
> Description: Different ways to run your RivetKit HTTP server.
|
||||
|
||||
---
|
||||
## Methods of Running Your Server
|
||||
|
||||
### registry.start()
|
||||
|
||||
The simplest way to run your server. Starts a local RivetKit server, serves static files from a `public` directory, and starts the actor runner:
|
||||
|
||||
Run with `npx tsx --watch index.ts` (Node.js), `bun --watch index.ts` (Bun), or `deno run --allow-net --allow-read --allow-env --watch index.ts` (Deno). Clients connect to the Rivet Engine on `http://localhost:6420`.
|
||||
|
||||
### With Fetch Handlers
|
||||
|
||||
A [fetch handler](https://wintercg.org/) is a function that takes a `Request` and returns a `Response`. This is the standard pattern used by Cloudflare Workers, Deno Deploy, Bun, and other modern runtimes.
|
||||
|
||||
Use `registry.serve()` to get a fetch handler:
|
||||
|
||||
To integrate with a router like [Hono](https://hono.dev/) or [Elysia](https://elysiajs.com/), use `registry.handler()`:
|
||||
|
||||
### Hono
|
||||
|
||||
### Elysia
|
||||
|
||||
Then run your server:
|
||||
|
||||
```bash Node.js
|
||||
npx tsx --watch server.ts
|
||||
```
|
||||
|
||||
```bash Bun
|
||||
bun --watch server.ts
|
||||
```
|
||||
|
||||
```bash Deno
|
||||
deno run --allow-net --allow-read --allow-env --watch server.ts
|
||||
```
|
||||
|
||||
### Explicit HTTP Server
|
||||
|
||||
If you need to explicitly start the HTTP server instead of using the fetch handler pattern:
|
||||
|
||||
### Node.js (Hono)
|
||||
|
||||
Using Hono with `@hono/node-server`:
|
||||
|
||||
### Node.js (Adapter)
|
||||
|
||||
Using `@whatwg-node/server` to adapt the fetch handler to Node's HTTP server:
|
||||
|
||||
```ts server.ts @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
import { createServer } from "node:http";
|
||||
import { createServerAdapter } from "@whatwg-node/server";
|
||||
|
||||
const myActor = actor({ state: {}, actions: {} });
|
||||
const registry = setup({ use: { myActor } });
|
||||
|
||||
const handler = createServerAdapter(registry.serve().fetch);
|
||||
const server = createServer(handler);
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
### Bun
|
||||
|
||||
Using Bun's native server:
|
||||
|
||||
```ts server.ts @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const myActor = actor({ state: {}, actions: {} });
|
||||
const registry = setup({ use: { myActor } });
|
||||
|
||||
Bun.serve({
|
||||
port: 3000,
|
||||
fetch: (request: Request) => registry.handler(request),
|
||||
});
|
||||
```
|
||||
|
||||
### Deno
|
||||
|
||||
Using Deno's native server:
|
||||
|
||||
```ts server.ts @nocheck
|
||||
import { actor, setup } from "rivetkit";
|
||||
|
||||
const myActor = actor({ state: {}, actions: {} });
|
||||
const registry = setup({ use: { myActor } });
|
||||
|
||||
Deno.serve({ port: 3000 }, (request: Request) => registry.handler(request));
|
||||
```
|
||||
|
||||
_Source doc path: /docs/general/http-server_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user