> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reactor.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# HappyOyster SDK reference

> The typed HappyOyster JavaScript and React API for worlds, travel, controls, and state.

This page documents the public surface of `@reactor-models/happy-oyster`. For a complete application
flow, see the [tutorial](/model-api-reference/happy-oyster/tutorial).

## Modes

Choose a mode before connecting:

| Mode          | World creation options                  | Live controls                              |
| ------------- | --------------------------------------- | ------------------------------------------ |
| `"adventure"` | `perspective`                           | Movement, look, interaction, and held axes |
| `"directing"` | `resolution`, `layout`, and `narrative` | Instructions, pause, resume, and rewind    |

The mode stays fixed for the client or provider session. Construct a new client, or remount the
provider, to switch modes.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const adventure = new HappyOysterModel({ mode: "adventure", videoElement });
const directing = new HappyOysterModel({ mode: "directing", videoElement });
```

## Client construction

### JavaScript

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
import { HappyOysterModel } from "@reactor-models/happy-oyster";

const model = new HappyOysterModel({
  mode: "adventure",
  videoElement,
});

await model.connect(jwt);
```

`videoElement` may be omitted at construction and supplied later with `attachVideo(element)`.

### React

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
import {
  HappyOysterProvider,
  HappyOysterVideo,
  useHappyOyster,
} from "@reactor-models/happy-oyster/react";

function App({ jwt }: { jwt: string }) {
  return (
    <HappyOysterProvider mode="directing" jwt={jwt} autoConnect>
      <HappyOysterVideo className="aspect-video w-full" />
      <Controller />
    </HappyOysterProvider>
  );
}
```

Provider props:

| Prop             | Type                                  | Required | Description                                         |
| ---------------- | ------------------------------------- | -------- | --------------------------------------------------- |
| `mode`           | `"adventure" \| "directing"`          | Yes      | Experience selected before connecting               |
| `jwt`            | `string \| (() => string \| Promise)` | No       | Static JWT or lazy resolver; omitted for local mode |
| `autoConnect`    | `boolean`                             | No       | Connect when mounted; default `false`               |
| `connectOptions` | `{ sessionId, connectionId? }`        | No       | Attach to an existing Reactor session               |
| `local`          | `boolean`                             | No       | Connect to a locally served model                   |
| `apiUrl`         | `string`                              | No       | Reactor API base URL                                |

## Lifecycle

The client lifecycle is available as `model.phase` or `useHappyOyster().phase`:

```text theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
idle → connecting → connected → starting_stream → streaming → ended / failed
```

### `connect`

Open the session and synchronize the first `worldState` snapshot.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.connect(jwt);
```

With a provider, use `autoConnect` or call the hook method:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { connect } = useHappyOyster();
await connect(getJwt);
```

### `disconnect`

End any active travel and close the session. Worlds remain available for later attachment.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.disconnect();
```

### `endTravelSession`

End the active travel while keeping the session and current world.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.endTravelSession();
```

## Worlds

### `createWorld`

Create a world for the client's selected mode and make it current. The promise resolves with a
`WorldStateMessage` once the world is ready.

Shared parameters:

| Parameter            | Type           | Required | Description                                           |
| -------------------- | -------------- | -------- | ----------------------------------------------------- |
| `prompt`             | `string`       | Yes      | Natural-language world description, up to 2,000 chars |
| `firstFrameImageUrl` | `string`       | No       | Public starting-image URL                             |
| `firstFrameImage`    | `File \| Blob` | No       | Local starting image, at most 2 MB                    |

Provide only one starting-image source.

Adventure parameters:

| Parameter     | Type                               | Default          |
| ------------- | ---------------------------------- | ---------------- |
| `perspective` | `"first_person" \| "third_person"` | `"third_person"` |

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const world = await model.createWorld({
  prompt: "A misty ancient forest with a lantern-lit trail.",
  perspective: "third_person",
});
```

Directing parameters:

| Parameter    | Type                               | Default  |
| ------------ | ---------------------------------- | -------- |
| `resolution` | `"480p" \| "720p"`                 | `"720p"` |
| `layout`     | `"Stable" \| "Fast"`               | —        |
| `narrative`  | `"Calm" \| "Dramatic" \| "Normal"` | —        |

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const world = await model.createWorld({
  prompt: "A courier enters a floating city as a storm approaches.",
  resolution: "720p",
  layout: "Stable",
  narrative: "Dramatic",
});
```

Save `world.encrypted_world_id` to reopen the world later.

### `attachWorld`

Make an existing world current and wait until it is ready:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const world = await model.attachWorld(encryptedWorldId);
```

The world must belong to the client's selected mode.

## Travel

### `startTravel`

Start the current ready world and render its stream into the attached video element:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const result = await model.startTravel();
```

The method is single-flight: concurrent calls share one attempt. Calling it while already streaming
returns the current result.

`StartTravelResult`:

| Field       | Type      | Description                                          |
| ----------- | --------- | ---------------------------------------------------- |
| `streaming` | `boolean` | Whether a live stream was opened                     |
| `session`   | `object`  | Session metadata, or `null` when no stream is opened |

### Video in React

Mount `HappyOysterVideo` under the provider before starting:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
<HappyOysterVideo autoPlay muted playsInline className="aspect-video w-full" />
```

## Adventure controls

Adventure controls are held state. Movement, look, and interaction axes compose until released.

### `move`

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.move("Front");
```

Values: `Front`, `Back`, `Left`, `Right`, the four diagonal combinations, and `None`.

### `look`

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.look("Mouse_Left");
```

Values: `Mouse_Up`, `Mouse_Down`, `Mouse_Left`, `Mouse_Right`, the four diagonal combinations, and
`None`.

### `interact`

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.interact("Jump");
```

Built-in verbs include `Jump`, `Attack`, `Crouch`, and `Sprint`. The current world's advertised
verbs are available in `travelState.character_actions` and `travelState.environment_actions`.

### `hold`

Set multiple axes in one call:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.hold({
  translation: "Front",
  rotation: "Mouse_Left",
  interaction: "Sprint",
});
```

### `release`

Release selected axes while leaving the others held:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.release({ translation: true, interaction: true });
```

### `stop`

Release every axis:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.stop();
```

## Directing controls

### `instruct`

Submit a text instruction:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { accepted } = await model.instruct("A storm rolls in over the city.");
```

Accepted instructions appear in `travelState.user_instructions`.

### `pause`

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.pause();
```

### `resume`

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await model.resume();
```

### `rewind`

Rewind a paused travel to an absolute second. Playback resumes automatically:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { resumedAtSec } = await model.rewind(8);
```

Targets use 4-second boundaries; other values are rounded down.

### Travel status

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
model.onTravelStatusChanged((status) => {
  console.log(status); // running, paused, or completed
});
```

React:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
useHappyOysterTravelStatus((status) => {
  setPaused(status === "paused");
});
```

## State

### `worldState`

The authoritative current-world snapshot:

| Field                | Type             | Description                                                           |
| -------------------- | ---------------- | --------------------------------------------------------------------- |
| `phase`              | `WorldPhase`     | `no_world`, `creating`, `building`, `ready`, `traveling`, or `failed` |
| `encrypted_world_id` | `string \| null` | Id used by `attachWorld()`                                            |
| `world_status`       | `string \| null` | Current build status                                                  |
| `first_frame`        | `string \| null` | Starting-frame preview URL                                            |
| `prompt`             | `string \| null` | Prompt used to create the world                                       |
| `mode`               | `number \| null` | `1` Adventure or `2` Directing                                        |

Subscribe from the class:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const unsubscribe = model.onWorldState((state) => {
  renderWorld(state);
});
```

Or read the reactive value from `useHappyOyster()`.

### `travelState`

The authoritative live-travel snapshot:

| Field                 | Type                  | Description                    |
| --------------------- | --------------------- | ------------------------------ |
| `status`              | `string`              | Current travel status          |
| `user_instructions`   | `TravelInstruction[]` | Directing instruction timeline |
| `chapters`            | `TravelChapter[]`     | Directing chapters             |
| `character_actions`   | `string[]`            | Adventure character verbs      |
| `environment_actions` | `string[]`            | Adventure environment verbs    |

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const unsubscribe = model.onTravelState((state) => {
  renderTravel(state);
});
```

## React hook surface

`useHappyOyster()` returns:

* `model`, `mode`, `phase`, `worldState`, `travelState`, and `streaming`
* `connect`, `disconnect`, `createWorld`, `attachWorld`, `startTravel`, and `endTravelSession`
* Adventure methods: `move`, `look`, `interact`, `hold`, `release`, `stop`, and `control`
* Directing methods: `instruct`, `pause`, `resume`, and `rewind`

Additional hooks:

* `useHappyOysterWorldState()`
* `useHappyOysterTravelState()`
* `useHappyOysterPhase()`
* `useHappyOysterTravelStatus(handler)`
* `useHappyOysterTravelError(handler)`

## Errors

`HappyOysterActionError` exposes the rejected `action` and a machine-readable `code`.

| Code              | Meaning                                            |
| ----------------- | -------------------------------------------------- |
| `TRAVELING`       | End the travel before creating or attaching        |
| `BUSY`            | Another world action is still running              |
| `NO_WORLD`        | Create or attach a world before starting           |
| `WORLD_NOT_READY` | Wait until the current world is ready              |
| `MODE_MISMATCH`   | The world belongs to the other experience          |
| `403001`          | The world is unknown or belongs to another account |
| `403004`          | Prompt rejected                                    |
| `403005`          | Starting image rejected                            |

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
try {
  await model.attachWorld(encryptedWorldId);
} catch (error) {
  if (error instanceof HappyOysterActionError) {
    console.error(error.code, error.action, error.message);
  }
}
```
