---
url: 'https://adk.nht.io/batteries/media/generating.md'
description: >-
  EMPTY_MIME and the empty:<format> sentinel: creating media from nothing,
  deterministic vs model-based semantic generation, and the reachability rule.
---

# Generating Media

## LLM summary — Media generation

* `EMPTY_MIME` (`application/x-adk-empty`) is the virtual source MIME for creating new media. An engine that can mint a blank/seed file declares `converts: [{ from: [EMPTY_MIME], to: [...] }]` and receives zero bytes + a target token. No generation API, no factory registry — generation is one more convert edge.
* Deterministic generation (same inputs → same bytes: blank workbook/canvas/silence) is bundled; model-based semantic generation (diffusion/TTS inventing content) is BYO via the SAME edge — declare `from: [EMPTY_MIME]`, consume a prompt from `request.options`.
* Creatable set = `convertTargets(EMPTY_MIME)` — pure graph reachability, multi-hop included (with soffice: EMPTY\_MIME→xlsx→pdf composes), never a policy list. Unavailable means unreachable, not forbidden.
* Bundled generation edges: data → txt/md/json/yaml/csv/html (literal seeds); sheetjs → every spreadsheet write target (blank Sheet1 workbook); exceljs → xlsx (blank Sheet1); jimp/sharp → blank 1024×1024 white canvas in any supported encoding; audio\_decode → wav (1 s 16-bit mono silence, 44100 Hz, dependency-free); soffice → its whole matrix via zero-byte seed files.
* Agent surface: `media_id: "empty:<format>"` on any forge tool creates instead of resolving (additive — harness ids are UUIDs, so `empty:*` was previously guaranteed MEDIA\_NOT\_FOUND); `@empty:<format>` works as a statement ref (`merge with=@empty:xlsx`). Output filename `untitled.<format>`. Unavailable format → `Error (EMPTY_FORMAT_UNAVAILABLE): … Creatable formats: … Do not retry`. Advertisement (description section, schema hint, MEDIA\_NOT\_FOUND exemplar) appears only when the creatable set is non-empty.
* Defaults the model is told: blank images are 1024×1024 white (resize in the same statement); blank workbooks ship one `Sheet1`.

An empty file is just a conversion wearing a costume — the source format happens to be nothing. The battery gives that nothing a name, `EMPTY_MIME` (`application/x-adk-empty`), and generation becomes one more convert edge: an engine that can mint a blank file declares `converts: [{ from: [EMPTY_MIME], to: [...] }]` and receives zero bytes plus a target token. No generation API, no factory registry, no special dispatch — `convertTargets(EMPTY_MIME)` already answers "what can this deployment create?" and multi-hop pathfinding already extends it (with soffice configured, `EMPTY_MIME → xlsx → pdf` composes like any other chain).

## Deterministic vs model-based semantic

Be precise about what kind of generation this is, because the vocabulary shaped the API. Generating an .xlsx, a blank canvas, or a second of silence IS media generation — it is **deterministic** generation: same inputs, same bytes. **Model-based semantic** generation (diffusion inventing an image, TTS inventing speech) is the same edge with different machinery: a BYO engine declares `from: [EMPTY_MIME]`, consumes a prompt from `request.options`, and slots in beside the bundled deterministic generators with zero forge changes. One seam, both kinds.

::: info Field note: the framing fight
"Generation vs population" was the first draft of this design, with diffusion/TTS reserved as the only "real" generation and a special "substrate engine" to hold the seeds. That distinction died in review: generating an .xlsx is absolutely media generation — it's *deterministic* generation, and reserving the word for model-based synthesis got the API wrong. The substrate engine died with it, for two reasons that generalize: a standalone generator engine drags its whole dependency bundle along (consumers take all of it to get any of it), and it puts distance between the consumer and the generator. Generation lives in each engine that can mint its own format, and the vocabulary in these docs exists because getting it wrong shaped the code wrong.
:::

## The reachability rule

The creatable set is purely graph reachability. There is no policy list of "generatable formats" — a format is creatable iff some engine path reaches it from `EMPTY_MIME`, and unavailable means unreachable, never forbidden. What that means per deployment, using the [bundled fleet](./fleet):

* `engines/data` alone gives txt/md/json/yaml/csv/html — literal seeds, zero dependencies.
* `engines/sheetjs` gives the whole spreadsheet family (a blank `Sheet1` workbook written to any of its targets); `engines/exceljs` gives xlsx.
* `engines/jimp` / `engines/sharp` give blank canvases in their supported encodings.
* `engines/audio_decode` gives a second of silence as wav.
* `engines/soffice` lights up docx/pptx/pdf and the rest of the office matrix.
* A BYO diffusion or TTS engine extends the same edge with semantic content.

::: info Field note: the zero-byte discovery
The soffice generation edge was going to be hand-rolled minimal OOXML templates until someone tried `touch input.docx && soffice --convert-to pdf` and LibreOffice just… treated it as an empty Writer document. Every silo, both directions — a 0-byte odt converts to docx, a 0-byte ods to xlsx, the minted xlsx has a `Sheet1`, the minted pptx has a slide, a master, and a layout. Undocumented tolerance, so a binary-gated spec pins it: if a future LibreOffice stops doing this, a test goes red instead of a capability silently lying.
:::

## The agent surface: `empty:<format>`

`media_id` has one reserved namespace: `empty:<format>`. Pass `empty:xlsx` instead of a real id and the forge mints a brand-new blank workbook, runs your statement against it, and returns the result as first-party `Media` named `untitled.xlsx` — creation and population in one round-trip:

```text
media_id: "empty:xlsx"
q: sheet update_cells updates='[{"address":"A1","value":"Q3 Totals"}]'
```

This is strictly additive input space: harness-minted ids are UUIDs, so every `empty:*` value was previously a guaranteed `MEDIA_NOT_FOUND`. The sentinel also works as a ref inside statements — `merge with=@empty:xlsx` mints the blank inline.

If a format isn't reachable, `Error (EMPTY_FORMAT_UNAVAILABLE)` names the creatable set and says not to retry. Two defaults worth knowing because the model is told them too: blank images are 1024×1024 white (resize in the same statement — `empty:png` + `image resize width=64`), and blank workbooks ship one `Sheet1` (every sheet verb expects at least one worksheet). When nothing is creatable, none of this is advertised — the description section, the schema hint, and the `MEDIA_NOT_FOUND` exemplar all disappear rather than promise what the deployment can't deliver.

Create-then-populate chains compose with everything else: `empty:json` + `data set path=config.retries value='3'`, `empty:txt` + `apply_patch patch='*** Begin Patch…'`, `empty:csv` + `append text="a,1"`.

::: info Field note: the sentinel taught by description alone
The `empty:<format>` sentinel was put on trial against a live 20B model during development. The prompt never said "empty:xlsx" — it said "create a brand-new spreadsheet" and relied on the tool description's "Creating new media" section to teach the mechanic. The model wrote `media_id: "empty:xlsx"` and populated cell A1 in one statement, on the first attempt. Same for `empty:json` + `data set` — the model composed a create-then-populate chain from the description alone. The `EMPTY_FORMAT_UNAVAILABLE` do-not-retry contract held too: asked to create a PNG in a deployment that couldn't, the model attempted `empty:png` at most once, read the failure, and fell back to creating a text file instead. The sentinel is additive input space, the description is the teacher, and the bet that a model can learn a namespace from a tool description paid.
:::

## Read next

* [Agent Tools](./forge) — the rest of the tool surface the sentinel lives in.
* [Text, Data & Patches](./text-ops) — the verbs that populate what you just created.
