Generating images
Generate images during gameplay with io.activities.generateImage(callId, params), which returns { mediaId } at dispatch while the image renders in the background. Render a mediaId with the built-in RenderMedia component (or useMedia for custom UI), store it in state or emit it as an event, and drive prompts from an illustrator agent. Covers the model prompting guide, idempotency, and pre-authored image assets.
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.
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 throw at the
call, but a generation that fails downstream resolves the media to a failed
status:
rejectedwhen the content filter denies it (terminal),errorfor anything else (a failed reference, a transient outage).RenderMediashows a broken-image state for both — with a built-in retry control when theerroris retryable — anduseMediareports the status plusretryable/retry(). No credits are charged for a failed generation, and retrying is free. - Credits scale with the model and pixel count, and each reference adds a surcharge. Don't put mediaIds in agent prompts — they're opaque handles, meaningless to an LLM.
Model prompting guide
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, highresin the prompt; a good negative prompt isworst_quality, bad_quality, poorly_detailed, text. - Best resolutions (aim for ~1MP): 1024×1024, 832×1216, 1216×832, 768×1344, 1344×768 — a multiple of 16 each side.
- 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 from 64×64 to 2048×2048, a multiple of 16 each side; aim for ~1MP.
- Negative prompts have limited effect.
- Supports
referenceMediaIds.
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.
import { RenderMedia } from '@omorashi/sdk'
<RenderMedia media={output.mediaId} alt="The scene" />For custom UI, subscribe to a mediaId's load state with useMedia:
import { useMedia } from '@omorashi/sdk'
const { url, status, error, retryable, retry } = useMedia(mediaId)
// status: 'loading' | 'generating' | 'done' | 'error' | 'rejected'; url is
// present once 'done'. retry() re-runs a failed generation (free) when
// retryable is true; 'rejected' (content policy) is terminal, never retryable.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.
// 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.
// 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 addMessage so
it accumulates narrative context, then use transient: true + responseFormat
sends to request a prompt without polluting its history — the caller runs
generateImage and decides what to do with the mediaId.
upsert_text_asset({ name: "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." })
const imagePromptSchema = z.object({
prompt: z.string().describe('Danbooru-style tag prompt'),
negativePrompt: z.string().describe('Negative prompt tags'),
})
io.agents.illustrator = new io.Agent('illustrator', {
systemPromptAsset: 'Illustrator Prompt',
model: 'fast',
})
// Keep the illustrator informed as the story progresses (persistent)
emit({ type: 'narration', text: narration })
io.agents.illustrator.addMessage({ role: 'user', content: `[Story] ${narration}` })
// Query for a prompt (transient — doesn't pollute the story context)
const { data } = await io.agents.illustrator.send('illustrate-scene', {
message: { role: 'user', content: 'Illustrate the current scene.' },
responseFormat: imagePromptSchema,
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.
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.
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]).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.