English · 简体中文
Translated to English for v0.3.2. Source-of-truth: the OCaml modules in lib/ and the README.
PAR Quickstart¶
From scratch to a working LLM agent with tool calls in 30 minutes using OCaml.
What is PAR?¶
PAR (Programmable Agent Runtime) is a modular, type-safe agent runtime for OCaml 5.4+. It includes a ReAct reasoning engine, OpenAI and Anthropic LLM providers (plus any OpenAI-compatible endpoint), 20 built-in tools (including a type-safe bash tool), an MCP client (stdio + HTTP/SSE), workflow orchestration, and SQLite/PostgreSQL persistence.
Prerequisites¶
| Dependency | Minimum version | Check command |
|---|---|---|
| OCaml | 5.4+ | ocaml --version |
| opam | 2.1+ | opam --version |
| dune | 3.16+ | dune --version |
| API Key | OpenAI or Anthropic | — |
If you don't have an OCaml environment, install it via opam:
bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
opam init --disable-sandboxing --bare
opam switch create 5.4.0
eval $(opam env)
Install¶
Build from source (recommended):
git clone https://github.com/jcz2020/par.git
cd par
opam install --deps-only . # install dependencies
dune build # compile
dune install # install into opam environment
After installation you get two packages:
- par — the SDK library
- par_cli — the CLI tool (par, par config, par ask)
Project setup¶
Create a new OCaml project. You need at least three files.
dune-project:
(lang dune 3.16)
(name my_par_app)
(executable
(name main)
(libraries par eio eio_main)
(preprocess (pps ppx_deriving_yojson)))
dune:
main.ml — start with a skeleton, we'll fill it in later:
Run it to verify the environment:
Configure an LLM provider¶
PAR's CLI uses a JSON configuration file stored at ~/.par/config.json.
The easiest way to create it is through the guided wizard:
The wizard prompts for provider, API key, model name, and other fields. If you edit the config file manually, the format is:
OpenAI (including any OpenAI-compatible endpoint):
{
"provider": "openai",
"api_key": "sk-...",
"api_base": null,
"model": "gpt-4",
"persistence": "sqlite",
"db_uri": null,
"temperature": 0.7,
"system_prompt": "You are a helpful assistant."
}
OpenAI-compatible endpoint (e.g. local vLLM, llama.cpp server):
{
"provider": "openai",
"api_key": "your-api-key",
"api_base": "http://localhost:8000/v1",
"model": "my-model",
"persistence": "sqlite",
"db_uri": null,
"temperature": 0.7,
"system_prompt": "You are a helpful assistant."
}
Anthropic:
{
"provider": "anthropic",
"api_key": "sk-ant-...",
"api_base": null,
"model": "claude-sonnet-4-20250514",
"persistence": "sqlite",
"db_uri": null,
"temperature": 0.7,
"system_prompt": "You are a helpful assistant."
}
You can also pass the API key via environment variables (useful for SDK usage):
Write your first agent¶
Here is a complete agent using the SDK. Replace main.ml with:
open Par
let () =
(* 1. Runtime configuration *)
let config = {
Types.persistence = `Sqlite "par.db";
event_bus = Runtime.default_event_bus_config;
default_quota = Runtime.default_quota;
shutdown = Runtime.default_shutdown_config;
llm_providers = [];
eval_limits = { max_depth = 10; max_node_visits = 1000 };
parallel_tool_execution = true;
bash_confirm = Runtime.default_bash_confirm;
event_retention_seconds = 604800.0;
} in
(* 2. Start the Eio event loop *)
Eio_main.run (fun _env ->
Eio.Switch.run (fun switch ->
(* 3. Create the runtime *)
match Runtime.create ~config switch with
| Error _err ->
Printf.eprintf "Failed to create runtime\n"
| Ok rt ->
(* 4. Register an echo tool *)
let tool = Runtime.register_tool rt
~name:"echo"
~description:"Echoes back the input"
~input_schema:(`Assoc [
("type", `String "object");
("properties", `Assoc []);
])
~handler:(fun input _token ->
Types.Success
(`String (Printf.sprintf "Echo: %s"
(Yojson.Safe.to_string input))))
()
in
(* 5. Define the agent configuration *)
let agent = {
Types.id = "echo-agent";
system_prompt = "You are an echo assistant. Use the echo tool.";
model = {
provider = `Openai;
model_name = "gpt-4";
api_base = None;
temperature = 0.7;
max_tokens = None;
top_p = None;
stop_sequences = None;
};
tools = [ tool.descriptor ];
max_iterations = 5;
middleware = [];
retry_policy = None;
context_strategy = None;
resource_quota = None;
} in
(* 6. Register and confirm *)
ignore (Runtime.register_agent rt agent);
Printf.printf "Agent registered: %s\n" agent.id;
ignore (Runtime.close rt)
)
)
Key steps explained:
- Runtime configuration —
runtime_configuses SQLite for persistence; event bus and quotas use defaults. - Eio event loop — PAR is built on Eio's structured concurrency; all code runs inside
Eio_main.run. - Create runtime —
Runtime.createreturnsResult.t; you must handle the error branch. - Register tool —
register_tooltakes a name, description, JSON Schema, and handler function; returns atool_binding. - Agent configuration —
agent_configspecifies the system prompt, model parameters, tool list, max iterations, and more. - Register agent —
register_agentadds the configuration to the runtime's agent table.
Run the agent¶
To actually converse with the agent, configure an LLM provider and call Runtime.invoke:
(* Add after Runtime.register_agent rt agent *)
match Runtime.invoke rt ~agent_id:"echo-agent"
~message:"Hello, echo!" ()
with
| Ok resp ->
(match resp.Types.text with
| Some txt -> Printf.printf "Response: %s\n" txt
| None -> Printf.printf "No text response\n")
| Error e -> Printf.eprintf "Error: %s\n" (Printexc.to_string (Failure ""))
Using the CLI¶
PAR ships an interactive REPL for zero-code experimentation.
Configuration:
Interactive conversation:
Single-shot query:
The CLI automatically registers all 20 built-in tools and supports command-line overrides:
par ask --provider anthropic --model claude-sonnet-4-20250514 "Hello"
par ask --temperature 0.3 "Explain quantum computing"
Using built-in tools¶
In the SDK, access all built-in tool bindings via Par.Builtin_tools:
open Par
let () =
let config = {
Types.persistence = `Sqlite "par.db";
event_bus = Runtime.default_event_bus_config;
default_quota = Runtime.default_quota;
shutdown = Runtime.default_shutdown_config;
llm_providers = [];
eval_limits = { max_depth = 10; max_node_visits = 1000 };
parallel_tool_execution = true;
bash_confirm = Runtime.default_bash_confirm;
event_retention_seconds = 604800.0;
} in
Eio_main.run (fun env ->
Eio.Switch.run (fun switch ->
match Runtime.create ~config switch with
| Error _ -> Printf.eprintf "Failed to create runtime\n"
| Ok rt ->
(* Get all built-in tools *)
let net = Eio.Stdenv.net env in
let tools = Builtin_tools.builtin_tools ~switch ~net in
List.iter (fun (tb : Types.tool_binding) ->
Tool_registry.register
(Runtime.tool_registry rt) tb.descriptor tb.handler
) tools;
let descriptors =
List.map (fun (tb : Types.tool_binding) -> tb.descriptor) tools
in
(* Create an agent with the calculator tool *)
let agent = {
Types.id = "math-agent";
system_prompt = "You are a math assistant. Use the calculator tool.";
model = {
provider = `Openai;
model_name = "gpt-4";
api_base = None;
temperature = 0.7;
max_tokens = None;
top_p = None;
stop_sequences = None;
};
tools = descriptors; (* all built-in tools *)
max_iterations = 10;
middleware = [];
retry_policy = None;
context_strategy = None;
resource_quota = None;
} in
ignore (Runtime.register_agent rt agent);
Printf.printf "Agent registered with %d tools\n"
(List.length descriptors);
ignore (Runtime.close rt)
)
)
Built-in tools include: calculator, get_time, echo, generate_uuid,
hash_text, generate_password, string_stats, json_format,
convert_temperature, url_encode, fetch_url, read_webpage, web_search,
read, ls, find, grep, write, edit, bash.
Persistence: SQLite¶
PAR uses SQLite persistence by default. Configure it in runtime_config:
let config = {
Types.persistence = `Sqlite "par.db"; (* file path *)
(* ... other fields ... *)
} in
The database file is created automatically at runtime if it doesn't exist. It stores task state, event logs, and workflow checkpoints.
Switch to PostgreSQL (recommended for production):
let config = {
Types.persistence = `Postgresql "postgresql://localhost/par";
(* ... other fields ... *)
} in
Note: the PostgreSQL backend requires installing the par_postgres opam package separately and recompiling.
Troubleshooting¶
| Symptom | Cause | Solution |
|---|---|---|
Unbound module Types |
Missing open Par |
Add open Par at the top of the file |
Unbound module Par |
par library not found | Confirm (libraries par ...) is declared in dune-project |
Connection refused |
Missing API key or network issue | Check ~/.par/config.json or environment variables |
LLM not initialized |
SDK mode without ~llm parameter |
Use CLI mode (par ask) which handles LLM init automatically |
Error creating OpenAI provider |
API key format error | Confirm key starts with sk- (OpenAI) or sk-ant- (Anthropic) |
dune build fails |
Dependencies not installed | Run opam install --deps-only . |
ppx_deriving_yojson error |
Missing preprocessor | Add (preprocess (pps ppx_deriving_yojson)) to the dune file |
Next steps¶
- agent.md — Agent configuration deep dive:
model_configfields,context_strategy,retry_policy - workflow.md — Workflow orchestration: sequential, parallel, conditional branching, map-reduce
- middleware.md — Middleware: logging, timeout, retry, rate limiting, PII masking, data validation
- examples/ — More complete examples (basic_agent.ml, otel_tracing.ml)