English · 简体中文
Content Blocks¶
Messages in PAR carry structured content through content_block list rather than flat strings. When you call Runtime.invoke, the response message contains a list of these blocks — text, tool calls, tool results, or images. Each block in the list can independently hold a cache_control field, which provider adapters (`Anthropic, `Openai) emit in the wire format for prompt caching.
The message type in Types looks like this:
type message = {
role : message_role;
content_blocks : content_block list;
tool_calls : tool_call list option;
tool_call_id : string option;
name : string option;
}
content_blocks replaced the old content : string option field. Every block is a variant of content_block, each with its own cache_control slot.
Block types¶
type content_block =
| Text_block of {
text : string;
cache_control : cache_control option;
}
| Tool_use_block of {
id : string;
name : string;
arguments : Yojson.Safe.t;
cache_control : cache_control option;
}
| Tool_result_block of {
tool_use_id : string;
content : string;
cache_control : cache_control option;
}
| Image_block of {
source : image_source;
media_type : string;
data : string;
cache_control : cache_control option;
}
Text_block holds plain text. The LLM's response text and user input both land here.
Tool_use_block represents an LLM's request to call a tool. The id matches the provider's tool-use identifier; name is the tool name; arguments is the JSON payload.
Tool_result_block carries the tool handler's output back to the conversation. tool_use_id links it to the originating Tool_use_block.
Image_block holds image data, either as a URL reference or base64-encoded bytes. media_type is the MIME type (e.g. "image/png").
Note on reasoning_content¶
reasoning_content : string option is a top-level field on both llm_response and message, not a content_block variant. Reasoning models (o1, o3, DeepSeek-R1) return chain-of-thought text through this field, separate from the main text and tool_calls. The field is populated by the OpenAI provider when the upstream API returns message.reasoning_content (non-streaming) or delta.reasoning_content (streaming).
This is a scope compromise. When Anthropic extended-thinking support lands, reasoning_content will migrate into a Reasoning_block variant in content_block, giving reasoning the same first-class treatment as text and tool use. Until then, check reasoning_content directly on the response or message record.
Construction¶
Build each variant by hand with record syntax:
(* Simple text block *)
let block = Text_block {
text = "Hello, world!";
cache_control = None;
}
(* Text block with prompt caching *)
let cached = Text_block {
text = "System prompt content";
cache_control = Some { type_ = `Ephemeral; ttl = Some `Five_min };
}
(* Tool result from a handler *)
let result = Tool_result_block {
tool_use_id = "call_abc123";
content = "42";
cache_control = None;
}
(* Tool use from LLM response *)
let tool_use = Tool_use_block {
id = "call_abc123";
name = "calculator";
arguments = `Assoc [("expression", `String "6 * 7")];
cache_control = None;
}
(* Image from URL *)
let img = Image_block {
source = Url "https://example.com/photo.png";
media_type = "image/png";
data = "";
cache_control = None;
}
(* Image from base64 *)
let img_b64 = Image_block {
source = Base64 "iVBORw0KGgo...";
media_type = "image/png";
data = "";
cache_control = None;
}
Build a full message by populating the record:
let msg : Types.message = {
role = User;
content_blocks = [
Text_block { text = "What is 2+2?"; cache_control = None };
];
tool_calls = None;
tool_call_id = None;
name = None;
}
Helper functions¶
The Message module provides three helpers for common string-to-block conversions.
content_of_string¶
Wraps a string in a single Text_block with cache_control = None. Returns an empty list for the empty string.
Message.content_of_string "Hello"
(* [Text_block { text = "Hello"; cache_control = None }] *)
Message.content_of_string ""
(* [] *)
string_of_content¶
Extracts text from all Text_block and Tool_result_block variants and concatenates them. Non-text blocks (Tool_use_block, Image_block) are silently skipped.
Message.string_of_content [
Text_block { text = "Hello "; cache_control = None };
Tool_use_block { id = "x"; name = "t"; arguments = `Assoc []; cache_control = None };
Text_block { text = "world"; cache_control = None };
]
(* "Hello world" *)
text_of_message¶
Convenience wrapper. Equivalent to Message.string_of_content msg.content_blocks.
content_opt¶
Returns Some text if the message has any extractable text, or None if the content is empty. Useful for pattern matching on responses.
match Message.content_opt resp with
| Some text -> Printf.printf "Got: %s\n" text
| None -> Printf.printf "No text in response\n"
cache_control¶
Every block variant carries an optional cache_control field:
type cache_control = {
type_ : [`Ephemeral];
ttl : cache_ttl option;
}
type cache_ttl = [`Five_min | `One_hour]
type_ is currently always `Ephemeral. The type is a variant, not a plain alias, so PAR can introduce persistent cache modes later without breaking changes. ttl is the time-to-live hint. The Anthropic wire format recognizes both values; other providers ignore cache_control gracefully.
Setting cache_control¶
There are three ways to mark blocks for caching.
Direct construction. Build the cache_control value inline:
Text_block {
text = "Expensive system prompt";
cache_control = Some { type_ = `Ephemeral; ttl = Some `Five_min };
}
Cache_breakpoint.mark_message. Marks the last block in a message's content_blocks list. This is the recommended user-facing API for caching a whole message:
let marked_msg = Cache_breakpoint.mark_message ~ttl:`One_hour msg
(* The last block in msg.content_blocks now has cache_control set *)
The function walks to the last block and applies cache_control to it regardless of variant. If content_blocks is empty, it's a no-op.
Engine auto-marking. The Engine calls apply_breakpoints internally before each LLM round trip. It evaluates breakpoint candidates (system prompt, per-message, per-tool) against the provider's cache capabilities, then stamps the chosen blocks automatically. You don't need to call this yourself; it runs as part of the ReAct loop.
Provider support¶
Prompt caching is currently supported by the `Anthropic wire format. When the provider adapter encounters a block with cache_control set, it emits the corresponding field in the JSON payload. The `Openai and `Ollama adapters ignore cache_control silently, so setting it on those providers is harmless.
Image blocks¶
Image_block carries a reference to image data along with its MIME type. Url points to an image hosted at a remote URL; the provider fetches it. Base64 embeds the raw bytes directly in the block.
Image blocks are part of PAR's type-level preparation for multimodal content. The Anthropic wire format already supports image blocks natively. User-facing image tools and higher-level APIs are planned for v0.7.
Migration from string¶
The old message type had content : string option. The new type, used by Runtime.create and all agent interactions, replaces this with content_blocks : content_block list. Here's how to migrate:
(* Old pattern *)
let msg = {
role = User;
content = Some "hello";
tool_calls = None;
tool_call_id = None;
name = None;
}
(* New pattern *)
let msg = {
role = User;
content_blocks = Message.content_of_string "hello";
tool_calls = None;
tool_call_id = None;
name = None;
}
For multi-block content, build the list explicitly:
let msg = {
role = User;
content_blocks = [
Text_block { text = "Describe this image:"; cache_control = None };
Image_block {
source = Url "https://example.com/photo.png";
media_type = "image/png";
data = "";
cache_control = None;
};
];
tool_calls = None;
tool_call_id = None;
name = None;
}
Reading content back out works through the same helpers, so code that previously used msg.content directly should switch to Message.text_of_message msg or Message.content_opt msg.
See also¶
- Agent API -- Runtime configuration, agent config, tool registration
- Streaming API -- Token streaming with content block events
- Middleware API -- Pipeline hooks that see content blocks