AGENTS.md
The app contract for coding agents. mixel new copies it into every app.
This folder is one app: Python actions that Mixel runs on the person's computer. Use only the API on this page. Don't read the SDK source.
Loop
- Edit
app.py. If what the code touches changes, updateapp.json›permissions. mixel checkuntil it reports no errors. Each error ends with afix:line; do that.--fixadds missing permissions and packages.mixel testrunstests/.mixel run ACTION --file song.wav --set NAME=VALUEruns one action on a real file.mixel devopens the app in Mixel and reloads it on save. Nevermixel publish, or change permissions, unless the person asks.
An action
import ox1audio as ox
@ox.action("Convert")
def convert(files: ox.Files("audio"), format: ox.Choice("wav", "flac", "mp3") = "wav"):
made = []
for i, path in enumerate(files): # each path is a pathlib.Path
ox.progress(i / len(files), path.name) # Stop takes effect here
out = ox.audio.convert(path, format) # a new file, in a temporary folder
made.append(ox.sound(out, title=path.stem))
return made # sounds are added to the Library- One decorated function per action. The parameters are the controls; Python defaults are their defaults. Mixel draws the form, checks the input, runs the action in the background, and shows progress and Stop.
- Controls:
ox.Files("audio" | "midi" | "any", multiple=True)giveslist[Path](onePathwithmultiple=False),ox.Folder()gives aPath,ox.Choice("a", "b")astr,ox.Text(placeholder=""),ox.Number(min, max, step),ox.Slider(min, max, step)(intif all three are ints),ox.Range(min, max, step)→(low, high),ox.Toggle. Number, Slider, Range takeunit="dB". Plainstr/int/float/boolwork too. Every control takeslabel=.ox.Folder() | None = Nonemakes one optional. Save beside an input withfolder or ox.beside(path)(Nonefor a read-only Open with file: copies go to the Library). - Return
None, astr(shown to the person), a JSON-abledictorlist(for a page), orox.sound(path, title),ox.midi(...),ox.file(...), or a list of those. Metadata (bpm=120, key="A minor") follows the title. ox.progress(fraction, label): fraction 0 to 1, orNonewhen unknown. Call it often, also from library callbacks. When the person presses Stop it raisesox.Cancelled(aBaseException, soexcept Exceptiondoesn't catch it): clean up inexcept ox.Cancelled:and re-raise. Everyox.sound/midi/filemade so far is kept in the Library.- A problem the person can fix:
raise ox.Problem("That file has no audio.", hint="Try a WAV or MP3."). Other exceptions show "The app ran into a problem"; details go to the log. printgoes to the log. Don't write a server, threads, orif __name__ == "__main__": Mixel runs the actions.
Testing
import unittest
import ox1audio as ox
import app
class ConvertTest(unittest.TestCase):
def test_converts_to_wav(self):
run = ox.test(app.convert, files=["tests/tone.wav"], format="wav")
self.assertEqual(run["items"][0]["title"], "tone") # what reached the Libraryox.test returns {"result", "items", "progress", "events"}: what a page gets, the Library items, each (fraction, label) and (name, body). stop_at=0.5 presses Stop halfway: it raises ox.Cancelled, whose .kept lists what was kept. ox.Problem is raised as is.
Helpers
| Need | Use |
|---|---|
| File info | ox.audio.probe(path) → .rate .channels .duration .codec |
| Another format | ox.audio.convert(path, "wav" / "aiff" / "flac" / "mp3" / "aac" / "ogg" / "opus", folder=None, bits=None) → Path (keeps depth, tags, cover, loops; .note if it lowered an over) |
| Samples (numpy) | ox.audio.read(path) → (samples, rate), float32 (frames, channels); ox.audio.write(samples, rate, "wav", folder=None, name="audio", bits=24) → Path. bits=32: float; 16 dithers. Over ±1 raises, with the fix. |
| A folder for new files | ox.temp() → an empty folder, removed when the app closes |
| The app's own files | ox.data.read / read_text / write / list / remove("presets/a.json") (needs data) |
app.json
{
"$schema": "https://0x1audio.com/schema/app/v1.json",
"name": "my-converter",
"displayName": "My Converter",
"summary": "One file to many formats at once.",
"version": "0.1.0",
"sdk": "^0.3",
"entry": "app.py",
"license": "Apache-2.0",
"permissions": {
"files": "Open the songs you choose.",
"library": "Save the converted copies to your Library."
},
"accent": "#20b4c4"
}- Permissions are binding:
files(Files and Folder controls),library(returning sounds),library-read(a page lists it),data(ox.data),gpu(torch, onnxruntime),networkas{"why": "…", "hosts": ["example.com"]}. Ask only for what the code uses. Each reason: one sentence (12–120 chars) saying why. - Optional:
icon(a square.svg/.pngin the app),window({"width", "height", "minWidth", "minHeight"}),tags(up to 5),license(SPDX),page(below). - Raise
versionbefore each release. Never changename.
Dependencies
Python packages go in pyproject.toml › [project].dependencies, wheels only; Mixel installs only those (mixel check --fix adds what the code imports, then runs uv sync; commit uv.lock). numpy and scipy are fine. The SDK already has ffmpeg; don't add ffmpeg wrappers, pydub, or requests. Never commit models, songs or files over 5 MB.
A page (only when the drawn form is not enough)
mixel new --page adds one: Vite + React, built to dist/index.html. It uses @0x1audio/design-system and the same actions:
import { ox } from "@ox1audio/sdk/page"
import { ActionForm } from "@0x1audio/design-system"
export const App = () => <ActionForm action="convert" /> // the form Mixel draws; build around it
// your own controls: await ox.run("convert", { files, format }, { onProgress: (f, label) => {}, onEvent: (name, body) => {}, signal })Page API: ox.run, ox.actions, ox.files.pick / fromDrop / read / reveal, ox.library.list / add / file / onChange, ox.data.*, ox.app.open. Errors are OxError with .code (cancelled after Stop, with .kept). ox.emit("partial", body) in Python reaches onEvent. Design-system components and tokens only, no hex colours. Mixel sets .dark on <html>: check both.
Words (what the person sees)
Say what it does for a musician: song, sound, file, folder, Library. Never: worker, manifest, SDK, ffmpeg. Sentence case. Buttons are verbs. No exclamation marks. Errors say what happened, then what to do.
Never
- Touch files other than the ones the person chose, files next to them,
ox.temp()andox.data. - Use
subprocess,os.system,eval/exec, raw sockets, or telemetry. - Edit
AGENTS.mdordist/by hand, or commit.venv/ornode_modules/.