MemoDocumentation
EN

Agent Mode

Agent Mode gives Memo the ability to use tools — it can read and edit files, run terminal commands, search the web, check your calendar, message on WhatsApp, and more. The agent operates within a sandboxed environment with a strict, danger-level-based permission system.


Agent Mode needs a model that actually supports tool/function calling to be any good. An active external provider (OpenAI, Claude, Gemini, etc.) is the reliable path. A running local llama.cpp model is also wrapped into the same tool-calling request — it isn't external-provider-only — but real tool-calling quality on local models varies a lot by which GGUF you loaded; many small/quantized models handle it poorly or not at all.

As of v3.3.3, an agent mode toggle sits directly in the Chat top bar next to the web-search toggle — no more leaving the Chat screen for a separate Agent tab just to turn tools on. Skill tools can also actually execute: a skill's SKILL.md can define a command: field, wired into the same agent tool pipeline and permission-prompt UI as run_command/file-edit tools, so a skill's tools get the same "ask before running" safety net with zero extra setup.

Built-in Tools

The agent ships with 22 tools (up from an original 8), each defined by a JSON Schema and registered with a danger level:

Tool Danger Function
read_file Safe Read a file's contents (max 1MB)
write_file Medium Create or overwrite a file, keeps a .bak backup
edit_file Medium Find-and-replace or line-range replace in an existing file, with a diff preview
insert_line Medium Insert content at a specific line number
delete_lines Medium Delete a range of lines
delete_file Dangerous Delete a file or directory (.git/ is protected)
list_directory Safe List a directory's contents (max 1000 entries)
get_file_info Safe File metadata — size, mode, modified time
search_files Safe Glob-pattern file search
run_command Dangerous Run a shell command (60s timeout, 23-pattern blacklist)
read_env Medium List environment variables (sensitive keys masked)
web_search Safe DuckDuckGo search — see below, this one recently changed
self_clone Dangerous Copy the whole project to another local directory
configure_provider Dangerous Add/update a provider from inside a chat, needs confirmation
get_calendar_events Safe Read real events from your calendar for a date range
create_routine Medium Create a scheduled routine from a plain-language description (v3.9.0)
list_routines Safe List existing routines (v3.9.0)
cancel_routine Medium Cancel a scheduled routine (v3.9.0)
whatsapp_send Medium Send a WhatsApp message
whatsapp_search Safe Search your WhatsApp message history
whatsapp_latest Safe List your most recent WhatsApp chats
whatsapp_messages Safe Get a specific chat's message history

web_search, and how it recently changed

The agent's web_search tool searches DuckDuckGo — the model is instructed, via the tool's own description, to reach for it only for current events, prices, or facts that may have changed since its training cutoff, not for greetings or things it already knows.

Until recently, the chat-level "web search" toggle (the globe icon next to the agent toggle) worked completely differently from this tool: turning it on made every single message — greeting or not — trigger a blind web search, whose results got stuffed into the prompt whether or not the message needed them. That's gone. The toggle now runs the exact same tool through a scoped, single-tool version of the agent pipeline: the model decides, per message, whether it actually needs to search — at zero extra cost when it decides not to — without exposing the rest of the agent's toolset (no file writes, no run_command) to a plain chat that never asked for full Agent Mode. A bare "how's it going" now triggers zero searches; "what's today's exchange rate" triggers exactly one, with a real, on-topic query instead of your entire raw message getting thrown at the search engine.

Tool Schema Example

{
  "name": "run_command",
  "description": "Executes a terminal command in a sandboxed environment",
  "parameters": {
    "type": "object",
    "properties": {
      "command": {
        "type": "string",
        "description": "The shell command to execute"
      },
      "cwd": {
        "type": "string",
        "description": "Working directory (defaults to the project root)"
      }
    },
    "required": ["command"]
  }
}

Permission System

Every tool has a danger level — safe, medium, or dangerous — and that decides whether the user is ever asked at all. safe tools always auto-run, no prompt, no exceptions. medium/dangerous tools check for an existing session/permanent decision first, and only fall through to an actual prompt if neither exists. When a prompt does fire, the user picks one of six policies:

Policy Behavior Persistence
AllowOnce Allow this one call Cleared right after it runs
AllowSession Allow for the rest of this session In memory, lost on restart
AllowForever Allow permanently Saved to data/permissions.json
DenyOnce Deny this one call Cleared right after
DenyForever Deny permanently Saved to data/permissions.json
(no response) Auto-denied after a 60s timeout

Command Blacklist

23 patterns are blacklisted from run_command regardless of any permission policy — an AllowForever on run_command in general doesn't reopen these:

Category Examples
Destructive rm -rf /, rm -rf ~, rm -rf .
Disk operations dd, mkfs, format, fdisk, parted
Permission changes chmod 777, chown on system paths
Privilege escalation sudo, su, pkexec
Fork bombs :(){ :|:& };: and variants
Reverse shells nc -e, bash -i, mkfifo
System control shutdown, reboot, halt, poweroff

Security Sandbox

File-touching tools validate every path: symlinks are resolved (so a symlink can't point an edit outside the sandbox), the resolved path must stay within the project root, .. traversal is rejected, and a fixed list of system directories (/etc/, /usr/, /boot/, etc.) is off-limits outright.

Rate Limiting

Tool calls are capped at 30 per minute across all tools combined, and the exact same call (same tool, same arguments) can't repeat more than once every 5 seconds — a cooldown, not a hard block, so a legitimately repeated action still goes through once the window passes.

Pipeline Execution

The agent runs an iterative loop: build the message list, attach every registered tool's schema, call the model (non-streaming, temperature 0.2), then either execute whatever tool calls came back and loop again with the results appended, or — if the model returned plain content instead — that's the final answer, done.

User Message
    │
    ▼
[Iteration 1]  Model sees the tools, decides what (if anything) to call
    │
    ├─▶ Tool executes (permission-checked, sandboxed, timed) ──▶ Result
    │
    ▼
[Iteration 2]  Model receives the tool result(s), decides the next step
    │
    ├─▶ Another tool call, or...
    │
    ▼
   ...
    │
    ▼
[Final]  Model returns plain content — no more tool calls — done

Pipeline Limits

Parameter Value
Max iterations 40
Per-tool timeout 120s
run_command timeout 60s
Rate limit 30 calls/min, 5s cooldown per identical call
Context budget Sized per-turn from the conversation + a 32K headroom for tool iterations, never below 64K

The pipeline stops when the model returns a final response with no tool calls, the iteration cap is hit, a permission wait times out (60s, auto-denied), or the request is cancelled.

Audit Trail

Every tool call is logged — timestamp, tool name, arguments (sensitive ones masked), result summary, and the permission decision. The last 1000 entries live in memory; every entry is also appended as a JSON line to agent-audit.jsonl on disk, so the trail survives a restart, not just the in-memory buffer.

┌─ Agent Run ──────────────────────────────────────────┐
│ Model: claude-3-5-sonnet                              │
│ Iterations: 3                                          │
│                                                          │
│ [1] web_search("sqlite-vec go example")                │
│     → 4 results (0.9s)                                  │
│ [2] read_file("internal/memory/store.go")               │
│     → 312 lines (0.02s)                                 │
│ [3] run_command("go test ./internal/memory/...")        │
│     → PASS (2.1s)                                        │
│                                                          │
│ Final response generated                                 │
└──────────────────────────────────────────────────────────┘

Provider Requirement

Agent mode relies on the active backend's function-calling (tool-use) support:

Provider Function-calling support
OpenAI Native
Anthropic Claude Native (tool_use blocks)
Google Gemini Native (function declarations)
Groq Via compatible hosted models
OpenRouter Depends on the routed model
xAI Grok Limited
OpenCode Zen / OpenCode Go Native
Ollama (configured as an external provider) Depends on the loaded model's chat template
Memo's own bundled local llama.cpp model Wrapped into the same request as everything above — depends entirely on the loaded GGUF's own tool-calling support, which varies widely by model


A v3.3.3 fix removed an internal bookkeeping field that was accidentally leaking into tool definitions sent to the provider's API — this had caused agent mode to fail outright with a cryptic 400 invalid_request_error against some external providers, even though plain chat worked fine on the same provider. Error messages shown in chat now surface just the human-readable message instead of the provider's full raw JSON error response.


A v3.3.4 fix budgets tool definitions against the model's context window — previously they weren't counted at all, so even a one-word message could fail on a small-context local model with a confusing "request exceeds context size" error. Starting a local model whose chat template doesn't support tool calling now shows a clear warning up front instead of failing unexplained mid-conversation.