# @pleach/base-tools (/docs/base-tools)



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`](/docs/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.

<SourceMeta pkg="{ name: &#x22;@pleach/base-tools&#x22;, href: &#x22;https://www.npmjs.com/package/@pleach/base-tools&#x22; }" source="{ label: &#x22;github.com/pleachhq/base-tools&#x22;, href: &#x22;https://github.com/pleachhq/base-tools&#x22; }" />

<Callout title="These tools now ship inside @pleach/core">
  The base tools have been hoisted into [`@pleach/core`](/docs/core&#x29; at
  the opt-in subpath &#x2A;*`@pleach/core/base-tools`**. If you already depend
  on `@pleach/core`, you can import them with zero extra install:

  ```typescript
  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.
</Callout>

## Install [#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:

```bash
npm install @pleach/base-tools
```

```bash
pnpm add @pleach/base-tools
```

```bash
bun add @pleach/base-tools
```

```typescript
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](https://www.npmjs.com/package/@pleach/base-tools) — check it
when you wire the tools in, since the bundle's surface evolves
faster than this page.

## The tool surface [#the-tool-surface]

### `math` [#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.

```json
{
  "name": "math",
  "args": { "mode": "rpn", "expr": "3 4 + 2 *" }
}
// → { "result": 14 }
```

```json
{
  "name": "math",
  "args": { "mode": "peek", "expr": "1.07 * 249.99" }
}
// → { "result": 267.4893, "committed": false }
```

### `datetime` [#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:

```json
{
  "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:

```json
{
  "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`):

```json
{
  "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` [#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.

```json
{ "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` [#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.

```json
{ "name": "unit_convert", "args": { "value": 5, "from": "km", "to": "mi" } }
// → { "value": 3.1068559611866697, "unit": "mi" }
```

```json
{ "name": "unit_convert", "args": { "value": 5, "from": "km", "to": "kg" } }
// → error: { "code": "UNIT_CATEGORY_MISMATCH", "from": "length", "to": "mass" }
```

### `text_search` [#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."

```json
{
  "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) [#url_fetch-opt-in]

A guarded HTTP GET. Off by default; the host opts in by registering
the policy plugin (see [Safety policies bundled](#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.

```json
{ "name": "url_fetch", "args": { "url": "https://example.com/article" } }
// → { "status": 200, "headers": { "content-type": "text/html" }, "body": "..." }
```

## Citation utility — `extractMarkdownLinks` [#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.

```typescript
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 [#safety-policies-bundled]

The package contributes safety policies via the standard
[`HarnessPlugin`](/docs/plugin-contract) 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.

```typescript
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](/docs/safety) for the policy contract the bundled entries
implement against.

{/* TODO: verified 2026-06-14 against packages/base-tools/dist/index.d.ts —
    none of toolExecutionLatencyObserver / toolFailureRateObserver /
    chunkSizeObserver are re-exported from @pleach/base-tools today.
    The package re-exports safety policies (BASE_TOOLS_SAFETY_POLICIES)
    and the citation extractor; stream observers must be imported from
    @pleach/core directly. Section preserved as TODO until either the
    re-exports land or the claim is replaced with an accurate inventory.

  ## Observer re-exports

  The package re-exports a handful of stream observers from
  `@pleach/core` so a host doesn't need to import from both places to
  wire common telemetry. The current re-export set:

  - `toolExecutionLatencyObserver` — emits a `metrics` channel envelope
  on every `tool.completed` with the wall-clock duration.
  - `toolFailureRateObserver` — emits a `metrics` envelope on
  `tool.failed`.
  - `chunkSizeObserver` — chunk-length histogram for streamed text
  content.

  The observer contracts themselves live in
  [Stream events](/docs/stream-events); the re-exports are convenience,
  not new behavior.
  */}

## Registration pattern [#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:

```typescript
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` [#position-vs-pleachtools]

| 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 [#where-to-go-next]

<Cards>
  <Card title="Tools" href="/docs/tools" description="The `defineTool` primitive `@pleach/base-tools` builds on — Zod schemas, per-invocation context, batching." />

  <Card title="Recipes" href="/docs/recipes" description="End-to-end patterns that wire the bundled tools into common agent shapes." />

  <Card title="Reference apps" href="/docs/reference-apps" description="Runnable agents that import `@pleach/base-tools` alongside domain-specific tools." />

  <Card title="Plugin contract" href="/docs/plugin-contract" description="The `HarnessPlugin` surface this package ships its tools and safety policies through." />

  <Card title="Safety" href="/docs/safety" description="The safety-policy contract the bundled `url_fetch` guard implements against." />
</Cards>
