@pleach/base-tools
Domain-agnostic tool primitives — math, datetime, scratchpad, unit_convert, text_search, json_query, memory, and opt-in web_search, web_fetch, code_exec, ask_user, filesystem, and recall.
⚠️ Deprecated — folded into
@pleach/core. These tools now ship inside@pleach/coreat the opt-in subpath@pleach/core/base-tools(with the same per-tool subpaths). Import from there — zero extra install for@pleach/coreconsumers, identical surface. The standalone@pleach/base-toolspackage is a thin re-export shim, retained only for back-compat, and is no longer maintained. New projects should not add it. The tools themselves are not going away — only the extra package is.
Starter implements for the gardener — shears, watering can,
scratchpad, fetcher. @pleach/base-tools is the batteries-included
bundle of domain-agnostic tools almost every agent ends up needing.
Zero-config: arithmetic, time, a per-session scratchpad, unit
conversion, in-text search, JSON extraction, and cross-session memory.
Opt-in and injected: web search, web-page reading, code execution,
human-in-the-loop, a sandboxed filesystem, and event-log recall. It's a
separate SKU from @pleach/tools on purpose:
@pleach/tools ships the defineTool primitive and the tool-loader
contracts; this package ships ready-made tools that drop straight into a
SessionRuntime.
The tools split two ways. The zero-config set works the moment you register the plugin — it needs no keys, no network, no host runtime. The opt-in set follows one rule: refuse by default, inject the capability. A tool that reaches the network, runs code, or opens the filesystem stays off until the host hands it an allowlist, a sandbox runner, or an adapter. The package ships the contract and the guard; your trusted runtime supplies the teeth.
Hosts that hand-roll their toolbelt don't need this package. Hosts that
want a sane default surface — without writing seven small Zod schemas
to wrap Date.now() — install it.
These tools now ship inside @pleach/core
The base tools have been hoisted into @pleach/core at
the opt-in subpath @pleach/core/base-tools. If you already depend
on @pleach/core, you can import them with zero extra install:
import { baseToolsPlugin } from "@pleach/core/base-tools";The subpath is opt-in (it is not part of the top-level @pleach/core
barrel), so it stays tree-shakeable. The standalone @pleach/base-tools
package below remains published as a thin re-export shim over that
subpath — same symbols, two import paths.
Install
The tools ship with @pleach/core already (import from
@pleach/core/base-tools). Install the standalone shim only if you want
to depend on the SKU by name:
npm install @pleach/base-toolspnpm add @pleach/base-toolsbun add @pleach/base-toolsimport {
mathTool,
datetimeTool,
scratchpadTool,
unitConvertTool,
textSearchTool,
} from "@pleach/base-tools";The import names above are illustrative. The authoritative export shape lives in the package README on npm — check it when you wire the tools in, since the bundle's surface evolves faster than this page.
The tool surface
math
Arithmetic with a peek mode (read the result without committing to
the conversation) and a reverse-polish-notation evaluator. Use it
when the model needs deterministic numeric work mid-turn instead of
guessing at sums in prose.
{
"name": "math",
"args": { "mode": "rpn", "expr": "3 4 + 2 *" }
}
// → { "result": 14 }{
"name": "math",
"args": { "mode": "peek", "expr": "1.07 * 249.99" }
}
// → { "result": 267.4893, "committed": false }datetime
Clock and date arithmetic. Four operations — now, parse,
format, and diff — selected by the operation param. Every
output is a string or number, never a JS Date, so the wire stays
portable across the language-agnostic contract.
now returns the current UTC instant as an ISO-8601 string plus
epoch milliseconds:
{
"name": "datetime",
"args": { "operation": "now" }
}
// → { "iso": "2026-06-06T16:14:22.117Z", "epochMs": 1781108062117, "timezone": "UTC" }format renders an instant for a given IANA timezone. The
formatted field is a wall-clock string (YYYY-MM-DDTHH:mm:ss)
with no offset or Z suffix — it is local-to-the-zone, not a
round-trippable ISO-8601 instant. The iso field alongside it is
the original UTC instant if you need the portable form:
{
"name": "datetime",
"args": { "operation": "format", "input": "2026-06-06T16:14:22Z", "timezone": "America/Los_Angeles" }
}
// → { "iso": "2026-06-06T16:14:22.000Z", "timezone": "America/Los_Angeles", "formatted": "2026-06-06T09:14:22" }diff returns the signed difference between two instants in a
chosen unit (milliseconds, seconds, minutes, hours, or
days; default milliseconds):
{
"name": "datetime",
"args": { "operation": "diff", "input": "2026-06-06T00:00:00Z", "other": "2026-06-20T00:00:00Z", "unit": "days" }
}
// → { "from": "2026-06-06T00:00:00.000Z", "to": "2026-06-20T00:00:00.000Z", "unit": "days", "value": 14 }There is no shift operation — to add an interval, compute it
host-side (or with the math tool) and parse the result.
scratchpad
A per-session key/value store the model can read and write across the
tool calls of a session. Keyed by ToolContext.chatId, so entries
persist across turns within the same session (D-BASE1d). Use it for
intermediate workings the model would otherwise have to thread through
prose between tool calls. The operation param selects the action
(set / get / list / delete / clear); values are strings.
{ "name": "scratchpad", "args": { "operation": "set", "key": "note", "value": "remember this" } }
{ "name": "scratchpad", "args": { "operation": "get", "key": "note" } }
// → { "found": true, "value": "remember this" }The store is per-session by design — it does not survive session boundaries. A model that wants cross-session persistence should write to a real tool that hits storage, not lean on scratchpad and be surprised when state is gone in a new session.
unit_convert
SI and imperial unit conversions. The tool rejects category mismatches (length to mass, currency to temperature) with a typed error rather than silently returning garbage.
{ "name": "unit_convert", "args": { "value": 5, "from": "km", "to": "mi" } }
// → { "value": 3.1068559611866697, "unit": "mi" }{ "name": "unit_convert", "args": { "value": 5, "from": "km", "to": "kg" } }
// → error: { "code": "UNIT_CATEGORY_MISMATCH", "from": "length", "to": "mass" }text_search
Regex and substring search over a text body the caller supplies. No external corpus and no network — the tool is for "find the line that mentions X in this document I'm already holding."
{
"name": "text_search",
"args": {
"body": "...long document text...",
"pattern": "section 4\\.\\d+",
"mode": "regex"
}
}
// → { "matches": [{ "line": 142, "text": "section 4.2" }, ...] }json_query
Extract a value from a JSON document by path. The natural partner to
http_json — fetch an API response, then pull the one field the model
needs instead of feeding the whole document back into the prompt. Pure
and network-free. Supports dotted keys, array indices (negative counts
from the end), quoted keys, and a [*] wildcard that collects a field
from every array element. It is not full JSONPath — no filters, no
recursive descent — so the evaluator is single-pass and cannot ReDoS.
{ "name": "json_query", "args": { "json": { "data": { "items": [{ "id": 1 }, { "id": 2 }] } }, "path": "data.items[*].id" } }
// → { "value": [1, 2], "found": true, "path": "data.items[*].id" }found reports false when a path segment is missing — that is a
result, not an error.
memory
Durable, cross-session key/value memory. Where scratchpad forgets
everything at the session boundary, memory persists — a value written
in one session is readable in the next. Use it for stable facts: a
user's preferences, a running profile, a decision made earlier. Values
are scoped by namespace (pass a user or tenant id) so one caller's
memory is never visible to another. Operations: set / get / list
/ delete / search.
{ "name": "memory", "args": { "operation": "set", "namespace": "user-42", "key": "preferred_units", "value": "metric" } }
{ "name": "memory", "args": { "operation": "get", "namespace": "user-42", "key": "preferred_units" } }
// → { "found": true, "namespace": "user-42", "key": "preferred_units", "value": "metric" }The backend is pluggable. The default is an in-process store — durable
across sessions, gone on restart. Inject a MemoryStore (a five-method
async interface) to back it with a real database and get restart-durable
memory:
import { createMemoryTool } from "@pleach/base-tools";
const tool = createMemoryTool({ store: myDatabaseBackedStore });Writes can be approval-gated. A model that can silently persist a
"learned" fact is a model you cannot audit. Set
requireApprovalForWrites: true and every set / delete routes
through an injected approveWrite gate before it commits; reads stay
ungated. createGraduatingApproval implements the common flow — prompt
the human for the first few writes, then let the agent write on its own
once trust is established:
import { createMemoryTool, createGraduatingApproval } from "@pleach/base-tools";
const tool = createMemoryTool({
store: myDatabaseBackedStore,
requireApprovalForWrites: true,
approveWrite: createGraduatingApproval({
confirm: (req) => askTheHuman(req), // your approval UI
autonomousAfter: 3, // ask 3 times, then autonomous
}),
});A denied write does not count toward graduation, so the human stays in control until they have approved enough writes to trust the agent.
url_fetch (opt-in)
A guarded HTTP GET. Off by default; the host opts in by registering the policy plugin (see Safety policies bundled below). The fetcher enforces:
- No
localhostor loopback targets. - No private CIDR ranges (RFC 1918, link-local, etc.).
- A configurable allowlist for the domains the agent is permitted to reach.
Returns the body and response headers. Don't reach for it before the host has decided which domains the agent is allowed to touch — the defaults are restrictive precisely because "fetch any URL the model emits" is rarely what production wants.
{ "name": "url_fetch", "args": { "url": "https://example.com/article" } }
// → { "status": 200, "headers": { "content-type": "text/html" }, "body": "..." }web_fetch (opt-in)
Read a web page as clean text. url_fetch returns the raw body; for an
HTML page that is a wall of markup the model cannot cheaply read.
web_fetch GETs an allowlisted URL through the same SSRF-safe fetch
core and reduces HTML to readable text — dropping scripts, styles, and
tags — returning the page title plus the body text. Non-HTML bodies pass
through untouched. Gated on the same allowlist as url_fetch.
{ "name": "web_fetch", "args": { "url": "https://example.com/article" } }
// → { "httpStatus": 200, "url": "...", "title": "...", "text": "Readable body text...", "kind": "html", "truncated": false }web_search (opt-in)
Search the web for a query and return a ranked list of
{ title, url, snippet }. The other half of the search-then-read loop:
web_search finds the sources, web_fetch reads the best hit. The
package ships no baked-in API key — you inject a searchProvider
(Tavily, Brave, SerpAPI, a self-hosted SearXNG). Without a provider the
tool refuses every call. A no-key createDuckDuckGoProvider is bundled
for the quickstart; production hosts inject a real API provider.
import { createWebSearchTool } from "@pleach/base-tools";
const tool = createWebSearchTool({ searchProvider: myTavilyProvider });{ "name": "web_search", "args": { "query": "error budget SRE", "maxResults": 5 } }
// → { "query": "error budget SRE", "results": [{ "title": "...", "url": "...", "snippet": "..." }, ...] }http_json and http_head (opt-in)
Two convenience network tools over the same allowlisted fetch core.
http_json GETs a URL and parses the body as JSON (returning the parsed
value, or the raw text plus a parseError when it is not JSON).
http_head issues a real HEAD request and returns metadata only —
status, content type, length, etag, last-modified — never the body.
Both share the url_fetch allowlist and refuse without one.
recall (opt-in)
Query the session event log for what already happened — past tool
calls, their outcomes, interrupts. Use it before re-running an
expensive tool to check whether you already ran it and what it
returned. Provider-neutral by injection: you supply a reader over
whatever holds the log — a Supabase table on a server, a local JSONL or
SQLite file on a personal machine. Without a reader the tool refuses.
{ "name": "recall", "args": { "toolName": "web_search", "limit": 5 } }
// → { "events": [{ "type": "tool.completed", "toolName": "web_search", "at": 1781108062117 }, ...] }The package also ships summarizeToolOutcomes, a pure function that
folds recalled events into per-tool { started, completed, failed, lastError } counts. Feed the summary back as a prompt hint so the agent
stops repeating what already failed — turning the audit log into a
learning signal, not just a record.
Personal-agent tools (store-backed, opt-in)
These turn a stateless assistant into an agent that grows with the user
— it maintains an evolving identity, a model of the user, learned facts,
a searchable history, reminders, and a task plan across sessions. All are
opt-in, refuse-by-default, and user-scoped (namespaced by (orgId, userId)) so one user's self is never another's. Each needs an injected
BaseStore; without one the writes refuse and the reads return empty.
You rarely wire these by hand: createPleachAgent from
@pleach/core/quickstart turns the whole family on automatically when
its db carries a durable memory store, keyed to the local user. What
the agent writes is re-injected into its own system prompt on the next
turn — a real self-modification loop, not just a log.
Self-model — self_view, revise_identity, set_user_profile, pin_preference
The "SOUL.md + USER.md" pattern, made durable. self_view reads the
composed self (identity + user-model + pinned preferences). The three
writers evolve it: revise_identity amends the agent's own persona,
set_user_profile records what it knows about the user, and
pin_preference pins a behaviour rule that is re-injected every turn.
Because identity rides the cacheable prompt prefix, revisions take effect
going forward — the agent you talk to tomorrow is shaped by what it
learned today.
{ "name": "pin_preference", "args": { "preference": "Always include a runnable example." } }
{ "name": "set_user_profile", "args": { "content": "Prefers TypeScript, no semicolons.", "mode": "append" } }A bundled safety policy (self-model.guard-self-writes) keeps the agent
from ever editing away its own safety, ethics, or honesty — those are not
part of the editable self.
Meta-learning — recall_facts, correct_fact, forget_fact, prune_memory, detect_conflicts, reflect
The typed, confidence-scored learning substrate, exposed to the agent
itself. recall_facts reads what it has learned about the user (with
categories + confidence); correct_fact pins a corrected belief
(confidence 1.0, user-verified, decay-exempt); forget_fact drops one;
prune_memory clears stale/low-confidence facts; detect_conflicts
surfaces contradictions to reconcile; reflect reads the agent's own
success/error rate and improving-or-degrading trend.
{ "name": "recall_facts", "args": { "minConfidence": 0.7 } }
{ "name": "correct_fact", "args": { "fact_key": "pref-42", "new_content": "Prefers Python, not TypeScript." } }Knowledge & history — skill_search, db_lookup, session_search
skill_search discovers reusable skills by intent or free text (the
discovery half of skill_view / create_skill). db_lookup reads the
agent's own durable namespaces (facts, self, skills, memory), bounded to
its own data. session_search is full-text recall over past
conversation — "have we discussed this?" — relevance-ranked; back it with
a reader over a text index (the SQLite store's FTS5 searchEvents).
{ "name": "session_search", "args": { "query": "docking results egfr", "limit": 5 } }Scheduling — schedule, list_schedules, cancel_schedule (opt-in)
Record a future task or proactive check-in for the agent itself
(schedule, with an optional every recurrence like "1d"), review
pending ones (list_schedules), or drop one (cancel_schedule). The
package owns the durable intent; a host supplies the clock — read due
tasks with readScheduledTasks and re-enter the agent with each task's
prompt.
{ "name": "schedule", "args": { "when": "2026-08-05T09:00:00Z", "prompt": "Follow up on the EGFR run.", "every": "1d" } }Task tracker — todo_write, todo_read
A visible, status-tracked checklist for a multi-step job — the Claude
Code TodoWrite pattern. todo_write replaces the whole list (mark one
item in_progress, flip to completed as you go); todo_read resumes
it, even in a later session.
{ "name": "todo_write", "args": { "todos": [
{ "content": "Read the config", "status": "completed" },
{ "content": "Add the field", "status": "in_progress" },
{ "content": "Write a test", "status": "pending" }
] } }Capability-gated tools (opt-in)
Three tools that touch code execution, the human, or the filesystem.
The package ships the tool contract and a permission gate, never a
bundled runtime — running untrusted code or opening the filesystem is
the host's trusted runtime's job. Each refuses by default until the host
injects the capability. That keeps @pleach/core free of a large attack
surface it should not carry.
code_exec
Execute code in a sandbox and return its stdout / stderr. You inject a
runner — your own sandbox (@pleach/coding-agent, Vercel Sandbox, a
Firecracker microVM). An optional language allowlist and a per-call
timeout bound what it runs. Without a runner the tool refuses.
import { createCodeExecTool } from "@pleach/base-tools";
const tool = createCodeExecTool({ runner: mySandbox, allowedLanguages: ["python"] });ask_user
Pause and ask the human a clarifying question. @pleach/core has a
richer interrupt system; this is the portable surface any host can wire,
including one not using the interrupt machinery — a CLI reading stdin, a
bot posting a question. You inject an ask handler that bridges to
whatever asks the human. Without it the tool refuses.
fs_read, fs_write, fs_list
Read, write, and list files on a sandboxed filesystem. You inject a
FileSystemAdapter plus an allowedRoots allowlist; every path is
confined under an allowed root — with .. escapes and null bytes
rejected — before the adapter is ever called. The filesystem is
read-only by default: fs_write refuses until the host opts into
mode: "read-write", the filesystem analogue of url_fetch's
allowHttp.
import { createFileSystemTools } from "@pleach/base-tools";
const { fsReadTool, fsWriteTool, fsListTool } = createFileSystemTools({
adapter: myFsAdapter,
allowedRoots: ["/workspace"],
mode: "read-write",
});Citation utility — extractMarkdownLinks
Given a markdown body, returns the extracted-link envelope. Each link
carries the anchor text, the URL, the recognized shape (inline /
reference / autolink / bare), and the offset into the input
where the match started. Useful for "what did the model cite" auditing
on top of url_fetch output, or for inspecting markdown the model
itself produced.
import { extractMarkdownLinks } from "@pleach/base-tools";
import type {
MarkdownLink,
MarkdownLinkExtractionResult,
} from "@pleach/base-tools";
const result: MarkdownLinkExtractionResult = extractMarkdownLinks(body);
// → {
// links: [
// { text: "the spec", url: "https://example.com/spec", kind: "inline", index: 42 },
// { text: "issue #142", url: "https://github.com/.../142", kind: "inline", index: 118 },
// ],
// truncated: false,
// }truncated flips to true when the input exceeds MAX_LINKS
(default 1000) and the returned array was capped. Input larger than
MAX_INPUT_BYTES (default 1 MB) throws — chunk the input first if
you're processing larger bodies.
This is a utility, not a tool — call it from host code that's processing tool output, not from a tool the model invokes.
Safety policies bundled
The package contributes safety policies via the standard
HarnessPlugin contract. The headline policy
is the url_fetch private-network guard described above; the package
ships it as a contributeSafetyPolicies entry on its plugin export so
that registering the plugin opts the host into the guard.
import { SessionRuntime } from "@pleach/core";
import { createBaseToolsPlugin } from "@pleach/base-tools";
const runtime = new SessionRuntime({
storage: myStorage,
plugins: [
createBaseToolsPlugin({
include: ["math", "url_fetch"],
urlFetch: { allowedHostnames: ["example.com"] },
}),
],
userId: "user_123",
});The host stays in control: don't register the plugin and url_fetch
isn't on the surface at all. Register it with a tight allowlist and
the model only reaches the domains the host approved. See
Safety for the policy contract the bundled entries
implement against.
The four named policies
contributeSafetyPolicies() returns four SafetyContribution
constants, each also importable by name from @pleach/base-tools (and
@pleach/core/base-tools). Registration is not activation — each policy
is inert until the operator opts in via enabledSafetyPolicies: [...]
at SessionRuntime construction.
| Constant | Policy id | Kind |
|---|---|---|
NO_EVAL_POLICY | base-tools.no-eval | refusal — reject arbitrary-code-execution requests; base-tools ships no eval surface |
NO_NETWORK_BY_DEFAULT_POLICY | base-tools.no-network-by-default | advisory — declare that the base toolset performs no network I/O |
BOUNDED_RESOURCE_USAGE_POLICY | base-tools.bounded-resource-usage | advisory — surface the explicit caps each tool enforces |
INPUT_VALIDATION_POLICY | base-tools.input-validation | advisory — encourage shape validation before invocation |
The frozen BASE_TOOLS_SAFETY_POLICIES array holds all four in
registration order. A host that does provide eval or network
capability should not enable base-tools.no-eval /
base-tools.no-network-by-default — the prose would contradict its own
tool catalog.
Registration pattern
The tools register through the plugin's contributeTools hook, the
same way any HarnessPlugin adds to the tool registry. The package
likely exposes a one-call helper that returns the configured plugin —
check the package README for the exact name. The narrative shape is:
Use createBaseToolsPlugin(...) to configure the set; baseToolsPlugin
is the zero-config singleton for the quickstart. The zero-config default
includes the seven tools that need no injected capability (math,
datetime, scratchpad, unit_convert, text_search, json_query,
memory); every capability-gated tool is opt-in via include.
import { SessionRuntime } from "@pleach/core";
import { createBaseToolsPlugin } from "@pleach/base-tools";
const runtime = new SessionRuntime({
storage: myStorage,
plugins: [
createBaseToolsPlugin({
// The seven zero-config tools plus two opt-in ones:
include: [
"math", "datetime", "scratchpad", "unit_convert", "text_search",
"json_query", "memory", "web_fetch", "web_search",
],
// Network tools stay off until an allowlist is supplied:
urlFetch: { allowedHostnames: ["docs.example.com"] },
webSearch: { searchProvider: myTavilyProvider },
}),
],
userId: "user_123",
});
const session = await runtime.createSession({
tools: { enabled: ["math", "datetime", "scratchpad"] },
});The session's tools.enabled array selects which of the registered
tools that particular session can see — registration and exposure are
separate concerns, so the same runtime can serve a numeric agent and
a search agent without each one inheriting the other's surface.
Position vs @pleach/tools
| Package | What it ships | Reach for it when |
|---|---|---|
@pleach/tools | defineTool primitive, tool-loader contracts, batching strategy hints | You're writing your own tools |
@pleach/base-tools | Pre-built tools — zero-config (math, datetime, scratchpad, unit_convert, text_search, json_query, memory), opt-in network/agentic (web_search, web_fetch, http_json, http_head, url_fetch, code_exec, ask_user, fs_read/fs_write/fs_list, recall), and store-backed personal-agent tools (self-model, meta-learning, skill_search/db_lookup/session_search, scheduling, todo_write/todo_read) — plus the safety policies that guard them | You want a sane default toolbelt without re-implementing the common cases |
Both compose. A typical host installs @pleach/tools to write its
domain-specific tools and @pleach/base-tools to cover the generic
surface; the two plugins register side-by-side and contribute to the
same tool registry.
Where to go next
Tools
The `defineTool` primitive `@pleach/base-tools` builds on — Zod schemas, per-invocation context, batching.
Recipes
End-to-end patterns that wire the bundled tools into common agent shapes.
Reference apps
Runnable agents that import `@pleach/base-tools` alongside domain-specific tools.
Plugin contract
The `HarnessPlugin` surface this package ships its tools and safety policies through.
Safety
The safety-policy contract the bundled `url_fetch` guard implements against.