Skip to content

English · 简体中文

Translated to English for v0.3.2. Source-of-truth: the OCaml modules in lib/tools/.

Tool API Reference

This document describes the 23 built-in tools provided by the P-A-R SDK. New tools are registered through Runtime.register_tool; the tool description gets injected into the LLM's system prompt so the model knows when to call which tool.

Version: v0.7.1 Total tools: 23 (19 from v0.3.0 + bash in v0.3.1 + 3 memory tools in v0.7.1)

Overview

Each tool carries the following metadata at registration time:

type tool_descriptor = {
  name : string;                         (* tool name, referenced by the agent call *)
  description : string;                  (* description, injected into the LLM system prompt *)
  input_schema : Yojson.Safe.t;          (* JSON Schema *)
  output_schema : Yojson.Safe.t option;  (* optional JSON Schema for structured output *)
  permission : tool_permission;          (* Allow / Confirm / Deny / ... *)
  timeout : float option;                (* seconds; None means no timeout *)
  concurrency_limit : int option;        (* max concurrent invocations *)
  on_update : (string -> unit) option;   (* v0.3+ progress callback *)
  cache_control : cache_control option;  (* optional provider-side cache hint, e.g. Anthropic prompt caching *)
}

The tools fall into five categories by purpose:

Category Count Tools
math 1 calculator
utility 9 get_time / echo / generate_uuid / hash_text / generate_password / string_stats / json_format / convert_temperature / url_encode
web 3 fetch_url / read_webpage / web_search
fs (read) 4 read / ls / find / grep
fs (write) 2 write / edit
exec 1 bash (added in v0.3.1, the only one with a security policy)
memory 3 recall_memory / remember_memory / search_history (added in v0.7.1, require Memory_service)

All tools except bash use permission = Allow. The file-path tools (read / ls / find / grep / write / edit) reject absolute paths and any path containing :. The memory tools (recall_memory / remember_memory / search_history) require a configured Memory_service and read scope from Invoke_context.get_current_exn().session_id.

Quick Index

Tool Category Risk Timeout Notes
calculator math low 5s +/-/*// expression evaluation
get_time utility low 2s returns UTC time in ISO 8601
echo utility low 2s echoes back the input string
generate_uuid utility low 1s UUID v4
hash_text utility low 2s md5 / sha1 / sha256 (default sha256)
generate_password utility low 1s length 4-128, symbols optional
string_stats utility low 1s character / word / line count
json_format utility low 2s validate + pretty-print
convert_temperature utility low 1s C / F / K conversion
url_encode utility low 1s encode / decode
fetch_url web medium 15s HTTP GET, 10MB cap
read_webpage web medium 15s fetch + HTML parse, strip script/style
web_search web medium 15s DuckDuckGo lite
read fs (read) low 30s 10MB cap; binary returns base64
ls fs (read) low 10s directory listing, sorted by name
find fs (read) low 30s glob pattern, skips .git / _build etc.
grep fs (read) low 30s regex match, output path:line:text
write fs (write) medium 30s optional create_dirs for auto mkdir -p
edit fs (write) medium 30s batch replace; overlapping ranges are rejected
bash exec high 60s added in v0.3.1, 9-layer safety mechanism
recall_memory memory low 10s search memories by keyword, scoped by session
remember_memory memory low 10s store a new memory, scoped by session
search_history memory low 10s search conversation history across sessions

math category

calculator

Evaluates arithmetic expressions, supports +, -, *, /, and parentheses.

Input:

{ "expression": "2 + 3 * 4" }

Output (number; irrationals are kept as float):

14

Note: the implementation is a hand-written lexer plus recursive descent (no external eval library). It accepts only numbers and the four basic operators. Whitespace is ignored, but negative number prefixes are not supported ("-1+2" parses as 1 + 2; write it as "0-1+2" instead).


utility category

get_time

Returns the current UTC time in ISO 8601 format.

Input: {} (no parameters)

Output:

"2026-06-04T12:34:56Z"

echo

Echoes back the input text (useful for debugging or inter-agent communication).

Input:

{ "text": "hello" }

Output:

"hello"

generate_uuid

Generates a random UUID v4.

Input: {}

Output:

"550e8400-e29b-41d4-a716-446655440000"

hash_text

Hashes text. Supports md5 / sha1 / sha256 (default sha256; case-insensitive algorithm name).

Input:

{ "text": "hello", "algorithm": "sha1" }

Output:

{ "hash": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", "algorithm": "sha1" }

Security note: MD5 and SHA1 are no longer collision-resistant. Use them only for non-security purposes (deduplication, cache keys).

generate_password

Generates a random password, length 4-128 (values outside this range are clamped automatically). Symbols are included by default.

Input:

{ "length": 24, "include_symbols": true }

Output:

"a8K!mZ3xQ9wL7nV2"

Note: uses Random.State.make_self_init, so there's no cryptographic-strength guarantee. For production, use a dedicated library.

string_stats

Counts characters, words, and lines. Words are split on whitespace.

Input:

{ "text": "hello world\nfoo bar" }

Output:

{ "characters": 22, "words": 4, "lines": 2 }

json_format

Validates and pretty-prints a JSON string.

Input:

{ "json": "{\"a\":1,\"b\":2}" }

Output:

"{\n  \"a\": 1;\n  \"b\": 2\n}"

Note: parse failure returns Error (Invalid_input "Invalid JSON: ...") with retryable = false.

convert_temperature

Converts between C, F, and K.

Input:

{ "value": 100, "from": "C", "to": "F" }

Output:

{ "value": 212, "unit": "F", "original_value": 100, "original_unit": "C" }

url_encode

URL-encodes or URL-decodes. Encode is the default; pass decode: true to reverse. On decode, + is treated as a space (form-encoding compatible).

Input:

{ "text": "hello world", "decode": false }

Output:

"hello%20world"


web category

The three network tools share validate_url (only http:// and https:// are allowed), a max_download_size of 10MB, system CA certificates, and TLS hostname verification.

fetch_url

HTTP GET, returns the raw text.

Input:

{ "url": "https://example.com", "max_length": 50000 }

Output:

{
  "url": "https://example.com",
  "status": 200,
  "content": "...",
  "content_length": 1234,
  "truncated": false
}

read_webpage

Fetches the URL, parses the HTML, strips <script> / <style> / <noscript>, and returns the plain text.

Input:

{ "url": "https://example.com", "max_length": 10000 }

Output:

{
  "url": "https://example.com",
  "title": "Example Domain",
  "text": "...",
  "text_length": 567,
  "truncated": false
}

Note: depends on lambdasoup. HTTP 4xx / 5xx returns Error (External_failure "HTTP <code>"); 5xx and 429 are flagged with retryable = true.

DuckDuckGo lite search.

Input:

{ "query": "ocaml lwt tutorial", "max_results": 5 }

Output:

{
  "query": "ocaml lwt tutorial",
  "results": [
    { "title": "...", "url": "...", "snippet": "..." }
  ],
  "result_count": 5
}

Note: scrapes DuckDuckGo lite HTML (no API key required). Network or parse failure returns Error (External_failure ...).


fs (read) category

The four read tools reject absolute paths and any path containing : (Windows drive-letter guard). find / grep skip .git / node_modules / _build / _opam by default.

read

Reads file contents with an optional line offset and line limit.

Input:

{ "path": "src/main.ml", "offset": 0, "limit": 100 }

Output (line-numbered, similar to cat -n):

"   1\topen Par\n   2\t...\n"

Limits: - File size <= 10MB (exceeding the cap returns Error (Invalid_input "File too large")) - Path must be relative to workspace root, or an absolute path under workspace root

ls

Lists directory contents.

Input:

{ "path": "." }

Output:

{
  "path": ".",
  "entries": [
    { "name": "src", "type": "dir", "size": null, "modified": 1717... },
    { "name": "README.md", "type": "file", "size": 4321, "modified": 1717... }
  ]
}

Note: entries are sorted by name ascending. type is one of dir / file / link / other / unknown. A non-directory path returns Error (Invalid_input "Not a directory").

find

Glob match on filenames (** crosses directories; * crosses a single path component and does not include /).

Input:

{ "pattern": "**/*.ml", "path": "." }

Output:

["src/main.ml", "src/agent.ml", "test/test.ml"]

grep

Regex search across file contents. The path directory is searched recursively; glob filters by filename.

Input:

{ "pattern": "TODO", "path": "lib", "glob": "*.ml", "context_lines": 2 }

Output (one path:line:match per entry):

["lib/runtime.ml:142:(* TODO: handle ... *)"]

Note: the regex syntax is OCaml Str (POSIX extended regex). The current implementation ignores context_lines (the parameter is kept for future extension).


fs (write) category

write

Writes a file. Pass create_dirs: true to run mkdir -p automatically.

Input:

{ "path": "out/result.txt", "content": "hello", "create_dirs": true }

Output:

"Wrote 5 bytes to out/result.txt"

Note: overwrites an existing file. If the directory is missing and create_dirs is false, the tool returns an error.

edit

Batch exact-string replace. Overlapping ranges are rejected (this avoids ambiguity in edit ordering).

Input:

{
  "path": "src/main.ml",
  "edits": [
    { "old": "let x = 1", "new": "let x = 2" },
    { "old": "foo", "new": "bar" }
  ]
}

Output:

"Applied 2 edit(s) to src/main.ml"

Note: - Every old value must be an exact substring of the file (including spaces, newlines, and indentation) - Uses Str.replace_first, so it does not modify later occurrences of the same string - Overlap detection: if two old ranges overlap in the file, the tool rejects them with Error (Invalid_input "Overlapping edits")


bash (added in v0.3.1)

An LLM calling a shell is the most dangerous built-in tool. v0.3.0 deliberately skipped bash, then v0.3.1 designed it as a standalone piece. The core idea: move from "raw shell string + blacklist" to "typed Safe_command ADT + policy functor + blacklist", pushing safety checks from runtime back toward compile time.

Purpose

Executes shell commands with three layers of defense:

  1. Type layer: argv is forced to be string list, with no Exec_raw_shell constructor (shell injection is unrepresentable in the type)
  2. Policy layer: POLICY.filter validates at runtime. Pick Coder / ReadOnly / ReadOnlyNoNet to fit the use case
  3. Blacklist layer: 31 regex rules in Bash_blacklist as a final safety net (catches rm -rf /, dd of=/dev/sda, fork bombs, and similar)

Input

{
  "argv": ["ls", "-la"],
  "cwd": "src",
  "timeout": 30
}

Field reference: - argv (required): argument array, not a shell string - cwd: Working directory (relative to workspace root, or absolute path under workspace root). Default: "." - timeout: max execution seconds, default 30, hard cap 600

Output

{
  "stdout": "...",
  "stderr": "...",
  "exit_code": 0,
  "duration": 0.123,
  "truncated": false
}

truncated: true means the output was clipped at 50KB / 2000 lines (a marker is appended at the end).

Three preset policies

Policy Network Write Use case
Coder (default) yes yes "AI writes code", only blacklist hits are blocked
ReadOnly yes no pure read-only tools (ls / cat / find / grep)
ReadOnlyNoNet no no maximum safety (sensitive code base review)

Custom policies

Implement the Bash_policy.POLICY module type and pass it to Runtime.create:

module type POLICY = sig
  val name : string
  val filter :
    Bash_safe_command.command ->
    (Bash_safe_command.command, Types.error_category) result
  val max_cpu_seconds : float
  val max_memory_kb : int
  val allow_network : bool
  val allow_write : bool
end

module MyStrictPolicy : Bash_policy.POLICY = struct
  let name = "MyStrict"
  let allow_network = false
  let allow_write = false
  let max_cpu_seconds = 10.0
  let max_memory_kb = 524288
  let filter cmd =
    (* your extra checks: blacklist, whitelist, argv limits, ... *)
    Ok cmd
end

(* Runtime.create ~bash_policy:(module MyStrictPolicy) *)

Installation

The bash tool is not registered automatically with Runtime.create. You need to call install_bash_tool after Runtime.create:

let () = Eio_main.run (fun env ->
  Eio.Switch.run (fun sw ->
    let mgr = Eio.Stdenv.process_mgr env in
    let clock = Eio.Stdenv.clock env in
    let fs = Eio.Stdenv.fs env in
    match Runtime.create ~config:my_config sw with
    | Error _ -> failwith "runtime create failed"
    | Ok rt ->
      (match Runtime.install_bash_tool ~process_mgr:mgr ~clock ~fs rt with
       | Ok () -> () (* bash tool is ready *)
       | Error e -> Printf.failwithf "bash install failed: %a"
           Yojson.Safe.pp (Types.error_category_to_yojson e))
  ))

Idempotent: a second call returns Error (Invalid_input "bash tool already installed").

Required parameters: - process_mgr: Eio.Stdenv.process_mgr env, used for Eio.Process.spawn - clock: Eio.Stdenv.clock env, used to enforce the timeout (without a clock, timeouts do nothing) - fs: Eio.Stdenv.fs env, the filesystem capability used to set the child process's cwd. Without it, bash commands would run in the PAR process's cwd, defeating the workspace sandbox.

The 9-layer safety mechanism

# Mechanism Implementation
1 Workspace-anchored sandbox Workspace.sandboxed_path abstract type; the constructor rejects .., :, and sensitive prefixes (/etc, ~/.ssh, etc.). Absolute paths under workspace root are admitted
2 Blacklist 31 regex rules in Bash_blacklist (rm -rf /, dd of=/dev/sda, :(){:|:&};:, and similar)
3 Whitelist (optional) implement whitelist logic in a custom POLICY
4 Timeout Eio.Process.spawn + Eio.Fiber.first race; hard cap 600s
5 Process group cleanup Eio.Process + setpgid; the timeout sends killpg to the whole group
6 Environment scrubbing Bash_policy.sanitize_env strips *_SECRET* / *_KEY* / AWS_* / OPENAI_API_KEY / ANTHROPIC_API_KEY / GITHUB_TOKEN and similar
7 Output truncation 50KB byte cap + 2000 line cap; a marker is appended when triggered
8 ANSI stripping removes CSI (ESC[...]) and OSC (ESC]...BEL) sequences
9 Audit log the event bus emits Bash_invoked / Bash_completed events (carrying a risk rating and argv)

Security recommendations

  • The default Coder policy is the right choice for "AI writes code" workflows (it matches pi's default behavior)
  • For code review where the LLM should only inspect, use ReadOnly
  • For sensitive code bases, use ReadOnlyNoNet
  • Never disable the timeout. It's the last line of defense against fork bombs and network hangs
  • Environment variables are scrubbed automatically. If you need to pass a secret, write it to a file and have the agent use the read tool (don't put it in the env field)
  • The blacklist is a last resort, not the main defense. For safety-critical setups, set allow_write:false and allow_network:false in a custom POLICY
  • OS-level sandboxing (bwrap / landlock) is not provided in v0.3.1

Risk scoring

Bash_safe_command.assess_risk returns Low / Medium / High / Critical, attached to the risk field of the Bash_invoked event.


memory category (added in v0.7.1)

The three memory tools require a configured Memory_service. They are auto-registered when Runtime.create receives a ?memory parameter. All tools read scope from Invoke_context.get_current_exn().session_id, so memories are automatically isolated by session.

recall_memory

Search stored memories by keyword query. Uses FTS5 full-text search with BM25 ranking.

Input:

{ "query": "ocaml concurrency", "limit": 5 }

Output:

{
  "results": [
    { "id": "...", "content": "...", "summary": "...", "scope": "...", "categories": [...], "created_at": 1717... }
  ],
  "count": 2
}

Note: limit defaults to 5, capped at 50. The scope is read from the current invoke_context.session_id.

remember_memory

Store a new memory for future recall. Memories are scoped to the current session.

Input:

{ "content": "The project uses OCaml 5.4 with Eio for concurrency", "summary": "OCaml concurrency stack", "categories": ["architecture", "tech-stack"] }

Output (the created memory_object):

{ "id": "550e8400-...", "content": "...", "summary": "...", "scope": "...", "categories": ["architecture", "tech-stack"], "created_at": 1717... }

Parameters: - content (required): the full text to remember - summary (optional): short summary, indexed by FTS5 - categories (optional): tags for categorization

search_history

Search conversation history from recent sessions. Performs a case-insensitive substring match across message text.

Input:

{ "query": "Eio concurrency", "limit": 10 }

Output:

{
  "results": [
    { "session_id": "...", "role": "user", "content": "Tell me about Eio..." }
  ],
  "count": 3
}

Note: limit controls the number of sessions to search (default 10, capped at 50). Matching text is truncated at 500 characters. The current session is searched first.


Registering a custom tool

A tool is a simple { descriptor; handler } pair. Runtime.register_tool is the convenience function; under the hood, you can register directly through Tool_registry.register:

let my_tool = { descriptor; handler } in
Tool_registry.register rt.tool_registry descriptor handler

The full example (with tool_descriptor field reference) is in agent.md.

Security audit checklist

Self-check before submitting a new tool:

  • [ ] The permission field is set (Allow / Confirm / Deny / Role_based / Condition_based)
  • [ ] Long-running tools have a timeout set
  • [ ] Resource-constrained tools have a concurrency_limit set
  • [ ] Network / write tools emit audit events through the event bus
  • [ ] File-path tools reject absolute paths and paths containing :
  • [ ] Dangerous tools go through Bash_policy (when applicable)
  • [ ] The description includes an input example (so the LLM knows how to call the tool)

Actionable validation errors

Tool rejections in PAR don't just say "rejected." They explain what went wrong and how to fix it. This is a design principle: the error message is the model's only feedback signal in the ReAct loop. Putting rules in the system prompt made models refuse to attempt work; teaching at the point of rejection lets the model learn and retry correctly.

Marker vocabulary

Three marker prefixes appear in tool error messages. Tool descriptions reference these same constants, so descriptions and rejections cannot drift apart (both are sourced from lib/tools/tool_error.ml).

Marker Meaning
[workspace] Path validation failure (workspace sandbox rejection)
[bash-policy] Bash command rejected by a policy (Coder / ReadOnly / custom)
[cancelled] Tool call aborted because the run was cancelled

Workspace path rejections

All file tools (read / ls / find / grep / write / edit) share the same path validation. Four distinct rejection kinds exist, each with a specific message and metadata code:

Path traversal (code workspace_parent_traversal):

[workspace] Path rejected: '../etc/passwd' contains '..' (path traversal is not allowed).
Use a clean relative path like 'src/main.ml', or an absolute path under the workspace root.

Colon in path (code workspace_colon):

[workspace] Path rejected: 'C:\Users' contains ':' (reserved/ambiguous in tool arguments).
Use a path without ':'.

Absolute path outside workspace (code workspace_absolute_outside):

[workspace] Path rejected: '/etc/passwd' is an absolute path outside the workspace (2 root(s)).
Use a relative path like 'src/main.ml' (resolved against the workspace root)
or an absolute path under a workspace root.

Note the root count: the message tells the model how many workspace roots exist, so it can reason about which absolute paths are allowed.

Protected location (code workspace_protected_location):

[workspace] Path rejected: '~/.ssh/id_rsa' matches a protected location.
Choose a path outside protected system areas.

Two additional metadata codes exist for missing arguments:

Code When
workspace_path_required Required path argument is missing
pattern_required Required pattern argument is missing (for find / grep)

Bash policy rejections

When the bash tool's policy filter rejects a command, the error names the policy and the command:

[bash-policy] Command rejected by ReadOnly policy: ["rm", "-rf", "node_modules"].
This policy does not allow write operations. Use a read-only command or switch to a different policy.

The marker [bash-policy] matches what the bash tool description teaches the model, so the model knows this is a policy constraint, not a bug.

Edit tool: old_text not found

Previously, the edit tool silently succeeded when old_text was not found in the file. This was the worst defect class: the model believed it made a change when nothing happened. Now it returns an error:

Edit failed: old_text was not found in 'src/main.ml'. The file may have changed
since it was last read, or the text does not match exactly (including whitespace).
Read the file first, then retry with the exact current text.

Metadata code: edit_old_text_not_found.

Metadata codes reference

Code Tool Meaning
workspace_parent_traversal read/ls/find/grep/write/edit Path contains ..
workspace_colon read/ls/find/grep/write/edit Path contains :
workspace_absolute_outside read/ls/find/grep/write/edit Absolute path outside workspace roots
workspace_protected_location read/ls/find/grep/write/edit Path matches a protected system area
workspace_path_required read/ls/find/grep/write/edit Missing required path argument
pattern_required find/grep Missing required pattern argument
edit_old_text_not_found edit old_text substring not found in file

Design principle: teach at the point of rejection

The markers and instructive messages exist because the error message is the only feedback signal a model receives during the ReAct loop. System-prompt rules ("never use absolute paths") caused models to refuse attempts entirely. In contrast, a rejection like [workspace] Path rejected: '/tmp/out.txt' is an absolute path outside the workspace (1 root(s)). Use a relative path like 'src/main.ml' ... gives the model enough context to retry with the correct form.

The constants in lib/tools/tool_error.ml are the single source of truth: tool descriptions TEACH the markers and tool handlers EMIT them. Both reference the same constants, so they cannot drift apart.

See also

  • agent.md -- Agent definitions, Runtime API, tool registration
  • overview.md -- SDK architecture overview
  • lib/tools/bash_safe_command.mli -- Full Safe_command ADT API
  • lib/tools/bash_policy.mli -- POLICY interface plus the 3 presets
  • lib/tools/bash_blacklist.mli -- the 31 blacklist regex rules