Skip to content
5 min read · 909 words

Generating Media

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.

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:

  • 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.

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".

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.