Development instance — data may be reset at any time

Generating images

Generate images during gameplay with io.activities.generateImage(callId, params), which returns { mediaId } at dispatch and can still fail minutes later — so every rendering of a mediaId must handle its status (RenderMedia does; with useMedia, url is absent in every status but 'done', and silently rendering nothing is a decision you have to make deliberately). Render a mediaId with RenderMedia or useMedia, store it in state or emit it as an event, and drive prompts from an illustrator agent. Covers the model prompting guide (each model's exact size limits — generating outside them fails), idempotency, credits, and pre-authored image assets (which may be looping video clips).

Games generate images at runtime with io.activities.generateImage. The call returns at dispatch with a { mediaId } while the image renders in the background — a turn never blocks on the image model. The frontend renders the mediaId with RenderMedia, showing a placeholder until the bytes are ready.

generateImage

Call it during event processing (onEvent or a tool's execute). The first argument is a callId — a unique, descriptive name for the call site, like the first argument to agent.send.

ts
const { mediaId } = await io.activities.generateImage('scene-img', {
  prompt: 'masterpiece, high_quality, highres, 1girl, castle courtyard, morning light',
  negativePrompt: 'worst_quality, bad_quality, text',
  height: 768,
})
emit({ type: 'illustration', mediaId })

Parameters:

| Field | Type | Default | Description | |-------|------|---------|-------------| | prompt | string | (required) | The whole instruction to the image model | | width | number | model default | Image width in pixels | | height | number | model default | Image height in pixels | | negativePrompt | string | — | What to avoid (model-dependent) | | model | 'omorashi-v1' | 'flux' | site default (omorashi-v1) | Which image model to use | | referenceMediaIds | string[] | — | Prior generated images to use as visual references (flux only) — see reference-images |

Returns { mediaId } — an opaque handle to the media item. Render it with RenderMedia / useMedia, store it in state, emit it as an event, or pass it as a reference to a later generation.

  • Idempotent. The mediaId is derived from the args, so re-calling with identical args returns the same mediaId at no extra charge — a revised prompt is a new generation with a new mediaId. Because of this, don't append a counter or timestamp to the prompt to "force" a fresh image; change the prompt meaningfully instead.
  • Errors surface on the media, not as a throw. Invalid arguments (a size the model doesn't support, an over-long prompt) throw at the call and cost nothing, but a generation that fails downstream resolves the media to a failed status: rejected when the content filter denies it (terminal), error for anything else (a failed reference, a transient outage). RenderMedia shows a broken-image state for both — with a built-in retry control when the error is retryable — and useMedia reports the status plus retryable/retry().
  • Credits scale with the model and pixel count, and each reference adds a surcharge. A dispatched generation is charged whether or not it succeeds; re-running a failed one is free either way — the built-in retry control (retry()) and calling generateImage again with the same args are the same operation, re-arming the failed image and any failed reference under it at no extra charge. Don't put mediaIds in agent prompts — they're opaque handles, meaningless to an LLM.

Model prompting guide

<!-- Each model's size limits mirror the gateway (image-gen/gateway/src/validate.ts) via web's model catalog (web/sites/<site>/config.ts); web/src/lib/__tests__/platform-docs-image-models.test.ts pins the numbers below to that catalog. Change a limit in all three places. -->

omorashi-v1 (anime-style, a finetune of SDXL, default):

  • Allows NSFW content.
  • Danbooru-style tag prompting — comma-separated tags (e.g. 1girl).
  • Include masterpiece, high_quality, highres in the prompt; a good negative prompt is worst_quality, bad_quality, poorly_detailed, text.
  • Resolutions 512–2048 on each side, in multiples of 64 (SDXL buckets — stricter than flux's 16). Anything outside that fails.
  • Best resolutions (aim for ~1MP): 1024×1024, 832×1216, 1216×832, 768×1344, 1344×768.
  • Does not support referenceMediaIds.

flux (photorealistic/general):

  • Cannot produce NSFW content.
  • Natural-language prompting — describe the scene in full sentences.
  • Example: "A cozy tavern interior with wooden beams, warm firelight, and a bard playing a lute in the corner".
  • Resolutions 512–2048 on each side, in multiples of 16; aim for ~1MP (e.g. 1024×1024). Anything outside that fails.
  • Negative prompts have limited effect.
  • Supports referenceMediaIds.

Choosing between them. Pick the model for the subject, not for the shape you want on screen: omorashi-v1 for character and scene art (and anything NSFW), flux for photoreal/SFW work and anything conditioned on referenceMediaIds. For prominent, persistent, once-off art — title banners, covers, backgrounds, key character portraits — spend on quality: generate at one of the model's best resolutions and crop to the display aspect in CSS (object-fit: cover on the <img>). A 4:1 title banner is a 1216×832 generation cropped by its container, not a reason to squeeze the model into an extreme aspect it renders badly. For frequent in-turn generation — an illustration per scene — keep the size modest (512–768 on a side) so turns stay fast and cheap.

Rendering a mediaId

RenderMedia is the drop-in way to display a generated image — it renders the <img> once ready, a pulsing placeholder while generating (and for a null mediaId), and a broken-image state on failure, with a small built-in retry control when the failure is retryable (a rejected image never is). It behaves like an img and takes an optional alt and className. A looping-clip image asset (see below) plays as a muted, looping, control-less <video> — no code change needed. When media changes it holds its last picture, under a light shimmer, until the new one is ready to draw — so a regenerated image never flashes blank. If a component reuses one RenderMedia for a different subject (another character's card), give it a key for the subject so it starts fresh.

tsx
import { RenderMedia } from '@omorashi/sdk'

<RenderMedia media={output.mediaId} alt="The scene" />

For custom UI, subscribe to a mediaId's load state with useMedia. It is discriminated on status — narrow before reading, because url exists only on done and error only on the failure states:

tsx
import { RenderMedia, useMedia } from '@omorashi/sdk'

const media = useMedia(mediaId)
if (media.status === 'done') {
  return media.type === 'video'   // a looping-clip image asset
    ? <RenderMedia media={mediaId} alt="The scene" />   // RenderMedia plays it
    : <img src={media.url} alt="The scene" />
}
if (media.status === 'error') return <button onClick={media.retry}>Retry — {media.error}</button>
if (media.status === 'rejected') return <p>That image wasn't allowed.</p>
return <div className="placeholder" />   // loading | generating

A clip needs RenderMedia (or the same muted, inline, autoplaying loop): Safari autoplays a <video> only with the muted attribute, which React's muted prop never writes — a hand-drawn clip must set it through a ref, with playsInline autoPlay loop.

retry() re-runs a failed generation for free when retryable is true; rejected is a content verdict, terminal and never retryable. Decide deliberately what each failure looks like — falling back to something neutral is fine for purely decorative art, but it should be a choice, not the accident you get from reading url without checking status. Branch on type as well: an <img> shows nothing for a clip.

Image on state (persistent, mutable)

Store a mediaId in state so it persists across events and updates over time — a character portrait, a scene background, a map that changes as the state does.

ts
// State
interface State {
  portraitMediaId: string | null
}
initialState: { portraitMediaId: null }

// Backend — update the portrait
const { mediaId } = await io.activities.generateImage('portrait', { prompt })
state.set(draft => { draft.portraitMediaId = mediaId })

// Frontend — render from state (updates automatically when state changes)
{state.portraitMediaId && <RenderMedia media={state.portraitMediaId} alt="Portrait" />}

Image as event (inline chat image)

Emit a mediaId as an output event so it appears inline in the chat history — a one-off illustration that flows with the narrative rather than persisting in a panel.

ts
// Backend — emit the mediaId
emit({ type: 'illustration', mediaId })

// Frontend — render inline in chat
function Output({ output }: { output: OutputEvent }) {
  switch (output.type) {
    case 'illustration':
      return <RenderMedia media={output.mediaId} alt="Scene illustration" className="chat-image" />
    // ...
  }
}

Driving generation from the story: an illustrator agent

For AI-driven illustration, use a dedicated illustrator agent that follows the story and composes prompts on demand. Feed it story events with addMessages so it accumulates narrative context. Make it a structured agent (a fixed responseFormat in its config), then use transient: true sends to request a prompt without polluting its history — the caller runs generateImage and decides what to do with the mediaId.

write_file({ path: "assets/Illustrator Prompt", content: "You are an illustrator following along with a story.\nWhen asked to illustrate, compose a danbooru-style tag prompt capturing the scene.\nAlways include: masterpiece, high_quality, highres." })
ts
const imagePromptSchema = z.object({
  prompt: z.string().describe('Danbooru-style tag prompt'),
  negativePrompt: z.string().describe('Negative prompt tags'),
})

// Create the illustrator once, where the game starts (a second createAgent throws)…
io.createAgent('illustrator', {
  systemPromptAsset: 'Illustrator Prompt',
  model: 'fast',
  responseFormat: imagePromptSchema,
})

// …and reach it by name in every handler after.
const illustrator = io.getAgent('illustrator', imagePromptSchema)

// Keep the illustrator informed as the story progresses (persistent)
emit({ type: 'narration', text: narration })
illustrator.addMessages({ role: 'user', content: `[Story] ${narration}` })

// Query for a prompt (transient — doesn't pollute the story context)
const { data } = await illustrator.send('illustrate-scene', {
  messages: [{ role: 'user', content: 'Illustrate the current scene.' }],
  transient: true,
})

// Caller controls generation — size, model, and destination
const { mediaId } = await io.activities.generateImage('scene', {
  prompt: data.prompt,
  negativePrompt: data.negativePrompt,
  width: 832,
  height: 1216,
})
state.set(draft => { draft.sceneMediaId = mediaId })

The illustrator's history stays clean — only story events accumulate, so compaction summarizes the narrative rather than a mix of story and prompt requests. (If the illustrator needs to keep a character looking the same across a long game, that is what agent topics are for — though a mediaId in game state is the cheaper answer; see reference-images.)

Pre-authored image assets

Game assets (import assets from '@game/assets') bundle pre-authored images — character portraits, backgrounds — that the game creator manages outside of code. Use assets for content that should be consistent across plays, and runtime generation for dynamic content. An image asset's .mediaId is string | null (null for an unfilled placeholder), and it renders and references exactly like a generated mediaId.

Animated images. Any image asset may hold a short looping video clip instead of a still — a leaf drifting down, a flickering torch — like a GIF, only far smaller. Whether it does is the uploader's choice (the Assets tab takes MP4/WebM up to 10 MB), so game code shouldn't assume a still: RenderMedia plays a clip automatically (holding its first frame under reduced motion), and hand-rendered useMedia code branches on type. A clip is refused where only a still works: the game thumbnail, a referenceMediaIds entry, and a notification image. For a good clip: loop it seamlessly, keep it to a few seconds, and export H.264 MP4 for the widest browser support. Audio is ignored.

ts
import assets from '@game/assets'

const portrait = assets["Hero Portrait"].mediaId  // string | null
// Render it: <RenderMedia media={portrait} alt="Hero" />
// …or pass it as a generateImage reference (referenceMediaIds: [portrait]) — stills only.

To keep a character visually consistent across many generated shots — the same face in new poses, or two characters sharing a frame — generate one canonical reference (with flux) and pass it via referenceMediaIds. See reference-images.