Starter tools
The default tool set (from tools/lg_tools.py::get_all_tools):
- Four keyless general-purpose tools —
current_time,calculator,web_search,fetch_url— that work without any state. - Two HITL tools —
ask_human(a free-text question) andrequest_user_input(a structured multi-step form) — pause the turn (A2Ainput-required) for the operator and resume with their answer (lead-agent only). - Four notes tools —
notes_list,notes_read,notes_write,notes_revert— bridge the operator console's Notes panel tabs to the agent (one agent-global notebook), gated per-tab by the operator's Agent-read/write toggles; writes are versioned (undoable) and surface live in the panel. - Four memory tools —
memory_ingest,memory_recall,memory_list,memory_stats— bound to the bundledKnowledgeStore(sqlite + FTS5, see Configuration). Omitted when no store. - Three scheduler tools —
schedule_task,list_schedules,cancel_schedule— bound to the bundled scheduler backend (local sqlite or the Workstacean adapter, see Schedule future work). Omitted when no scheduler. - Four beads tools —
beads_create,beads_list,beads_update,beads_close— the agent's in-process planning board, bridged to the console Beads panel. Bound when a beads store is present (default inserver/agent_init.py). - One inbox tool —
check_inbox— bound to the durable inbound inbox (ADR 0003) when configured; pulls stimuli pushed toPOST /api/inbox. - GitHub read tools (
github_get_pr,github_get_issue,github_list_issues,github_get_commit_diff) — moved to the opt-ingithubplugin (plugins/github/); not in the default set. Enable withplugins: { enabled: [github] }. Over theghCLI (auth viaGITHUB_TOKEN/GH_TOKEN, else gh's ambient login); each needs an explicitrepo. - Peer-consult tools (
peer_list/peer_consult) — added only when at least onePEER_<HANDLE>_URLis set (A2A federation). Deprecated — preferdelegate_to(below). - Delegation tools (plugins) — enable the delegates plugin for
delegate_to(target, query), which routes a sub-task to another agent or endpoint over a2a / openai / acp, managed + hot-swappable from the console (ADR 0025; supersedespeer_consult). The coding_agent plugin addscode_with(agent, task)to drive a CLI coding agent over ACP (ADR 0024; itself now superseded bydelegate_towith anacpdelegate). See Delegates + Spawn CLI coding agents.
get_all_tools(knowledge_store=None, scheduler=None, inbox_store=None, beads_store=None) is the registry; the conditional groups above are included only when their backend is passed (all are constructed by default in server/agent_init.py; opt out via middleware.knowledge: false / middleware.scheduler: false). To drop a core tool without editing this function, list it in tools.disabled; to add tools, ship a plugin (register_tools) — editing get_all_tools is the legacy core-edit path that conflicts on re-sync.
current_time
@tool
async def current_time(timezone: str = "UTC") -> strReturns the current wall-clock time in the given IANA timezone (e.g. "UTC", "America/New_York", "Asia/Tokyo"). Defaults to UTC.
Output:
2026-04-17T13:23:42.644606-04:00 (America/New_York)
Human: Friday, April 17 2026, 13:23:42 EDTUnknown timezones return "Error: unknown timezone 'Not/A_Zone'. ..." — never raises.
calculator
@tool
async def calculator(expression: str) -> strSafely evaluates a numeric expression using AST parsing. Does not call eval().
Supported:
| Op | Example |
|---|---|
+ - * / | 1 + 2 * 3 |
// floor div | 10 // 3 |
% mod | 10 % 3 |
** power | 2 ** 10 |
Unary - | -5 + 3 |
| Parens | (1 + 2) * 3 |
Rejected (returns error string):
- Names (
__import__, any identifier) - Function calls (
abs(-5)) - Attribute access (
(1).__class__) - Anything that's not pure arithmetic
Output on success: "2 ** 10 = 1024". Division by zero returns "Error: division by zero".
web_search
@tool
async def web_search(query: str, max_results: int = 5) -> strDuckDuckGo text search via the ddgs package. No API key. max_results is clamped to 1–10.
Output:
3 result(s) for 'LangGraph tutorial':
1. LangGraph Introduction — https://langchain.com/langgraph
LangGraph is a framework for building...
2. ...Failures (network, rate-limit, import error) return "Error: ..." strings. The LLM reads the error and retries or degrades gracefully.
fetch_url
@tool
async def fetch_url(url: str, max_chars: int = 8000) -> strFetches a URL and returns cleaned plain-text content.
Guarantees:
- URL scheme must be
http://orhttps://.file://,javascript:,ftp://, etc. are rejected. - Response body is capped at 2MB before parsing (blast-radius cap).
- Text output is truncated at
max_charswith…[truncated]marker. - HTML pages: scripts, styles, nav, footer, noscript are stripped. Prefers
<main>/<article>over the full body. - Non-HTML content (JSON, plain text, CSV) is decoded and returned as-is.
User-Agent is protoAgent/0.1 (+https://github.com/protoLabsAI/protoAgent). Customize in the tool body if your fork hits rate-limited APIs that need something specific.
Output:
[200] https://example.com
Example Domain
This domain is for use in documentation examples...memory_ingest
@tool
async def memory_ingest(content: str, domain: str = "general", heading: str | None = None) -> strStores a chunk in the bundled KnowledgeStore. Use for things the operator wants you to remember across sessions — preferences, environment facts, decisions worth recalling later.
domain is a logical bucket ("preferences", "context", "general", …). heading is an optional short label that doubles as a stable de-dupe key.
Returns "Stored chunk 17 in 'preferences'." on success, an error string when the store is unavailable.
memory_recall
@tool
async def memory_recall(query: str, k: int = 5) -> strTop-k keyword search over the store via FTS5 (LIKE fallback). Returns one match per line:
[preferences] coffee: Operator's preferred coffee is a Gibraltar with oat milk.
[context] lab: Primary lab is Snickerdoodle in Spokane.Returns "No matches." when nothing scores above the keyword threshold.
memory_list
@tool
async def memory_list(domain: str | None = None, limit: int = 10) -> strMost-recent-first listing of stored chunks. Filter by domain when given. Useful for "what did I log today?" style queries.
memory_stats
@tool
async def memory_stats() -> strPer-domain chunk counts plus a total. Useful for sanity-checking that ingest landed.
daily_log
@tool
async def daily_log(content: str) -> strConvenience wrapper around memory_ingest that writes to domain='daily-log' with today's UTC date as the heading. Same-day entries cluster under the same heading for memory_list(domain='daily-log').
schedule_task
@tool
async def schedule_task(prompt: str, when: str, job_id: str | None = None) -> strPersist a future invocation. The agent receives prompt as a fresh turn when the schedule fires.
when is either a 5-field cron expression ("0 9 * * 1-5" = every weekday at 9am) or an ISO-8601 datetime ("2026-05-01T15:00:00" = once at 3pm UTC on May 1). Backends auto-detect.
job_id is optional — auto-generated as <agent_name>-<uuid> when omitted. You'll need it later for cancel_schedule.
Output: "Scheduled job <id> next at <iso>." on success. Returns "Error: ..." on malformed when or backend failure.
Prompts are self-contained — the agent has no memory of the scheduling moment when the task fires, so write the prompt as a fresh turn ("review last week's pipeline incidents and post a summary"), not a reference ("do that thing we discussed").
list_schedules
@tool
async def list_schedules() -> strList the current scheduled jobs for this agent. Multi-agent isolation: each agent only sees jobs it created.
Output: one job per line with id, next-fire timestamp, schedule, and prompt preview. Returns "No scheduled jobs." when empty.
The Workstacean adapter intentionally returns [] (Workstacean owns scheduling state and its list action publishes asynchronously to a topic). Run the local backend or query Workstacean directly for live introspection there.
cancel_schedule
@tool
async def cancel_schedule(job_id: str) -> strCancel a scheduled job by id. Returns "Canceled <id>." or "Error: no such job <id>.".
Cross-agent cancellation is blocked — gina-personal cannot cancel gina-work's jobs even when sharing a sqlite path or a Workstacean install.
ask_human
@tool
def ask_human(question: str) -> strPause the turn and ask the human operator a question, then continue with their answer (human-in-the-loop, ADR 0003). Issues a LangGraph interrupt(), which checkpoints the graph at the call site; A2A callers see the task transition to input-required carrying the question, and resume it by sending a follow-up message with the same taskId. Lead-agent only — it's excluded from subagent allowlists, since the interrupt is resumed by the lead turn's runner. Use it only for a decision/input you genuinely must wait on (an approval, a missing fact), never for narration.
check_inbox
@tool
async def check_inbox(priority_floor: str = "next", limit: int = 10) -> strPull pending inbound messages (ADR 0003) — webhooks, external systems, and sister agents that posted to POST /api/inbox — and mark them delivered. Bound only when an InboxStore is configured. priority_floor selects the tiers: now (now only), next (now + next, default), or later (everything pending). now-priority items have already fired an Activity turn; next/later wait for this call so the agent decides when to surface them. Returns the items one per line, or "Inbox empty.".
notes_list / notes_read / notes_write
Read and write the operator console's Notes panel tabs — the human-curated notes the operator keeps in the UI, distinct from the agent's private memory_* store. Each tab carries agentRead / agentWrite permission toggles in the panel; these tools honor them per tab:
@tool
async def notes_list() -> str # tabs + read/write flags
async def notes_read(tab: str = "") -> str # agent-readable tabs only
async def notes_write(tab: str, content: str, mode: str = "append") -> str
async def notes_revert(tab: str, steps: int = 1) -> str # undo recent writesnotes_readreturns only tabs with Agent read on (a named tab with it off reports "not shared", never the content); blanktabreturns every readable tab.notes_writeonly writes tabs with Agent write on, to an existing tab (appendorreplace), updates the tab's word/char counts, and snapshots the prior content (last 10 versions) so a write can be undone.notes_revertrolls a tab backstepsversions from that history (the Notes panel also has an Undo button that does the same client-side).- Writes land live in the Notes panel (it polls + adopts newer versions without clobbering unsaved edits).
- Notes are agent-global: one persistent, instance-scoped notebook the agent and the console's Notes panel share (not per-project). It lives at
$NOTES_PATH(default/sandbox/notes/workspace.json, falling back to~/.protoagent/notes/workspace.json) — the same shape as the beads store.
These bridge the Notes panel's permission toggles to the agent — without them, "what's on my Todo tab?" never reaches the notes (the agent would only see its memory_* store).
discord_send / discord_read / discord_react
Provided by the first-party discord plugin (plugins/discord/, ADR 0018/0019), not by get_all_tools — the outbound (REST) half of the Discord surface (ADR 0015). Raw Discord REST v10 over httpx (no discord.py). The plugin registers them only when a token is set (discord.bot_token in Settings or DISCORD_BOT_TOKEN); disabling the plugin (plugins.disabled: [discord]) removes the surface and these tools. A direct call without a token degrades to a readable error.
@tool
async def discord_send(channel_id: str, content: str) -> str # markdown; auto-splits at 2000 chars
async def discord_read(channel_id: str, limit: int = 20) -> str # newest first, 1–100
async def discord_react(channel_id: str, message_id: str, emoji: str) -> strchannel_idis required per call — there's no default-channel env var; the persona/operator names the channel.discord_sendchunks long messages at line boundaries;discord_readclampslimitto Discord's 1–100; rate limits (429) are surfaced with theretry_after.- This is the stateless request/response half. The persistent inbound gateway (DMs + @-mentions, burst debounce, reactions, threads, return-address capture) is a separate native surface — see ADR 0015.
- Set up the bot token + Message Content Intent before use (Developer Portal).
Adding your own
For tools that shell out, build on tools/shell.py::run_command (async; handles timeout/kill, missing-binary → structured error, env merge, stdin/cwd) or tools/gh_cli.py for gh specifically — don't hand-roll subprocess.
Follow the same pattern:
from langchain_core.tools import tool
@tool
async def my_tool(required_arg: str, optional_arg: int = 5) -> str:
"""First line becomes the LLM's summary of the tool.
Args:
required_arg: What this argument is. LLM reads these docstrings.
optional_arg: Optional with a sensible default.
"""
try:
result = await do_the_thing(required_arg, optional_arg)
except Exception as e:
return f"Error: {e}"
return f"Success: {result}"Then append it to the keyless tool list in get_all_tools() — keep the two conditional extensions below it so the bundled memory + scheduler tools still ship when their backends are configured:
# Illustrative — the real signature is
# get_all_tools(knowledge_store=None, scheduler=None, inbox_store=None, beads_store=None)
def get_all_tools(knowledge_store=None, scheduler=None, **backends):
tools = [current_time, calculator, web_search, fetch_url, my_tool]
if knowledge_store is not None:
tools.extend(_build_memory_tools(knowledge_store))
if scheduler is not None:
tools.extend(_build_scheduler_tools(scheduler))
return toolsPrefer shipping new tools via a plugin (
register_tools) — editingget_all_toolsis the legacy core-edit path that conflicts on upstream re-sync.
See Write your first tool for the full walkthrough.
Related
- Configure subagents — tools are allowlisted per subagent
- Environment variables — SSRF allowlist vars affect
fetch_url; scheduler backend selection lives there too - Eval your fork — the eval harness exercises every tool listed here end-to-end
- Schedule future work — the firing model + multi-agent isolation story behind the scheduler tools