Built-in Tools
25 tools always register, plus pkg_manage on Linux; find_note is dropped from the registry until at least one operator note exists, and four more appear only when their prerequisite exists (generate_audio, generate_image, list_chatbot, send_to_chatbot) — 31 built-in tools in total. Tools that cover several related actions take a mode argument rather than splitting into separate names — edit_file replaces the former write_file / patch_file / remove_file / restore_file, find_files replaces list_files / glob_files / search_files, and so on.
Fifteen tools ship with full schemas — ask_user, calculate, chat_history, edit_file, fetch_page, find_files, find_note, find_tools, read_files, reasoning_guide, run_command, run_skill, search_web, write_report, write_todo. Everything else arrives as a name and a description; its parameters load on first use through find_tools(mode=search), keeping the initial tool payload well under the full registry.
Files
| Tool | Description |
|---|---|
find_files |
Locate files (queries: [{dir, pattern, file_pattern, recursive}]). mode=list returns a directory's entries, mode=glob matches paths against a filename pattern, mode=search greps file contents by RE2 regex. Mode is inferred when omitted: pattern + file_pattern → search, pattern alone → glob, neither → list. Glob patterns must carry a literal — all-wildcard (**/*) is rejected. Matches merge and deduplicate across queries. The result is capped at 100 KiB total and 100 matches per file; when it truncates, the tail says how many files were dropped and how many were cut short, so the model narrows the query instead of re-running it unchanged |
read_files |
Batched read of one or more files (files: [{path, offset, limit}]); text, PDF, DOCX, PPTX, CSV/TSV, image, or audio/video — a media file comes back as a verbatim transcript through the configured speech-to-text model, so there is no separate transcription tool. Reads the whole file by default (1 MB cap); offset/limit page through larger files — page for PDF, slide for PPTX, row for CSV. Text lines arrive as "<row>\t<line>"; the number is added by the reader and is not in the file, so it must be stripped before a line is reused as an edit_file anchor. Must be called before edit_file(mode=patch) unless the file was already read this session. Sensitive file guard: SSH keys, .pem, .key, .env always require confirmation regardless of sudo or allowlist |
edit_file |
Every change to a file on disk. mode=write creates a first version or deliberately replaces one wholesale; mode=patch edits regions via a targets array — each target is {old_string, new_string} plus optional replace_all, and every anchor is located against the original file content before anything is applied. Two targets covering the same region, or a target whose old_string occurs inside an earlier target's new_string, reject the whole call with nothing written. An empty new_string deletes; an insert is expressed by repeating old_string at the start of new_string. The line-anchored row / insert_string form was removed — row numbers counted the earlier targets in the same call, which are not yet on disk; mode=remove moves the file aside, still restorable; mode=restore puts a recorded version back by version id, or undoes a whole task via task_id (current = the task running now). Mode is inferred from content → write and targets → patch; remove and restore are never inferred. A blank path on write lands in ~/Downloads |
file_history |
Recorded versions of every file the tools changed — when each changed, what the task was after, and what the content was. mode=list returns versions newest-first, filterable by path, task_id, from/to local time, and limit (capped at 24); mode=read diffs the newest recorded version of each path against what is on disk now. This is the snapshot layer edit_file(mode=restore) restores from |
open_file |
Open a file with the OS default application (play a video, view an image, open a PDF viewer). Replaces run_command open/xdg-open, which the sandbox cannot reach. 10 s cap |
Tools & skills
| Tool | Description |
|---|---|
find_tools |
The tool registry. mode=search matches by keywords (all must match) or select:<name>,<name> for exact activation, and injects the matching schemas; mode=list returns names plus one-line descriptions with no schema injection — mcp=true narrows to MCP-exposed tools, and a system flag also lists the internal bookkeeping tools. Mode is inferred: query → search, otherwise list. A capability that looks missing is checked here before anything is built |
edit_tool |
The tool definitions themselves. mode=write creates or overwrites, mode=patch fixes an exact string after test_tool failed, mode=remove trashes the directory (recoverable from .Trash). tag picks the file: json = tool.json (schema), script = script.py (runtime), api = <name>.json (API tool). Names are snake_case with no script_ prefix — the runtime adds it |
test_tool |
Runs a script tool's script.py in the sandbox with JSON on stdin and returns what it printed. The verification step after every edit_tool write or patch; on failure the loop is edit_tool(mode=patch) → test again |
run_skill |
Load a named skill's reference material into the current turn. The result is advisory — reference, not a script to execute line by line |
edit_skill |
The files under ~/.config/agenvoy/skills/. mode=write creates or rewrites a file at a relative path (my-skill/SKILL.md), mode=patch replaces an exact string, mode=remove trashes a whole skill directory by single-segment name (recoverable from .Trash) |
Web & network
| Tool | Concurrent | Description |
|---|---|---|
search_web |
✓ | Live web lookup returning DuckDuckGo results and Google News headlines together as {"web":[...],"news":[...]}. source picks all / web / news; time_range applies a lookback window to both (news caps at 7 d); edition sets the news locale (TW:zh-Hant, US:en); cdp=true forces a browser fetch (auto-enabled on HTTP 202); a refresh flag skips the result cache. 90 s cap |
fetch_page |
✓ | Fetch one web page as markdown, html or json (readability + 4xx/5xx skip cache via ToriiDB). Cookies from the persistent Chrome profile are sent by default so login-required sites work transparently; headless is attempted first and retried visibly on failure. save=true writes to a local file instead of returning the content, and a same-domain link option supports recursive document research. 90 s cap |
http_request |
✓ | Raw HTTP request (GET/POST/PUT/DELETE/PATCH), returns status + headers + body. content_type selects json / form / multipart; multipart takes {"fields":{...},"files":[{"name","path","content_type"}]} with absolute paths read as binary. Timeout up to 300 s. Built-in SSRF guard (DNS-resolved against loopback / private / link-local); bypass specific hosts via net_white_list. An endpoint you will call again belongs in an api_* tool built with edit_tool |
download_file |
✓ | Download a binary file to local disk (tar.gz, images, archives, installers). Absolute path used as-is, relative joined under ~/.config/agenvoy/download/, parent dir auto-created. Timeout up to 600 s. For JSON/HTML use http_request or fetch_page(save=true) |
Execution
| Tool | Description |
|---|---|
run_command |
Execute a binary with argv (argv-only schema, sandbox-wrapped via go-pkg/sandbox). A plain command with no shell metacharacter (|, &&, >, *, ~) is called directly, never wrapped; pipes, redirects and globbing need an explicit ['sh','-c','...'], whose script is parsed and validated command-by-command — every binary must be a bare command name, and anything on denied_command is rejected outright. ['cd','<path>'] is special-cased and mutates Executor.WorkDir after verifying the path; rm is routed to the trash rather than deleting. Commands on the read-only list (git status, ls, cat, pwd, ...) skip the confirm gate. To write outside $HOME the call carries write_paths, which are bound in only after a password-backed approval. sudo is rejected outright (bare or inside sh -c) with guidance to drop it and declare write_paths, which raises that sudo confirm instead |
pkg_manage |
Drive the Linux package manager (apt / dnf / yum / pacman / apk) outside the sandbox, so the root operations bwrap cannot grant still work. action is install / remove / update / upgrade / search / info; package is a bare name — no flags, no version pin, no second package — and is required for everything except update and upgrade. Registered on Linux only. run_command cannot substitute: sudo is powerless inside bwrap. Language runtimes (node / python) → run_command with mise, fnm or uv; language-level packages (pip / npm / cargo) → run_command |
Coordination
| Tool | Description |
|---|---|
subagents |
mode=invoke runs a subtask in its own session (in-process, no HTTP); mode=list returns the reusable named sessions and their roles. Written to stand alone — the leg sees none of the parent conversation. At most 3 legs run concurrently; a 4th queues while its own timeout runs, so wide fan-outs dispatch in batches of 3. Results carry a [subagent · <model> · session=<id> · usage: in=X out=Y cached=Z] prefix that rolls up into parent-session usage. self_id reuses an existing non-temp session verbatim — it replaced name / session_id in v0.35.0 and is now required for mode=list, which resolves one delegated name instead of dumping every session; model and reasoning apply only when the run lands in a temp session. Timeout MaxSubagentTimeoutMin (30 min). A temp leg's model is picked by the leg's job from S / A / B / C tiers (collect → C>B>A>S, transform → B>C>A>S, review and reason → A>S>B>C, code or high-precision → S>A>B>C); a pass-tier model only when the user names it. Full dispatch protocol and model tiers → reasoning_guide(topic=subagent_dispatch) |
schedules |
Scheduled runs bound to a scheduler skill. mode=list shows what this session has queued, mode=patch moves an entry to a new time, mode=remove cancels it and trashes its skill, mode=write is the internal binding step of the scheduler-skill-creator flow. target is task (one-shot), cron (5-field expression), or all (list only). Task time formats: +5m / +1h30m (relative), 15:04 (today), 2006-01-02 15:04 (local), or RFC3339. skill_name carries a generated hash suffix — hand-made values always fail |
write_todo |
Live task checklist the user watches in real time. The entire ordered list is resent on every call (state is replaced, not merged); exactly one step stays in_progress. While a plan runs the step set is fixed — only status advances. The checklist is stored per task hash, so concurrent tasks in one session keep separate lists. When the last step completes and no report-*.md was written in that task, the result tells the model to call write_report for a long-form deliverable |
ask_user |
Free-text / single-select / multi-select / masked-secret prompts; execution pauses and resumes in a new turn with full context. Each question carries an optional multi-line hint for supporting detail. Routes through the pending registry when a listener is active, else falls back to stdin (CLI) or non-interactive guidance. A credential is never asked for here → store_secret |
State & memory
| Tool | Description |
|---|---|
chat_history |
This session's own action log. mode=list returns recent runs one row each with their task_id; mode=read opens one or more runs in full — what each was asked for, every tool it called with what came back, and the reply it ended on (scope=reference keeps only the expensive payloads, scope=full keeps everything); mode=tool_list flattens the reusable calls across the last 16 runs so an already-paid-for fetch is found before it is re-run, and mode=tool returns one such call's raw output by task_id + name; mode=search searches past messages, match=semantic matching meaning in recent messages and match=keyword matching text across the full archive (SQLite FTS5, trigram tokenizer), bounded by time_range. The records live in the daemon's own store, so file tools never reach them |
error_history |
Tool failures kept across sessions. mode=search looks up past records by keyword — each carries the fix that worked; mode=read fetches one record by its 8-char hash (used when a tool returns no data: {hash}); mode=write records an error with tool, keywords, observed behavior, root cause, what was done, and an outcome. Only outcome=resolved is stored — failed and abandoned are accepted but discarded. Search caps at 16 records |
find_note |
The operator's own notes — house rules, conventions, background this workspace assumes. mode=search returns the names whose name or body matches the keywords, most-matched first (capped at 20); mode=read returns one whole note by name. Keywords are required — the former mode=list that dumped every name was removed in v0.35.0. Search returns names only; the content of whichever names look relevant is pulled with mode=read. Notes live in the SQLite note table (FTS5 trigram index, moved off ToriiDB in v0.35.3) and are authored from the web dashboard over the local HTTP API, never by the agent. Renamed from find_knowledge in v0.35.4 |
reasoning_guide |
Fetch the full rule set for one topic — tool_generate, tool_error, rag_web, market_analysis, targeted_read, ask_user, subagent_dispatch, write_todo, html_render, office (rules for creating or modifying .docx / .xlsx / .pptx so Office and iWork apps accept the file). Replaces the former per-topic guide tools |
write_report |
Added in v0.35.4. Saves one long-form deliverable as report-YYYYMMDD-HHMMSS.md in the output directory and returns the write receipt with its path — the research / analysis / comparison body the reply summarises instead of reprinting. The only parameter is content; the former path parameter was removed. The output directory is output_dir from config.json (set with /config), else ~/Downloads when it exists, else ~/.config/agenvoy/download/. Any other file, or a change to a file that already exists → edit_file |
Support
| Tool | Concurrent | Description |
|---|---|---|
calculate |
✓ | Batch expression evaluator (arithmetic, unit conversion, currency arithmetic). Supports sqrt, pow, variadic min/max, %, ^. Returns {expression: result}; a failed expression returns an error string instead of failing the whole call. It computes but never looks anything up — a rate or price comes from its own tool first and is passed in as a literal |
store_secret |
Captures a value via masked input and writes directly to keychain — the value never enters the LLM context, history, or logs. The schema does not accept a value parameter; the agent only sees name + description. Fires on an auth failure (missing key, 401, 403, expired token): take the key name from the error, call this, then re-invoke the tool that failed. Max 2 rounds per tool per turn |
Conditional registration
These register only when their prerequisite is present; the LLM never sees them otherwise.
| Tool | Concurrent | Description |
|---|---|---|
generate_audio |
Speaks text with the configured text-to-speech model and writes a .wav under the download directory; returns the saved path, not the audio data. voice picks a provider voice (alloy for OpenAI, Kore for Gemini), blank uses the provider default. 5 min cap. (excluded while no text-to-speech model is selected — set it with /model tts or POST /v1/model {tts}) |
|
generate_image |
Text to image, written to disk; also handles edits when a reference image is given. Returns the saved path, not the image data. 15 min cap. (excluded while the image generator is off — set it with /model or POST /v1/model {image}; the provider's image model is fixed inside go-llm-router, so the field names a provider endpoint, not a model) |
|
list_chatbot |
List authorized chats for the specified platform (platform=telegram or platform=discord). (gated on the telegram_enabled / discord_enabled flag plus keychain credential) |
|
send_to_chatbot |
Send a formatted message to an authorized chat by target_id. Telegram: HTML + transient client. Discord: markdown + transient client. Platform formatting rules live in the channel system prompts, not in a tool |
Dynamic tool groups
Beyond the built-in registry, four prefixes resolve at call time:
| Prefix | Source |
|---|---|
script_* |
Generated script tools under the script-tool directory — script.py executed in the sandbox |
api_* |
Generated API tools — a single JSON descriptor per endpoint |
ext_* |
Extension tools, API or script backed |
mcp__* |
Tools exposed by connected MCP servers |
RAG reaches the agent this way rather than as a built-in: KuraDB is registered as an ordinary MCP server, so its list_rag / search_rag tools arrive under the mcp__ prefix and disappear entirely when it is unregistered or disconnected.
Output markers (channel-specific behavior)
Output text from any tool or LLM response is post-processed for these markers:
| Marker | Behavior |
|---|---|
[SEND_FILE:<path>] |
Channel runtime auto-attaches the file (Telegram → photo/document split by ext, Discord → unified SendFiles batched 10/msg) |
[SEND_VOICE:<text>] was removed along with channel voice output — Telegram and Discord no longer synthesize replies. Speech synthesis is now the generate_audio tool, which writes a file rather than posting to a chat.
Marker regex + dedupe + os.Stat filtering lives in internal/utils/utils.go.
Scheduler runtime
scheduler-skill-creator is the high-level skill that creates a scheduler skill body and calls schedules(mode=write) to bind it. New recurring / one-shot requests should activate that skill, not call the low-level tool directly.
The daemon-side runtime watches ~/.config/agenvoy/{tasks,crons}.json with fsnotify and hot-reloads on Write / Create / Rename. Past-due tasks are auto-fired and removed on startup or reload; fire executes via runtime.SetRunner(app.RunSkill) → in-process subagent over the scheduler skill body.
The TUI /cron and /task commands were removed; use /schedule, which lists cron and one-shot entries together (enter fires one now, d deletes it, add / edit by asking the agent), plus /sched-<name> to manually trigger an existing scheduler skill body.