@pleach/base-tools
Domain-agnostic tool primitives — math, datetime, scratchpad, unit_convert, text_search, and opt-in url_fetch with a citation extractor.
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 —
arithmetic, time, a per-session scratchpad, unit conversion, in-text
search, and an opt-in HTTP fetcher with a citation extractor. 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.
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" }, ...] }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": "..." }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 { baseToolsPlugin } from "@pleach/base-tools";
const runtime = new SessionRuntime({
storage: myStorage,
plugins: [baseToolsPlugin({ urlFetch: { allowList: ["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.
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:
import { SessionRuntime } from "@pleach/core";
import { baseToolsPlugin } from "@pleach/base-tools";
const runtime = new SessionRuntime({
storage: myStorage,
plugins: [
baseToolsPlugin({
enable: ["math", "datetime", "scratchpad", "unit_convert", "text_search"],
// url_fetch stays off unless explicitly listed:
urlFetch: { allowList: ["docs.example.com"] },
}),
],
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 (math, datetime, scratchpad, unit_convert, text_search, opt-in url_fetch) 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.