pleach
Build

@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-tools
pnpm add @pleach/base-tools
bun add @pleach/base-tools
import {
  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" }

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 localhost or 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": "..." }

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

PackageWhat it shipsReach for it when
@pleach/toolsdefineTool primitive, tool-loader contracts, batching strategy hintsYou're writing your own tools
@pleach/base-toolsPre-built tools (math, datetime, scratchpad, unit_convert, text_search, opt-in url_fetch) plus the safety policies that guard themYou 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

On this page