Built-in Tool Reference

LibreFang ships with a wide built-in tool catalog beyond the file / shell / web tools that show up in standard agent types. This page covers the families that historically lived only in source code — browser automation, subprocess lifecycle, runtime scheduling, knowledge graphs, media, and inter-agent messaging.

Per-agent capability grants control which families an agent can actually call. Use agent.toml [capabilities] (or the librefang agent grant CLI) to opt agents into the tool families below.

Meta-Tools (Lazy Loading)

Two meta-tools let an agent discover and load tools on demand instead of carrying the full schema in every prompt.

ToolPurpose
tool_search(query)Returns tools matching query (name, description, tag). Cheap to call — no side effects.
tool_load(name)Pulls a tool's full schema into the active session. Subsequent turns can call it.

Lazy loading is opt-in per agent via lazy_tools = true in the manifest. Disable to restore the legacy "all tools always loaded" behaviour. This is what the Lazy Tool Loading mention on Agent Types refers to.


Inter-Agent Messaging (A2A)

Agents in different LibreFang instances can discover and message each other over the A2A protocol.

ToolArgumentsPurpose
a2a_discoverurlProbe a remote A2A endpoint, return the agent card (/.well-known/agent.json).
a2a_sendagent_id, message, optional task_idSend a task to a remote A2A agent and wait for the reply.

a2a_send is synchronous from the calling agent's perspective — it blocks until the remote turn completes (or the task TTL expires). For fire-and-forget delivery, use notify_owner or a webhook target instead.


Same-Daemon Inter-Agent Tools

For talking to other agents inside the same LibreFang daemon (no A2A protocol overhead) — spawn, list, find, kill, and send messages directly.

ToolArgumentsPurpose
agent_sendagent_id, message, optional asyncSend a message to another agent in this daemon. Non-blocking by default: returns a task_id and the reply is delivered to the caller's session on completion. Pass async: false to block and get the reply directly. Uses the in-process kernel router; respects the agent_call depth limit.
agent_spawntemplate, optional name, goalSpawn a child agent from a template. Returns the new agent_id. Sub-agents are denied agent_spawn and agent_kill by default to prevent fork bombs.
agent_list—List accessible agents (id, name, status).
agent_findquerySearch agents by name, ID, or tag.
agent_killagent_idTerminate a running agent. Forbidden in sub-agent context by default.
goal_updategoalUpdate this agent's goal/objective mid-turn. The new goal is written into the agent's manifest and surfaces in subsequent prompts.
agent_type_createname, optional description, system_prompt, provider, model, tools, skillsAuthor a new agent type — a reusable manifest stored on the daemon that agent_spawn, the dashboard and the TUI can all spawn from afterwards. Only those seven fields are settable here; everything else in a manifest starts at its default and is edited later. A name already used by an agent type or a live agent is rejected rather than overwritten.

agent_type_create writes through the same AgentTypeSpec validation and the same store as POST /api/templates, so a type an agent authors is indistinguishable from one an operator authored in the dashboard. "Agent type" is the term used by the tool, the dashboard and the TUI; the HTTP path is and stays /api/templates, with no second alias for the same resource.

max_agent_call_depth in [kernel] (default 5) caps how deep an agent_send chain can recurse — prevents A→B→A loops from ever reaching production. A nested workflow_run (below) whose step targets an agent that runs a workflow again is charged to this same budget, since each step nests a whole agent turn exactly as an agent_send hop does.


Memory Tools (per-agent)

There are two memory stores, with two separate tool families, and the names are close enough to be worth reading carefully.

memory_store / memory_recall / memory_list are a key/value store. memory_recall takes an exact key and does no matching of any kind — it is not a search.

ToolArgumentsPurpose
memory_storekey, valuePersist a value under an exact key. Retrievable only by passing that same key back.
memory_recallkeyLook up one value by its exact key. Returns "not found" unless the key matches character for character.
memory_listoptional limit, offsetEnumerate key names (not values) in the key/value store.

memory_semantic_* are the semantic store — the embedding-backed one the auto-recall machinery reads from before each turn. These are the tools that let an agent query it deliberately instead of only receiving whatever was injected for it.

ToolArgumentsPurpose
memory_semantic_searchquery, optional limit, min_confidenceSearch remembered fragments by meaning. Returns each fragment with the id needed to retract it. min_confidence drops fragments whose confidence has decayed, so a caller can ask for nothing rather than stale noise.
memory_semantic_addcontentDeliberately record a durable fact. The content goes through the memory extractor, which may distil it, merge it into an existing memory, or decline it as redundant — the result reports exactly what was stored, including "nothing".
memory_semantic_forgetmemory_idRetract one memory so it stops being recalled. This is the remedy for a stale memory that keeps contradicting what the agent can observe directly.
memory_semantic_stats—Counts per level and per category, plus whether auto-memorize, auto-recall and LLM extraction are on. Distinguishes "the store is empty" from "the store is switched off".

Only memory_semantic_search ships its schema on every turn; the other three are reachable through tool_search / tool_load.

The semantic tools need both a capabilities.tools entry that admits them (a bare memory_* glob does) and memory scopes that cover the agent's own memory: capabilities.memory_read for search / stats, capabilities.memory_write for add / forget. Undeclared scopes stay open; declaring scopes that cover only the key/value store (memory_read = ["kv:*"]) withholds the semantic tools while leaving the key/value ones intact. The same two scopes gate automatic memory as of #7605, with one difference: there a declared-but-empty list (memory_read = []) denies, where the tool gate treats it as undeclared. They are also withheld entirely when [proactive_memory] enabled = false, since the store is then never built.

See Memory System for the entry schema, scopes, decay formula, and consolidation trigger.


Task Queue Tools

The shared task board lets agents post work that other agents can claim. Useful for fan-out, oncall handoff, and long-running async jobs.

ToolArgumentsPurpose
task_posttitle, body, optional assignee, priorityCreate a new task in the shared queue.
task_claimoptional priority_filterClaim ownership of the highest-priority pending task. The TTL is [task_board] claim_ttl_secs (default 600s).
task_completetask_id, optional resultMark a claimed task done.
task_listoptional status, assigneeList tasks with optional filters.
task_statustask_idLook up one task by ID and return its status, result, title, assigned_to, created_at, completed_at. Native counterpart of the comms_task_status MCP bridge tool — agents that delegated work via task_post can poll the outcome without loading the bridge.

The claim TTL exists to recover from worker crashes — if the claimer doesn't task_complete within the TTL, the sweeper resets the task to pending so another worker can pick it up.


Channel Tools

ToolArgumentsPurpose
channel_sendchannel, target, content (text / image / file / poll)Send a message, image, file, or poll to a channel user — without going through the agent loop. Useful for pushing alerts, async results, or out-of-band notifications.
channel_dmuser_id, messageDeliver a message privately to one member of the group conversation the current turn arrived on, instead of posting where everyone can read it.
channel_membersoptional channel, chat_idList the known members of a group conversation — user_id, display_name, username and source for each. Read-only.

channel_send is what [deliver_only] webhook mode uses internally; agents can also call it directly. Capability: channel.*.

channel_members reads the roster the channel bridge builds as it observes group messages, and — on adapters configured to supply a platform member list — everyone that list names. Each member carries a source, and the reply carries observed_count and enumerated_count alongside it.

  • observed — this person has spoken in the conversation. channel_dm can reach them.
  • enumerated — the platform lists them as a member and they have never addressed the agent. They are reported here and channel_dm refuses them.

The split is a security boundary rather than a label. Bulk-filling the roster from a platform's member list would have widened channel_dm's authorization set from "people this agent has interacted with" to "everyone the workspace lists", letting an agent privately message someone who has never spoken to it. Speaking promotes an enumerated member to observed; a later enumeration sweep never demotes them back.

Only the Slack adapter enumerates today, and only with SLACK_ENUMERATE_MEMBERS = "true" — see Slack. Both arguments default to the conversation the current message arrived on, so during message handling it can be called with no arguments at all; a direct message has no roster. Naming a different conversation on the channel the turn arrived on is refused, for the same reason channel_send refuses a cross-chat dispatch — one group must not be able to enumerate another's membership. Use it to attribute a request to the person who made it, and to obtain the platform user_id an external system needs in order to notify them.

Reaching one person in a group

In a group conversation channel_send may only address the group itself — re-targeting it at an individual is refused by the cross-chat dispatch guard, which exists because an agent that can name any recipient can be talked into naming the wrong one. channel_dm is the narrow exception: it takes a user_id and a message and nothing else. The channel, the conversation and the bot account all come from the turn being handled, and the recipient must be someone the daemon has seen speak in that conversation — a channel_members entry whose source is observed. A member listed as enumerated is known to the platform but has never addressed the agent, and is refused. An agent therefore cannot address a platform id it has not met, and cannot use one conversation to reach into another.

Out of band — from a cron job, a trigger, or an API-driven run — there is no conversation and so no roster to authorize anyone; channel_dm is unavailable there by design, and channel_send with an explicit recipient is the tool for that case.

What "privately" means is not identical across platforms, and the tool does not pretend otherwise. Slack lets a bot open a DM with any workspace member, so a U… id always works. Telegram and Discord bots cannot start a conversation with someone who has never messaged them, and that send fails — it never quietly falls back to posting in the group, because a private notice that silently becomes public is worse than an error.

notify_owner is not an alternative to this: it produces an operator-facing notice delivered out of band on the operator's own surface, and no channel adapter routes it to a participant.


Workflow / Event Tools

ToolArgumentsPurpose
workflow_runworkflow_id, optional inputsExecute a stored workflow synchronously and return its result. Nesting is bounded by the same max_agent_call_depth quota as agent_send (above); a run entered too deep is refused rather than left to recurse.
event_publishevent_type, payloadPublish a structured event onto the event bus. Triggers ([[trigger]] blocks on agent manifests) listening on event_type will fire.
system_time—Get the daemon's current wall-clock time (RFC3339 UTC). Cheap; no external clock call.

event_publish is the bridge between agents and the trigger system — an agent that detects "a new file landed in /inbox" can publish inbox.new_file, and any trigger configured on that event runs.


Skill Evolution (extended)

In addition to skill_evolve_patch (documented above with its 5-strategy fuzzy matcher) and skill_read_file:

ToolArgumentsPurpose
skill_evolve_createname, manifest, optional bodyCreate a brand-new skill from scratch.
skill_evolve_updatename, bodyReplace a skill's body wholesale (no fuzzy match — just overwrite).
skill_evolve_deletenameDelete a skill entirely.
skill_evolve_rollbackname, optional versionRevert a skill to a prior version. Without version, rolls back one step.
skill_evolve_write_fileskill, path, bodyAdd a supporting file under references/ / templates/ / scripts/ / assets/.
skill_evolve_remove_fileskill, pathRemove a supporting file.

Every skill_evolve_* operation appends to the skill's evolution history; GET /api/skills/{name} returns the full audit trail.


Browser Automation

Eight tools wrap a Playwright-controlled headless browser. All of them require the agent to have the browser.* capability and a configured [browser] section in config.toml (see Core Configuration).

ToolPurpose
browser_navigate(url)Open or navigate the active tab.
browser_back()Navigate back one step in the tab's history stack.
browser_click(selector)Click the element matching the CSS / text selector.
browser_type(selector, text)Focus the element and type text (key-by-key, debounced).
browser_scroll(direction, amount?)Scroll up / down / to selector.
browser_wait(condition, timeout_ms?)Wait for selector / network-idle / time.
browser_screenshot(full_page?)Capture the viewport (or full page) as a ContentBlock::ImageFile.
browser_read_page()Extract the page's full text + structural outline (cheaper than a screenshot when the model just needs text).
browser_run_js(script)Run arbitrary JS in the page context, return the JSON-serialised result.
browser_close()Close the active tab and release the slot.

Pages opened by an agent are scoped to that agent's tab pool (default 4). When the pool is full, browser_navigate rejects with ToolError::ResourceExhausted rather than silently evicting another tab.


Process Lifecycle

Five tools manage long-running subprocesses (build watches, test loops, log tailers, sidecar daemons). They expose stdin / stdout / stderr as a stream the agent can poll.

ToolPurpose
process_start(command, cwd?, env?)Spawn a subprocess. Returns a process_id.
process_write(process_id, data)Write bytes to the subprocess's stdin.
process_list()List active processes for this agent (id, cmd, age, pid).
process_poll(process_id, max_bytes?)Read up to max_bytes of new output since the last poll. Non-blocking.
process_kill(process_id, signal?)Send a signal (default SIGTERM); the runtime escalates to SIGKILL after the grace period.

Subprocesses inherit the agent's workspace as cwd by default, the agent's environment minus other providers' secrets, and are stopped on agent shutdown. Total wall-clock budget is enforced via the [exec_policy] section.


Runtime Scheduling

Manifest [[cron]] entries are the static way to schedule turns. These tools let an agent dynamically create / list / pause / resume scheduled work from the loop itself.

ToolPurpose
schedule_create(when, prompt, channel?)Schedule a single future turn at a one-shot timestamp.
schedule_list()List the agent's scheduled turns.
schedule_delete(schedule_id)Pause (disable) a scheduled turn — its config is preserved, not deleted.
schedule_resume(schedule_id)Resume a paused scheduled turn.
cron_create(expression, prompt, channel?)Schedule a recurring turn (5-field cron).
cron_list()List the agent's runtime-created cron jobs.
cron_cancel(cron_id)Pause (disable) a runtime cron job — its config is preserved, not deleted.
cron_enable(cron_id)Re-enable a paused runtime cron job.

Agents cannot hard-delete a scheduled job: schedule_delete / cron_cancel only pause it, so a misbehaving or over-eager agent can never silently lose a configured schedule. Permanent deletion is a human-only operation via the dashboard (#6159). Runtime jobs created with these tools are persisted in the same store as manifest crons — they survive daemon restarts. Their cron_id / schedule_id is opaque; round-trip through cron_list() to discover it.


Knowledge Graph

The agent-private knowledge graph stores entities and typed relations between them. Use it for structured recall ("which projects use which library") that's awkward to express as flat memory entries.

ToolPurpose
knowledge_add_entity(name, type, attributes?)Insert or update an entity.
knowledge_add_relation(from, to, type, weight?)Add a directed relation between two entities.
knowledge_query(start, depth?, relation_filter?)BFS from start, return reachable nodes + edges.

The graph is per-agent and persisted in SQLite alongside the agent's session history. Use the proactive-memory tools for unstructured recall.


Media Generation & Analysis

Tool calls into provider-specific media APIs. Per-tool capability grants required.

ToolPurpose
image_generate(prompt, size?, style?)Generate an image (OpenAI / Replicate / Vertex).
image_analyze(image, question)Vision Q&A on an existing image.
media_describe(image_or_video)Caption / describe an existing media asset.
media_transcribe(audio_or_video, language?, prompt?, start_sec?, max_secs?, out_path?)Speech-to-text on an audio file, or a video container's audio track (extracted server-side). Rejects unrecognised file extensions. start_sec / max_secs transcribe one window and return has_more / next_start_sec to walk the rest; out_path writes the transcript to a workspace file and returns only path, size, sha256 and a preview. Long recordings need both — see below.
speech_to_text(audio_or_video, language?, prompt?)Same providers and video-container support as media_transcribe, and falls back to a permissive best-guess MIME type instead of rejecting an unrecognised extension. Has no windowing or out_path, so a recording long enough to need either belongs on media_transcribe.
text_to_speech(text, voice?)TTS synthesis.
music_generate(prompt, lyrics?)Music generation (Suno / Replicate).
video_generate(prompt, duration_secs?)Submit a video-generation task; returns task_id.
video_status(task_id)Poll a video task.
canvas_present(html)Render an interactive HTML canvas in the dashboard.

Cost and latency vary widely between providers — check [media] providers in config.toml to control which provider serves each tool.

Transcribing a long recording

A whole meeting returned in one tool result does not reach the agent intact: the kernel spills any result over [tool_results] spill_threshold_bytes (16 KB by default) to the artifact store and hands the agent a stub, and a single transcription request is bounded by a wall-clock timeout that does not scale with the length of the input. Both limits are reached long before any file-size limit is — a ten-minute recording is a couple of megabytes of extracted audio.

So transcribe in windows, and write to a file:

media_transcribe(path="meeting.mp4", max_secs=600, out_path="meeting.txt")
→ { "written_to": "...", "file_bytes": 16831, "has_more": true, "next_start_sec": 600.0, "window_preview": "..." }

media_transcribe(path="meeting.mp4", start_sec=600, max_secs=600, out_path="meeting.txt")
→ ... repeat while has_more is true

A window starting at 0 begins a new file; later windows append to it separated by a newline, so the calls assemble one transcript without the agent holding any of it. Advance by next_start_sec rather than by max_secs: a seek lands on a keyframe and the final window is short, so the produced length is the only edge that does not drift.

Do not guess start_sec either. A window starting more than about ten seconds past the end of the recording does not come back empty — ffmpeg gives up on the seek and returns the recording from the beginning, which then reads as though it were the part that was asked for. The margin is absolute rather than a share of the length, so on a long recording it is a small fraction of one: asking for minute fifty of a forty-five-minute meeting is enough.

Lower max_secs if a call times out. How much media fits in one request depends on provider speed, and a self-hosted endpoint can be an order of magnitude slower than a hosted one.

Lower it as well if a call is rejected for being over the post-extraction audio budget. Two size limits apply, and they measure different things: the input container is checked against the 50 MB video limit before anything is read, and the audio track extracted from it is checked against the 20 MB audio limit before it is sent to a provider. The second is the one a long recording reaches, because the extracted track is what actually leaves the process — the rejection names the measured size and the cap, so the window can be scaled down from those two numbers rather than guessed at.


Owner Notifications

ToolPurpose
notify_owner(reason, summary, urgency?)Send a private notice to the agent's owner without posting to chat.

notify_owner writes to the owner's inbox and optionally fans out to a configured side channel (Telegram, email). Distinct from a regular reply — the message stays out of the active chat history, so it won't pollute downstream summarisation. See ReplyEnvelope.owner_notice in the API reference for the wire shape.


Geolocation, Docker, Skills

ToolPurpose
location_get()Current location (IP-based by default; can be overridden per-agent).
docker_exec(container, command)Run a command inside a running Docker container the agent has been granted access to.
skill_read_file(skill, file)Read a skill's supporting file (references/, templates/, scripts/, assets/). Companion to skill_evolve_*.

docker_exec requires the agent to be in [capabilities] docker.* allowlist plus the container name in [exec_policy] docker_allowlist. Without both, calls reject with ToolError::Forbidden.


apply_patch Move Semantics

apply_patch accepts an UpdateFile { path, move_to, hunks } shape. When move_to is set, the patch first renames path to move_to, then applies the hunks against the new path. This collapses a rename + edit into a single tool call so the patch is atomic — readers never see the file at the old path post-rename.

Empty hunks with a non-null move_to is a pure rename. Useful when refactoring multiple files where one of them changes path but not contents.


skill_evolve_patch Fuzzy Matching

skill_evolve_patch modifies installed skills in-place. To survive small upstream drift, the matcher walks 5 strategies in order, first match wins:

  1. Exact — bytewise identical (Levenshtein 0).
  2. Line-trimmed — strip trailing whitespace and CRLFs from every line, then compare.
  3. Whitespace-normalised — collapse runs of intra-line whitespace; ignores tab vs. spaces drift.
  4. Indent-flexible — strip leading whitespace per line; ignores reformat-to-different-indent changes.
  5. Block-anchor — find a contiguous run of N anchor lines (default 3) in the target, splice the new content around them.

If all five fail, the patch is rejected with the exact diff context so the next turn can see what drifted. There's no automatic "best-effort" merge — fuzzy matching is bounded.

The companion tools skill_evolve_write_file and skill_evolve_remove_file manage skill supporting files (references/, templates/, scripts/, assets/) without going through patch semantics.


Where to Look in Code

  • Tool registry and schemas — crates/librefang-runtime/src/tool_runner/definitions.rs — builtin_tool_definitions() returns the name, description and JSON schema the LLM sees for every builtin.
  • Dispatch — crates/librefang-runtime/src/tool_runner/dispatch.rs — the arm that routes a tool name to its handler; the handlers themselves sit in per-domain siblings (agent.rs, workflow.rs, memory.rs, …).
  • Capability gating — crates/librefang-types/src/capability.rs plus the allowed_tools check in execute_tool — which agents can call which tools.
  • Browser pool — crates/librefang-runtime/src/browser.rs and browser_tools.rs.
  • Schedule / cron runtime tools — crates/librefang-runtime/src/tool_runner/schedule.rs and cron.rs.

When in doubt, the rustdoc on the tool struct is the canonical specification. The dashboard's "Tools" inspector also renders the live, agent-resolved tool set with current capability grants applied.