English · 简体中文
PAR SDK Overview¶
Note (v0.6.7): PAR's CLI was removed; the SDK and Python binding are the two supported surfaces. The table row "Trying PAR for the first time" historically pointed at
par config; new users should install the Python binding (pip install par-runtime) or the OCaml SDK (opam install par) — or, for an interactive coding-agent experience, use par-code.
The PAR SDK is an OCaml 5.4+ library for building LLM-powered agents. It provides a ReAct reasoning loop, multi-provider LLM abstraction, type-safe shell execution, MCP client integration (stdio + HTTP/SSE), and a 9-middleware pipeline. This overview is the deep dive; the README is the landing.
What is the PAR SDK?¶
PAR (Programmable Agent Runtime) ships as one opam package (par) plus a PyPI Python binding (par_runtime). The par_cli CLI package was removed in v0.6.7; the SDK is the production surface — everything is accessible through Par.Runtime.* functions. For the interactive coding-agent experience, see par-code.
- One opam package:
par(SDK library) - One PyPI package:
par_runtime(Python ctypes binding) - 11 sub-libraries under
lib/plus a facade modulePar(re-exports all sub-modules) - 23 built-in tools including the type-safe
bashtool and 3 memory tools - 9 built-in middlewares (logging, retry, rate-limit, timeout, input validation, PII mask, tool-output sanitization, Think_tag_strip)
- Multi-provider: OpenAI compatible + Anthropic Messages API + custom registration
- 1505 OCaml tests + Python bindings passing
Architecture at a glance¶
graph TD
P["Par facade: open Par"]
P --> CORE["lib/core"]
P --> PROV["lib/providers"]
P --> PERS["lib/persistence"]
P --> EVB["lib/event_bus"]
P --> MID["lib/middleware"]
P --> TOOL["lib/tools"]
P --> DOC["lib/documents"]
P --> MEM["lib/memory"]
P --> SKILL["lib/skills"]
P --> MCP["lib/mcp"]
P --> FFI["lib/ffi"]
CORE --> RT["Runtime / Engine / Workflow_engine / Expression / State_machine / Context_manager / Cancellation / Tool_registry / Template / Steering_queue / Hook / Metrics / Capability / Invoke_context / Deprecation"]
PROV --> PROV_API["Openai_provider / Anthropic_provider / Mock_provider"]
PERS --> PERS_API["Sqlite_persistence / Noop_persistence"]
EVB --> EVB_API["Event_bus with DLQ + Eio.Stream"]
MID --> MID_API["Logging / Retry / Rate_limit / Timeout / Arg_validation / Output_validation / Pii_mask / Sanitize_tool_output / Think_tag_strip"]
TOOL --> TOOL_API["Builtin_tools (23 incl. bash) + Bash_safe_command + Bash_policy + Bash_blacklist"]
DOC --> DOC_API["Document / Text_loader / Markdown_loader / Html_loader / Csv_loader / Pdf_loader / Directory_loader"]
SKILL --> SKILL_API["Skill_loader / Builtin_skills"]
MCP --> MCP_API["Mcp_types / Mcp_server / Mcp_client / Mcp_transport_stdio / Mcp_transport_http / Mcp_naming / Mcp_errors"]
FFI --> FFI_API["par_capi.so + par_ffi.h for Python ctypes"]
RT -.cancellation.-> EIO["Eio.Switch.t (whole-tree cancel)"]
EVB -.events.-> RT
PROV -.stream.-> RT
The facade Par is the single import surface: open Par brings every sub-module listed above into scope, so call sites read Par.Runtime.invoke only when they want to disambiguate from a local Runtime name. The dashed edges are the cross-cutting concerns: cancellation flows from Eio.Switch.t into every tool handler, the event bus feeds observability back into the runtime, and provider streaming responses drive the ReAct loop in Engine.run_agent.
Five-minute SDK tour¶
Five self-contained examples, one per subsystem. Each compiles against the published par opam package.
1. Runtime + tool¶
Create a runtime, register a tool, register an agent. The handler receives JSON input and a cancellation token, returns a typed handler_result.
open Par
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;
}
let () = Eio_main.run (fun _env ->
Eio.Switch.run (fun switch ->
let rt = Runtime.create ~config switch |> Result.get_ok in
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))))
() |> Result.get_ok in
ignore (Runtime.register_agent rt
{ id = "echo-agent"; system_prompt = Types.stable_prompt "You are an echo assistant.";
system_prompt_template = None;
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; tool_timeout = None }))
2. LLM invoke¶
Once an agent is registered, Runtime.invoke runs the ReAct loop until the LLM signals Stop, all tool calls return, or max_iterations is reached.
match Runtime.invoke ~agent_id:"echo-agent" ~message:"Hello!" rt with
| Ok resp -> Printf.printf "Response: %s\n"
(Option.value resp.Types.text ~default:"(no text)")
| Error err -> Printf.eprintf "Error: %s\n"
(Yojson.Safe.to_string (Types.error_category_to_yojson err))
Pass ?cancellation_token: to abort a long-running invoke; the cancellation propagates to every tool handler through the Eio switch. Full signature and error_category variants live in agent.md.
3. Event subscription¶
Subscribe to the event bus and pattern-match on lifecycle events. MCP server events arrive on the same bus as task and tool events, so one subscriber covers the whole runtime.
(* Event subscription: pass ?on_tool_event:(event -> unit) callback to Runtime.create *)
Event_bus.subscribe bus (fun ev ->
match ev with
| Mcp_tool_completed { server_id; tool_name; duration_ms } ->
Printf.printf "[mcp] %s/%s done in %.1fms\n"
server_id tool_name duration_ms
| Bash_invoked { argv; risk; _ } ->
Printf.printf "[bash] risk=%s argv=%s\n"
risk (String.concat " " argv)
| _ -> ())
ignore (Runtime.invoke ~agent_id:"echo-agent" ~message:"hi" rt)
4. Workflow¶
A workflow is a tree of steps: agent calls, tool calls, parallel, sequential, conditional, map-reduce, human approval, and sub-workflow. The runtime submits a workflow definition and returns a Workflow_run_id.t for status queries.
let wf = {
Types.id = "research"; Types.name = "Research and Summarize";
Types.version = 1;
Types.steps = Sequential [
Agent_call { agent_id = "researcher";
prompt_template = "Research: {{topic}}";
response_schema = None };
Human_approval { prompt_template = "Approve summary of {{topic}}?";
timeout = 60.0; allowed_roles = ["admin"] };
Agent_call { agent_id = "summarizer";
prompt_template = "Summarize: {{topic}}";
response_schema = None } ];
Types.variables = [("topic", `String "OCaml 5 effects")];
Types.failure_policy = Fail_fast;
Types.parallel_limit = 3; Types.timeout = 600.0;
Types.on_complete = None;
} in
ignore (Runtime.register_workflow rt wf);
let run_id = Runtime.submit_workflow rt wf |> Result.get_ok in
ignore (Runtime.approve_workflow rt run_id ~approver:"alice")
Reference: workflow.md covers checkpointing, resume, and the full step taxonomy.
5. MCP client¶
Pass MCP server configs to Runtime.create; the runtime spawns each child, completes the initialize handshake, and hands back a Runtime whose mcp_server accessor returns a typed client handle.
let mcp_fs : Mcp_types.server_config = {
name = "fs"; command = "npx";
args = [ "-y"; "@modelcontextprotocol/server-filesystem"; "/tmp" ];
env = []; cwd = None; startup_timeout = 10.0;
}
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 rt = Runtime.create
~mcp_servers:[mcp_fs] ~mcp_process_mgr:mgr ~mcp_clock:clock
~config sw |> Result.get_ok in
let sid = Mcp_types.server_id_of_string "fs" |> Result.get_ok in
let client = Mcp_client.of_server
(Runtime.mcp_server rt sid |> Result.get_ok) in
match Mcp_client.list_tools client with
| Ok tools -> List.iter (fun t ->
Printf.printf "- %s\n" t.Mcp_types.name) tools
| Error _ -> ())
The seven MCP event types (started, failed, stopped, tool invoked, tool completed, resource read, prompt rendered) all flow through the same event bus shown in tour step 3. Reference: mcp.md.
When to use the SDK vs the CLI¶
The CLI is a thin exercise of the SDK, so anything the CLI can do, the SDK can do in code. The split is about ergonomics: the CLI favors the person typing a one-shot question; the SDK favors the person embedding PAR in another system.
| Use case | SDK | CLI |
|---|---|---|
| Production agent serving web traffic | recommended | not designed for it |
| Ad-hoc LLM question from terminal | Possible but verbose | par ask "..." |
| Reproducible batch jobs | Runtime.invoke in a script |
REPL is interactive |
| Custom UIs (web, Slack, IDE plugin) | embed the SDK | not applicable |
| Trying PAR for the first time | Possible but heavy setup | par config then par |
| Multi-agent orchestration with workflows | register and submit_workflow |
not exposed |
| MCP server integration in your app | pass ~mcp_servers to Runtime.create |
not exposed |
| Long-running event monitoring | subscribe to Event_bus in-process |
pipe logs |
(The previous CLI shipped as a separate opam package par_cli so consumers of par did not pull in cmdliner and the REPL dependencies; that package was removed in v0.6.7.)
Module map¶
Every public module lives under one of the 9 sub-libraries below, plus the facade in lib/par.ml. open Par re-exports the marked modules; unmarked modules are still addressable as Par.<Module>.
| Library | Public modules (excerpt) | Purpose |
|---|---|---|
lib/core |
Par.Types, Par.Runtime, Par.Engine, Par.Workflow_engine, Par.Expression, Par.State_machine, Par.Context_manager, Par.Cancellation, Par.Tool_registry, Par.Template, Par.Steering_queue, Par.Hook, Par.Metrics, Par.Capability, Par.Invoke_context, Par.Deprecation |
Core types, runtime, ReAct loop, workflow, expression evaluator, state machine, context manager, cancellation tokens, tool registry, capability detection, per-call isolation, deprecation framework |
lib/providers |
Par.Openai_provider, Par.Anthropic_provider, Par.Mock_provider |
LLM provider implementations |
lib/persistence |
Par.Sqlite_persistence, Par.Noop_persistence |
Sqlite backend (dev), no-op (tests) |
lib/event_bus |
Par.Event_bus |
Eio-based event bus with DLQ |
lib/middleware |
Par.Logging, Par.Retry, Par.Rate_limit, Par.Timeout, Par.Arg_validation, Par.Output_validation, Par.Pii_mask, Par.Sanitize_tool_output, Par.Think_tag_strip |
9 built-in middlewares |
lib/tools |
Par.Builtin_tools, Par.Bash_safe_command, Par.Bash_policy, Par.Bash_blacklist |
23 built-in tools including type-safe bash and 3 memory tools |
lib/documents |
Par.Document, Par.Text_loader, Par.Markdown_loader, Par.Html_loader, Par.Csv_loader, Par.Pdf_loader, Par.Directory_loader |
Document loaders for RAG (text, Markdown, HTML, CSV, PDF) |
lib/memory |
Par.Memory_service, Par.Sqlite_memory, Par.Memory_error, Par.Memory_object |
Agent memory with FTS5 keyword search |
lib/skills |
Par.Skill_loader, Par.Builtin_skills |
Skill system (auto-loading instruction bundles with triggers) |
lib/mcp |
Par.Mcp_types, Par.Mcp_server, Par.Mcp_client, Par.Mcp_transport_stdio, Par.Mcp_transport_http, Par.Mcp_naming, Par.Mcp_errors |
MCP client (stdio v0.3.1, HTTP/SSE v0.4.3) |
lib/ffi |
Par_capi (build artifact) |
C ABI for Python binding |
lib/par.ml |
(facade, re-exports above) | open Par entry point |
bindings/python/ |
par_runtime PyPI package |
Python ctypes binding |
The facade Par is generated by lib/par.ml. Everything links against par directly (the bin/ CLI directory was removed in v0.6.7; the interactive coding-agent product lives in par-code).
Key invariants¶
The type system enforces five guarantees that would be runtime checks in a less strict language. Each one moves a class of bugs from "ship and discover" to "does not compile".
- The 8-state machine in
Par.State_machinerejects illegal task transitions at compile time (configurable in the type itself). - The
Bash_safe_command.commandADT has noExec_raw_shellconstructor; shell injection is unrepresentable. - The
error_categorysum type forces every error path to be explicitly handled; no untyped exceptions in the public API. Eio.Switch.tcancellation propagates to every tool handler viaCancellation_token.t;Runtime.closetriggers whole-tree cancel.- Tool registration surfaces
Error (\Duplicate_tool)` instead of silently overwriting; tool names are unique by construction.
Together these turn the SDK into a library that fails loudly at the boundary (compile error, typed Error variant, Cancelled exception) rather than silently at runtime.
Platform support¶
PAR runs on Linux, macOS, and Windows. The core runtime (agents, LLM calls, persistence, memory, workflows, HTTP/SSE MCP) works on all three platforms. Some capabilities depend on the operating system, and PAR detects them at runtime.
| Platform | Core runtime | Process spawning | Pipe I/O | Signal-based kill |
|---|---|---|---|---|
| Linux | full support | available | available | available |
| macOS | full support | available | available | available |
| Windows | full support | unavailable | unavailable | unavailable |
Windows notes:
- Agents, LLM calls (OpenAI, Anthropic, Ollama), SQLite persistence, memory (Memory_service with FTS5), workflows, and HTTP/SSE MCP all work on Windows.
- Process spawning (the bash tool, MCP stdio transport) returns a typed Unavailable error via Capability.detect instead of crashing. The agent loop handles this gracefully.
- sqlite-vec vector store works on Windows (the vec0.dll is vendored in the build).
- MCP over HTTP/SSE is the recommended transport on Windows since stdio depends on process spawning.
Capability detection API¶
The Capability module provides a single detection point for platform-dependent features:
open Par
let status = Capability.detect () `Process_spawning
(* Linux/macOS: `Available
Windows: `Unavailable "Process spawning requires Eio.Process, ..." *)
let is_win = Capability.is_windows () (* true on Win32, false elsewhere *)
let platform = Capability.platform_name () (* "Linux", "macOS", "Windows", etc. *)
Tool handlers and runtime internals consult Capability.detect rather than scattering Sys.os_type checks across the codebase. This keeps platform gating centralized and testable.
Next steps¶
docs/sdk/agent.md: primary reference, coversRuntime.create,register_agent,register_tool, the ReAct loopdocs/sdk/workflow.md: step taxonomy, checkpointing, human approval, JSON workflow formatdocs/sdk/mcp.md: MCP client lifecycle, event types, server naming and isolationdocs/sdk/middleware.md: all 9 built-in middlewares and themiddleware_hookshapedocs/sdk/tools.md: all 23 built-in tools with input/output schemasdocs/sdk/persistence.md: persistence service, SQLite backend, scope dimensiondocs/quickstart.md: 30-minute hands-on for new usersdocs/explanation/architecture.md: deep dive on data flow, the Eio model, and event payload schemaCHANGES.md: release notes, including the v0.3.1 bash addition and the current test count