An agent is a small program in a folder that she can call. No rebuild, no plugin manager, no restart-and-pray: drop the folder in, restart, and the model can use it the next time you ask. Five ship with her already — including the ones that play your music and make her dance while it does.
When you ask for something she cannot do alone, she calls an agent. The whole contract is one line in and one line out — which is why an agent can be a Python script, a shell script or a compiled binary and the app does not care.
brain → agent RUN {json object} one line, validated before your code starts agent → brain INFO <text> # optional, repeatable. Logged, never shown to you agent → brain PET action dance # optional, at most one, before OK. Makes her act agent → brain OK <message> # terminal. Joins her reply agent → brain ERR <text> # terminal. Shown as the failure
No sockets, no daemon, no handshake. One process per call, working directory set to the agent's own folder, then it answers and exits. Anything it starts itself has to be detached — she will not wait for your background job. Parameters are checked before your code starts: a wrong type, a missing required one or an unknown name means your agent is never spawned at all, and she says so instead.
Five bundled agents.
On a package install they live in /opt/onidia/share/agents and are switched on
already — you never install anything to get these.
Fuzzy-matches your music folder and starts a detached player. She dances along. “play some jazz”
The same for video, fullscreen by default. “put on the marathon”
Pauses, resumes or stops whatever is playing, and answers “what's this?” The chat window's transport buttons call this same agent.
Drives her animation directly: expressions, overlay effects, named animations and movement. “do a happy dance”
The reference agent, and the one to copy when you write your own. Greets you by name.
This is the whole
play_song agent end to end. Nothing is hidden from the app — the lines below
are exactly what crosses the pipe.
Some audio files in a folder, and a player on PATH. It tries
mpv, cvlc, vlc, ffplay and takes the first
one it finds. The folder defaults to ~/Music; point it elsewhere with
music-dir.
music-dir = /mnt/pi/audio/music
Any of these reaches it. She reads the catalog entry — the description in the
manifest — and decides when to use it, so you never have to remember a command.
One line of JSON. query is the only parameter; leave it out and the agent picks a
track at random.
RUN {"query":"jazz"}
It walks your music folder, scores every filename against the query with a fuzzy match and takes the best one. Then it starts the player detached, so the agent can exit without taking the player with it, records the session so the transport buttons know which process to drive, asks her to dance, and answers.
INFO 812 tracks in /home/pi/Music INFO player: mpv --no-video --really-quiet "/home/pi/Music/Miles Davis - So What.flac" PET action dance OK Playing Miles Davis - So What
INFO goes to the log, PET makes her dance, and the
OK message is the part you actually read. That is the entire reply.
If nothing scores above the threshold it fails loudly rather than playing something random and pretending, and it hands her the closest filenames so she can offer you a choice.
ERR no match for 'kraftwerk' (closest: Kraftwerk - Autobahn, Kraftwerk - The Mix, Radio-Activity)
music folder ~/Music not found - set music-dir in chat-app.ini means she has no
folder to search. no media player found - install mpv, vlc or ffmpeg means the
files are there but nothing can play them. Both name the exact thing to fix.
Ask her in words — “pause”, “stop that”, “what's
playing?” — or use the transport strip in the chat window, which calls the very same
media_control agent.
play_movie
is play_song's twin, line for line. It searches ~/Videos (override with
video-dir) for .mp4 .mkv .webm .avi .mov .m4v .wmv .flv .mpg .mpeg, and it
has one extra parameter.
true. Set it to false for a
windowed player — “play X in a window” is enough to get there.RUN {"query":"the marathon"}
RUN {"query":"the marathon","fullscreen":"false"}
Both media agents write a small JSON session file recording the player they started, and
media_control reads it to decide which process to drive. That is why
“stop” works from her keyboard and stops the right thing, and why
“what's playing?” gets a real answer.
All four operations are modes of the binary you already have. There is no second tool to install.
What is installed, where it came from, and whether it is on. Disabled and broken agents are listed on purpose — a list built only from what discovery returns would make a disabled agent look uninstalled, which is the exact confusion this mode exists to end.
onidia-chat -agents-list
ID VERSION STATE LOCATION play_song 1.0.0 enabled ~/.config/chat-app/agents/play_song pet_control 1.0.0 disabled /opt/onidia/share/agents/pet_control hello_world 1.0.0 broken ~/.config/chat-app/agents/hello_world (exec not found)
From a folder, a zip, a URL, or a registry id. It always installs into
your directory and never the packaged one — /opt is
root-owned, and a surprise sudo prompt because you wanted a buddy behaviour would be
a bad afternoon.
onidia-chat -agents-install ~/agents/my_agent onidia-chat -agents-install ~/downloads/weather.zip onidia-chat -agents-install https://example.com/agents/tea.zip onidia-chat -agents-install some_registry_id
installed coffee -> /home/pi/.config/chat-app/agents/coffee restart onidia-chat to load it (onidia-chat -agents-list shows it now)
A bare id only goes through a registry if you configured
agents-registry. Anything containing a /, a ~ or ending in
.zip is treated as a path, so relative paths are safe.
Disabling keeps the files and just stops offering the agent. Deleting removes your copy. For the bundled agents, disable is the only option.
onidia-chat -agents-disabled play_song,play_movie onidia-chat -agents-remove coffee
removed coffee from /home/pi/.config/chat-app/agents restart onidia-chat to drop it from the catalog
A restart is always required. Discovery runs at startup only — there is
no hot reload, and both commands say so. The disable flag is per-run.
-agents-disabled on the command line applies to that one invocation and is not
remembered; to make it stick, put agents-disabled = play_song in
chat-app.ini.
They belong to the package, so -agents-remove refuses and tells you the
alternative. apt should be the only thing that removes those.
onidia-chat: pet_control belongs to the package (/opt/onidia/share/agents), not to you. Switch it off instead - it keeps its files: onidia-chat -agents-disabled pet_control
/opt.Exit codes follow the same convention as the rest of the app:
0 success, 2 failed — no $HOME, unreadable
registry, a bad agents-key, or a refused delete of a packaged agent.
A folder with an
agent.json at its root. That is the entire requirement — the folder name is
cosmetic, and any other files in there are yours to use.
{
"id": "tea",
"version": "1.0.0",
"description": "Announce that the kettle is on. Use when the user mentions tea, kettle, or a hot drink.",
"exec": ["./tea.sh"],
"timeout_ms": 5000,
"params": [
{ "name": "kind", "type": "string",
"enum": ["tea", "coffee"], "default": "tea",
"description": "which drink" }
]
}
It is the only thing she sees. Say what the agent does and when to use it — “Control the desktop buddy's animation… not for playing music” is what stops her reaching for the wrong one. Vague descriptions produce an agent she never calls.
Read one line, answer one line, exit. Params arrive as a JSON object on that line.
#!/bin/sh # one RUN line in, one OK line out, then exit. set -eu IFS= read -r line || true case "$line" in RUN *) payload="${line#RUN }" ;; *) echo "ERR expected a RUN line"; exit 1 ;; esac # pull "key":"value" out of the payload kind=$(printf '%s' "$payload" \ | sed -n 's/.*"kind":"\([^"]*\)".*/\1/p') : "${kind:=tea}" echo "PET action dance" # optional: make her act it out echo "OK The kettle is on - one $kind coming"
Remember chmod +x tea.sh. An agent whose exec cannot be resolved is
listed as broken rather than silently skipped.
^[a-z][a-z0-9_]{1,31}$. This is the name
in the internal [AGENT: id] tag, so keep it short and stable.exec[0] resolves relative to the agent
folder, as an absolute path, or as a bare name on PATH. The rest are static
arguments.-agents-list.agent-line-v1, or omit it to get that
default.agent tea: timed out after 15000.true skips it at discovery but keeps it on disk. Same
effect as listing the id in agents-disabled.name, type
(string, int, bool), required,
enum, default, description.onidia-chat -agents-install ~/.config/chat-app/agents/tea onidia-chat -agents-list # restart her, then just ask: # "put the kettle on"
Two rules worth knowing before you write anything clever. A run answers with
OK or ERR and then exits — if your work takes
longer than the reply, spawn it detached. And at most one PET line is allowed,
before OK; a second one is a protocol error, because a run must not be able to
quietly swap its command.
agents-dir, agents-off,
agents-disabled, agents-registry,
agents-require-sig, agents-key, plus music-dir and
video-dir for the media agents.CHAT_APP_MUSIC_DIR, CHAT_APP_VIDEO_DIR,
CHAT_APP_PLAYER and CHAT_APP_STATE_DIR. Read these instead of guessing
paths.Set agents-require-sig = true and an Ed25519 public key in
agents-key, and unsigned installs are refused — useful when the agents folder
is populated from somewhere you do not control. Registry installs can also be pinned by a
SHA-256 in the index.
The folder was found but something in it is wrong: agent.json does not parse,
the id is invalid, or exec does not resolve. The reason is printed next to the
location. One broken agent never stops the others from loading.
Almost always the description. If it does not say when to use the
agent, she has no reason to reach for it. Confirm it is in the catalog with
-agents-list and not listed as disabled.
You did not restart. Discovery is startup-only, so a running window is working from the agent list it built when it launched.
The exec target is not executable — chmod +x it. Note that a bare name in
exec is looked up on PATH, while ./name is resolved
against the agent's own folder: the dot is what makes it relative.
In order: is the folder right (music-dir), are there audio files in it, and is
mpv, vlc or ffplay installed? She says which of the
three it was, in the reply or the log.
A PET line only applies if she did not already pick an animation for that reply
— the model's choice wins. It is also dropped when buddy forwarding is off
(-pet-pipe off), and an unknown animation name is ignored on purpose.