My agentic slop goes here. Not intended for anyone else!

claude

-253
claudeio/TODO.md
···
-
# TODO: Missing Features from Python SDK
-
-
## 1. Hook Support
-
-
### Overview
-
Hooks allow users to intercept and modify Claude's behavior at specific points during execution. The Python SDK supports several hook events that are not yet implemented in the OCaml library.
-
-
### Required Components
-
-
#### Hook Events
-
```ocaml
-
type hook_event =
-
| Pre_tool_use (* Before a tool is invoked *)
-
| Post_tool_use (* After a tool completes *)
-
| User_prompt_submit (* When user submits a prompt *)
-
| Stop (* When stopping execution *)
-
| Subagent_stop (* When a subagent stops *)
-
| Pre_compact (* Before context compaction *)
-
```
-
-
#### Hook Context
-
```ocaml
-
module Hook_context : sig
-
type t = {
-
signal : [ `Abort ] option; (* Future: abort signal support *)
-
}
-
end
-
```
-
-
#### Hook Output
-
```ocaml
-
module Hook_output : sig
-
type t = {
-
decision : [ `Block | `Continue ] option;
-
system_message : string option;
-
hook_specific_output : Ezjsonm.value option;
-
}
-
end
-
```
-
-
#### Hook Callback
-
```ocaml
-
type hook_callback =
-
input:Ezjsonm.value ->
-
tool_use_id:string option ->
-
context:Hook_context.t ->
-
Hook_output.t Eio.Promise.t
-
```
-
-
#### Hook Matcher
-
```ocaml
-
module Hook_matcher : sig
-
type t = {
-
matcher : string option; (* e.g., "Bash" or "Write|MultiEdit|Edit" *)
-
hooks : hook_callback list;
-
}
-
end
-
```
-
-
### Implementation Plan
-
-
1. **Add hook types to a new `lib/hooks.mli` module**
-
2. **Integrate hooks into `Options.t`**:
-
- Add `hooks : (hook_event * Hook_matcher.t list) list` field
-
3. **Update `Client` module to handle hook callbacks**:
-
- Intercept tool use events
-
- Call registered hooks before/after operations
-
- Handle hook responses (block, modify, continue)
-
4. **Update SDK control protocol** to support hook registration via `SDKControlInitializeRequest`
-
-
### Usage Example
-
```ocaml
-
let pre_tool_hook ~input ~tool_use_id:_ ~context:_ =
-
match Ezjsonm.find input ["name"] |> Ezjsonm.get_string with
-
| "Bash" ->
-
Eio.Promise.resolve Hook_output.{
-
decision = Some `Block;
-
system_message = Some "Bash commands blocked by hook";
-
hook_specific_output = None;
-
}
-
| _ ->
-
Eio.Promise.resolve Hook_output.{
-
decision = Some `Continue;
-
system_message = None;
-
hook_specific_output = None;
-
}
-
-
let options = Options.create
-
~hooks:[
-
Pre_tool_use, [{
-
matcher = Some "Bash";
-
hooks = [pre_tool_hook]
-
}]
-
]
-
()
-
```
-
-
## 2. MCP (Model Context Protocol) Server Support
-
-
### Overview
-
MCP servers allow Claude to interact with external services and tools. The Python SDK supports multiple MCP server configurations.
-
-
### Required Components
-
-
#### MCP Server Types
-
```ocaml
-
module Mcp_server : sig
-
type stdio_config = {
-
command : string;
-
args : string list option;
-
env : (string * string) list option;
-
}
-
-
type sse_config = {
-
url : string;
-
headers : (string * string) list option;
-
}
-
-
type http_config = {
-
url : string;
-
headers : (string * string) list option;
-
}
-
-
type sdk_config = {
-
name : string;
-
(* In OCaml, we'd need to define an MCP server interface *)
-
instance : mcp_server;
-
}
-
-
and mcp_server = <
-
(* MCP server methods would go here *)
-
>
-
-
type config =
-
| Stdio of stdio_config
-
| SSE of sse_config
-
| HTTP of http_config
-
| SDK of sdk_config
-
end
-
```
-
-
### Implementation Plan
-
-
1. **Create `lib/mcp.mli` module** with server configuration types
-
2. **Add MCP support to `Options.t`**:
-
- Add `mcp_servers : (string * Mcp_server.config) list` field
-
3. **Create MCP transport layer**:
-
- Stdio: Use Eio.Process for subprocess communication
-
- SSE: Use Cohttp and event stream parsing
-
- HTTP: Use Cohttp for REST API calls
-
- SDK: Direct OCaml object interface
-
4. **Update SDK control protocol** to handle `SDKControlMcpMessageRequest`
-
5. **Implement MCP message routing** in Client module
-
-
### Usage Example
-
```ocaml
-
let stdio_server = Mcp_server.Stdio {
-
command = "calculator-server";
-
args = Some ["--mode", "advanced"];
-
env = None;
-
}
-
-
let http_server = Mcp_server.HTTP {
-
url = "https://api.example.com/mcp";
-
headers = Some [("Authorization", "Bearer token")];
-
}
-
-
let options = Options.create
-
~mcp_servers:[
-
"calculator", stdio_server;
-
"api", http_server;
-
]
-
()
-
```
-
-
### MCP Message Flow
-
-
1. Claude requests tool use from MCP server
-
2. Client sends `mcp_message` control request
-
3. SDK routes message to appropriate MCP server
-
4. MCP server responds with result
-
5. Client forwards result back to Claude
-
-
## 3. Integration with SDK Control Protocol
-
-
Both hooks and MCP will require updates to the SDK control protocol:
-
-
### Control Request Types
-
```ocaml
-
module Sdk_control : sig
-
type interrupt_request = {
-
subtype : [`Interrupt];
-
}
-
-
type permission_request = {
-
subtype : [`Can_use_tool];
-
tool_name : string;
-
input : Ezjsonm.value;
-
permission_suggestions : Permissions.Update.t list option;
-
blocked_path : string option;
-
}
-
-
type initialize_request = {
-
subtype : [`Initialize];
-
hooks : (hook_event * Ezjsonm.value) list option;
-
}
-
-
type set_permission_mode_request = {
-
subtype : [`Set_permission_mode];
-
mode : Permissions.Mode.t;
-
}
-
-
type hook_callback_request = {
-
subtype : [`Hook_callback];
-
callback_id : string;
-
input : Ezjsonm.value;
-
tool_use_id : string option;
-
}
-
-
type mcp_message_request = {
-
subtype : [`Mcp_message];
-
server_name : string;
-
message : Ezjsonm.value;
-
}
-
-
type request =
-
| Interrupt of interrupt_request
-
| Permission of permission_request
-
| Initialize of initialize_request
-
| Set_permission_mode of set_permission_mode_request
-
| Hook_callback of hook_callback_request
-
| Mcp_message of mcp_message_request
-
end
-
```
-
-
## Implementation Priority
-
-
1. **Phase 1**: Implement typed SDK control protocol (prerequisite for both)
-
2. **Phase 2**: Implement hook support (simpler, self-contained)
-
3. **Phase 3**: Implement MCP server support (requires external dependencies)
-
-
## Testing Strategy
-
-
### Hooks
-
- Unit tests for hook registration and matching
-
- Integration tests with mock tool invocations
-
- Test hook blocking, modification, and pass-through scenarios
-
-
### MCP
-
- Unit tests for configuration parsing
-
- Mock MCP server for integration testing
-
- Test different transport types (stdio, HTTP, SSE)
-
- Test message routing and error handling
+1 -1
claudeio/claude.opam
···
"eio"
"fmt"
"logs"
-
"ezjsonm"
"jsont" {>= "0.2.0"}
+
"jsont_bytesrw" {>= "0.2.0"}
"alcotest" {with-test}
"odoc" {with-doc}
]
+1 -1
claudeio/dune-project
···
eio
fmt
logs
-
ezjsonm
(jsont (>= 0.2.0))
+
(jsont_bytesrw (>= 0.2.0))
(alcotest :with-test)))
+1
claudeio/lib/claude.ml
···
module Permissions = Permissions
module Hooks = Hooks
module Sdk_control = Sdk_control
+
module Incoming = Incoming
module Structured_output = Structured_output
module Options = Options
module Transport = Transport
+3
claudeio/lib/claude.mli
···
module Sdk_control = Sdk_control
(** SDK control protocol for dynamic configuration. *)
+
module Incoming = Incoming
+
(** Discriminated union of all incoming message types from Claude CLI. *)
+
module Structured_output = Structured_output
(** Structured output support using JSON Schema. *)
+184 -104
claudeio/lib/client.ml
···
let src = Logs.Src.create "claude.client" ~doc:"Claude client"
module Log = (val Logs.src_log src : Logs.LOG)
+
(* Helper functions for JSON manipulation *)
+
let json_to_string json =
+
match Jsont_bytesrw.encode_string' Jsont.json json with
+
| Ok s -> s
+
| Error err -> failwith (Jsont.Error.to_string err)
+
+
let json_of_string s =
+
match Jsont_bytesrw.decode_string' Jsont.json s with
+
| Ok j -> j
+
| Error err -> failwith (Jsont.Error.to_string err)
+
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let rec find json path =
+
match path with
+
| [] -> json
+
| key :: rest ->
+
match get_field json key with
+
| Some value -> find value rest
+
| None -> raise Not_found
+
+
let find_string json path =
+
let value = find json path in
+
match value with
+
| Jsont.String (s, _) -> s
+
| _ -> raise (Invalid_argument "Expected string value")
+
+
let json_string s = Jsont.String (s, Jsont.Meta.none)
+
let json_null = Jsont.Null ((), Jsont.Meta.none)
+
+
let json_dict pairs =
+
let members = List.map (fun (k, v) -> ((k, Jsont.Meta.none), v)) pairs in
+
Jsont.Object (members, Jsont.Meta.none)
+
type t = {
transport : Transport.t;
permission_callback : Permissions.callback option;
···
hook_callbacks : (string, Hooks.callback) Hashtbl.t;
mutable next_callback_id : int;
mutable session_id : string option;
-
control_responses : (string, Ezjsonm.value) Hashtbl.t;
+
control_responses : (string, Jsont.json) Hashtbl.t;
control_mutex : Eio.Mutex.t;
control_condition : Eio.Condition.t;
}
let handle_control_request t control_msg =
-
let open Ezjsonm in
let data = Control.data control_msg in
Log.info (fun m -> m "Handling control request: %s" (Control.subtype control_msg));
-
Log.info (fun m -> m "Control request data: %s" (value_to_string data));
-
match Json_utils.find_string data ["request"; "subtype"] with
+
Log.info (fun m -> m "Control request data: %s" (json_to_string data));
+
match find_string data ["request"; "subtype"] with
| "can_use_tool" ->
-
let tool_name = Json_utils.find_string data ["request"; "tool_name"] in
+
let tool_name = find_string data ["request"; "tool_name"] in
let input = find data ["request"; "input"] in
Log.info (fun m -> m "Permission request for tool '%s' with input: %s"
-
tool_name (value_to_string input));
+
tool_name (json_to_string input));
let suggestions =
try
let sugg_json = find data ["request"; "permission_suggestions"] in
match sugg_json with
-
| `A _ ->
+
| Jsont.Array _ ->
(* TODO: Parse permission suggestions *)
[]
| _ -> []
···
(* Convert permission result to CLI format: {"behavior": "allow", "updatedInput": ...} or {"behavior": "deny", "message": ...} *)
let response_data = match result with
-
| Permissions.Result.Allow { updated_input; updated_permissions = _ } ->
+
| Permissions.Result.Allow { updated_input; updated_permissions = _; unknown = _ } ->
(* updatedInput is required when allowing - use original input if not modified *)
let updated_input = match updated_input with
| Some inp -> inp
| None -> input (* Use original input *)
in
-
dict [
-
("behavior", string "allow");
+
json_dict [
+
("behavior", json_string "allow");
("updatedInput", updated_input);
]
-
| Permissions.Result.Deny { message; interrupt = _ } ->
-
dict [
-
("behavior", string "deny");
-
("message", string message);
+
| Permissions.Result.Deny { message; interrupt = _; unknown = _ } ->
+
json_dict [
+
("behavior", json_string "deny");
+
("message", json_string message);
]
in
-
let response = dict [
-
"type", string "control_response";
-
"response", dict [
-
"subtype", string "success";
-
"request_id", string (Control.request_id control_msg);
+
let response = json_dict [
+
"type", json_string "control_response";
+
"response", json_dict [
+
"subtype", json_string "success";
+
"request_id", json_string (Control.request_id control_msg);
"response", response_data
]
] in
-
Log.info (fun m -> m "Sending control response: %s" (value_to_string response));
+
Log.info (fun m -> m "Sending control response: %s" (json_to_string response));
Transport.send t.transport response
| "hook_callback" ->
-
let callback_id = Json_utils.find_string data ["request"; "callback_id"] in
+
let callback_id = find_string data ["request"; "callback_id"] in
let input = find data ["request"; "input"] in
let tool_use_id =
-
try Some (Json_utils.find_string data ["request"; "tool_use_id"])
+
try Some (find_string data ["request"; "tool_use_id"])
with Not_found -> None
in
Log.info (fun m -> m "Hook callback request for callback_id: %s" callback_id);
···
let context = Hooks.Context.create () in
let result = callback ~input ~tool_use_id ~context in
-
let response = dict [
-
"type", string "control_response";
-
"response", dict [
-
"subtype", string "success";
-
"request_id", string (Control.request_id control_msg);
-
"response", Hooks.result_to_json result
+
let result_json = match Jsont.Json.encode Hooks.result_jsont result with
+
| Ok j -> j
+
| Error msg -> failwith ("Failed to encode hook result: " ^ msg)
+
in
+
+
let response = json_dict [
+
"type", json_string "control_response";
+
"response", json_dict [
+
"subtype", json_string "success";
+
"request_id", json_string (Control.request_id control_msg);
+
"response", result_json
]
] in
Log.info (fun m -> m "Hook callback succeeded, sending response");
···
| Not_found ->
let error_msg = Printf.sprintf "Hook callback not found: %s" callback_id in
Log.err (fun m -> m "%s" error_msg);
-
let response = dict [
-
"type", string "control_response";
-
"response", dict [
-
"subtype", string "error";
-
"request_id", string (Control.request_id control_msg);
-
"error", string error_msg
+
let response = json_dict [
+
"type", json_string "control_response";
+
"response", json_dict [
+
"subtype", json_string "error";
+
"request_id", json_string (Control.request_id control_msg);
+
"error", json_string error_msg
]
] in
Transport.send t.transport response
| exn ->
let error_msg = Printf.sprintf "Hook callback error: %s" (Printexc.to_string exn) in
Log.err (fun m -> m "%s" error_msg);
-
let response = dict [
-
"type", string "control_response";
-
"response", dict [
-
"subtype", string "error";
-
"request_id", string (Control.request_id control_msg);
-
"error", string error_msg
+
let response = json_dict [
+
"type", json_string "control_response";
+
"response", json_dict [
+
"subtype", json_string "error";
+
"request_id", json_string (Control.request_id control_msg);
+
"error", json_string error_msg
]
] in
Transport.send t.transport response)
| subtype ->
(* Respond with error for unknown control requests *)
-
let response = dict [
-
"type", string "control_response";
-
"response", dict [
-
"subtype", string "error";
-
"request_id", string (Control.request_id control_msg);
-
"error", string (Printf.sprintf "Unsupported control request: %s" subtype)
+
let response = json_dict [
+
"type", json_string "control_response";
+
"response", json_dict [
+
"subtype", json_string "error";
+
"request_id", json_string (Control.request_id control_msg);
+
"error", json_string (Printf.sprintf "Unsupported control request: %s" subtype)
]
] in
Transport.send t.transport response
+
let handle_control_response t control_resp =
+
let request_id = match control_resp.Sdk_control.response with
+
| Sdk_control.Response.Success s -> s.request_id
+
| Sdk_control.Response.Error e -> e.request_id
+
in
+
Log.debug (fun m -> m "Received control response for request_id: %s" request_id);
+
+
(* Store the response as JSON and signal waiting threads *)
+
let json = match Jsont.Json.encode Sdk_control.control_response_jsont control_resp with
+
| Ok j -> j
+
| Error err -> failwith ("Failed to encode control response: " ^ err)
+
in
+
Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
+
Hashtbl.replace t.control_responses request_id json;
+
Eio.Condition.broadcast t.control_condition
+
)
+
let handle_messages t =
let rec loop () =
match Transport.receive_line t.transport with
-
| None ->
+
| None ->
(* EOF *)
Log.debug (fun m -> m "Handle messages: EOF received");
Seq.Nil
| Some line ->
try
-
let json = Ezjsonm.value_from_string line in
-
-
(* Check if it's a control request or response *)
-
match Json_utils.find_string json ["type"] with
+
(* First check if it's a control_request (special case, not in Incoming) *)
+
let json = json_of_string line in
+
match find_string json ["type"] with
| "control_request" ->
let control_msg = Control.create
-
~request_id:(Json_utils.find_string json ["request_id"])
-
~subtype:(Json_utils.find_string json ["request"; "subtype"])
+
~request_id:(find_string json ["request_id"])
+
~subtype:(find_string json ["request"; "subtype"])
~data:json in
-
Log.info (fun m -> m "🎯 Received control request: %s (request_id: %s)"
+
Log.info (fun m -> m "Received control request: %s (request_id: %s)"
(Control.subtype control_msg) (Control.request_id control_msg));
handle_control_request t control_msg;
loop ()
-
| "control_response" ->
-
(* Handle control responses (e.g., initialize response) *)
-
let request_id = Json_utils.find_string json ["response"; "request_id"] in
-
Log.debug (fun m -> m "Received control response for request_id: %s" request_id);
-
(* Store the response and signal waiting threads *)
-
Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
-
Hashtbl.replace t.control_responses request_id json;
-
Eio.Condition.broadcast t.control_condition
-
);
-
loop ()
-
| _ ->
-
(* Regular message *)
-
let msg = Message.of_json json in
-
Log.info (fun m -> m "← %a" Message.pp msg);
-
-
(* Extract session ID from system messages *)
-
(match msg with
-
| Message.System sys when Message.System.subtype sys = "init" ->
-
(match Message.System.Data.get_string (Message.System.data sys) "session_id" with
-
| Some session_id ->
-
t.session_id <- Some session_id;
-
Log.debug (fun m -> m "Stored session ID: %s" session_id)
-
| None -> ())
-
| _ -> ());
-
-
Seq.Cons (msg, loop)
+
(* Use Incoming codec for all other message types *)
+
match Jsont_bytesrw.decode_string' Incoming.jsont line with
+
| Ok (Incoming.Message msg) ->
+
Log.info (fun m -> m "← %a" Message.pp msg);
+
+
(* Extract session ID from system messages *)
+
(match msg with
+
| Message.System sys when Message.System.subtype sys = "init" ->
+
(match Message.System.Data.get_string (Message.System.data sys) "session_id" with
+
| Some session_id ->
+
t.session_id <- Some session_id;
+
Log.debug (fun m -> m "Stored session ID: %s" session_id)
+
| None -> ())
+
| _ -> ());
+
+
Seq.Cons (msg, loop)
+
+
| Ok (Incoming.Control_response resp) ->
+
handle_control_response t resp;
+
loop ()
+
+
| Error err ->
+
Log.err (fun m -> m "Failed to decode incoming message: %s\nLine: %s"
+
(Jsont.Error.to_string err) line);
+
loop ()
with
| exn ->
-
Log.err (fun m -> m "Failed to parse message: %s\nLine: %s"
+
Log.err (fun m -> m "Failed to parse message: %s\nLine: %s"
(Printexc.to_string exn) line);
loop ()
in
···
Log.debug (fun m -> m "Registered callback: %s for event: %s" callback_id event_name);
callback_id
) matcher.Hooks.callbacks in
-
Ezjsonm.dict [
+
json_dict [
"matcher", (match matcher.Hooks.matcher with
-
| Some p -> Ezjsonm.string p
-
| None -> `Null);
-
"hookCallbackIds", `A (List.map (fun id -> Ezjsonm.string id) callback_ids);
+
| Some p -> json_string p
+
| None -> json_null);
+
"hookCallbackIds", Jsont.Array (List.map (fun id -> json_string id) callback_ids, Jsont.Meta.none);
]
) matchers in
-
(event_name, `A matchers_json) :: acc
+
(event_name, Jsont.Array (matchers_json, Jsont.Meta.none)) :: acc
) [] hooks_config in
(* Send initialize control request *)
-
let initialize_msg = Ezjsonm.dict [
-
"type", Ezjsonm.string "control_request";
-
"request_id", Ezjsonm.string "init_hooks";
-
"request", Ezjsonm.dict [
-
"subtype", Ezjsonm.string "initialize";
-
"hooks", Ezjsonm.dict hooks_json;
+
let initialize_msg = json_dict [
+
"type", json_string "control_request";
+
"request_id", json_string "init_hooks";
+
"request", json_dict [
+
"subtype", json_string "initialize";
+
"hooks", json_dict hooks_json;
]
] in
Log.info (fun m -> m "Sending hooks initialize request");
···
let query t prompt =
let msg = Message.user_string prompt in
Log.info (fun m -> m "→ %a" Message.pp msg);
-
Transport.send t.transport (Message.to_json msg)
+
let json = match Jsont.Json.encode Message.jsont msg with
+
| Ok j -> j
+
| Error err -> failwith ("Failed to encode message: " ^ err)
+
in
+
Transport.send t.transport json
let send_message t msg =
Log.info (fun m -> m "→ %a" Message.pp msg);
-
Transport.send t.transport (Message.to_json msg)
+
let json = match Jsont.Json.encode Message.jsont msg with
+
| Ok j -> j
+
| Error err -> failwith ("Failed to encode message: " ^ err)
+
in
+
Transport.send t.transport json
let send_user_message t user_msg =
let msg = Message.User user_msg in
Log.info (fun m -> m "→ %a" Message.pp msg);
-
Transport.send t.transport (Message.User.to_json user_msg)
+
let json = match Jsont.Json.encode Message.User.jsont user_msg with
+
| Ok j -> j
+
| Error err -> failwith ("Failed to encode user message: " ^ err)
+
in
+
Transport.send t.transport json
let receive t =
handle_messages t
···
(* Helper to send a control request and wait for response *)
let send_control_request t ~request_id request =
-
let open Ezjsonm in
(* Send the control request *)
-
let control_msg = Sdk_control.create_request ~request_id ~request in
-
let json = Sdk_control.to_json control_msg in
-
Log.info (fun m -> m "Sending control request: %s" (value_to_string json));
+
let control_msg = Sdk_control.create_request ~request_id ~request () in
+
let json = match Jsont.Json.encode Sdk_control.jsont control_msg with
+
| Ok j -> j
+
| Error msg -> failwith ("Failed to encode control request: " ^ msg)
+
in
+
Log.info (fun m -> m "Sending control request: %s" (json_to_string json));
Transport.send t.transport json;
(* Wait for the response with timeout *)
···
in
let response_json = wait_for_response () in
-
Log.debug (fun m -> m "Received control response: %s" (value_to_string response_json));
+
Log.debug (fun m -> m "Received control response: %s" (json_to_string response_json));
(* Parse the response *)
-
let response = find response_json ["response"] |> Sdk_control.Response.of_json in
+
let response_data = find response_json ["response"] in
+
let response = match Jsont.Json.decode Sdk_control.Response.jsont response_data with
+
| Ok r -> r
+
| Error msg -> raise (Invalid_argument ("Failed to decode response: " ^ msg))
+
in
match response with
| Sdk_control.Response.Success s -> s.response
| Sdk_control.Response.Error e ->
···
let set_permission_mode t mode =
let request_id = Printf.sprintf "set_perm_mode_%f" (Unix.gettimeofday ()) in
-
let request = Sdk_control.Request.set_permission_mode ~mode in
+
let request = Sdk_control.Request.set_permission_mode ~mode () in
let _response = send_control_request t ~request_id request in
Log.info (fun m -> m "Permission mode set to: %a" Permissions.Mode.pp mode)
let set_model t model =
let model_str = Model.to_string model in
let request_id = Printf.sprintf "set_model_%f" (Unix.gettimeofday ()) in
-
let request = Sdk_control.Request.set_model ~model:model_str in
+
let request = Sdk_control.Request.set_model ~model:model_str () in
let _response = send_control_request t ~request_id request in
Log.info (fun m -> m "Model set to: %a" Model.pp model)
···
let request = Sdk_control.Request.get_server_info () in
match send_control_request t ~request_id request with
| Some response_data ->
-
let server_info = Sdk_control.Server_info.of_json response_data in
+
let server_info = match Jsont.Json.decode Sdk_control.Server_info.jsont response_data with
+
| Ok si -> si
+
| Error msg -> raise (Invalid_argument ("Failed to decode server info: " ^ msg))
+
in
Log.info (fun m -> m "Retrieved server info: %a" Sdk_control.Server_info.pp server_info);
server_info
| None ->
+215 -130
claudeio/lib/content_block.ml
···
-
open Ezjsonm
-
module JU = Json_utils
-
let src = Logs.Src.create "claude.content_block" ~doc:"Claude content blocks"
module Log = (val Logs.src_log src : Logs.LOG)
module Text = struct
-
type t = { text : string }
-
-
let create text = { text }
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type t = {
+
text : string;
+
unknown : Unknown.t;
+
}
+
+
let create text = { text; unknown = Unknown.empty }
+
+
let make text unknown = { text; unknown }
let text t = t.text
-
-
let to_json t =
-
`O [("type", `String "text"); ("text", `String t.text)]
-
-
let of_json = function
-
| `O fields ->
-
let text = JU.assoc_string "text" fields in
-
{ text }
-
| _ -> raise (Invalid_argument "Text.of_json: expected object")
-
-
let pp fmt t =
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Text" make
+
|> Jsont.Object.mem "text" Jsont.string ~enc:text
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
+
let to_json t =
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Text.to_json: " ^ msg)
+
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Text.of_json: " ^ msg))
+
+
let pp fmt t =
if String.length t.text > 60 then
let truncated = String.sub t.text 0 57 in
Fmt.pf fmt "Text[%s...]" truncated
···
module Tool_use = struct
module Input = struct
-
type t = value
-
+
type t = Jsont.json
+
+
let jsont = Jsont.json
+
let of_string_pairs pairs =
-
`O (List.map (fun (k, v) -> (k, `String v)) pairs)
-
-
let of_assoc assoc = `O assoc
-
-
let get_string t key = JU.get_field_string_opt t key
-
-
let get_int t key = JU.get_field_int_opt t key
-
-
let get_bool t key = JU.get_field_bool_opt t key
-
-
let get_float t key = JU.get_field_float_opt t key
-
+
Jsont.Object (
+
List.map (fun (k, v) ->
+
((Jsont.Json.name k), Jsont.String (v, Jsont.Meta.none))
+
) pairs,
+
Jsont.Meta.none
+
)
+
+
let of_assoc (assoc : (string * Jsont.json) list) : t =
+
Jsont.Object (
+
List.map (fun (k, v) -> (Jsont.Json.name k, v)) assoc,
+
Jsont.Meta.none
+
)
+
+
let get_field t key =
+
match t with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string t key =
+
match get_field t key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
+
+
let get_int t key =
+
match get_field t key with
+
| Some (Jsont.Number (f, _)) ->
+
let i = int_of_float f in
+
if float_of_int i = f then Some i else None
+
| _ -> None
+
+
let get_bool t key =
+
match get_field t key with
+
| Some (Jsont.Bool (b, _)) -> Some b
+
| _ -> None
+
+
let get_float t key =
+
match get_field t key with
+
| Some (Jsont.Number (f, _)) -> Some f
+
| _ -> None
+
let keys t =
match t with
-
| `O fields -> List.map fst fields
+
| Jsont.Object (members, _) -> List.map (fun ((name, _), _) -> name) members
| _ -> []
-
+
let to_json t = t
let of_json json = json
end
-
+
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
id : string;
name : string;
input : Input.t;
+
unknown : Unknown.t;
}
-
-
let create ~id ~name ~input = { id; name; input }
+
+
let create ~id ~name ~input = { id; name; input; unknown = Unknown.empty }
+
+
let make id name input unknown = { id; name; input; unknown }
let id t = t.id
let name t = t.name
let input t = t.input
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Tool_use" make
+
|> Jsont.Object.mem "id" Jsont.string ~enc:id
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "input" Input.jsont ~enc:input
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
-
`O [
-
("type", `String "tool_use");
-
("id", `String t.id);
-
("name", `String t.name);
-
("input", Input.to_json t.input);
-
]
-
-
let of_json = function
-
| `O fields ->
-
let id = JU.assoc_string "id" fields in
-
let name = JU.assoc_string "name" fields in
-
let input = Input.of_json (List.assoc "input" fields) in
-
{ id; name; input }
-
| _ -> raise (Invalid_argument "Tool_use.of_json: expected object")
-
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Tool_use.to_json: " ^ msg)
+
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Tool_use.of_json: " ^ msg))
+
let pp fmt t =
let keys = Input.keys t.input in
let key_info = match keys with
···
end
module Tool_result = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
tool_use_id : string;
content : string option;
is_error : bool option;
+
unknown : Unknown.t;
}
-
-
let create ~tool_use_id ?content ?is_error () =
-
{ tool_use_id; content; is_error }
-
+
+
let create ~tool_use_id ?content ?is_error () =
+
{ tool_use_id; content; is_error; unknown = Unknown.empty }
+
+
let make tool_use_id content is_error unknown =
+
{ tool_use_id; content; is_error; unknown }
let tool_use_id t = t.tool_use_id
let content t = t.content
let is_error t = t.is_error
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Tool_result" make
+
|> Jsont.Object.mem "tool_use_id" Jsont.string ~enc:tool_use_id
+
|> Jsont.Object.opt_mem "content" Jsont.string ~enc:content
+
|> Jsont.Object.opt_mem "is_error" Jsont.bool ~enc:is_error
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
-
let fields = [
-
("type", `String "tool_result");
-
("tool_use_id", `String t.tool_use_id);
-
] in
-
let fields = match t.content with
-
| Some c -> ("content", `String c) :: fields
-
| None -> fields
-
in
-
let fields = match t.is_error with
-
| Some e -> ("is_error", `Bool e) :: fields
-
| None -> fields
-
in
-
`O fields
-
-
let of_json = function
-
| `O fields ->
-
let tool_use_id = JU.assoc_string "tool_use_id" fields in
-
let content =
-
match List.assoc_opt "content" fields with
-
| Some (`String s) -> Some s
-
| Some (`A blocks) ->
-
(* Handle content as array of blocks - extract text *)
-
let texts = List.filter_map (function
-
| `O block_fields ->
-
(match List.assoc_opt "type" block_fields with
-
| Some (`String "text") ->
-
(match List.assoc_opt "text" block_fields with
-
| Some (`String text) -> Some text
-
| _ -> None)
-
| _ -> None)
-
| _ -> None
-
) blocks in
-
if texts = [] then None else Some (String.concat "\n" texts)
-
| _ -> None
-
in
-
let is_error = JU.assoc_bool_opt "is_error" fields in
-
{ tool_use_id; content; is_error }
-
| _ -> raise (Invalid_argument "Tool_result.of_json: expected object")
-
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Tool_result.to_json: " ^ msg)
+
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Tool_result.of_json: " ^ msg))
+
let pp fmt t =
match t.is_error, t.content with
-
| Some true, Some c ->
+
| Some true, Some c ->
if String.length c > 40 then
let truncated = String.sub c 0 37 in
Fmt.pf fmt "ToolResult[error: %s...]" truncated
···
end
module Thinking = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
thinking : string;
signature : string;
+
unknown : Unknown.t;
}
-
-
let create ~thinking ~signature = { thinking; signature }
+
+
let create ~thinking ~signature = { thinking; signature; unknown = Unknown.empty }
+
+
let make thinking signature unknown = { thinking; signature; unknown }
let thinking t = t.thinking
let signature t = t.signature
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Thinking" make
+
|> Jsont.Object.mem "thinking" Jsont.string ~enc:thinking
+
|> Jsont.Object.mem "signature" Jsont.string ~enc:signature
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
-
`O [
-
("type", `String "thinking");
-
("thinking", `String t.thinking);
-
("signature", `String t.signature);
-
]
-
-
let of_json = function
-
| `O fields ->
-
let thinking = JU.assoc_string "thinking" fields in
-
let signature = JU.assoc_string "signature" fields in
-
{ thinking; signature }
-
| _ -> raise (Invalid_argument "Thinking.of_json: expected object")
-
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Thinking.to_json: " ^ msg)
+
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Thinking.of_json: " ^ msg))
+
let pp fmt t =
if String.length t.thinking > 50 then
let truncated = String.sub t.thinking 0 47 in
···
let text s = Text (Text.create s)
let tool_use ~id ~name ~input = Tool_use (Tool_use.create ~id ~name ~input)
-
let tool_result ~tool_use_id ?content ?is_error () =
+
let tool_result ~tool_use_id ?content ?is_error () =
Tool_result (Tool_result.create ~tool_use_id ?content ?is_error ())
-
let thinking ~thinking ~signature =
+
let thinking ~thinking ~signature =
Thinking (Thinking.create ~thinking ~signature)
-
let to_json = function
-
| Text t -> Text.to_json t
-
| Tool_use t -> Tool_use.to_json t
-
| Tool_result t -> Tool_result.to_json t
-
| Thinking t -> Thinking.to_json t
+
let jsont : t Jsont.t =
+
let case_map kind obj dec = Jsont.Object.Case.map kind obj ~dec in
+
+
let case_text = case_map "text" Text.jsont (fun v -> Text v) in
+
let case_tool_use = case_map "tool_use" Tool_use.jsont (fun v -> Tool_use v) in
+
let case_tool_result = case_map "tool_result" Tool_result.jsont (fun v -> Tool_result v) in
+
let case_thinking = case_map "thinking" Thinking.jsont (fun v -> Thinking v) in
+
+
let enc_case = function
+
| Text v -> Jsont.Object.Case.value case_text v
+
| Tool_use v -> Jsont.Object.Case.value case_tool_use v
+
| Tool_result v -> Jsont.Object.Case.value case_tool_result v
+
| Thinking v -> Jsont.Object.Case.value case_thinking v
+
in
+
+
let cases = Jsont.Object.Case.[
+
make case_text;
+
make case_tool_use;
+
make case_tool_result;
+
make case_thinking
+
] in
+
+
Jsont.Object.map ~kind:"Content_block" Fun.id
+
|> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
+
~tag_to_string:Fun.id ~tag_compare:String.compare
+
|> Jsont.Object.finish
+
+
let to_json t =
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Content_block.to_json: " ^ msg)
let of_json json =
-
match json with
-
| `O fields -> (
-
match List.assoc_opt "type" fields with
-
| Some (`String "text") -> Text (Text.of_json json)
-
| Some (`String "tool_use") -> Tool_use (Tool_use.of_json json)
-
| Some (`String "tool_result") -> Tool_result (Tool_result.of_json json)
-
| Some (`String "thinking") -> Thinking (Thinking.of_json json)
-
| _ -> raise (Invalid_argument "Content_block.of_json: unknown type")
-
)
-
| _ -> raise (Invalid_argument "Content_block.of_json: expected object")
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Content_block.of_json: " ^ msg))
let pp fmt = function
| Text t -> Text.pp fmt t
+102 -56
claudeio/lib/content_block.mli
···
(** Content blocks for Claude messages.
-
+
This module defines the various types of content blocks that can appear
in Claude messages, including text, tool use, tool results, and thinking blocks. *)
···
module Text : sig
(** Plain text content blocks. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of text blocks. *)
-
+
val create : string -> t
(** [create text] creates a new text block with the given text content. *)
-
+
val text : t -> string
(** [text t] returns the text content of the block. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for text blocks. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the text block to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
-
(** [of_json json] parses a text block from JSON.
+
+
val of_json : Jsont.json -> t
+
(** [of_json json] parses a text block from JSON.
@raise Invalid_argument if the JSON is not a valid text block. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the text block. *)
end
···
module Tool_use : sig
(** Tool invocation requests from the assistant. *)
-
+
module Input : sig
(** Tool input parameters. *)
-
+
type t
-
(** Abstract type for tool inputs. *)
-
+
(** Abstract type for tool inputs (opaque JSON). *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for tool inputs. *)
+
val of_string_pairs : (string * string) list -> t
(** [of_string_pairs pairs] creates tool input from string key-value pairs. *)
-
-
val of_assoc : (string * Ezjsonm.value) list -> t
+
+
val of_assoc : (string * Jsont.json) list -> t
(** [of_assoc assoc] creates tool input from an association list. *)
-
+
val get_string : t -> string -> string option
(** [get_string t key] returns the string value for [key], if present. *)
-
+
val get_int : t -> string -> int option
(** [get_int t key] returns the integer value for [key], if present. *)
-
+
val get_bool : t -> string -> bool option
(** [get_bool t key] returns the boolean value for [key], if present. *)
-
+
val get_float : t -> string -> float option
(** [get_float t key] returns the float value for [key], if present. *)
-
+
val keys : t -> string list
(** [keys t] returns all keys in the input. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts to JSON representation. Internal use only. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses from JSON. Internal use only. *)
end
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of tool use blocks. *)
-
+
val create : id:string -> name:string -> input:Input.t -> t
(** [create ~id ~name ~input] creates a new tool use block.
@param id Unique identifier for this tool invocation
@param name Name of the tool to invoke
@param input Parameters for the tool *)
-
+
val id : t -> string
(** [id t] returns the unique identifier of the tool use. *)
-
+
val name : t -> string
(** [name t] returns the name of the tool being invoked. *)
-
+
val input : t -> Input.t
(** [input t] returns the input parameters for the tool. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for tool use blocks. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the tool use block to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses a tool use block from JSON.
@raise Invalid_argument if the JSON is not a valid tool use block. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the tool use block. *)
end
···
module Tool_result : sig
(** Results from tool invocations. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of tool result blocks. *)
-
+
val create : tool_use_id:string -> ?content:string -> ?is_error:bool -> unit -> t
(** [create ~tool_use_id ?content ?is_error ()] creates a new tool result block.
@param tool_use_id The ID of the corresponding tool use block
@param content Optional result content
@param is_error Whether the tool execution resulted in an error *)
-
+
val tool_use_id : t -> string
(** [tool_use_id t] returns the ID of the corresponding tool use. *)
-
+
val content : t -> string option
(** [content t] returns the optional result content. *)
-
+
val is_error : t -> bool option
(** [is_error t] returns whether this result represents an error. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for tool result blocks. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the tool result block to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses a tool result block from JSON.
@raise Invalid_argument if the JSON is not a valid tool result block. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the tool result block. *)
end
···
module Thinking : sig
(** Assistant's internal reasoning blocks. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of thinking blocks. *)
-
+
val create : thinking:string -> signature:string -> t
(** [create ~thinking ~signature] creates a new thinking block.
@param thinking The assistant's internal reasoning
@param signature Cryptographic signature for verification *)
-
+
val thinking : t -> string
(** [thinking t] returns the thinking content. *)
-
+
val signature : t -> string
(** [signature t] returns the cryptographic signature. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for thinking blocks. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the thinking block to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses a thinking block from JSON.
@raise Invalid_argument if the JSON is not a valid thinking block. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the thinking block. *)
end
···
val thinking : thinking:string -> signature:string -> t
(** [thinking ~thinking ~signature] creates a thinking content block. *)
-
val to_json : t -> Ezjsonm.value
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for content blocks. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts any content block to its JSON representation. *)
-
val of_json : Ezjsonm.value -> t
+
val of_json : Jsont.json -> t
(** [of_json json] parses a content block from JSON.
@raise Invalid_argument if the JSON is not a valid content block. *)
+38 -18
claudeio/lib/control.ml
···
-
open Ezjsonm
-
let src = Logs.Src.create "claude.control" ~doc:"Claude control messages"
module Log = (val Logs.src_log src : Logs.LOG)
(* Helper for pretty-printing JSON *)
let pp_json fmt json =
-
Fmt.string fmt (value_to_string json)
+
let s = match Jsont_bytesrw.encode_string' Jsont.json json with
+
| Ok s -> s
+
| Error err -> Jsont.Error.to_string err
+
in
+
Fmt.string fmt s
+
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
type t = {
request_id : string;
subtype : string;
-
data : value;
+
data : Jsont.json;
+
unknown : Unknown.t;
}
-
let create ~request_id ~subtype ~data = { request_id; subtype; data }
+
let jsont =
+
Jsont.Object.map ~kind:"Control"
+
(fun request_id subtype data unknown -> {request_id; subtype; data; unknown})
+
|> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun t -> t.request_id)
+
|> Jsont.Object.mem "subtype" Jsont.string ~enc:(fun t -> t.subtype)
+
|> Jsont.Object.mem "data" Jsont.json ~enc:(fun t -> t.data)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun t -> t.unknown)
+
|> Jsont.Object.finish
+
+
let create ~request_id ~subtype ~data =
+
{ request_id; subtype; data; unknown = Unknown.empty }
let request_id t = t.request_id
let subtype t = t.subtype
let data t = t.data
let to_json t =
-
`O [
-
("type", `String "control");
-
("request_id", `String t.request_id);
-
("subtype", `String t.subtype);
-
("data", t.data);
-
]
+
match Jsont_bytesrw.encode_string ~format:Jsont.Minify jsont t with
+
| Ok s ->
+
(match Jsont_bytesrw.decode_string' Jsont.json s with
+
| Ok json -> json
+
| Error e -> failwith (Jsont.Error.to_string e))
+
| Error e -> failwith e
-
let of_json = function
-
| `O fields ->
-
let request_id = get_string (List.assoc "request_id" fields) in
-
let subtype = get_string (List.assoc "subtype" fields) in
-
let data = List.assoc "data" fields in
-
{ request_id; subtype; data }
-
| _ -> raise (Invalid_argument "Control.of_json: expected object")
+
let of_json json =
+
match Jsont_bytesrw.encode_string ~format:Jsont.Minify Jsont.json json with
+
| Ok s ->
+
(match Jsont_bytesrw.decode_string jsont s with
+
| Ok t -> t
+
| Error e -> raise (Invalid_argument ("Control.of_json: " ^ e)))
+
| Error e -> raise (Invalid_argument ("Control.of_json: " ^ e))
let pp fmt t =
Fmt.pf fmt "@[<2>Control@ { request_id = %S;@ subtype = %S;@ data = %a }@]"
+16 -7
claudeio/lib/control.mli
···
(** Control messages for Claude session management.
-
+
Control messages are used to manage the interaction flow with Claude,
including session control, cancellation requests, and other operational
commands. *)
-
open Ezjsonm
-
(** The log source for control message operations *)
val src : Logs.Src.t
+
+
(** Unknown field preservation *)
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
type t
(** The type of control messages. *)
-
val create : request_id:string -> subtype:string -> data:value -> t
+
val jsont : t Jsont.t
+
(** [jsont] is the jsont codec for control messages. *)
+
+
val create : request_id:string -> subtype:string -> data:Jsont.json -> t
(** [create ~request_id ~subtype ~data] creates a new control message.
@param request_id Unique identifier for this control request
@param subtype The specific type of control message
···
val subtype : t -> string
(** [subtype t] returns the control message subtype. *)
-
val data : t -> value
+
val data : t -> Jsont.json
(** [data t] returns the additional data associated with the control message. *)
-
val to_json : t -> value
+
val to_json : t -> Jsont.json
(** [to_json t] converts the control message to its JSON representation. *)
-
val of_json : value -> t
+
val of_json : Jsont.json -> t
(** [of_json json] parses a control message from JSON.
@raise Invalid_argument if the JSON is not a valid control message. *)
+1 -1
claudeio/lib/dune
···
(library
(public_name claude)
(name claude)
-
(libraries eio eio.unix ezjsonm fmt logs jsont))
+
(libraries eio eio.unix fmt logs jsont jsont.bytesrw))
+478 -201
claudeio/lib/hooks.ml
···
-
open Ezjsonm
-
let src = Logs.Src.create "claude.hooks" ~doc:"Claude hooks system"
module Log = (val Logs.src_log src : Logs.LOG)
···
| "PreCompact" -> Pre_compact
| s -> raise (Invalid_argument (Printf.sprintf "Unknown hook event: %s" s))
+
let event_jsont : event Jsont.t =
+
Jsont.enum [
+
"PreToolUse", Pre_tool_use;
+
"PostToolUse", Post_tool_use;
+
"UserPromptSubmit", User_prompt_submit;
+
"Stop", Stop;
+
"SubagentStop", Subagent_stop;
+
"PreCompact", Pre_compact;
+
]
+
(** Context provided to hook callbacks *)
module Context = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
signal: unit option; (* Future: abort signal support *)
+
unknown : Unknown.t;
}
-
let create ?(signal = None) () = { signal }
+
let create ?(signal = None) ?(unknown = Unknown.empty) () = { signal; unknown }
+
+
let signal t = t.signal
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
let make unknown = { signal = None; unknown } in
+
Jsont.Object.map ~kind:"Context" make
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
end
(** Hook decision control *)
···
| Continue
| Block
+
let decision_jsont : decision Jsont.t =
+
Jsont.enum [
+
"continue", Continue;
+
"block", Block;
+
]
+
(** Generic hook result *)
+
module Result_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type result = {
decision: decision option;
system_message: string option;
-
hook_specific_output: value option;
+
hook_specific_output: Jsont.json option;
+
unknown : Result_unknown.t;
}
+
let result_jsont : result Jsont.t =
+
let make decision system_message hook_specific_output unknown =
+
{ decision; system_message; hook_specific_output; unknown }
+
in
+
Jsont.Object.map ~kind:"Result" make
+
|> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun r -> r.decision)
+
|> Jsont.Object.opt_mem "systemMessage" Jsont.string ~enc:(fun r -> r.system_message)
+
|> Jsont.Object.opt_mem "hookSpecificOutput" Jsont.json ~enc:(fun r -> r.hook_specific_output)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun r -> r.unknown)
+
|> Jsont.Object.finish
+
(** {1 PreToolUse Hook} *)
module PreToolUse = struct
-
type t = {
+
module Input_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type input = {
session_id: string;
transcript_path: string;
tool_name: string;
-
tool_input: value;
+
tool_input: Jsont.json;
+
unknown : Input_unknown.t;
}
-
type permission_decision = Allow | Deny | Ask
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
-
type output = {
-
permission_decision: permission_decision option;
-
permission_decision_reason: string option;
-
}
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
let of_json json =
-
{
-
session_id = get_string (find json ["session_id"]);
-
transcript_path = get_string (find json ["transcript_path"]);
-
tool_name = get_string (find json ["tool_name"]);
-
tool_input = find json ["tool_input"];
-
}
+
match get_string json "session_id" with
+
| None -> raise (Invalid_argument "PreToolUse: missing session_id")
+
| Some session_id ->
+
match get_string json "transcript_path" with
+
| None -> raise (Invalid_argument "PreToolUse: missing transcript_path")
+
| Some transcript_path ->
+
match get_string json "tool_name" with
+
| None -> raise (Invalid_argument "PreToolUse: missing tool_name")
+
| Some tool_name ->
+
match get_field json "tool_input" with
+
| None -> raise (Invalid_argument "PreToolUse: missing tool_input")
+
| Some tool_input ->
+
{ session_id; transcript_path; tool_name; tool_input; unknown = json }
+
+
type t = input
let session_id t = t.session_id
let transcript_path t = t.transcript_path
let tool_name t = t.tool_name
let tool_input t = t.tool_input
-
let raw_json t =
-
dict [
-
"session_id", string t.session_id;
-
"transcript_path", string t.transcript_path;
-
"hook_event_name", string "PreToolUse";
-
"tool_name", string t.tool_name;
-
"tool_input", t.tool_input;
+
let unknown t = t.unknown
+
+
let input_jsont : input Jsont.t =
+
let make session_id transcript_path tool_name tool_input unknown =
+
{ session_id; transcript_path; tool_name; tool_input; unknown }
+
in
+
Jsont.Object.map ~kind:"PreToolUseInput" make
+
|> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
+
|> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
+
|> Jsont.Object.mem "tool_input" Jsont.json ~enc:tool_input
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
+
type permission_decision = [ `Allow | `Deny | `Ask ]
+
+
let permission_decision_jsont : permission_decision Jsont.t =
+
Jsont.enum [
+
"allow", `Allow;
+
"deny", `Deny;
+
"ask", `Ask;
]
-
let permission_decision_to_string = function
-
| Allow -> "allow"
-
| Deny -> "deny"
-
| Ask -> "ask"
+
module Output_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
-
let output_to_json output =
-
let fields = [("hookEventName", string "PreToolUse")] in
-
let fields = match output.permission_decision with
-
| Some pd -> ("permissionDecision", string (permission_decision_to_string pd)) :: fields
-
| None -> fields
+
type output = {
+
permission_decision: permission_decision option;
+
permission_decision_reason: string option;
+
updated_input: Jsont.json option;
+
unknown : Output_unknown.t;
+
}
+
+
let output_jsont : output Jsont.t =
+
let make permission_decision permission_decision_reason updated_input unknown =
+
{ permission_decision; permission_decision_reason; updated_input; unknown }
in
-
let fields = match output.permission_decision_reason with
-
| Some reason -> ("permissionDecisionReason", string reason) :: fields
-
| None -> fields
-
in
-
dict fields
+
Jsont.Object.map ~kind:"PreToolUseOutput" make
+
|> Jsont.Object.opt_mem "permissionDecision" permission_decision_jsont ~enc:(fun o -> o.permission_decision)
+
|> Jsont.Object.opt_mem "permissionDecisionReason" Jsont.string ~enc:(fun o -> o.permission_decision_reason)
+
|> Jsont.Object.opt_mem "updatedInput" Jsont.json ~enc:(fun o -> o.updated_input)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
+
|> Jsont.Object.finish
-
let allow ?reason () =
-
{ permission_decision = Some Allow; permission_decision_reason = reason }
+
let output_to_json output =
+
match Jsont.Json.encode output_jsont output with
+
| Ok json -> json
+
| Error msg -> failwith ("PreToolUse.output_to_json: " ^ msg)
-
let deny ?reason () =
-
{ permission_decision = Some Deny; permission_decision_reason = reason }
+
let allow ?reason ?updated_input ?(unknown = Output_unknown.empty) () =
+
{ permission_decision = Some `Allow; permission_decision_reason = reason;
+
updated_input; unknown }
-
let ask ?reason () =
-
{ permission_decision = Some Ask; permission_decision_reason = reason }
+
let deny ?reason ?(unknown = Output_unknown.empty) () =
+
{ permission_decision = Some `Deny; permission_decision_reason = reason;
+
updated_input = None; unknown }
+
+
let ask ?reason ?(unknown = Output_unknown.empty) () =
+
{ permission_decision = Some `Ask; permission_decision_reason = reason;
+
updated_input = None; unknown }
-
let continue () =
-
{ permission_decision = None; permission_decision_reason = None }
+
let continue ?(unknown = Output_unknown.empty) () =
+
{ permission_decision = None; permission_decision_reason = None;
+
updated_input = None; unknown }
end
(** {1 PostToolUse Hook} *)
module PostToolUse = struct
-
type t = {
+
module Input_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type input = {
session_id: string;
transcript_path: string;
tool_name: string;
-
tool_input: value;
-
tool_response: value;
+
tool_input: Jsont.json;
+
tool_response: Jsont.json;
+
unknown : Input_unknown.t;
}
-
type output = {
-
decision: decision option;
-
reason: string option;
-
additional_context: string option;
-
}
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
let of_json json =
-
{
-
session_id = get_string (find json ["session_id"]);
-
transcript_path = get_string (find json ["transcript_path"]);
-
tool_name = get_string (find json ["tool_name"]);
-
tool_input = find json ["tool_input"];
-
tool_response = find json ["tool_response"];
-
}
+
match get_string json "session_id" with
+
| None -> raise (Invalid_argument "PostToolUse: missing session_id")
+
| Some session_id ->
+
match get_string json "transcript_path" with
+
| None -> raise (Invalid_argument "PostToolUse: missing transcript_path")
+
| Some transcript_path ->
+
match get_string json "tool_name" with
+
| None -> raise (Invalid_argument "PostToolUse: missing tool_name")
+
| Some tool_name ->
+
match get_field json "tool_input" with
+
| None -> raise (Invalid_argument "PostToolUse: missing tool_input")
+
| Some tool_input ->
+
match get_field json "tool_response" with
+
| None -> raise (Invalid_argument "PostToolUse: missing tool_response")
+
| Some tool_response ->
+
{ session_id; transcript_path; tool_name; tool_input; tool_response; unknown = json }
+
+
type t = input
let session_id t = t.session_id
let transcript_path t = t.transcript_path
let tool_name t = t.tool_name
let tool_input t = t.tool_input
let tool_response t = t.tool_response
-
let raw_json t =
-
dict [
-
"session_id", string t.session_id;
-
"transcript_path", string t.transcript_path;
-
"hook_event_name", string "PostToolUse";
-
"tool_name", string t.tool_name;
-
"tool_input", t.tool_input;
-
"tool_response", t.tool_response;
-
]
+
let unknown t = t.unknown
-
let output_to_json output =
-
let fields = [("hookEventName", string "PostToolUse")] in
-
let fields = match output.decision with
-
| Some Block -> ("decision", string "block") :: fields
-
| Some Continue | None -> fields
+
let input_jsont : input Jsont.t =
+
let make session_id transcript_path tool_name tool_input tool_response unknown =
+
{ session_id; transcript_path; tool_name; tool_input; tool_response; unknown }
in
-
let fields = match output.reason with
-
| Some r -> ("reason", string r) :: fields
-
| None -> fields
+
Jsont.Object.map ~kind:"PostToolUseInput" make
+
|> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
+
|> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
+
|> Jsont.Object.mem "tool_input" Jsont.json ~enc:tool_input
+
|> Jsont.Object.mem "tool_response" Jsont.json ~enc:tool_response
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
+
module Output_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type output = {
+
decision: decision option;
+
reason: string option;
+
additional_context: string option;
+
unknown : Output_unknown.t;
+
}
+
+
let output_jsont : output Jsont.t =
+
let make decision reason additional_context unknown =
+
{ decision; reason; additional_context; unknown }
in
-
let fields = match output.additional_context with
-
| Some ctx -> ("additionalContext", string ctx) :: fields
-
| None -> fields
-
in
-
dict fields
+
Jsont.Object.map ~kind:"PostToolUseOutput" make
+
|> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun o -> o.decision)
+
|> Jsont.Object.opt_mem "reason" Jsont.string ~enc:(fun o -> o.reason)
+
|> Jsont.Object.opt_mem "additionalContext" Jsont.string ~enc:(fun o -> o.additional_context)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
+
|> Jsont.Object.finish
-
let continue ?additional_context () =
-
{ decision = None; reason = None; additional_context }
+
let output_to_json output =
+
match Jsont.Json.encode output_jsont output with
+
| Ok json -> json
+
| Error msg -> failwith ("PostToolUse.output_to_json: " ^ msg)
-
let block ?reason ?additional_context () =
-
{ decision = Some Block; reason; additional_context }
+
let continue ?additional_context ?(unknown = Output_unknown.empty) () =
+
{ decision = None; reason = None; additional_context; unknown }
+
+
let block ?reason ?additional_context ?(unknown = Output_unknown.empty) () =
+
{ decision = Some Block; reason; additional_context; unknown }
end
(** {1 UserPromptSubmit Hook} *)
module UserPromptSubmit = struct
-
type t = {
+
module Input_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type input = {
session_id: string;
transcript_path: string;
prompt: string;
+
unknown : Input_unknown.t;
}
-
type output = {
-
decision: decision option;
-
reason: string option;
-
additional_context: string option;
-
}
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
let of_json json =
-
{
-
session_id = get_string (find json ["session_id"]);
-
transcript_path = get_string (find json ["transcript_path"]);
-
prompt = get_string (find json ["prompt"]);
-
}
+
match get_string json "session_id" with
+
| None -> raise (Invalid_argument "UserPromptSubmit: missing session_id")
+
| Some session_id ->
+
match get_string json "transcript_path" with
+
| None -> raise (Invalid_argument "UserPromptSubmit: missing transcript_path")
+
| Some transcript_path ->
+
match get_string json "prompt" with
+
| None -> raise (Invalid_argument "UserPromptSubmit: missing prompt")
+
| Some prompt ->
+
{ session_id; transcript_path; prompt; unknown = json }
+
+
type t = input
let session_id t = t.session_id
let transcript_path t = t.transcript_path
let prompt t = t.prompt
-
let raw_json t =
-
dict [
-
"session_id", string t.session_id;
-
"transcript_path", string t.transcript_path;
-
"hook_event_name", string "UserPromptSubmit";
-
"prompt", string t.prompt;
-
]
+
let unknown t = t.unknown
-
let output_to_json output =
-
let fields = [("hookEventName", string "UserPromptSubmit")] in
-
let fields = match output.decision with
-
| Some Block -> ("decision", string "block") :: fields
-
| Some Continue | None -> fields
+
let input_jsont : input Jsont.t =
+
let make session_id transcript_path prompt unknown =
+
{ session_id; transcript_path; prompt; unknown }
in
-
let fields = match output.reason with
-
| Some r -> ("reason", string r) :: fields
-
| None -> fields
-
in
-
let fields = match output.additional_context with
-
| Some ctx -> ("additionalContext", string ctx) :: fields
-
| None -> fields
+
Jsont.Object.map ~kind:"UserPromptSubmitInput" make
+
|> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
+
|> Jsont.Object.mem "prompt" Jsont.string ~enc:prompt
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
+
module Output_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type output = {
+
decision: decision option;
+
reason: string option;
+
additional_context: string option;
+
unknown : Output_unknown.t;
+
}
+
+
let output_jsont : output Jsont.t =
+
let make decision reason additional_context unknown =
+
{ decision; reason; additional_context; unknown }
in
-
dict fields
+
Jsont.Object.map ~kind:"UserPromptSubmitOutput" make
+
|> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun o -> o.decision)
+
|> Jsont.Object.opt_mem "reason" Jsont.string ~enc:(fun o -> o.reason)
+
|> Jsont.Object.opt_mem "additionalContext" Jsont.string ~enc:(fun o -> o.additional_context)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
+
|> Jsont.Object.finish
-
let continue ?additional_context () =
-
{ decision = None; reason = None; additional_context }
+
let output_to_json output =
+
match Jsont.Json.encode output_jsont output with
+
| Ok json -> json
+
| Error msg -> failwith ("UserPromptSubmit.output_to_json: " ^ msg)
-
let block ?reason () =
-
{ decision = Some Block; reason; additional_context = None }
+
let continue ?additional_context ?(unknown = Output_unknown.empty) () =
+
{ decision = None; reason = None; additional_context; unknown }
+
+
let block ?reason ?(unknown = Output_unknown.empty) () =
+
{ decision = Some Block; reason; additional_context = None; unknown }
end
(** {1 Stop Hook} *)
module Stop = struct
-
type t = {
+
module Input_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type input = {
session_id: string;
transcript_path: string;
stop_hook_active: bool;
+
unknown : Input_unknown.t;
}
-
type output = {
-
decision: decision option;
-
reason: string option;
-
}
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
+
+
let get_bool json key =
+
match get_field json key with
+
| Some (Jsont.Bool (b, _)) -> Some b
+
| _ -> None
let of_json json =
-
{
-
session_id = get_string (find json ["session_id"]);
-
transcript_path = get_string (find json ["transcript_path"]);
-
stop_hook_active = get_bool (find json ["stop_hook_active"]);
-
}
+
match get_string json "session_id" with
+
| None -> raise (Invalid_argument "Stop: missing session_id")
+
| Some session_id ->
+
match get_string json "transcript_path" with
+
| None -> raise (Invalid_argument "Stop: missing transcript_path")
+
| Some transcript_path ->
+
match get_bool json "stop_hook_active" with
+
| None -> raise (Invalid_argument "Stop: missing stop_hook_active")
+
| Some stop_hook_active ->
+
{ session_id; transcript_path; stop_hook_active; unknown = json }
+
+
type t = input
let session_id t = t.session_id
let transcript_path t = t.transcript_path
let stop_hook_active t = t.stop_hook_active
-
let raw_json t =
-
dict [
-
"session_id", string t.session_id;
-
"transcript_path", string t.transcript_path;
-
"hook_event_name", string "Stop";
-
"stop_hook_active", bool t.stop_hook_active;
-
]
+
let unknown t = t.unknown
-
let output_to_json output =
-
let fields = [] in
-
let fields = match output.decision with
-
| Some Block -> ("decision", string "block") :: fields
-
| Some Continue | None -> fields
+
let input_jsont : input Jsont.t =
+
let make session_id transcript_path stop_hook_active unknown =
+
{ session_id; transcript_path; stop_hook_active; unknown }
in
-
let fields = match output.reason with
-
| Some r -> ("reason", string r) :: fields
-
| None -> fields
+
Jsont.Object.map ~kind:"StopInput" make
+
|> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
+
|> Jsont.Object.mem "stop_hook_active" Jsont.bool ~enc:stop_hook_active
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
+
module Output_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type output = {
+
decision: decision option;
+
reason: string option;
+
unknown : Output_unknown.t;
+
}
+
+
let output_jsont : output Jsont.t =
+
let make decision reason unknown =
+
{ decision; reason; unknown }
in
-
dict fields
+
Jsont.Object.map ~kind:"StopOutput" make
+
|> Jsont.Object.opt_mem "decision" decision_jsont ~enc:(fun o -> o.decision)
+
|> Jsont.Object.opt_mem "reason" Jsont.string ~enc:(fun o -> o.reason)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun o -> o.unknown)
+
|> Jsont.Object.finish
+
+
let output_to_json output =
+
match Jsont.Json.encode output_jsont output with
+
| Ok json -> json
+
| Error msg -> failwith ("Stop.output_to_json: " ^ msg)
-
let continue () = { decision = None; reason = None }
-
let block ?reason () = { decision = Some Block; reason }
+
let continue ?(unknown = Output_unknown.empty) () = { decision = None; reason = None; unknown }
+
let block ?reason ?(unknown = Output_unknown.empty) () = { decision = Some Block; reason; unknown }
end
(** {1 SubagentStop Hook} - Same structure as Stop *)
module SubagentStop = struct
include Stop
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
+
+
let get_bool json key =
+
match get_field json key with
+
| Some (Jsont.Bool (b, _)) -> Some b
+
| _ -> None
+
let of_json json =
-
{
-
session_id = get_string (find json ["session_id"]);
-
transcript_path = get_string (find json ["transcript_path"]);
-
stop_hook_active = get_bool (find json ["stop_hook_active"]);
-
}
-
-
let raw_json t =
-
dict [
-
"session_id", string t.session_id;
-
"transcript_path", string t.transcript_path;
-
"hook_event_name", string "SubagentStop";
-
"stop_hook_active", bool t.stop_hook_active;
-
]
+
match get_string json "session_id" with
+
| None -> raise (Invalid_argument "SubagentStop: missing session_id")
+
| Some session_id ->
+
match get_string json "transcript_path" with
+
| None -> raise (Invalid_argument "SubagentStop: missing transcript_path")
+
| Some transcript_path ->
+
match get_bool json "stop_hook_active" with
+
| None -> raise (Invalid_argument "SubagentStop: missing stop_hook_active")
+
| Some stop_hook_active ->
+
{ session_id; transcript_path; stop_hook_active; unknown = json }
end
(** {1 PreCompact Hook} *)
module PreCompact = struct
-
type t = {
+
module Input_unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type input = {
session_id: string;
transcript_path: string;
+
unknown : Input_unknown.t;
}
-
type output = unit (* No specific output for PreCompact *)
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
let of_json json =
-
{
-
session_id = get_string (find json ["session_id"]);
-
transcript_path = get_string (find json ["transcript_path"]);
-
}
+
match get_string json "session_id" with
+
| None -> raise (Invalid_argument "PreCompact: missing session_id")
+
| Some session_id ->
+
match get_string json "transcript_path" with
+
| None -> raise (Invalid_argument "PreCompact: missing transcript_path")
+
| Some transcript_path ->
+
{ session_id; transcript_path; unknown = json }
+
+
type t = input
let session_id t = t.session_id
let transcript_path t = t.transcript_path
-
let raw_json t =
-
dict [
-
"session_id", string t.session_id;
-
"transcript_path", string t.transcript_path;
-
"hook_event_name", string "PreCompact";
-
]
+
let unknown t = t.unknown
+
+
let input_jsont : input Jsont.t =
+
let make session_id transcript_path unknown =
+
{ session_id; transcript_path; unknown }
+
in
+
Jsont.Object.map ~kind:"PreCompactInput" make
+
|> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
+
type output = unit (* No specific output for PreCompact *)
-
let output_to_json () = dict []
+
let output_to_json () = Jsont.Object ([], Jsont.Meta.none)
let continue () = ()
end
(** {1 Generic Callback Type} *)
type callback =
-
input:value ->
+
input:Jsont.json ->
tool_use_id:string option ->
context:Context.t ->
result
···
type config = (event * matcher list) list
(** {1 Result Builders} *)
-
let continue ?system_message ?hook_specific_output () =
-
{ decision = None; system_message; hook_specific_output }
+
let continue ?system_message ?hook_specific_output ?(unknown = Result_unknown.empty) () =
+
{ decision = None; system_message; hook_specific_output; unknown }
-
let block ?system_message ?hook_specific_output () =
-
{ decision = Some Block; system_message; hook_specific_output }
+
let block ?system_message ?hook_specific_output ?(unknown = Result_unknown.empty) () =
+
{ decision = Some Block; system_message; hook_specific_output; unknown }
(** {1 Matcher Builders} *)
let matcher ?pattern callbacks = { matcher = pattern; callbacks }
···
(** {1 JSON Conversion} *)
let result_to_json result =
-
let fields = [] in
-
let fields = match result.decision with
-
| Some Block -> ("decision", string "block") :: fields
-
| Some Continue | None -> fields
-
in
-
let fields = match result.system_message with
-
| Some msg -> ("systemMessage", string msg) :: fields
-
| None -> fields
-
in
-
let fields = match result.hook_specific_output with
-
| Some output -> ("hookSpecificOutput", output) :: fields
-
| None -> fields
-
in
-
dict fields
+
match Jsont.Json.encode result_jsont result with
+
| Ok json -> json
+
| Error msg -> failwith ("result_to_json: " ^ msg)
let config_to_protocol_format config =
let hooks_dict = List.map (fun (event, matchers) ->
let event_name = event_to_string event in
let matchers_json = List.map (fun m ->
(* matcher and hookCallbackIds will be filled in by client *)
-
dict [
-
"matcher", (match m.matcher with Some p -> string p | None -> `Null);
-
"callbacks", `A []; (* Placeholder, filled by client *)
-
]
+
let mems = [
+
Jsont.Json.mem (Jsont.Json.name "matcher") (match m.matcher with
+
| Some p -> Jsont.Json.string p
+
| None -> Jsont.Json.null ());
+
Jsont.Json.mem (Jsont.Json.name "callbacks") (Jsont.Json.list []); (* Placeholder, filled by client *)
+
] in
+
Jsont.Json.object' mems
) matchers in
-
(event_name, `A matchers_json)
+
Jsont.Json.mem (Jsont.Json.name event_name) (Jsont.Json.list matchers_json)
) config in
-
dict hooks_dict
+
Jsont.Json.object' hooks_dict
+228 -64
claudeio/lib/hooks.mli
···
open Eio.Std
(* Block dangerous bash commands *)
+
let get_string json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then
+
match value with
+
| Jsont.String (s, _) -> Some s
+
| _ -> None
+
else None
+
) members
+
| _ -> None
+
in
let block_rm_rf ~input ~tool_use_id:_ ~context:_ =
let hook = Hooks.PreToolUse.of_json input in
if Hooks.PreToolUse.tool_name hook = "Bash" then
let tool_input = Hooks.PreToolUse.tool_input hook in
-
match Ezjsonm.find tool_input ["command"] with
-
| `String cmd when String.contains cmd "rm -rf" ->
+
match get_string tool_input "command" with
+
| Some cmd when String.contains cmd "rm -rf" ->
let output = Hooks.PreToolUse.deny ~reason:"Dangerous command" () in
Hooks.continue
~hook_specific_output:(Hooks.PreToolUse.output_to_json output)
···
val event_to_string : event -> string
val event_of_string : string -> event
+
val event_jsont : event Jsont.t
(** {1 Context} *)
module Context : sig
-
type t
-
val create : ?signal:unit option -> unit -> t
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
+
type t = {
+
signal: unit option;
+
unknown : Unknown.t;
+
}
+
+
val create : ?signal:unit option -> ?unknown:Unknown.t -> unit -> t
+
val signal : t -> unit option
+
val unknown : t -> Unknown.t
+
val jsont : t Jsont.t
end
(** {1 Decisions} *)
···
type decision =
| Continue (** Allow the action to proceed *)
| Block (** Block the action *)
+
+
val decision_jsont : decision Jsont.t
(** {1 Generic Hook Result} *)
+
module Result_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
(** Generic result structure for hooks *)
type result = {
decision: decision option;
system_message: string option;
-
hook_specific_output: Ezjsonm.value option;
+
hook_specific_output: Jsont.json option;
+
unknown: Result_unknown.t;
}
+
+
val result_jsont : result Jsont.t
(** {1 Typed Hook Modules} *)
(** PreToolUse hook - fires before tool execution *)
module PreToolUse : sig
+
module Input_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
(** Typed input for PreToolUse hooks *)
-
type t
+
type input = {
+
session_id: string;
+
transcript_path: string;
+
tool_name: string;
+
tool_input: Jsont.json;
+
unknown: Input_unknown.t;
+
}
+
+
type t = input
+
+
(** Parse hook input from JSON *)
+
val of_json : Jsont.json -> t
+
+
(** {2 Accessors} *)
+
val session_id : t -> string
+
val transcript_path : t -> string
+
val tool_name : t -> string
+
val tool_input : t -> Jsont.json
+
val unknown : t -> Input_unknown.t
+
+
val input_jsont : input Jsont.t
(** Permission decision for tool usage *)
-
type permission_decision = Allow | Deny | Ask
+
type permission_decision = [ `Allow | `Deny | `Ask ]
+
+
val permission_decision_jsont : permission_decision Jsont.t
+
+
module Output_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
(** Typed output for PreToolUse hooks *)
type output = {
permission_decision: permission_decision option;
permission_decision_reason: string option;
+
updated_input: Jsont.json option;
+
unknown: Output_unknown.t;
}
-
(** Parse hook input from JSON *)
-
val of_json : Ezjsonm.value -> t
-
-
(** {2 Accessors} *)
-
val session_id : t -> string
-
val transcript_path : t -> string
-
val tool_name : t -> string
-
val tool_input : t -> Ezjsonm.value
-
val raw_json : t -> Ezjsonm.value
+
val output_jsont : output Jsont.t
(** {2 Response Builders} *)
-
val allow : ?reason:string -> unit -> output
-
val deny : ?reason:string -> unit -> output
-
val ask : ?reason:string -> unit -> output
-
val continue : unit -> output
+
val allow : ?reason:string -> ?updated_input:Jsont.json -> ?unknown:Output_unknown.t -> unit -> output
+
val deny : ?reason:string -> ?unknown:Output_unknown.t -> unit -> output
+
val ask : ?reason:string -> ?unknown:Output_unknown.t -> unit -> output
+
val continue : ?unknown:Output_unknown.t -> unit -> output
(** Convert output to JSON for hook_specific_output *)
-
val output_to_json : output -> Ezjsonm.value
+
val output_to_json : output -> Jsont.json
end
(** PostToolUse hook - fires after tool execution *)
module PostToolUse : sig
-
type t
+
module Input_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
-
type output = {
-
decision: decision option;
-
reason: string option;
-
additional_context: string option;
+
type input = {
+
session_id: string;
+
transcript_path: string;
+
tool_name: string;
+
tool_input: Jsont.json;
+
tool_response: Jsont.json;
+
unknown: Input_unknown.t;
}
-
val of_json : Ezjsonm.value -> t
+
type t = input
+
+
val of_json : Jsont.json -> t
val session_id : t -> string
val transcript_path : t -> string
val tool_name : t -> string
-
val tool_input : t -> Ezjsonm.value
-
val tool_response : t -> Ezjsonm.value
-
val raw_json : t -> Ezjsonm.value
+
val tool_input : t -> Jsont.json
+
val tool_response : t -> Jsont.json
+
val unknown : t -> Input_unknown.t
-
val continue : ?additional_context:string -> unit -> output
-
val block : ?reason:string -> ?additional_context:string -> unit -> output
-
val output_to_json : output -> Ezjsonm.value
-
end
+
val input_jsont : input Jsont.t
-
(** UserPromptSubmit hook - fires when user submits a prompt *)
-
module UserPromptSubmit : sig
-
type t
+
module Output_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
type output = {
decision: decision option;
reason: string option;
additional_context: string option;
+
unknown: Output_unknown.t;
}
-
val of_json : Ezjsonm.value -> t
+
val output_jsont : output Jsont.t
+
+
val continue : ?additional_context:string -> ?unknown:Output_unknown.t -> unit -> output
+
val block : ?reason:string -> ?additional_context:string -> ?unknown:Output_unknown.t -> unit -> output
+
val output_to_json : output -> Jsont.json
+
end
+
+
(** UserPromptSubmit hook - fires when user submits a prompt *)
+
module UserPromptSubmit : sig
+
module Input_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
+
type input = {
+
session_id: string;
+
transcript_path: string;
+
prompt: string;
+
unknown: Input_unknown.t;
+
}
+
+
type t = input
+
+
val of_json : Jsont.json -> t
val session_id : t -> string
val transcript_path : t -> string
val prompt : t -> string
-
val raw_json : t -> Ezjsonm.value
+
val unknown : t -> Input_unknown.t
-
val continue : ?additional_context:string -> unit -> output
-
val block : ?reason:string -> unit -> output
-
val output_to_json : output -> Ezjsonm.value
-
end
+
val input_jsont : input Jsont.t
-
(** Stop hook - fires when conversation stops *)
-
module Stop : sig
-
type t
+
module Output_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
type output = {
decision: decision option;
reason: string option;
+
additional_context: string option;
+
unknown: Output_unknown.t;
}
-
val of_json : Ezjsonm.value -> t
+
val output_jsont : output Jsont.t
+
+
val continue : ?additional_context:string -> ?unknown:Output_unknown.t -> unit -> output
+
val block : ?reason:string -> ?unknown:Output_unknown.t -> unit -> output
+
val output_to_json : output -> Jsont.json
+
end
+
+
(** Stop hook - fires when conversation stops *)
+
module Stop : sig
+
module Input_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
+
type input = {
+
session_id: string;
+
transcript_path: string;
+
stop_hook_active: bool;
+
unknown: Input_unknown.t;
+
}
+
+
type t = input
+
+
val of_json : Jsont.json -> t
val session_id : t -> string
val transcript_path : t -> string
val stop_hook_active : t -> bool
-
val raw_json : t -> Ezjsonm.value
+
val unknown : t -> Input_unknown.t
+
+
val input_jsont : input Jsont.t
+
+
module Output_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
+
type output = {
+
decision: decision option;
+
reason: string option;
+
unknown: Output_unknown.t;
+
}
-
val continue : unit -> output
-
val block : ?reason:string -> unit -> output
-
val output_to_json : output -> Ezjsonm.value
+
val output_jsont : output Jsont.t
+
+
val continue : ?unknown:Output_unknown.t -> unit -> output
+
val block : ?reason:string -> ?unknown:Output_unknown.t -> unit -> output
+
val output_to_json : output -> Jsont.json
end
(** SubagentStop hook - fires when a subagent stops *)
module SubagentStop : sig
include module type of Stop
-
val of_json : Ezjsonm.value -> t
-
val raw_json : t -> Ezjsonm.value
+
val of_json : Jsont.json -> t
end
(** PreCompact hook - fires before message compaction *)
module PreCompact : sig
-
type t
+
module Input_unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
+
type input = {
+
session_id: string;
+
transcript_path: string;
+
unknown: Input_unknown.t;
+
}
+
+
type t = input
+
type output = unit
-
val of_json : Ezjsonm.value -> t
+
val of_json : Jsont.json -> t
val session_id : t -> string
val transcript_path : t -> string
-
val raw_json : t -> Ezjsonm.value
+
val unknown : t -> Input_unknown.t
+
+
val input_jsont : input Jsont.t
val continue : unit -> output
-
val output_to_json : output -> Ezjsonm.value
+
val output_to_json : output -> Jsont.json
end
(** {1 Callbacks} *)
···
And return a generic [result] with optional hook-specific output.
*)
type callback =
-
input:Ezjsonm.value ->
+
input:Jsont.json ->
tool_use_id:string option ->
context:Context.t ->
result
···
(** {1 Generic Result Builders} *)
-
(** [continue ?system_message ?hook_specific_output ()] creates a continue result *)
-
val continue : ?system_message:string -> ?hook_specific_output:Ezjsonm.value -> unit -> result
+
(** [continue ?system_message ?hook_specific_output ?unknown ()] creates a continue result *)
+
val continue : ?system_message:string -> ?hook_specific_output:Jsont.json -> ?unknown:Result_unknown.t -> unit -> result
-
(** [block ?system_message ?hook_specific_output ()] creates a block result *)
-
val block : ?system_message:string -> ?hook_specific_output:Ezjsonm.value -> unit -> result
+
(** [block ?system_message ?hook_specific_output ?unknown ()] creates a block result *)
+
val block : ?system_message:string -> ?hook_specific_output:Jsont.json -> ?unknown:Result_unknown.t -> unit -> result
(** {1 Configuration Builders} *)
···
(** {1 JSON Serialization} *)
-
val result_to_json : result -> Ezjsonm.value
-
val config_to_protocol_format : config -> Ezjsonm.value
+
val result_to_json : result -> Jsont.json
+
val config_to_protocol_format : config -> Jsont.json
+55
claudeio/lib/incoming.ml
···
+
let src = Logs.Src.create "claude.incoming" ~doc:"Incoming messages from Claude CLI"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
+
type t =
+
| Message of Message.t
+
| Control_response of Sdk_control.control_response
+
+
let jsont : t Jsont.t =
+
(* Custom decoder that checks the type field and dispatches to the appropriate codec.
+
+
The challenge is that Message can have multiple type values ("user", "assistant",
+
"system", "result"), while control_response has only one type value. Jsont's
+
case_mem discriminator doesn't support multiple tags per case, so we implement
+
a custom decoder/encoder. *)
+
+
let dec json =
+
(* First check if it has a type field *)
+
match json with
+
| Jsont.Object (members, _meta) ->
+
let type_field = List.find_map (fun ((name, _), value) ->
+
if name = "type" then
+
match value with
+
| Jsont.String (s, _) -> Some s
+
| _ -> None
+
else None
+
) members in
+
(match type_field with
+
| Some "control_response" ->
+
(match Jsont.Json.decode Sdk_control.control_response_jsont json with
+
| Ok resp -> Control_response resp
+
| Error err -> failwith ("Failed to decode control_response: " ^ err))
+
| Some ("user" | "assistant" | "system" | "result") | Some _ | None ->
+
(* Try to decode as message *)
+
(match Jsont.Json.decode Message.jsont json with
+
| Ok msg -> Message msg
+
| Error err -> failwith ("Failed to decode message: " ^ err)))
+
| _ -> failwith "Expected JSON object for incoming message"
+
in
+
+
let enc = function
+
| Message msg ->
+
(match Jsont.Json.encode Message.jsont msg with
+
| Ok json -> json
+
| Error err -> failwith ("Failed to encode message: " ^ err))
+
| Control_response resp ->
+
(match Jsont.Json.encode Sdk_control.control_response_jsont resp with
+
| Ok json -> json
+
| Error err -> failwith ("Failed to encode control response: " ^ err))
+
in
+
+
Jsont.map ~kind:"Incoming" ~dec ~enc Jsont.json
+
+
let pp fmt = function
+
| Message msg -> Format.fprintf fmt "@[<2>Message@ %a@]" Message.pp msg
+
| Control_response resp -> Format.fprintf fmt "@[<2>ControlResponse@ %a@]" Sdk_control.pp (Sdk_control.Response resp)
+22
claudeio/lib/incoming.mli
···
+
(** Incoming messages from the Claude CLI.
+
+
This module defines a discriminated union of all possible message types
+
that can be received from the Claude CLI, with a single jsont codec.
+
+
The codec uses the "type" field to discriminate between message types:
+
- "user", "assistant", "system", "result" -> Message variant
+
- "control_response" -> Control_response variant
+
- "control_request" is handled separately in the client (not incoming to SDK user)
+
+
This provides a clean, type-safe way to decode incoming messages in a single
+
operation, avoiding the parse-then-switch-then-parse pattern. *)
+
+
type t =
+
| Message of Message.t
+
| Control_response of Sdk_control.control_response
+
+
val jsont : t Jsont.t
+
(** Codec for incoming messages. Uses the "type" field to discriminate. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the incoming message. *)
-85
claudeio/lib/json_utils.ml
···
-
open Ezjsonm
-
-
(* Combinations of find + get_* that are commonly used *)
-
let find_string json path =
-
find json path |> get_string
-
-
let find_int json path =
-
find json path |> get_int
-
-
let find_bool json path =
-
find json path |> get_bool
-
-
let find_float json path =
-
find json path |> get_float
-
-
(* Optional versions using Ezjsonm's find_opt *)
-
let find_string_opt json path =
-
Option.map get_string (find_opt json path)
-
-
let find_int_opt json path =
-
Option.map get_int (find_opt json path)
-
-
let find_bool_opt json path =
-
Option.map get_bool (find_opt json path)
-
-
let find_float_opt json path =
-
Option.map get_float (find_opt json path)
-
-
let assoc_string key fields =
-
List.assoc key fields |> get_string
-
-
let assoc_int key fields =
-
List.assoc key fields |> get_int
-
-
let assoc_bool key fields =
-
List.assoc key fields |> get_bool
-
-
let assoc_float key fields =
-
List.assoc key fields |> get_float
-
-
let assoc_string_opt key fields =
-
Option.map get_string (List.assoc_opt key fields)
-
-
let assoc_int_opt key fields =
-
Option.map get_int (List.assoc_opt key fields)
-
-
let assoc_bool_opt key fields =
-
Option.map get_bool (List.assoc_opt key fields)
-
-
let assoc_float_opt key fields =
-
Option.map get_float (List.assoc_opt key fields)
-
-
(* Ezjsonm.get_dict extracts fields, but we keep get_fields as an alias for clarity *)
-
let get_fields = get_dict
-
-
(* Single field access - simpler than using find with a single-element path *)
-
let get_field json key =
-
List.assoc key (get_dict json)
-
-
let get_field_opt json key =
-
List.assoc_opt key (try get_dict json with _ -> [])
-
-
let get_field_string json key =
-
get_field json key |> get_string
-
-
let get_field_int json key =
-
get_field json key |> get_int
-
-
let get_field_bool json key =
-
get_field json key |> get_bool
-
-
let get_field_float json key =
-
get_field json key |> get_float
-
-
let get_field_string_opt json key =
-
Option.map get_string (get_field_opt json key)
-
-
let get_field_int_opt json key =
-
Option.map get_int (get_field_opt json key)
-
-
let get_field_bool_opt json key =
-
Option.map get_bool (get_field_opt json key)
-
-
let get_field_float_opt json key =
-
Option.map get_float (get_field_opt json key)
-51
claudeio/lib/json_utils.mli
···
-
(** JSON utility functions for working with Ezjsonm.
-
-
This module provides convenience combinators that combine common
-
Ezjsonm operations. Most functions are thin wrappers that combine
-
find/get operations or provide Option-based error handling. *)
-
-
(** {2 Finding values by path}
-
-
These combine [Ezjsonm.find] with type extraction functions. *)
-
-
val find_string : Ezjsonm.value -> string list -> string
-
val find_int : Ezjsonm.value -> string list -> int
-
val find_bool : Ezjsonm.value -> string list -> bool
-
val find_float : Ezjsonm.value -> string list -> float
-
-
val find_string_opt : Ezjsonm.value -> string list -> string option
-
val find_int_opt : Ezjsonm.value -> string list -> int option
-
val find_bool_opt : Ezjsonm.value -> string list -> bool option
-
val find_float_opt : Ezjsonm.value -> string list -> float option
-
-
(** {2 Association list operations} *)
-
-
val assoc_string : string -> (string * Ezjsonm.value) list -> string
-
val assoc_int : string -> (string * Ezjsonm.value) list -> int
-
val assoc_bool : string -> (string * Ezjsonm.value) list -> bool
-
val assoc_float : string -> (string * Ezjsonm.value) list -> float
-
-
val assoc_string_opt : string -> (string * Ezjsonm.value) list -> string option
-
val assoc_int_opt : string -> (string * Ezjsonm.value) list -> int option
-
val assoc_bool_opt : string -> (string * Ezjsonm.value) list -> bool option
-
val assoc_float_opt : string -> (string * Ezjsonm.value) list -> float option
-
-
(** {2 Object field operations}
-
-
Direct field access without needing to build paths. *)
-
-
(** Alias for [Ezjsonm.get_dict] *)
-
val get_fields : Ezjsonm.value -> (string * Ezjsonm.value) list
-
-
val get_field : Ezjsonm.value -> string -> Ezjsonm.value
-
val get_field_opt : Ezjsonm.value -> string -> Ezjsonm.value option
-
-
val get_field_string : Ezjsonm.value -> string -> string
-
val get_field_int : Ezjsonm.value -> string -> int
-
val get_field_bool : Ezjsonm.value -> string -> bool
-
val get_field_float : Ezjsonm.value -> string -> float
-
-
val get_field_string_opt : Ezjsonm.value -> string -> string option
-
val get_field_int_opt : Ezjsonm.value -> string -> int option
-
val get_field_bool_opt : Ezjsonm.value -> string -> bool option
-
val get_field_float_opt : Ezjsonm.value -> string -> float option
+396 -179
claudeio/lib/message.ml
···
-
open Ezjsonm
-
module JU = Json_utils
-
let src = Logs.Src.create "claude.message" ~doc:"Claude messages"
module Log = (val Logs.src_log src : Logs.LOG)
module User = struct
-
type content =
+
type content =
| String of string
| Blocks of Content_block.t list
-
-
type t = { content : content }
-
-
let create_string s = { content = String s }
-
let create_blocks blocks = { content = Blocks blocks }
-
+
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
+
type t = {
+
content : content;
+
unknown : Unknown.t;
+
}
+
+
let create_string s = { content = String s; unknown = Unknown.empty }
+
let create_blocks blocks = { content = Blocks blocks; unknown = Unknown.empty }
+
let create_with_tool_result ~tool_use_id ~content ?is_error () =
let tool_result = Content_block.tool_result ~tool_use_id ~content ?is_error () in
-
{ content = Blocks [tool_result] }
-
+
{ content = Blocks [tool_result]; unknown = Unknown.empty }
+
let create_mixed ~text ~tool_results =
-
let blocks =
+
let blocks =
let text_blocks = match text with
| Some t -> [Content_block.text t]
| None -> []
···
) tool_results in
text_blocks @ tool_blocks
in
-
{ content = Blocks blocks }
-
+
{ content = Blocks blocks; unknown = Unknown.empty }
+
+
let make content unknown = { content; unknown }
let content t = t.content
-
+
let unknown t = t.unknown
+
let as_text t = match t.content with
| String s -> Some s
| Blocks _ -> None
-
+
let get_blocks t = match t.content with
| String s -> [Content_block.text s]
| Blocks blocks -> blocks
-
+
+
(* Decode content from json value *)
+
let decode_content json = match json with
+
| Jsont.String (s, _) -> String s
+
| Jsont.Array (items, _) ->
+
let blocks = List.map (fun j ->
+
match Jsont.Json.decode Content_block.jsont j with
+
| Ok b -> b
+
| Error msg -> failwith ("Invalid content block: " ^ msg)
+
) items in
+
Blocks blocks
+
| _ -> failwith "Content must be string or array"
+
+
(* Encode content to json value *)
+
let encode_content = function
+
| String s -> Jsont.String (s, Jsont.Meta.none)
+
| Blocks blocks -> Jsont.Array (List.map Content_block.to_json blocks, Jsont.Meta.none)
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"User" (fun json_content unknown ->
+
let content = decode_content json_content in
+
make content unknown
+
)
+
|> Jsont.Object.mem "content" Jsont.json ~enc:(fun t -> encode_content (content t))
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
let content_json = match t.content with
-
| String s -> `String s
-
| Blocks blocks ->
-
`A (List.map Content_block.to_json blocks)
+
| String s -> Jsont.String (s, Jsont.Meta.none)
+
| Blocks blocks ->
+
Jsont.Array (List.map Content_block.to_json blocks, Jsont.Meta.none)
in
-
`O [
-
("type", `String "user");
-
("message", `O [
-
("role", `String "user");
-
("content", content_json);
-
]);
-
]
-
-
let of_json = function
-
| `O fields ->
-
let message = List.assoc "message" fields in
+
Jsont.Object ([
+
(Jsont.Json.name "type", Jsont.String ("user", Jsont.Meta.none));
+
(Jsont.Json.name "message", Jsont.Object ([
+
(Jsont.Json.name "role", Jsont.String ("user", Jsont.Meta.none));
+
(Jsont.Json.name "content", content_json);
+
], Jsont.Meta.none));
+
], Jsont.Meta.none)
+
+
let of_json json =
+
match json with
+
| Jsont.Object (fields, _) ->
+
let message = List.assoc (Jsont.Json.name "message") fields in
let content = match message with
-
| `O msg_fields ->
-
(match List.assoc "content" msg_fields with
-
| `String s -> String s
-
| `A blocks -> Blocks (List.map Content_block.of_json blocks)
+
| Jsont.Object (msg_fields, _) ->
+
(match List.assoc (Jsont.Json.name "content") msg_fields with
+
| Jsont.String (s, _) -> String s
+
| Jsont.Array (items, _) ->
+
Blocks (List.map Content_block.of_json items)
| _ -> raise (Invalid_argument "User.of_json: invalid content"))
| _ -> raise (Invalid_argument "User.of_json: invalid message")
in
-
{ content }
+
{ content; unknown = Unknown.empty }
| _ -> raise (Invalid_argument "User.of_json: expected object")
-
+
let pp fmt t =
match t.content with
-
| String s ->
+
| String s ->
if String.length s > 60 then
let truncated = String.sub s 0 57 in
Fmt.pf fmt "@[<2>User:@ %s...@]" truncated
else
Fmt.pf fmt "@[<2>User:@ %S@]" s
| Blocks blocks ->
-
let text_count = List.length (List.filter (function
+
let text_count = List.length (List.filter (function
| Content_block.Text _ -> true | _ -> false) blocks) in
-
let tool_result_count = List.length (List.filter (function
+
let tool_result_count = List.length (List.filter (function
| Content_block.Tool_result _ -> true | _ -> false) blocks) in
match text_count, tool_result_count with
-
| 1, 0 ->
+
| 1, 0 ->
let text = List.find_map (function
| Content_block.Text t -> Some (Content_block.Text.text t)
| _ -> None) blocks in
···
| "server_error" -> `Server_error
| "unknown" | _ -> `Unknown
+
let error_jsont : error Jsont.t =
+
Jsont.enum [
+
("authentication_failed", `Authentication_failed);
+
("billing_error", `Billing_error);
+
("rate_limit", `Rate_limit);
+
("invalid_request", `Invalid_request);
+
("server_error", `Server_error);
+
("unknown", `Unknown);
+
]
+
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
content : Content_block.t list;
model : string;
error : error option;
+
unknown : Unknown.t;
}
-
let create ~content ~model ?error () = { content; model; error }
+
let create ~content ~model ?error () = { content; model; error; unknown = Unknown.empty }
+
let make content model error unknown = { content; model; error; unknown }
let content t = t.content
let model t = t.model
let error t = t.error
-
+
let unknown t = t.unknown
+
let get_text_blocks t =
List.filter_map (function
| Content_block.Text text -> Some (Content_block.Text.text text)
| _ -> None
) t.content
-
+
let get_tool_uses t =
List.filter_map (function
| Content_block.Tool_use tool -> Some tool
| _ -> None
) t.content
-
+
let get_thinking t =
List.filter_map (function
| Content_block.Thinking thinking -> Some thinking
| _ -> None
) t.content
-
+
let has_tool_use t =
List.exists (function
| Content_block.Tool_use _ -> true
| _ -> false
) t.content
-
+
let combined_text t =
String.concat "\n" (get_text_blocks t)
-
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Assistant" make
+
|> Jsont.Object.mem "content" (Jsont.list Content_block.jsont) ~enc:content
+
|> Jsont.Object.mem "model" Jsont.string ~enc:model
+
|> Jsont.Object.opt_mem "error" error_jsont ~enc:error
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
let msg_fields = [
-
("content", `A (List.map Content_block.to_json t.content));
-
("model", `String t.model);
+
(Jsont.Json.name "content", Jsont.Array (List.map Content_block.to_json t.content, Jsont.Meta.none));
+
(Jsont.Json.name "model", Jsont.String (t.model, Jsont.Meta.none));
] in
let msg_fields = match t.error with
-
| Some err -> ("error", `String (error_to_string err)) :: msg_fields
+
| Some err -> (Jsont.Json.name "error", Jsont.String (error_to_string err, Jsont.Meta.none)) :: msg_fields
| None -> msg_fields
in
-
`O [
-
("type", `String "assistant");
-
("message", `O msg_fields);
-
]
-
-
let of_json = function
-
| `O fields ->
-
let message = List.assoc "message" fields in
+
Jsont.Object ([
+
(Jsont.Json.name "type", Jsont.String ("assistant", Jsont.Meta.none));
+
(Jsont.Json.name "message", Jsont.Object (msg_fields, Jsont.Meta.none));
+
], Jsont.Meta.none)
+
+
let of_json json =
+
match json with
+
| Jsont.Object (fields, _) ->
+
let message = List.assoc (Jsont.Json.name "message") fields in
let content, model, error = match message with
-
| `O msg_fields ->
+
| Jsont.Object (msg_fields, _) ->
let content =
-
match List.assoc "content" msg_fields with
-
| `A blocks -> List.map Content_block.of_json blocks
+
match List.assoc (Jsont.Json.name "content") msg_fields with
+
| Jsont.Array (items, _) -> List.map Content_block.of_json items
| _ -> raise (Invalid_argument "Assistant.of_json: invalid content")
in
-
let model = JU.assoc_string "model" msg_fields in
+
let model = match List.assoc (Jsont.Json.name "model") msg_fields with
+
| Jsont.String (s, _) -> s
+
| _ -> raise (Invalid_argument "Assistant.of_json: invalid model")
+
in
let error =
-
match JU.assoc_string_opt "error" msg_fields with
-
| Some err_str -> Some (error_of_string err_str)
+
match List.assoc_opt (Jsont.Json.name "error") msg_fields with
+
| Some (Jsont.String (err_str, _)) -> Some (error_of_string err_str)
+
| Some _ -> raise (Invalid_argument "Assistant.of_json: invalid error")
| None -> None
in
content, model, error
| _ -> raise (Invalid_argument "Assistant.of_json: invalid message")
in
-
{ content; model; error }
+
{ content; model; error; unknown = Unknown.empty }
| _ -> raise (Invalid_argument "Assistant.of_json: expected object")
-
+
let pp fmt t =
let text_count = List.length (get_text_blocks t) in
let tool_count = List.length (get_tool_uses t) in
···
| _ ->
(* Mixed content *)
let parts = [] in
-
let parts = if text_count > 0 then
+
let parts = if text_count > 0 then
Printf.sprintf "%d text" text_count :: parts else parts in
-
let parts = if tool_count > 0 then
+
let parts = if tool_count > 0 then
Printf.sprintf "%d tools" tool_count :: parts else parts in
-
let parts = if thinking_count > 0 then
+
let parts = if thinking_count > 0 then
Printf.sprintf "%d thinking" thinking_count :: parts else parts in
Fmt.pf fmt "@[<2>Assistant@ [%s]:@ %s@]"
t.model (String.concat ", " (List.rev parts))
···
module System = struct
module Data = struct
-
(* Store both the raw JSON and provide typed accessors *)
-
type t = value (* The full JSON data *)
-
-
let empty = `O []
-
-
let of_assoc assoc = `O assoc
-
-
let get_string t key = JU.get_field_string_opt t key
-
-
let get_int t key = JU.get_field_int_opt t key
-
-
let get_bool t key = JU.get_field_bool_opt t key
-
-
let get_float t key = JU.get_field_float_opt t key
-
-
let get_list t key =
-
match t with
-
| `O fields ->
-
(match List.assoc_opt key fields with
-
| Some (`A lst) -> Some lst
-
| _ -> None)
-
| _ -> None
-
+
(* Opaque JSON type with typed accessors *)
+
type t = Jsont.json
+
+
let jsont = Jsont.json
+
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
+
let of_assoc (assoc : (string * Jsont.json) list) : t =
+
Jsont.Object (
+
List.map (fun (k, v) -> (Jsont.Json.name k, v)) assoc,
+
Jsont.Meta.none
+
)
+
let get_field t key =
match t with
-
| `O fields -> List.assoc_opt key fields
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
| _ -> None
-
+
+
let get_string t key =
+
match get_field t key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
+
+
let get_int t key =
+
match get_field t key with
+
| Some (Jsont.Number (f, _)) ->
+
let i = int_of_float f in
+
if float_of_int i = f then Some i else None
+
| _ -> None
+
+
let get_bool t key =
+
match get_field t key with
+
| Some (Jsont.Bool (b, _)) -> Some b
+
| _ -> None
+
+
let get_float t key =
+
match get_field t key with
+
| Some (Jsont.Number (f, _)) -> Some f
+
| _ -> None
+
+
let get_list t key =
+
match get_field t key with
+
| Some (Jsont.Array (items, _)) -> Some items
+
| _ -> None
+
let raw_json t = t
-
+
let to_json t = t
let of_json json = json
end
-
+
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
subtype : string;
data : Data.t;
+
unknown : Unknown.t;
}
-
-
let create ~subtype ~data = { subtype; data }
+
+
let create ~subtype ~data = { subtype; data; unknown = Unknown.empty }
+
let make subtype data unknown = { subtype; data; unknown }
let subtype t = t.subtype
let data t = t.data
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"System" make
+
|> Jsont.Object.mem "subtype" Jsont.string ~enc:subtype
+
|> Jsont.Object.mem "data" Data.jsont ~enc:data
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
-
`O [
-
("type", `String "system");
-
("subtype", `String t.subtype);
-
("data", Data.to_json t.data);
-
]
-
-
let of_json = function
-
| `O fields ->
-
let subtype = JU.assoc_string "subtype" fields in
-
let data = Data.of_json (try List.assoc "data" fields with Not_found -> `O fields) in
-
{ subtype; data }
+
Jsont.Object ([
+
(Jsont.Json.name "type", Jsont.String ("system", Jsont.Meta.none));
+
(Jsont.Json.name "subtype", Jsont.String (t.subtype, Jsont.Meta.none));
+
(Jsont.Json.name "data", Data.to_json t.data);
+
], Jsont.Meta.none)
+
+
let of_json json =
+
match json with
+
| Jsont.Object (fields, _) ->
+
let subtype = match List.assoc (Jsont.Json.name "subtype") fields with
+
| Jsont.String (s, _) -> s
+
| _ -> raise (Invalid_argument "System.of_json: invalid subtype")
+
in
+
let data = Data.of_json (
+
try List.assoc (Jsont.Json.name "data") fields
+
with Not_found -> Jsont.Object (fields, Jsont.Meta.none)
+
) in
+
{ subtype; data; unknown = Unknown.empty }
| _ -> raise (Invalid_argument "System.of_json: expected object")
-
+
let pp fmt t =
match t.subtype with
| "init" ->
···
module Result = struct
module Usage = struct
-
type t = value
-
-
let create ?input_tokens ?output_tokens ?total_tokens
+
(* Opaque JSON type with typed accessors *)
+
type t = Jsont.json
+
+
let jsont = Jsont.json
+
+
let create ?input_tokens ?output_tokens ?total_tokens
?cache_creation_input_tokens ?cache_read_input_tokens () =
let fields = [] in
let fields = match input_tokens with
-
| Some n -> ("input_tokens", `Float (float_of_int n)) :: fields
+
| Some n -> (Jsont.Json.name "input_tokens", Jsont.Number (float_of_int n, Jsont.Meta.none)) :: fields
| None -> fields in
let fields = match output_tokens with
-
| Some n -> ("output_tokens", `Float (float_of_int n)) :: fields
+
| Some n -> (Jsont.Json.name "output_tokens", Jsont.Number (float_of_int n, Jsont.Meta.none)) :: fields
| None -> fields in
let fields = match total_tokens with
-
| Some n -> ("total_tokens", `Float (float_of_int n)) :: fields
+
| Some n -> (Jsont.Json.name "total_tokens", Jsont.Number (float_of_int n, Jsont.Meta.none)) :: fields
| None -> fields in
let fields = match cache_creation_input_tokens with
-
| Some n -> ("cache_creation_input_tokens", `Float (float_of_int n)) :: fields
+
| Some n -> (Jsont.Json.name "cache_creation_input_tokens", Jsont.Number (float_of_int n, Jsont.Meta.none)) :: fields
| None -> fields in
let fields = match cache_read_input_tokens with
-
| Some n -> ("cache_read_input_tokens", `Float (float_of_int n)) :: fields
+
| Some n -> (Jsont.Json.name "cache_read_input_tokens", Jsont.Number (float_of_int n, Jsont.Meta.none)) :: fields
| None -> fields in
-
`O fields
-
-
let input_tokens t = JU.get_field_int_opt t "input_tokens"
-
-
let output_tokens t = JU.get_field_int_opt t "output_tokens"
-
-
let total_tokens t = JU.get_field_int_opt t "total_tokens"
-
-
let cache_creation_input_tokens t = JU.get_field_int_opt t "cache_creation_input_tokens"
-
-
let cache_read_input_tokens t = JU.get_field_int_opt t "cache_read_input_tokens"
-
+
Jsont.Object (fields, Jsont.Meta.none)
+
+
let get_field t key =
+
match t with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_int t key =
+
match get_field t key with
+
| Some (Jsont.Number (f, _)) ->
+
let i = int_of_float f in
+
if float_of_int i = f then Some i else None
+
| _ -> None
+
+
let input_tokens t = get_int t "input_tokens"
+
+
let output_tokens t = get_int t "output_tokens"
+
+
let total_tokens t = get_int t "total_tokens"
+
+
let cache_creation_input_tokens t = get_int t "cache_creation_input_tokens"
+
+
let cache_read_input_tokens t = get_int t "cache_read_input_tokens"
+
let effective_input_tokens t =
match input_tokens t with
| None -> 0
| Some input ->
let cached = Option.value (cache_read_input_tokens t) ~default:0 in
max 0 (input - cached)
-
+
let total_cost_estimate t ~input_price ~output_price =
match input_tokens t, output_tokens t with
| Some input, Some output ->
···
let output_cost = float_of_int output *. output_price /. 1_000_000. in
Some (input_cost +. output_cost)
| _ -> None
-
+
let pp fmt t =
Fmt.pf fmt "@[<2>Usage@ { input = %a;@ output = %a;@ total = %a;@ \
cache_creation = %a;@ cache_read = %a }@]"
···
Fmt.(option int) (total_tokens t)
Fmt.(option int) (cache_creation_input_tokens t)
Fmt.(option int) (cache_read_input_tokens t)
-
+
let to_json t = t
let of_json json = json
end
-
+
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
subtype : string;
duration_ms : int;
···
total_cost_usd : float option;
usage : Usage.t option;
result : string option;
-
structured_output : value option;
+
structured_output : Jsont.json option;
+
unknown : Unknown.t;
}
-
+
let create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
~session_id ?total_cost_usd ?usage ?result ?structured_output () =
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
-
session_id; total_cost_usd; usage; result; structured_output }
-
+
session_id; total_cost_usd; usage; result; structured_output; unknown = Unknown.empty }
+
+
let make subtype duration_ms duration_api_ms is_error num_turns
+
session_id total_cost_usd usage result structured_output unknown =
+
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
+
session_id; total_cost_usd; usage; result; structured_output; unknown }
+
let subtype t = t.subtype
let duration_ms t = t.duration_ms
let duration_api_ms t = t.duration_api_ms
···
let usage t = t.usage
let result t = t.result
let structured_output t = t.structured_output
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Result" make
+
|> Jsont.Object.mem "subtype" Jsont.string ~enc:subtype
+
|> Jsont.Object.mem "duration_ms" Jsont.int ~enc:duration_ms
+
|> Jsont.Object.mem "duration_api_ms" Jsont.int ~enc:duration_api_ms
+
|> Jsont.Object.mem "is_error" Jsont.bool ~enc:is_error
+
|> Jsont.Object.mem "num_turns" Jsont.int ~enc:num_turns
+
|> Jsont.Object.mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.opt_mem "total_cost_usd" Jsont.number ~enc:total_cost_usd
+
|> Jsont.Object.opt_mem "usage" Usage.jsont ~enc:usage
+
|> Jsont.Object.opt_mem "result" Jsont.string ~enc:result
+
|> Jsont.Object.opt_mem "structured_output" Jsont.json ~enc:structured_output
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
let fields = [
-
("type", `String "result");
-
("subtype", `String t.subtype);
-
("duration_ms", `Float (float_of_int t.duration_ms));
-
("duration_api_ms", `Float (float_of_int t.duration_api_ms));
-
("is_error", `Bool t.is_error);
-
("num_turns", `Float (float_of_int t.num_turns));
-
("session_id", `String t.session_id);
+
(Jsont.Json.name "type", Jsont.String ("result", Jsont.Meta.none));
+
(Jsont.Json.name "subtype", Jsont.String (t.subtype, Jsont.Meta.none));
+
(Jsont.Json.name "duration_ms", Jsont.Number (float_of_int t.duration_ms, Jsont.Meta.none));
+
(Jsont.Json.name "duration_api_ms", Jsont.Number (float_of_int t.duration_api_ms, Jsont.Meta.none));
+
(Jsont.Json.name "is_error", Jsont.Bool (t.is_error, Jsont.Meta.none));
+
(Jsont.Json.name "num_turns", Jsont.Number (float_of_int t.num_turns, Jsont.Meta.none));
+
(Jsont.Json.name "session_id", Jsont.String (t.session_id, Jsont.Meta.none));
] in
let fields = match t.total_cost_usd with
-
| Some cost -> ("total_cost_usd", `Float cost) :: fields
+
| Some cost -> (Jsont.Json.name "total_cost_usd", Jsont.Number (cost, Jsont.Meta.none)) :: fields
| None -> fields
in
let fields = match t.usage with
-
| Some usage -> ("usage", Usage.to_json usage) :: fields
+
| Some usage -> (Jsont.Json.name "usage", Usage.to_json usage) :: fields
| None -> fields
in
let fields = match t.result with
-
| Some result -> ("result", `String result) :: fields
+
| Some result -> (Jsont.Json.name "result", Jsont.String (result, Jsont.Meta.none)) :: fields
| None -> fields
in
let fields = match t.structured_output with
-
| Some output -> ("structured_output", output) :: fields
+
| Some output -> (Jsont.Json.name "structured_output", output) :: fields
| None -> fields
in
-
`O fields
-
-
let of_json = function
-
| `O fields ->
-
let subtype = JU.assoc_string "subtype" fields in
-
let duration_ms = int_of_float (JU.assoc_float "duration_ms" fields) in
-
let duration_api_ms = int_of_float (JU.assoc_float "duration_api_ms" fields) in
-
let is_error = JU.assoc_bool "is_error" fields in
-
let num_turns = int_of_float (JU.assoc_float "num_turns" fields) in
-
let session_id = JU.assoc_string "session_id" fields in
-
let total_cost_usd = JU.assoc_float_opt "total_cost_usd" fields in
-
let usage = Option.map Usage.of_json (List.assoc_opt "usage" fields) in
-
let result = JU.assoc_string_opt "result" fields in
-
let structured_output = List.assoc_opt "structured_output" fields in
+
Jsont.Object (fields, Jsont.Meta.none)
+
+
let of_json json =
+
match json with
+
| Jsont.Object (fields, _) ->
+
let subtype = match List.assoc (Jsont.Json.name "subtype") fields with
+
| Jsont.String (s, _) -> s
+
| _ -> raise (Invalid_argument "Result.of_json: invalid subtype")
+
in
+
let duration_ms = match List.assoc (Jsont.Json.name "duration_ms") fields with
+
| Jsont.Number (f, _) -> int_of_float f
+
| _ -> raise (Invalid_argument "Result.of_json: invalid duration_ms")
+
in
+
let duration_api_ms = match List.assoc (Jsont.Json.name "duration_api_ms") fields with
+
| Jsont.Number (f, _) -> int_of_float f
+
| _ -> raise (Invalid_argument "Result.of_json: invalid duration_api_ms")
+
in
+
let is_error = match List.assoc (Jsont.Json.name "is_error") fields with
+
| Jsont.Bool (b, _) -> b
+
| _ -> raise (Invalid_argument "Result.of_json: invalid is_error")
+
in
+
let num_turns = match List.assoc (Jsont.Json.name "num_turns") fields with
+
| Jsont.Number (f, _) -> int_of_float f
+
| _ -> raise (Invalid_argument "Result.of_json: invalid num_turns")
+
in
+
let session_id = match List.assoc (Jsont.Json.name "session_id") fields with
+
| Jsont.String (s, _) -> s
+
| _ -> raise (Invalid_argument "Result.of_json: invalid session_id")
+
in
+
let total_cost_usd = match List.assoc_opt (Jsont.Json.name "total_cost_usd") fields with
+
| Some (Jsont.Number (f, _)) -> Some f
+
| Some _ -> raise (Invalid_argument "Result.of_json: invalid total_cost_usd")
+
| None -> None
+
in
+
let usage = Option.map Usage.of_json (List.assoc_opt (Jsont.Json.name "usage") fields) in
+
let result = match List.assoc_opt (Jsont.Json.name "result") fields with
+
| Some (Jsont.String (s, _)) -> Some s
+
| Some _ -> raise (Invalid_argument "Result.of_json: invalid result")
+
| None -> None
+
in
+
let structured_output = List.assoc_opt (Jsont.Json.name "structured_output") fields in
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
-
session_id; total_cost_usd; usage; result; structured_output }
+
session_id; total_cost_usd; usage; result; structured_output; unknown = Unknown.empty }
| _ -> raise (Invalid_argument "Result.of_json: expected object")
-
+
let pp fmt t =
if t.is_error then
Fmt.pf fmt "@[<2>Result.error@ { session = %S;@ result = %a }@]"
···
| None -> ""
in
Fmt.pf fmt "@[<2>Result.%s@ { duration = %dms;@ cost = $%.4f%s }@]"
-
t.subtype
+
t.subtype
t.duration_ms
(Option.value t.total_cost_usd ~default:0.0)
tokens_info
···
let system ~subtype ~data = System (System.create ~subtype ~data)
let system_init ~session_id =
-
let data = System.Data.of_assoc [("session_id", `String session_id)] in
+
let data = System.Data.of_assoc [(("session_id", Jsont.String (session_id, Jsont.Meta.none)))] in
System (System.create ~subtype:"init" ~data)
let system_error ~error =
-
let data = System.Data.of_assoc [("error", `String error)] in
+
let data = System.Data.of_assoc [(("error", Jsont.String (error, Jsont.Meta.none)))] in
System (System.create ~subtype:"error" ~data)
let result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
···
Result (Result.create ~subtype ~duration_ms ~duration_api_ms ~is_error
~num_turns ~session_id ?total_cost_usd ?usage ?result ?structured_output ())
+
(* Jsont codec for the main Message variant type *)
+
let jsont : t Jsont.t =
+
let case_map kind obj dec = Jsont.Object.Case.map kind obj ~dec in
+
+
let case_user = case_map "user" User.jsont (fun v -> User v) in
+
let case_assistant = case_map "assistant" Assistant.jsont (fun v -> Assistant v) in
+
let case_system = case_map "system" System.jsont (fun v -> System v) in
+
let case_result = case_map "result" Result.jsont (fun v -> Result v) in
+
+
let enc_case = function
+
| User v -> Jsont.Object.Case.value case_user v
+
| Assistant v -> Jsont.Object.Case.value case_assistant v
+
| System v -> Jsont.Object.Case.value case_system v
+
| Result v -> Jsont.Object.Case.value case_result v
+
in
+
+
let cases = Jsont.Object.Case.[
+
make case_user;
+
make case_assistant;
+
make case_system;
+
make case_result
+
] in
+
+
Jsont.Object.map ~kind:"Message" Fun.id
+
|> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
+
~tag_to_string:Fun.id ~tag_compare:String.compare
+
|> Jsont.Object.finish
+
let to_json = function
| User t -> User.to_json t
| Assistant t -> Assistant.to_json t
···
let of_json json =
match json with
-
| `O fields -> (
-
match List.assoc_opt "type" fields with
-
| Some (`String "user") -> User (User.of_json json)
-
| Some (`String "assistant") -> Assistant (Assistant.of_json json)
-
| Some (`String "system") -> System (System.of_json json)
-
| Some (`String "result") -> Result (Result.of_json json)
-
| _ -> raise (Invalid_argument "Message.of_json: unknown type")
+
| Jsont.Object (fields, _) -> (
+
match List.assoc_opt (Jsont.Json.name "type") fields with
+
| Some (Jsont.String ("user", _)) -> User (User.of_json json)
+
| Some (Jsont.String ("assistant", _)) -> Assistant (Assistant.of_json json)
+
| Some (Jsont.String ("system", _)) -> System (System.of_json json)
+
| Some (Jsont.String ("result", _)) -> Result (Result.of_json json)
+
| Some _ -> raise (Invalid_argument "Message.of_json: invalid type")
+
| None -> raise (Invalid_argument "Message.of_json: missing type field")
)
| _ -> raise (Invalid_argument "Message.of_json: expected object")
+171 -110
claudeio/lib/message.mli
···
(** Messages exchanged with Claude.
-
-
This module defines the various types of messages that can be sent to and
-
received from Claude, including user input, assistant responses, system
+
+
This module defines the various types of messages that can be sent to and
+
received from Claude, including user input, assistant responses, system
messages, and result metadata. *)
(** The log source for message operations *)
···
module User : sig
(** Messages sent by the user. *)
-
-
type content =
+
+
type content =
| String of string (** Simple text message *)
| Blocks of Content_block.t list (** Complex message with multiple content blocks *)
(** The content of a user message. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of user messages. *)
-
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for user messages. *)
+
val create_string : string -> t
(** [create_string s] creates a user message with simple text content. *)
-
+
val create_blocks : Content_block.t list -> t
(** [create_blocks blocks] creates a user message with content blocks. *)
-
-
val create_with_tool_result :
-
tool_use_id:string ->
-
content:string ->
-
?is_error:bool ->
+
+
val create_with_tool_result :
+
tool_use_id:string ->
+
content:string ->
+
?is_error:bool ->
unit -> t
-
(** [create_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user
+
(** [create_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user
message containing a tool result. *)
-
+
val create_mixed : text:string option -> tool_results:(string * string * bool option) list -> t
-
(** [create_mixed ?text ~tool_results] creates a user message with optional text
+
(** [create_mixed ?text ~tool_results] creates a user message with optional text
and tool results. Each tool result is (tool_use_id, content, is_error). *)
-
+
val content : t -> content
(** [content t] returns the content of the user message. *)
-
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields preserved from JSON. *)
+
val as_text : t -> string option
(** [as_text t] returns the text content if the message is a simple string, None otherwise. *)
-
+
val get_blocks : t -> Content_block.t list
(** [get_blocks t] returns the content blocks, or a single text block if it's a string message. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the user message to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses a user message from JSON.
@raise Invalid_argument if the JSON is not a valid user message. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the user message. *)
end
···
val error_of_string : string -> error
(** [error_of_string s] parses an error string. Unknown strings become [`Unknown]. *)
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of assistant messages. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for assistant messages. *)
val create : content:Content_block.t list -> model:string -> ?error:error -> unit -> t
(** [create ~content ~model ?error ()] creates an assistant message.
···
val error : t -> error option
(** [error t] returns the optional error that occurred during message generation. *)
-
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields preserved from JSON. *)
+
val get_text_blocks : t -> string list
(** [get_text_blocks t] extracts all text content from the message. *)
-
+
val get_tool_uses : t -> Content_block.Tool_use.t list
(** [get_tool_uses t] extracts all tool use blocks from the message. *)
-
+
val get_thinking : t -> Content_block.Thinking.t list
(** [get_thinking t] extracts all thinking blocks from the message. *)
-
+
val has_tool_use : t -> bool
(** [has_tool_use t] returns true if the message contains any tool use blocks. *)
-
+
val combined_text : t -> string
(** [combined_text t] concatenates all text blocks into a single string. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the assistant message to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses an assistant message from JSON.
@raise Invalid_argument if the JSON is not a valid assistant message. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the assistant message. *)
end
···
module System : sig
(** System control and status messages. *)
-
+
module Data : sig
(** System message data. *)
-
-
type t
-
(** Abstract type for system message data. Contains both the raw JSON
-
and typed accessors for common fields. *)
-
+
+
type t = Jsont.json
+
(** Opaque type for system message data. Contains the raw JSON
+
with typed accessors for common fields. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for system data. *)
+
val empty : t
(** [empty] creates empty data. *)
-
-
val of_assoc : (string * Ezjsonm.value) list -> t
+
+
val of_assoc : (string * Jsont.json) list -> t
(** [of_assoc assoc] creates data from an association list. *)
-
+
val get_string : t -> string -> string option
(** [get_string t key] returns the string value for [key], if present. *)
-
+
val get_int : t -> string -> int option
(** [get_int t key] returns the integer value for [key], if present. *)
-
+
val get_bool : t -> string -> bool option
(** [get_bool t key] returns the boolean value for [key], if present. *)
-
+
val get_float : t -> string -> float option
(** [get_float t key] returns the float value for [key], if present. *)
-
-
val get_list : t -> string -> Ezjsonm.value list option
+
+
val get_list : t -> string -> Jsont.json list option
(** [get_list t key] returns the list value for [key], if present. *)
-
-
val get_field : t -> string -> Ezjsonm.value option
+
+
val get_field : t -> string -> Jsont.json option
(** [get_field t key] returns the raw JSON value for [key], if present. *)
-
-
val raw_json : t -> Ezjsonm.value
+
+
val raw_json : t -> Jsont.json
(** [raw_json t] returns the full underlying JSON data. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts to JSON representation. Internal use only. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses from JSON. Internal use only. *)
end
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of system messages. *)
-
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for system messages. *)
+
val create : subtype:string -> data:Data.t -> t
(** [create ~subtype ~data] creates a system message.
@param subtype The subtype of the system message
@param data Additional data for the message *)
-
+
val subtype : t -> string
(** [subtype t] returns the subtype of the system message. *)
-
+
val data : t -> Data.t
(** [data t] returns the additional data of the system message. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields preserved from JSON. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the system message to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses a system message from JSON.
@raise Invalid_argument if the JSON is not a valid system message. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the system message. *)
end
···
module Result : sig
(** Final result messages with metadata about the conversation. *)
-
+
module Usage : sig
(** Usage statistics for API calls. *)
-
-
type t
-
(** Abstract type for usage statistics. *)
-
-
val create :
-
?input_tokens:int ->
-
?output_tokens:int ->
+
+
type t = Jsont.json
+
(** Opaque type for usage statistics. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for usage statistics. *)
+
+
val create :
+
?input_tokens:int ->
+
?output_tokens:int ->
?total_tokens:int ->
?cache_creation_input_tokens:int ->
?cache_read_input_tokens:int ->
unit -> t
-
(** [create ?input_tokens ?output_tokens ?total_tokens ?cache_creation_input_tokens
+
(** [create ?input_tokens ?output_tokens ?total_tokens ?cache_creation_input_tokens
?cache_read_input_tokens ()] creates usage statistics. *)
-
+
val input_tokens : t -> int option
(** [input_tokens t] returns the number of input tokens used. *)
-
+
val output_tokens : t -> int option
(** [output_tokens t] returns the number of output tokens generated. *)
-
+
val total_tokens : t -> int option
(** [total_tokens t] returns the total number of tokens. *)
-
+
val cache_creation_input_tokens : t -> int option
(** [cache_creation_input_tokens t] returns cache creation input tokens. *)
-
+
val cache_read_input_tokens : t -> int option
(** [cache_read_input_tokens t] returns cache read input tokens. *)
-
+
val effective_input_tokens : t -> int
(** [effective_input_tokens t] returns input tokens minus cached tokens, or 0 if not available. *)
-
+
val total_cost_estimate : t -> input_price:float -> output_price:float -> float option
-
(** [total_cost_estimate t ~input_price ~output_price] estimates the cost based on token
+
(** [total_cost_estimate t ~input_price ~output_price] estimates the cost based on token
prices per million tokens. Returns None if token counts are not available. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the usage statistics. *)
-
-
val to_json : t -> Ezjsonm.value
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts to JSON representation. Internal use only. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses from JSON. Internal use only. *)
end
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of result messages. *)
-
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for result messages. *)
+
val create :
subtype:string ->
duration_ms:int ->
···
?total_cost_usd:float ->
?usage:Usage.t ->
?result:string ->
-
?structured_output:Ezjsonm.value ->
+
?structured_output:Jsont.json ->
unit -> t
-
(** [create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
(** [create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
~session_id ?total_cost_usd ?usage ?result ()] creates a result message.
@param subtype The subtype of the result
@param duration_ms Total duration in milliseconds
···
@param usage Optional usage statistics as JSON
@param result Optional result string
@param structured_output Optional structured JSON output from Claude *)
-
+
val subtype : t -> string
(** [subtype t] returns the subtype of the result. *)
-
+
val duration_ms : t -> int
(** [duration_ms t] returns the total duration in milliseconds. *)
-
+
val duration_api_ms : t -> int
(** [duration_api_ms t] returns the API duration in milliseconds. *)
-
+
val is_error : t -> bool
(** [is_error t] returns whether this result represents an error. *)
-
+
val num_turns : t -> int
(** [num_turns t] returns the number of conversation turns. *)
-
+
val session_id : t -> string
(** [session_id t] returns the session identifier. *)
-
+
val total_cost_usd : t -> float option
(** [total_cost_usd t] returns the optional total cost in USD. *)
-
+
val usage : t -> Usage.t option
(** [usage t] returns the optional usage statistics. *)
-
+
val result : t -> string option
(** [result t] returns the optional result string. *)
-
val structured_output : t -> Ezjsonm.value option
+
val structured_output : t -> Jsont.json option
(** [structured_output t] returns the optional structured JSON output. *)
-
val to_json : t -> Ezjsonm.value
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields preserved from JSON. *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts the result message to its JSON representation. *)
-
-
val of_json : Ezjsonm.value -> t
+
+
val of_json : Jsont.json -> t
(** [of_json json] parses a result message from JSON.
@raise Invalid_argument if the JSON is not a valid result message. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the result message. *)
end
···
| Result of Result.t
(** The type of messages, which can be user, assistant, system, or result. *)
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for messages. *)
+
val user_string : string -> t
(** [user_string s] creates a user message with text content. *)
···
(** [user_blocks blocks] creates a user message with content blocks. *)
val user_with_tool_result : tool_use_id:string -> content:string -> ?is_error:bool -> unit -> t
-
(** [user_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user message
+
(** [user_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user message
containing a tool result. *)
val assistant : content:Content_block.t list -> model:string -> ?error:Assistant.error -> unit -> t
···
?total_cost_usd:float ->
?usage:Result.Usage.t ->
?result:string ->
-
?structured_output:Ezjsonm.value ->
+
?structured_output:Jsont.json ->
unit -> t
-
(** [result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
(** [result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
~session_id ?total_cost_usd ?usage ?result ()] creates a result message. *)
-
val to_json : t -> Ezjsonm.value
+
val to_json : t -> Jsont.json
(** [to_json t] converts any message to its JSON representation. *)
-
val of_json : Ezjsonm.value -> t
+
val of_json : Jsont.json -> t
(** [of_json json] parses a message from JSON.
@raise Invalid_argument if the JSON is not a valid message. *)
+27
claudeio/lib/model.ml
···
+
type t = [
+
| `Sonnet_4_5
+
| `Sonnet_4
+
| `Sonnet_3_5
+
| `Opus_4
+
| `Haiku_4
+
| `Custom of string
+
]
+
+
let to_string = function
+
| `Sonnet_4_5 -> "claude-sonnet-4-5"
+
| `Sonnet_4 -> "claude-sonnet-4"
+
| `Sonnet_3_5 -> "claude-sonnet-3-5"
+
| `Opus_4 -> "claude-opus-4"
+
| `Haiku_4 -> "claude-haiku-4"
+
| `Custom s -> s
+
+
let of_string = function
+
| "claude-sonnet-4-5" -> `Sonnet_4_5
+
| "claude-sonnet-4" -> `Sonnet_4
+
| "claude-sonnet-3-5" -> `Sonnet_3_5
+
| "claude-opus-4" -> `Opus_4
+
| "claude-haiku-4" -> `Haiku_4
+
| s -> `Custom s
+
+
let pp fmt t =
+
Fmt.string fmt (to_string t)
+36
claudeio/lib/model.mli
···
+
(** Claude AI model identifiers.
+
+
This module provides type-safe model identifiers based on the Python SDK's
+
model strings. Use polymorphic variants for known models with a custom
+
escape hatch for future or unknown models. *)
+
+
type t = [
+
| `Sonnet_4_5 (** claude-sonnet-4-5 - Most recent Sonnet model *)
+
| `Sonnet_4 (** claude-sonnet-4 - Sonnet 4 model *)
+
| `Sonnet_3_5 (** claude-sonnet-3-5 - Sonnet 3.5 model *)
+
| `Opus_4 (** claude-opus-4 - Opus 4 model for complex tasks *)
+
| `Haiku_4 (** claude-haiku-4 - Fast, cost-effective Haiku model *)
+
| `Custom of string (** Custom model string for future/unknown models *)
+
]
+
(** The type of Claude models. *)
+
+
val to_string : t -> string
+
(** [to_string t] converts a model to its CLI string representation.
+
+
Examples:
+
- [`Sonnet_4_5] becomes "claude-sonnet-4-5"
+
- [`Opus_4] becomes "claude-opus-4"
+
- [`Custom "my-model"] becomes "my-model" *)
+
+
val of_string : string -> t
+
(** [of_string s] parses a model string into a typed model.
+
+
Known model strings are converted to their typed variants.
+
Unknown strings become [`Custom s].
+
+
Examples:
+
- "claude-sonnet-4-5" becomes [`Sonnet_4_5]
+
- "future-model" becomes [`Custom "future-model"] *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints a model identifier. *)
+82 -99
claudeio/lib/options.ml
···
-
open Ezjsonm
-
let src = Logs.Src.create "claude.options" ~doc:"Claude configuration options"
module Log = (val Logs.src_log src : Logs.LOG)
type setting_source = User | Project | Local
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let _is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let _jsont = Jsont.json
+
end
+
type t = {
allowed_tools : string list;
disallowed_tools : string list;
···
max_buffer_size : int option;
user : string option;
output_format : Structured_output.t option;
+
unknown : Unknown.t;
}
let default = {
···
max_buffer_size = None;
user = None;
output_format = None;
+
unknown = Unknown.empty;
}
let create
···
?max_buffer_size
?user
?output_format
+
?(unknown = Unknown.empty)
() =
{ allowed_tools; disallowed_tools; max_thinking_tokens;
system_prompt; append_system_prompt; permission_mode;
···
permission_prompt_tool_name; settings; add_dirs;
extra_args; debug_stderr; hooks;
max_budget_usd; fallback_model; setting_sources;
-
max_buffer_size; user; output_format }
+
max_buffer_size; user; output_format; unknown }
let allowed_tools t = t.allowed_tools
let disallowed_tools t = t.disallowed_tools
···
let max_buffer_size t = t.max_buffer_size
let user t = t.user
let output_format t = t.output_format
+
let unknown t = t.unknown
let with_allowed_tools tools t = { t with allowed_tools = tools }
let with_disallowed_tools tools t = { t with disallowed_tools = tools }
···
let with_user user t = { t with user = Some user }
let with_output_format format t = { t with output_format = Some format }
+
(* Helper codec for Model.t *)
+
let model_jsont : Model.t Jsont.t =
+
Jsont.map ~kind:"Model"
+
~dec:Model.of_string
+
~enc:Model.to_string
+
Jsont.string
+
+
(* Helper codec for env - list of string pairs encoded as object *)
+
let env_jsont : (string * string) list Jsont.t =
+
Jsont.map ~kind:"Env"
+
~dec:(fun obj ->
+
match obj with
+
| Jsont.Object (members, _) ->
+
List.map (fun ((name, _), value) ->
+
match value with
+
| Jsont.String (s, _) -> (name, s)
+
| _ -> (name, "")
+
) members
+
| _ -> [])
+
~enc:(fun pairs ->
+
let mems = List.map (fun (k, v) ->
+
Jsont.Json.mem (Jsont.Json.name k) (Jsont.Json.string v)
+
) pairs in
+
Jsont.Json.object' mems)
+
Jsont.json
+
+
let jsont : t Jsont.t =
+
let make allowed_tools disallowed_tools max_thinking_tokens
+
system_prompt append_system_prompt permission_mode
+
model env unknown =
+
{ allowed_tools; disallowed_tools; max_thinking_tokens;
+
system_prompt; append_system_prompt; permission_mode;
+
permission_callback = Some Permissions.default_allow_callback;
+
model; cwd = None; env;
+
continue_conversation = false;
+
resume = None;
+
max_turns = None;
+
permission_prompt_tool_name = None;
+
settings = None;
+
add_dirs = [];
+
extra_args = [];
+
debug_stderr = None;
+
hooks = None;
+
max_budget_usd = None;
+
fallback_model = None;
+
setting_sources = None;
+
max_buffer_size = None;
+
user = None;
+
output_format = None;
+
unknown }
+
in
+
Jsont.Object.map ~kind:"Options" make
+
|> Jsont.Object.mem "allowed_tools" (Jsont.list Jsont.string) ~enc:allowed_tools ~dec_absent:[]
+
|> Jsont.Object.mem "disallowed_tools" (Jsont.list Jsont.string) ~enc:disallowed_tools ~dec_absent:[]
+
|> Jsont.Object.mem "max_thinking_tokens" Jsont.int ~enc:max_thinking_tokens ~dec_absent:8000
+
|> Jsont.Object.opt_mem "system_prompt" Jsont.string ~enc:system_prompt
+
|> Jsont.Object.opt_mem "append_system_prompt" Jsont.string ~enc:append_system_prompt
+
|> Jsont.Object.opt_mem "permission_mode" Permissions.Mode.jsont ~enc:permission_mode
+
|> Jsont.Object.opt_mem "model" model_jsont ~enc:model
+
|> Jsont.Object.mem "env" env_jsont ~enc:env ~dec_absent:[]
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let to_json t =
-
let fields = [] in
-
let fields =
-
if t.allowed_tools <> [] then
-
("allowed_tools", `A (List.map (fun s -> `String s) t.allowed_tools)) :: fields
-
else fields
-
in
-
let fields =
-
if t.disallowed_tools <> [] then
-
("disallowed_tools", `A (List.map (fun s -> `String s) t.disallowed_tools)) :: fields
-
else fields
-
in
-
let fields =
-
if t.max_thinking_tokens <> 8000 then
-
("max_thinking_tokens", `Float (float_of_int t.max_thinking_tokens)) :: fields
-
else fields
-
in
-
let fields = match t.system_prompt with
-
| Some p -> ("system_prompt", `String p) :: fields
-
| None -> fields
-
in
-
let fields = match t.append_system_prompt with
-
| Some p -> ("append_system_prompt", `String p) :: fields
-
| None -> fields
-
in
-
let fields = match t.permission_mode with
-
| Some m -> ("permission_mode", Permissions.Mode.to_json m) :: fields
-
| None -> fields
-
in
-
let fields = match t.model with
-
| Some m -> ("model", `String (Model.to_string m)) :: fields
-
| None -> fields
-
in
-
let fields =
-
if t.env <> [] then
-
let env_obj = `O (List.map (fun (k, v) -> (k, `String v)) t.env) in
-
("env", env_obj) :: fields
-
else fields
-
in
-
`O fields
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Options.to_json: " ^ msg)
-
let of_json = function
-
| `O fields ->
-
let allowed_tools =
-
try get_list get_string (List.assoc "allowed_tools" fields)
-
with Not_found -> []
-
in
-
let disallowed_tools =
-
try get_list get_string (List.assoc "disallowed_tools" fields)
-
with Not_found -> []
-
in
-
let max_thinking_tokens =
-
try int_of_float (get_float (List.assoc "max_thinking_tokens" fields))
-
with Not_found -> 8000
-
in
-
let system_prompt =
-
try Some (get_string (List.assoc "system_prompt" fields))
-
with Not_found -> None
-
in
-
let append_system_prompt =
-
try Some (get_string (List.assoc "append_system_prompt" fields))
-
with Not_found -> None
-
in
-
let permission_mode =
-
try Some (Permissions.Mode.of_json (List.assoc "permission_mode" fields))
-
with Not_found -> None
-
in
-
let model =
-
try Some (Model.of_string (get_string (List.assoc "model" fields)))
-
with Not_found -> None
-
in
-
let env =
-
try
-
match List.assoc "env" fields with
-
| `O pairs -> List.map (fun (k, v) -> (k, get_string v)) pairs
-
| _ -> []
-
with Not_found -> []
-
in
-
{ allowed_tools; disallowed_tools; max_thinking_tokens;
-
system_prompt; append_system_prompt; permission_mode;
-
permission_callback = Some Permissions.default_allow_callback;
-
model; cwd = None; env;
-
continue_conversation = false;
-
resume = None;
-
max_turns = None;
-
permission_prompt_tool_name = None;
-
settings = None;
-
add_dirs = [];
-
extra_args = [];
-
debug_stderr = None;
-
hooks = None;
-
max_budget_usd = None;
-
fallback_model = None;
-
setting_sources = None;
-
max_buffer_size = None;
-
user = None;
-
output_format = None; }
-
| _ -> raise (Invalid_argument "Options.of_json: expected object")
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok t -> t
+
| Error msg -> raise (Invalid_argument ("Options.of_json: " ^ msg))
let pp fmt t =
Fmt.pf fmt "@[<v>Options {@ \
+10 -5
claudeio/lib/options.mli
···
{3 Structured Output: Type-Safe Responses}
{[
-
let schema = Ezjsonm.(`O [
+
let schema = Jsont.json_of_json (`O [
("type", `String "object");
("properties", `O [
("count", `O [("type", `String "integer")]);
···
Use {!with_fallback_model} to specify an alternative model when the
primary model is unavailable or overloaded. This improves reliability. *)
-
-
open Ezjsonm
(** The log source for options operations *)
val src : Logs.Src.t
···
?max_buffer_size:int ->
?user:string ->
?output_format:Structured_output.t ->
+
?unknown:Jsont.json ->
unit -> t
(** [create ?allowed_tools ?disallowed_tools ?max_thinking_tokens ?system_prompt
?append_system_prompt ?permission_mode ?permission_callback ?model ?cwd ?env
···
val output_format : t -> Structured_output.t option
(** [output_format t] returns the optional structured output format. *)
+
+
val unknown : t -> Jsont.json
+
(** [unknown t] returns any unknown JSON fields that were preserved during decoding. *)
(** {1 Builders} *)
···
(** {1 Serialization} *)
-
val to_json : t -> value
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for Options.t *)
+
+
val to_json : t -> Jsont.json
(** [to_json t] converts options to JSON representation. *)
-
val of_json : value -> t
+
val of_json : Jsont.json -> t
(** [of_json json] parses options from JSON.
@raise Invalid_argument if the JSON is not valid options. *)
+192 -168
claudeio/lib/permissions.ml
···
-
open Ezjsonm
-
let src = Logs.Src.create "claude.permission" ~doc:"Claude permission system"
module Log = (val Logs.src_log src : Logs.LOG)
(* Helper for pretty-printing JSON *)
let pp_json fmt json =
-
Fmt.string fmt (value_to_string json)
+
let s = match Jsont_bytesrw.encode_string' Jsont.json json with
+
| Ok s -> s
+
| Error err -> Jsont.Error.to_string err
+
in
+
Fmt.string fmt s
(** Permission modes *)
module Mode = struct
-
type t =
+
type t =
| Default
| Accept_edits
| Plan
| Bypass_permissions
-
+
let to_string = function
| Default -> "default"
| Accept_edits -> "acceptEdits"
| Plan -> "plan"
| Bypass_permissions -> "bypassPermissions"
-
+
let of_string = function
| "default" -> Default
| "acceptEdits" -> Accept_edits
| "plan" -> Plan
| "bypassPermissions" -> Bypass_permissions
| s -> raise (Invalid_argument (Printf.sprintf "Mode.of_string: unknown mode %s" s))
-
-
let to_json t = `String (to_string t)
-
-
let of_json = function
-
| `String s -> of_string s
-
| _ -> raise (Invalid_argument "Mode.of_json: expected string")
-
+
let pp fmt t = Fmt.string fmt (to_string t)
+
+
let jsont : t Jsont.t =
+
Jsont.enum [
+
"default", Default;
+
"acceptEdits", Accept_edits;
+
"plan", Plan;
+
"bypassPermissions", Bypass_permissions;
+
]
end
(** Permission behaviors *)
module Behavior = struct
type t = Allow | Deny | Ask
-
+
let to_string = function
| Allow -> "allow"
| Deny -> "deny"
| Ask -> "ask"
-
+
let of_string = function
| "allow" -> Allow
| "deny" -> Deny
| "ask" -> Ask
| s -> raise (Invalid_argument (Printf.sprintf "Behavior.of_string: unknown behavior %s" s))
-
-
let to_json t = `String (to_string t)
-
-
let of_json = function
-
| `String s -> of_string s
-
| _ -> raise (Invalid_argument "Behavior.of_json: expected string")
-
+
let pp fmt t = Fmt.string fmt (to_string t)
+
+
let jsont : t Jsont.t =
+
Jsont.enum [
+
"allow", Allow;
+
"deny", Deny;
+
"ask", Ask;
+
]
end
(** Permission rules *)
module Rule = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
tool_name : string;
rule_content : string option;
+
unknown : Unknown.t;
}
-
-
let create ~tool_name ?rule_content () = { tool_name; rule_content }
+
+
let create ~tool_name ?rule_content ?(unknown = Unknown.empty) () =
+
{ tool_name; rule_content; unknown }
let tool_name t = t.tool_name
let rule_content t = t.rule_content
-
-
let to_json t =
-
let fields = [("tool_name", `String t.tool_name)] in
-
let fields = match t.rule_content with
-
| Some c -> ("rule_content", `String c) :: fields
-
| None -> fields
-
in
-
`O fields
-
-
let of_json = function
-
| `O fields ->
-
let tool_name = get_string (List.assoc "tool_name" fields) in
-
let rule_content =
-
try Some (get_string (List.assoc "rule_content" fields))
-
with Not_found -> None
-
in
-
{ tool_name; rule_content }
-
| _ -> raise (Invalid_argument "Rule.of_json: expected object")
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
let make tool_name rule_content unknown = { tool_name; rule_content; unknown } in
+
Jsont.Object.map ~kind:"Rule" make
+
|> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
+
|> Jsont.Object.opt_mem "rule_content" Jsont.string ~enc:rule_content
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let pp fmt t =
Fmt.pf fmt "@[<2>Rule@ { tool_name = %S;@ rule_content = %a }@]"
t.tool_name Fmt.(option string) t.rule_content
···
(** Permission updates *)
module Update = struct
-
type destination =
+
type destination =
| User_settings
| Project_settings
| Local_settings
| Session
-
+
let destination_to_string = function
| User_settings -> "userSettings"
| Project_settings -> "projectSettings"
| Local_settings -> "localSettings"
| Session -> "session"
-
-
let destination_of_string = function
+
+
let _destination_of_string = function
| "userSettings" -> User_settings
| "projectSettings" -> Project_settings
| "localSettings" -> Local_settings
| "session" -> Session
| s -> raise (Invalid_argument (Printf.sprintf "destination_of_string: unknown %s" s))
-
+
+
let destination_jsont : destination Jsont.t =
+
Jsont.enum [
+
"userSettings", User_settings;
+
"projectSettings", Project_settings;
+
"localSettings", Local_settings;
+
"session", Session;
+
]
+
type update_type =
| Add_rules
| Replace_rules
···
| Set_mode
| Add_directories
| Remove_directories
-
+
let update_type_to_string = function
| Add_rules -> "addRules"
| Replace_rules -> "replaceRules"
···
| Set_mode -> "setMode"
| Add_directories -> "addDirectories"
| Remove_directories -> "removeDirectories"
-
-
let update_type_of_string = function
+
+
let _update_type_of_string = function
| "addRules" -> Add_rules
| "replaceRules" -> Replace_rules
| "removeRules" -> Remove_rules
···
| "addDirectories" -> Add_directories
| "removeDirectories" -> Remove_directories
| s -> raise (Invalid_argument (Printf.sprintf "update_type_of_string: unknown %s" s))
+
+
let update_type_jsont : update_type Jsont.t =
+
Jsont.enum [
+
"addRules", Add_rules;
+
"replaceRules", Replace_rules;
+
"removeRules", Remove_rules;
+
"setMode", Set_mode;
+
"addDirectories", Add_directories;
+
"removeDirectories", Remove_directories;
+
]
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
update_type : update_type;
rules : Rule.t list option;
···
mode : Mode.t option;
directories : string list option;
destination : destination option;
+
unknown : Unknown.t;
}
-
-
let create ~update_type ?rules ?behavior ?mode ?directories ?destination () =
-
{ update_type; rules; behavior; mode; directories; destination }
-
+
+
let create ~update_type ?rules ?behavior ?mode ?directories ?destination ?(unknown = Unknown.empty) () =
+
{ update_type; rules; behavior; mode; directories; destination; unknown }
+
let update_type t = t.update_type
let rules t = t.rules
let behavior t = t.behavior
let mode t = t.mode
let directories t = t.directories
let destination t = t.destination
-
-
let to_json t =
-
let fields = [("type", `String (update_type_to_string t.update_type))] in
-
let fields = match t.rules with
-
| Some rules -> ("rules", `A (List.map Rule.to_json rules)) :: fields
-
| None -> fields
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
let make update_type rules behavior mode directories destination unknown =
+
{ update_type; rules; behavior; mode; directories; destination; unknown }
in
-
let fields = match t.behavior with
-
| Some b -> ("behavior", Behavior.to_json b) :: fields
-
| None -> fields
-
in
-
let fields = match t.mode with
-
| Some m -> ("mode", Mode.to_json m) :: fields
-
| None -> fields
-
in
-
let fields = match t.directories with
-
| Some dirs -> ("directories", `A (List.map (fun s -> `String s) dirs)) :: fields
-
| None -> fields
-
in
-
let fields = match t.destination with
-
| Some d -> ("destination", `String (destination_to_string d)) :: fields
-
| None -> fields
-
in
-
`O fields
-
-
let of_json = function
-
| `O fields ->
-
let update_type = update_type_of_string (get_string (List.assoc "type" fields)) in
-
let rules =
-
try Some (get_list Rule.of_json (List.assoc "rules" fields))
-
with Not_found -> None
-
in
-
let behavior =
-
try Some (Behavior.of_json (List.assoc "behavior" fields))
-
with Not_found -> None
-
in
-
let mode =
-
try Some (Mode.of_json (List.assoc "mode" fields))
-
with Not_found -> None
-
in
-
let directories =
-
try Some (get_list get_string (List.assoc "directories" fields))
-
with Not_found -> None
-
in
-
let destination =
-
try Some (destination_of_string (get_string (List.assoc "destination" fields)))
-
with Not_found -> None
-
in
-
{ update_type; rules; behavior; mode; directories; destination }
-
| _ -> raise (Invalid_argument "Update.of_json: expected object")
+
Jsont.Object.map ~kind:"Update" make
+
|> Jsont.Object.mem "type" update_type_jsont ~enc:update_type
+
|> Jsont.Object.opt_mem "rules" (Jsont.list Rule.jsont) ~enc:rules
+
|> Jsont.Object.opt_mem "behavior" Behavior.jsont ~enc:behavior
+
|> Jsont.Object.opt_mem "mode" Mode.jsont ~enc:mode
+
|> Jsont.Object.opt_mem "directories" (Jsont.list Jsont.string) ~enc:directories
+
|> Jsont.Object.opt_mem "destination" destination_jsont ~enc:destination
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
let pp fmt t =
Fmt.pf fmt "@[<2>Update@ { type = %s;@ rules = %a;@ behavior = %a;@ \
···
(** Permission context for callbacks *)
module Context = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
suggestions : Update.t list;
+
unknown : Unknown.t;
}
-
-
let create ?(suggestions = []) () = { suggestions }
+
+
let create ?(suggestions = []) ?(unknown = Unknown.empty) () = { suggestions; unknown }
let suggestions t = t.suggestions
-
-
let to_json t =
-
`O [("suggestions", `A (List.map Update.to_json t.suggestions))]
-
-
let of_json = function
-
| `O fields ->
-
let suggestions =
-
try get_list Update.of_json (List.assoc "suggestions" fields)
-
with Not_found -> []
-
in
-
{ suggestions }
-
| _ -> raise (Invalid_argument "Context.of_json: expected object")
-
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
let make suggestions unknown = { suggestions; unknown } in
+
Jsont.Object.map ~kind:"Context" make
+
|> Jsont.Object.mem "suggestions" (Jsont.list Update.jsont) ~enc:suggestions ~dec_absent:[]
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
let pp fmt t =
Fmt.pf fmt "@[<2>Context@ { suggestions = @[<v>%a@] }@]"
Fmt.(list ~sep:(any "@,") Update.pp) t.suggestions
···
(** Permission results *)
module Result = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t =
| Allow of {
-
updated_input : value option;
+
updated_input : Jsont.json option;
updated_permissions : Update.t list option;
+
unknown : Unknown.t;
}
| Deny of {
message : string;
interrupt : bool;
+
unknown : Unknown.t;
}
-
-
let allow ?updated_input ?updated_permissions () =
-
Allow { updated_input; updated_permissions }
-
-
let deny ~message ~interrupt = Deny { message; interrupt }
-
-
let to_json = function
-
| Allow { updated_input; updated_permissions } ->
-
let fields = [("behavior", `String "allow")] in
-
let fields = match updated_input with
-
| Some input -> ("updated_input", input) :: fields
-
| None -> fields
-
in
-
let fields = match updated_permissions with
-
| Some perms -> ("updated_permissions", `A (List.map Update.to_json perms)) :: fields
-
| None -> fields
-
in
-
`O fields
-
| Deny { message; interrupt } ->
-
`O [
-
("behavior", `String "deny");
-
("message", `String message);
-
("interrupt", `Bool interrupt);
-
]
-
-
let of_json = function
-
| `O fields -> (
-
match List.assoc "behavior" fields with
-
| `String "allow" ->
-
let updated_input = List.assoc_opt "updated_input" fields in
-
let updated_permissions =
-
try Some (get_list Update.of_json (List.assoc "updated_permissions" fields))
-
with Not_found -> None
-
in
-
Allow { updated_input; updated_permissions }
-
| `String "deny" ->
-
let message = get_string (List.assoc "message" fields) in
-
let interrupt = get_bool (List.assoc "interrupt" fields) in
-
Deny { message; interrupt }
-
| _ -> raise (Invalid_argument "Result.of_json: unknown behavior")
-
)
-
| _ -> raise (Invalid_argument "Result.of_json: expected object")
+
+
let allow ?updated_input ?updated_permissions ?(unknown = Unknown.empty) () =
+
Allow { updated_input; updated_permissions; unknown }
+
+
let deny ~message ~interrupt ?(unknown = Unknown.empty) () =
+
Deny { message; interrupt; unknown }
+
+
let jsont : t Jsont.t =
+
let allow_record =
+
let make updated_input updated_permissions unknown =
+
Allow { updated_input; updated_permissions; unknown }
+
in
+
Jsont.Object.map ~kind:"AllowRecord" make
+
|> Jsont.Object.opt_mem "updated_input" Jsont.json ~enc:(function
+
| Allow { updated_input; _ } -> updated_input
+
| _ -> None)
+
|> Jsont.Object.opt_mem "updated_permissions" (Jsont.list Update.jsont) ~enc:(function
+
| Allow { updated_permissions; _ } -> updated_permissions
+
| _ -> None)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(function
+
| Allow { unknown; _ } -> unknown
+
| _ -> Unknown.empty)
+
|> Jsont.Object.finish
+
in
+
let deny_record =
+
let make message interrupt unknown =
+
Deny { message; interrupt; unknown }
+
in
+
Jsont.Object.map ~kind:"DenyRecord" make
+
|> Jsont.Object.mem "message" Jsont.string ~enc:(function
+
| Deny { message; _ } -> message
+
| _ -> "")
+
|> Jsont.Object.mem "interrupt" Jsont.bool ~enc:(function
+
| Deny { interrupt; _ } -> interrupt
+
| _ -> false)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(function
+
| Deny { unknown; _ } -> unknown
+
| _ -> Unknown.empty)
+
|> Jsont.Object.finish
+
in
+
let case_allow = Jsont.Object.Case.map "allow" allow_record ~dec:(fun v -> v) in
+
let case_deny = Jsont.Object.Case.map "deny" deny_record ~dec:(fun v -> v) in
+
+
let enc_case = function
+
| Allow _ as v -> Jsont.Object.Case.value case_allow v
+
| Deny _ as v -> Jsont.Object.Case.value case_deny v
+
in
+
+
let cases = Jsont.Object.Case.[
+
make case_allow;
+
make case_deny
+
] in
+
+
Jsont.Object.map ~kind:"Result" Fun.id
+
|> Jsont.Object.case_mem "behavior" Jsont.string ~enc:Fun.id ~enc_case cases
+
~tag_to_string:Fun.id ~tag_compare:String.compare
+
|> Jsont.Object.finish
let pp fmt = function
-
| Allow { updated_input; updated_permissions } ->
+
| Allow { updated_input; updated_permissions; _ } ->
Fmt.pf fmt "@[<2>Allow@ { updated_input = %a;@ updated_permissions = %a }@]"
Fmt.(option pp_json) updated_input
Fmt.(option (list Update.pp)) updated_permissions
-
| Deny { message; interrupt } ->
+
| Deny { message; interrupt; _ } ->
Fmt.pf fmt "@[<2>Deny@ { message = %S;@ interrupt = %b }@]" message interrupt
end
(** Permission callback type *)
-
type callback =
-
tool_name:string ->
-
input:value ->
-
context:Context.t ->
+
type callback =
+
tool_name:string ->
+
input:Jsont.json ->
+
context:Context.t ->
Result.t
(** Default callbacks *)
+127 -106
claudeio/lib/permissions.mli
···
(** Permission system for Claude tool invocations.
-
+
This module provides a permission system for controlling
which tools Claude can invoke and how they can be used. It includes
support for permission modes, rules, updates, and callbacks. *)
-
-
open Ezjsonm
(** The log source for permission operations *)
val src : Logs.Src.t
···
module Mode : sig
(** Permission modes control the overall behavior of the permission system. *)
-
-
type t =
+
+
type t =
| Default (** Standard permission mode with normal checks *)
| Accept_edits (** Automatically accept file edits *)
| Plan (** Planning mode with restricted execution *)
| Bypass_permissions (** Bypass all permission checks *)
(** The type of permission modes. *)
-
+
val to_string : t -> string
(** [to_string t] converts a mode to its string representation. *)
-
+
val of_string : string -> t
(** [of_string s] parses a mode from its string representation.
@raise Invalid_argument if the string is not a valid mode. *)
-
-
val to_json : t -> value
-
(** [to_json t] converts a mode to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a mode from JSON.
-
@raise Invalid_argument if the JSON is not a valid mode. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the mode. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for permission modes. *)
end
(** {1 Permission Behaviors} *)
module Behavior : sig
(** Behaviors determine how permission requests are handled. *)
-
-
type t =
+
+
type t =
| Allow (** Allow the operation *)
| Deny (** Deny the operation *)
| Ask (** Ask the user for permission *)
(** The type of permission behaviors. *)
-
+
val to_string : t -> string
(** [to_string t] converts a behavior to its string representation. *)
-
+
val of_string : string -> t
(** [of_string s] parses a behavior from its string representation.
@raise Invalid_argument if the string is not a valid behavior. *)
-
-
val to_json : t -> value
-
(** [to_json t] converts a behavior to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a behavior from JSON.
-
@raise Invalid_argument if the JSON is not a valid behavior. *)
-
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the behavior. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for permission behaviors. *)
end
(** {1 Permission Rules} *)
module Rule : sig
(** Rules define specific permissions for tools. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t = {
tool_name : string; (** Name of the tool *)
rule_content : string option; (** Optional rule specification *)
+
unknown : Unknown.t; (** Unknown fields *)
}
(** The type of permission rules. *)
-
-
val create : tool_name:string -> ?rule_content:string -> unit -> t
-
(** [create ~tool_name ?rule_content ()] creates a new rule.
+
+
val create : tool_name:string -> ?rule_content:string -> ?unknown:Unknown.t -> unit -> t
+
(** [create ~tool_name ?rule_content ?unknown ()] creates a new rule.
@param tool_name The name of the tool this rule applies to
-
@param rule_content Optional rule specification or pattern *)
-
+
@param rule_content Optional rule specification or pattern
+
@param unknown Optional unknown fields to preserve *)
+
val tool_name : t -> string
(** [tool_name t] returns the tool name. *)
-
+
val rule_content : t -> string option
(** [rule_content t] returns the optional rule content. *)
-
-
val to_json : t -> value
-
(** [to_json t] converts a rule to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a rule from JSON.
-
@raise Invalid_argument if the JSON is not a valid rule. *)
-
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields. *)
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the rule. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for permission rules. *)
end
(** {1 Permission Updates} *)
module Update : sig
(** Updates modify permission settings. *)
-
-
type destination =
+
+
type destination =
| User_settings (** Apply to user settings *)
| Project_settings (** Apply to project settings *)
| Local_settings (** Apply to local settings *)
| Session (** Apply to current session only *)
(** The destination for permission updates. *)
-
+
type update_type =
| Add_rules (** Add new rules *)
| Replace_rules (** Replace existing rules *)
···
| Add_directories (** Add allowed directories *)
| Remove_directories (** Remove allowed directories *)
(** The type of permission update. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t
(** The type of permission updates. *)
-
-
val create :
-
update_type:update_type ->
-
?rules:Rule.t list ->
-
?behavior:Behavior.t ->
-
?mode:Mode.t ->
-
?directories:string list ->
-
?destination:destination ->
+
+
val create :
+
update_type:update_type ->
+
?rules:Rule.t list ->
+
?behavior:Behavior.t ->
+
?mode:Mode.t ->
+
?directories:string list ->
+
?destination:destination ->
+
?unknown:Unknown.t ->
unit -> t
-
(** [create ~update_type ?rules ?behavior ?mode ?directories ?destination ()]
+
(** [create ~update_type ?rules ?behavior ?mode ?directories ?destination ?unknown ()]
creates a new permission update.
@param update_type The type of update to perform
@param rules Optional list of rules to add/remove/replace
@param behavior Optional behavior to set
@param mode Optional permission mode to set
@param directories Optional directories to add/remove
-
@param destination Optional destination for the update *)
-
+
@param destination Optional destination for the update
+
@param unknown Optional unknown fields to preserve *)
+
val update_type : t -> update_type
(** [update_type t] returns the update type. *)
-
+
val rules : t -> Rule.t list option
(** [rules t] returns the optional list of rules. *)
-
+
val behavior : t -> Behavior.t option
(** [behavior t] returns the optional behavior. *)
-
+
val mode : t -> Mode.t option
(** [mode t] returns the optional mode. *)
-
+
val directories : t -> string list option
(** [directories t] returns the optional list of directories. *)
-
+
val destination : t -> destination option
(** [destination t] returns the optional destination. *)
-
-
val to_json : t -> value
-
(** [to_json t] converts an update to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses an update from JSON.
-
@raise Invalid_argument if the JSON is not a valid update. *)
-
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields. *)
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the update. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for permission updates. *)
end
(** {1 Permission Context} *)
module Context : sig
(** Context provided to permission callbacks. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t = {
suggestions : Update.t list; (** Suggested permission updates *)
+
unknown : Unknown.t; (** Unknown fields *)
}
(** The type of permission context. *)
-
-
val create : ?suggestions:Update.t list -> unit -> t
-
(** [create ?suggestions ()] creates a new context.
-
@param suggestions Optional list of suggested permission updates *)
-
+
+
val create : ?suggestions:Update.t list -> ?unknown:Unknown.t -> unit -> t
+
(** [create ?suggestions ?unknown ()] creates a new context.
+
@param suggestions Optional list of suggested permission updates
+
@param unknown Optional unknown fields to preserve *)
+
val suggestions : t -> Update.t list
(** [suggestions t] returns the list of suggested updates. *)
-
-
val to_json : t -> value
-
(** [to_json t] converts a context to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a context from JSON.
-
@raise Invalid_argument if the JSON is not a valid context. *)
-
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields. *)
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the context. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for permission context. *)
end
(** {1 Permission Results} *)
module Result : sig
(** Results of permission checks. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t =
| Allow of {
-
updated_input : value option; (** Modified tool input *)
+
updated_input : Jsont.json option; (** Modified tool input *)
updated_permissions : Update.t list option; (** Permission updates to apply *)
+
unknown : Unknown.t; (** Unknown fields *)
}
| Deny of {
message : string; (** Reason for denial *)
interrupt : bool; (** Whether to interrupt execution *)
+
unknown : Unknown.t; (** Unknown fields *)
}
(** The type of permission results. *)
-
-
val allow : ?updated_input:value -> ?updated_permissions:Update.t list -> unit -> t
-
(** [allow ?updated_input ?updated_permissions ()] creates an allow result.
+
+
val allow : ?updated_input:Jsont.json -> ?updated_permissions:Update.t list -> ?unknown:Unknown.t -> unit -> t
+
(** [allow ?updated_input ?updated_permissions ?unknown ()] creates an allow result.
@param updated_input Optional modified tool input
-
@param updated_permissions Optional permission updates to apply *)
-
-
val deny : message:string -> interrupt:bool -> t
-
(** [deny ~message ~interrupt] creates a deny result.
+
@param updated_permissions Optional permission updates to apply
+
@param unknown Optional unknown fields to preserve *)
+
+
val deny : message:string -> interrupt:bool -> ?unknown:Unknown.t -> unit -> t
+
(** [deny ~message ~interrupt ?unknown ()] creates a deny result.
@param message The reason for denying permission
-
@param interrupt Whether to interrupt further execution *)
-
-
val to_json : t -> value
-
(** [to_json t] converts a result to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a result from JSON.
-
@raise Invalid_argument if the JSON is not a valid result. *)
-
+
@param interrupt Whether to interrupt further execution
+
@param unknown Optional unknown fields to preserve *)
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the result. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for permission results. *)
end
(** {1 Permission Callbacks} *)
-
type callback =
-
tool_name:string ->
-
input:value ->
-
context:Context.t ->
+
type callback =
+
tool_name:string ->
+
input:Jsont.json ->
+
context:Context.t ->
Result.t
(** The type of permission callbacks. Callbacks are invoked when Claude
attempts to use a tool, allowing custom permission logic. *)
+288 -256
claudeio/lib/sdk_control.ml
···
-
open Ezjsonm
-
let src = Logs.Src.create "claude.sdk_control" ~doc:"Claude SDK control protocol"
module Log = (val Logs.src_log src : Logs.LOG)
-
module JU = Json_utils
+
module Request = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
-
module Request = struct
type interrupt = {
subtype : [`Interrupt];
+
unknown : Unknown.t;
}
-
+
type permission = {
subtype : [`Can_use_tool];
tool_name : string;
-
input : value;
+
input : Jsont.json;
permission_suggestions : Permissions.Update.t list option;
blocked_path : string option;
+
unknown : Unknown.t;
}
-
+
type initialize = {
subtype : [`Initialize];
-
hooks : (string * value) list option;
+
hooks : (string * Jsont.json) list option;
+
unknown : Unknown.t;
}
-
+
type set_permission_mode = {
subtype : [`Set_permission_mode];
mode : Permissions.Mode.t;
+
unknown : Unknown.t;
}
-
+
type hook_callback = {
subtype : [`Hook_callback];
callback_id : string;
-
input : value;
+
input : Jsont.json;
tool_use_id : string option;
+
unknown : Unknown.t;
}
-
+
type mcp_message = {
subtype : [`Mcp_message];
server_name : string;
-
message : value;
+
message : Jsont.json;
+
unknown : Unknown.t;
}
type set_model = {
subtype : [`Set_model];
model : string;
+
unknown : Unknown.t;
}
type get_server_info = {
subtype : [`Get_server_info];
+
unknown : Unknown.t;
}
type t =
···
| Set_model of set_model
| Get_server_info of get_server_info
-
let interrupt () = Interrupt { subtype = `Interrupt }
-
-
let permission ~tool_name ~input ?permission_suggestions ?blocked_path () =
+
let interrupt ?(unknown = Unknown.empty) () =
+
Interrupt { subtype = `Interrupt; unknown }
+
+
let permission ~tool_name ~input ?permission_suggestions ?blocked_path ?(unknown = Unknown.empty) () =
Permission {
subtype = `Can_use_tool;
tool_name;
input;
permission_suggestions;
blocked_path;
+
unknown;
}
-
-
let initialize ?hooks () =
-
Initialize { subtype = `Initialize; hooks }
-
-
let set_permission_mode ~mode =
-
Set_permission_mode { subtype = `Set_permission_mode; mode }
-
-
let hook_callback ~callback_id ~input ?tool_use_id () =
+
+
let initialize ?hooks ?(unknown = Unknown.empty) () =
+
Initialize { subtype = `Initialize; hooks; unknown }
+
+
let set_permission_mode ~mode ?(unknown = Unknown.empty) () =
+
Set_permission_mode { subtype = `Set_permission_mode; mode; unknown }
+
+
let hook_callback ~callback_id ~input ?tool_use_id ?(unknown = Unknown.empty) () =
Hook_callback {
subtype = `Hook_callback;
callback_id;
input;
tool_use_id;
+
unknown;
}
-
-
let mcp_message ~server_name ~message =
+
+
let mcp_message ~server_name ~message ?(unknown = Unknown.empty) () =
Mcp_message {
subtype = `Mcp_message;
server_name;
message;
+
unknown;
}
-
let set_model ~model =
-
Set_model { subtype = `Set_model; model }
+
let set_model ~model ?(unknown = Unknown.empty) () =
+
Set_model { subtype = `Set_model; model; unknown }
-
let get_server_info () =
-
Get_server_info { subtype = `Get_server_info }
+
let get_server_info ?(unknown = Unknown.empty) () =
+
Get_server_info { subtype = `Get_server_info; unknown }
-
let to_json = function
-
| Interrupt _ ->
-
`O [("subtype", `String "interrupt")]
-
| Permission p ->
-
let fields = [
-
("subtype", `String "can_use_tool");
-
("tool_name", `String p.tool_name);
-
("input", p.input);
-
] in
-
let fields = match p.permission_suggestions with
-
| Some suggestions ->
-
("permission_suggestions",
-
`A (List.map Permissions.Update.to_json suggestions)) :: fields
-
| None -> fields
-
in
-
let fields = match p.blocked_path with
-
| Some path -> ("blocked_path", `String path) :: fields
-
| None -> fields
-
in
-
`O fields
-
| Initialize i ->
-
let fields = [("subtype", `String "initialize")] in
-
let fields = match i.hooks with
-
| Some hooks ->
-
("hooks", `O hooks) :: fields
-
| None -> fields
-
in
-
`O fields
-
| Set_permission_mode s ->
-
`O [
-
("subtype", `String "set_permission_mode");
-
("mode", Permissions.Mode.to_json s.mode);
-
]
-
| Hook_callback h ->
-
let fields = [
-
("subtype", `String "hook_callback");
-
("callback_id", `String h.callback_id);
-
("input", h.input);
-
] in
-
let fields = match h.tool_use_id with
-
| Some id -> ("tool_use_id", `String id) :: fields
-
| None -> fields
-
in
-
`O fields
-
| Mcp_message m ->
-
`O [
-
("subtype", `String "mcp_message");
-
("server_name", `String m.server_name);
-
("message", m.message);
-
]
-
| Set_model s ->
-
`O [
-
("subtype", `String "set_model");
-
("model", `String s.model);
-
]
-
| Get_server_info _ ->
-
`O [("subtype", `String "get_server_info")]
+
(* Individual record codecs *)
+
let interrupt_jsont : interrupt Jsont.t =
+
let make (unknown : Unknown.t) : interrupt = { subtype = `Interrupt; unknown } in
+
Jsont.Object.map ~kind:"Interrupt" make
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : interrupt) -> r.unknown)
+
|> Jsont.Object.finish
-
let of_json = function
-
| `O fields ->
-
let subtype = JU.assoc_string "subtype" fields in
-
(match subtype with
-
| "interrupt" ->
-
Interrupt { subtype = `Interrupt }
-
| "can_use_tool" ->
-
let tool_name = JU.assoc_string "tool_name" fields in
-
let input = List.assoc "input" fields in
-
let permission_suggestions =
-
match List.assoc_opt "permission_suggestions" fields with
-
| Some (`A lst) ->
-
Some (List.map Permissions.Update.of_json lst)
-
| _ -> None
-
in
-
let blocked_path = JU.assoc_string_opt "blocked_path" fields in
-
Permission {
-
subtype = `Can_use_tool;
-
tool_name;
-
input;
-
permission_suggestions;
-
blocked_path;
-
}
-
| "initialize" ->
-
let hooks =
-
match List.assoc_opt "hooks" fields with
-
| Some (`O hooks) -> Some hooks
-
| _ -> None
-
in
-
Initialize { subtype = `Initialize; hooks }
-
| "set_permission_mode" ->
-
let mode = List.assoc "mode" fields |> Permissions.Mode.of_json in
-
Set_permission_mode { subtype = `Set_permission_mode; mode }
-
| "hook_callback" ->
-
let callback_id = JU.assoc_string "callback_id" fields in
-
let input = List.assoc "input" fields in
-
let tool_use_id = JU.assoc_string_opt "tool_use_id" fields in
-
Hook_callback {
-
subtype = `Hook_callback;
-
callback_id;
-
input;
-
tool_use_id;
-
}
-
| "mcp_message" ->
-
let server_name = JU.assoc_string "server_name" fields in
-
let message = List.assoc "message" fields in
-
Mcp_message {
-
subtype = `Mcp_message;
-
server_name;
-
message;
-
}
-
| "set_model" ->
-
let model = JU.assoc_string "model" fields in
-
Set_model { subtype = `Set_model; model }
-
| "get_server_info" ->
-
Get_server_info { subtype = `Get_server_info }
-
| _ -> raise (Invalid_argument ("Unknown request subtype: " ^ subtype)))
-
| _ -> raise (Invalid_argument "Request.of_json: expected object")
+
let permission_jsont : permission Jsont.t =
+
let make tool_name input permission_suggestions blocked_path (unknown : Unknown.t) : permission =
+
{ subtype = `Can_use_tool; tool_name; input; permission_suggestions; blocked_path; unknown }
+
in
+
Jsont.Object.map ~kind:"Permission" make
+
|> Jsont.Object.mem "tool_name" Jsont.string ~enc:(fun (r : permission) -> r.tool_name)
+
|> Jsont.Object.mem "input" Jsont.json ~enc:(fun (r : permission) -> r.input)
+
|> Jsont.Object.opt_mem "permission_suggestions" (Jsont.list Permissions.Update.jsont) ~enc:(fun (r : permission) -> r.permission_suggestions)
+
|> Jsont.Object.opt_mem "blocked_path" Jsont.string ~enc:(fun (r : permission) -> r.blocked_path)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : permission) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let initialize_jsont : initialize Jsont.t =
+
(* The hooks field is an object with string keys and json values *)
+
let hooks_map_jsont = Jsont.Object.as_string_map Jsont.json in
+
let module StringMap = Map.Make(String) in
+
let hooks_jsont = Jsont.map
+
~dec:(fun m -> StringMap.bindings m)
+
~enc:(fun l -> StringMap.of_seq (List.to_seq l))
+
hooks_map_jsont
+
in
+
let make hooks (unknown : Unknown.t) : initialize = { subtype = `Initialize; hooks; unknown } in
+
Jsont.Object.map ~kind:"Initialize" make
+
|> Jsont.Object.opt_mem "hooks" hooks_jsont ~enc:(fun (r : initialize) -> r.hooks)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : initialize) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let set_permission_mode_jsont : set_permission_mode Jsont.t =
+
let make mode (unknown : Unknown.t) : set_permission_mode = { subtype = `Set_permission_mode; mode; unknown } in
+
Jsont.Object.map ~kind:"SetPermissionMode" make
+
|> Jsont.Object.mem "mode" Permissions.Mode.jsont ~enc:(fun (r : set_permission_mode) -> r.mode)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : set_permission_mode) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let hook_callback_jsont : hook_callback Jsont.t =
+
let make callback_id input tool_use_id (unknown : Unknown.t) : hook_callback =
+
{ subtype = `Hook_callback; callback_id; input; tool_use_id; unknown }
+
in
+
Jsont.Object.map ~kind:"HookCallback" make
+
|> Jsont.Object.mem "callback_id" Jsont.string ~enc:(fun (r : hook_callback) -> r.callback_id)
+
|> Jsont.Object.mem "input" Jsont.json ~enc:(fun (r : hook_callback) -> r.input)
+
|> Jsont.Object.opt_mem "tool_use_id" Jsont.string ~enc:(fun (r : hook_callback) -> r.tool_use_id)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : hook_callback) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let mcp_message_jsont : mcp_message Jsont.t =
+
let make server_name message (unknown : Unknown.t) : mcp_message =
+
{ subtype = `Mcp_message; server_name; message; unknown }
+
in
+
Jsont.Object.map ~kind:"McpMessage" make
+
|> Jsont.Object.mem "server_name" Jsont.string ~enc:(fun (r : mcp_message) -> r.server_name)
+
|> Jsont.Object.mem "message" Jsont.json ~enc:(fun (r : mcp_message) -> r.message)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : mcp_message) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let set_model_jsont : set_model Jsont.t =
+
let make model (unknown : Unknown.t) : set_model = { subtype = `Set_model; model; unknown } in
+
Jsont.Object.map ~kind:"SetModel" make
+
|> Jsont.Object.mem "model" Jsont.string ~enc:(fun (r : set_model) -> r.model)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : set_model) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let get_server_info_jsont : get_server_info Jsont.t =
+
let make (unknown : Unknown.t) : get_server_info = { subtype = `Get_server_info; unknown } in
+
Jsont.Object.map ~kind:"GetServerInfo" make
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : get_server_info) -> r.unknown)
+
|> Jsont.Object.finish
+
+
(* Main variant codec using subtype discriminator *)
+
let jsont : t Jsont.t =
+
let case_interrupt = Jsont.Object.Case.map "interrupt" interrupt_jsont ~dec:(fun v -> Interrupt v) in
+
let case_permission = Jsont.Object.Case.map "can_use_tool" permission_jsont ~dec:(fun v -> Permission v) in
+
let case_initialize = Jsont.Object.Case.map "initialize" initialize_jsont ~dec:(fun v -> Initialize v) in
+
let case_set_permission_mode = Jsont.Object.Case.map "set_permission_mode" set_permission_mode_jsont ~dec:(fun v -> Set_permission_mode v) in
+
let case_hook_callback = Jsont.Object.Case.map "hook_callback" hook_callback_jsont ~dec:(fun v -> Hook_callback v) in
+
let case_mcp_message = Jsont.Object.Case.map "mcp_message" mcp_message_jsont ~dec:(fun v -> Mcp_message v) in
+
let case_set_model = Jsont.Object.Case.map "set_model" set_model_jsont ~dec:(fun v -> Set_model v) in
+
let case_get_server_info = Jsont.Object.Case.map "get_server_info" get_server_info_jsont ~dec:(fun v -> Get_server_info v) in
+
+
let enc_case = function
+
| Interrupt v -> Jsont.Object.Case.value case_interrupt v
+
| Permission v -> Jsont.Object.Case.value case_permission v
+
| Initialize v -> Jsont.Object.Case.value case_initialize v
+
| Set_permission_mode v -> Jsont.Object.Case.value case_set_permission_mode v
+
| Hook_callback v -> Jsont.Object.Case.value case_hook_callback v
+
| Mcp_message v -> Jsont.Object.Case.value case_mcp_message v
+
| Set_model v -> Jsont.Object.Case.value case_set_model v
+
| Get_server_info v -> Jsont.Object.Case.value case_get_server_info v
+
in
+
+
let cases = Jsont.Object.Case.[
+
make case_interrupt;
+
make case_permission;
+
make case_initialize;
+
make case_set_permission_mode;
+
make case_hook_callback;
+
make case_mcp_message;
+
make case_set_model;
+
make case_get_server_info;
+
] in
+
+
Jsont.Object.map ~kind:"Request" Fun.id
+
|> Jsont.Object.case_mem "subtype" Jsont.string ~enc:Fun.id ~enc_case cases
+
~tag_to_string:Fun.id ~tag_compare:String.compare
+
|> Jsont.Object.finish
let pp fmt = function
| Interrupt _ ->
···
end
module Response = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type success = {
subtype : [`Success];
request_id : string;
-
response : value option;
+
response : Jsont.json option;
+
unknown : Unknown.t;
}
-
+
type error = {
subtype : [`Error];
request_id : string;
error : string;
+
unknown : Unknown.t;
}
-
+
type t =
| Success of success
| Error of error
-
-
let success ~request_id ?response () =
+
+
let success ~request_id ?response ?(unknown = Unknown.empty) () =
Success {
subtype = `Success;
request_id;
response;
+
unknown;
}
-
-
let error ~request_id ~error =
+
+
let error ~request_id ~error ?(unknown = Unknown.empty) () =
Error {
subtype = `Error;
request_id;
error;
+
unknown;
}
-
-
let to_json = function
-
| Success s ->
-
let fields = [
-
("subtype", `String "success");
-
("request_id", `String s.request_id);
-
] in
-
let fields = match s.response with
-
| Some resp -> ("response", resp) :: fields
-
| None -> fields
-
in
-
`O fields
-
| Error e ->
-
`O [
-
("subtype", `String "error");
-
("request_id", `String e.request_id);
-
("error", `String e.error);
-
]
-
-
let of_json = function
-
| `O fields ->
-
let subtype = JU.assoc_string "subtype" fields in
-
let request_id = JU.assoc_string "request_id" fields in
-
(match subtype with
-
| "success" ->
-
let response = List.assoc_opt "response" fields in
-
Success {
-
subtype = `Success;
-
request_id;
-
response;
-
}
-
| "error" ->
-
let error = JU.assoc_string "error" fields in
-
Error {
-
subtype = `Error;
-
request_id;
-
error;
-
}
-
| _ -> raise (Invalid_argument ("Unknown response subtype: " ^ subtype)))
-
| _ -> raise (Invalid_argument "Response.of_json: expected object")
+
+
(* Individual record codecs *)
+
let success_jsont : success Jsont.t =
+
let make request_id response (unknown : Unknown.t) : success =
+
{ subtype = `Success; request_id; response; unknown }
+
in
+
Jsont.Object.map ~kind:"Success" make
+
|> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun (r : success) -> r.request_id)
+
|> Jsont.Object.opt_mem "response" Jsont.json ~enc:(fun (r : success) -> r.response)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : success) -> r.unknown)
+
|> Jsont.Object.finish
+
+
let error_jsont : error Jsont.t =
+
let make request_id error (unknown : Unknown.t) : error =
+
{ subtype = `Error; request_id; error; unknown }
+
in
+
Jsont.Object.map ~kind:"Error" make
+
|> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun (r : error) -> r.request_id)
+
|> Jsont.Object.mem "error" Jsont.string ~enc:(fun (r : error) -> r.error)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : error) -> r.unknown)
+
|> Jsont.Object.finish
+
+
(* Main variant codec using subtype discriminator *)
+
let jsont : t Jsont.t =
+
let case_success = Jsont.Object.Case.map "success" success_jsont ~dec:(fun v -> Success v) in
+
let case_error = Jsont.Object.Case.map "error" error_jsont ~dec:(fun v -> Error v) in
+
+
let enc_case = function
+
| Success v -> Jsont.Object.Case.value case_success v
+
| Error v -> Jsont.Object.Case.value case_error v
+
in
+
+
let cases = Jsont.Object.Case.[
+
make case_success;
+
make case_error;
+
] in
+
+
Jsont.Object.map ~kind:"Response" Fun.id
+
|> Jsont.Object.case_mem "subtype" Jsont.string ~enc:Fun.id ~enc_case cases
+
~tag_to_string:Fun.id ~tag_compare:String.compare
+
|> Jsont.Object.finish
let pp fmt = function
| Success s ->
···
e.request_id e.error
end
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type control_request = {
type_ : [`Control_request];
request_id : string;
request : Request.t;
+
unknown : Unknown.t;
}
type control_response = {
type_ : [`Control_response];
response : Response.t;
+
unknown : Unknown.t;
}
type t =
| Request of control_request
| Response of control_response
-
let create_request ~request_id ~request =
+
let create_request ~request_id ~request ?(unknown = Unknown.empty) () =
Request {
type_ = `Control_request;
request_id;
request;
+
unknown;
}
-
let create_response ~response =
+
let create_response ~response ?(unknown = Unknown.empty) () =
Response {
type_ = `Control_response;
response;
+
unknown;
}
-
let to_json = function
-
| Request r ->
-
`O [
-
("type", `String "control_request");
-
("request_id", `String r.request_id);
-
("request", Request.to_json r.request);
-
]
-
| Response r ->
-
`O [
-
("type", `String "control_response");
-
("response", Response.to_json r.response);
-
]
+
(* Individual record codecs *)
+
let control_request_jsont : control_request Jsont.t =
+
let make request_id request (unknown : Unknown.t) : control_request =
+
{ type_ = `Control_request; request_id; request; unknown }
+
in
+
Jsont.Object.map ~kind:"ControlRequest" make
+
|> Jsont.Object.mem "request_id" Jsont.string ~enc:(fun (r : control_request) -> r.request_id)
+
|> Jsont.Object.mem "request" Request.jsont ~enc:(fun (r : control_request) -> r.request)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : control_request) -> r.unknown)
+
|> Jsont.Object.finish
-
let of_json = function
-
| `O fields ->
-
let type_ = JU.assoc_string "type" fields in
-
(match type_ with
-
| "control_request" ->
-
let request_id = JU.assoc_string "request_id" fields in
-
let request = List.assoc "request" fields |> Request.of_json in
-
Request {
-
type_ = `Control_request;
-
request_id;
-
request;
-
}
-
| "control_response" ->
-
let response = List.assoc "response" fields |> Response.of_json in
-
Response {
-
type_ = `Control_response;
-
response;
-
}
-
| _ -> raise (Invalid_argument ("Unknown control type: " ^ type_)))
-
| _ -> raise (Invalid_argument "of_json: expected object")
+
let control_response_jsont : control_response Jsont.t =
+
let make response (unknown : Unknown.t) : control_response =
+
{ type_ = `Control_response; response; unknown }
+
in
+
Jsont.Object.map ~kind:"ControlResponse" make
+
|> Jsont.Object.mem "response" Response.jsont ~enc:(fun (r : control_response) -> r.response)
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : control_response) -> r.unknown)
+
|> Jsont.Object.finish
+
+
(* Main variant codec using type discriminator *)
+
let jsont : t Jsont.t =
+
let case_request = Jsont.Object.Case.map "control_request" control_request_jsont ~dec:(fun v -> Request v) in
+
let case_response = Jsont.Object.Case.map "control_response" control_response_jsont ~dec:(fun v -> Response v) in
+
+
let enc_case = function
+
| Request v -> Jsont.Object.Case.value case_request v
+
| Response v -> Jsont.Object.Case.value case_response v
+
in
+
+
let cases = Jsont.Object.Case.[
+
make case_request;
+
make case_response;
+
] in
+
+
Jsont.Object.map ~kind:"Control" Fun.id
+
|> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
+
~tag_to_string:Fun.id ~tag_compare:String.compare
+
|> Jsont.Object.finish
let pp fmt = function
| Request r ->
···
(** Server information *)
module Server_info = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
+
let jsont = Jsont.json
+
end
+
type t = {
version : string;
capabilities : string list;
commands : string list;
output_styles : string list;
+
unknown : Unknown.t;
}
-
let create ~version ~capabilities ~commands ~output_styles =
-
{ version; capabilities; commands; output_styles }
+
let create ~version ~capabilities ~commands ~output_styles ?(unknown = Unknown.empty) () =
+
{ version; capabilities; commands; output_styles; unknown }
let version t = t.version
let capabilities t = t.capabilities
let commands t = t.commands
let output_styles t = t.output_styles
-
-
let of_json = function
-
| `O fields ->
-
let version = JU.assoc_string "version" fields in
-
let capabilities =
-
match List.assoc_opt "capabilities" fields with
-
| Some (`A lst) -> List.map Ezjsonm.get_string lst
-
| _ -> []
-
in
-
let commands =
-
match List.assoc_opt "commands" fields with
-
| Some (`A lst) -> List.map Ezjsonm.get_string lst
-
| _ -> []
-
in
-
let output_styles =
-
match List.assoc_opt "outputStyles" fields with
-
| Some (`A lst) -> List.map Ezjsonm.get_string lst
-
| _ -> []
-
in
-
{ version; capabilities; commands; output_styles }
-
| _ -> raise (Invalid_argument "Server_info.of_json: expected object")
+
let unknown t = t.unknown
-
let to_json t =
-
`O [
-
("version", `String t.version);
-
("capabilities", `A (List.map (fun s -> `String s) t.capabilities));
-
("commands", `A (List.map (fun s -> `String s) t.commands));
-
("outputStyles", `A (List.map (fun s -> `String s) t.output_styles));
-
]
+
let jsont : t Jsont.t =
+
let make version capabilities commands output_styles (unknown : Unknown.t) : t =
+
{ version; capabilities; commands; output_styles; unknown }
+
in
+
Jsont.Object.map ~kind:"ServerInfo" make
+
|> Jsont.Object.mem "version" Jsont.string ~enc:(fun (r : t) -> r.version)
+
|> Jsont.Object.mem "capabilities" (Jsont.list Jsont.string) ~enc:(fun (r : t) -> r.capabilities) ~dec_absent:[]
+
|> Jsont.Object.mem "commands" (Jsont.list Jsont.string) ~enc:(fun (r : t) -> r.commands) ~dec_absent:[]
+
|> Jsont.Object.mem "outputStyles" (Jsont.list Jsont.string) ~enc:(fun (r : t) -> r.output_styles) ~dec_absent:[]
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun (r : t) -> r.unknown)
+
|> Jsont.Object.finish
let pp fmt t =
Fmt.pf fmt "@[<2>ServerInfo@ { version = %S;@ capabilities = [%a];@ commands = [%a];@ output_styles = [%a] }@]"
+117 -82
claudeio/lib/sdk_control.mli
···
See {!Client.set_permission_mode}, {!Client.set_model}, and
{!Client.get_server_info} for high-level APIs that use this protocol. *)
-
open Ezjsonm
-
(** The log source for SDK control operations *)
val src : Logs.Src.t
···
module Request : sig
(** SDK control request types. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type interrupt = {
subtype : [`Interrupt];
+
unknown : Unknown.t;
}
(** Interrupt request to stop execution. *)
-
+
type permission = {
subtype : [`Can_use_tool];
tool_name : string;
-
input : value;
+
input : Jsont.json;
permission_suggestions : Permissions.Update.t list option;
blocked_path : string option;
+
unknown : Unknown.t;
}
(** Permission request for tool usage. *)
-
+
type initialize = {
subtype : [`Initialize];
-
hooks : (string * value) list option; (* Hook event to configuration *)
+
hooks : (string * Jsont.json) list option; (* Hook event to configuration *)
+
unknown : Unknown.t;
}
(** Initialize request with optional hook configuration. *)
-
+
type set_permission_mode = {
subtype : [`Set_permission_mode];
mode : Permissions.Mode.t;
+
unknown : Unknown.t;
}
(** Request to change permission mode. *)
-
+
type hook_callback = {
subtype : [`Hook_callback];
callback_id : string;
-
input : value;
+
input : Jsont.json;
tool_use_id : string option;
+
unknown : Unknown.t;
}
(** Hook callback request. *)
-
+
type mcp_message = {
subtype : [`Mcp_message];
server_name : string;
-
message : value;
+
message : Jsont.json;
+
unknown : Unknown.t;
}
(** MCP server message request. *)
type set_model = {
subtype : [`Set_model];
model : string;
+
unknown : Unknown.t;
}
(** Request to change the AI model. *)
type get_server_info = {
subtype : [`Get_server_info];
+
unknown : Unknown.t;
}
(** Request to get server information. *)
···
| Set_model of set_model
| Get_server_info of get_server_info
(** The type of SDK control requests. *)
-
-
val interrupt : unit -> t
-
(** [interrupt ()] creates an interrupt request. *)
-
-
val permission :
-
tool_name:string ->
-
input:value ->
-
?permission_suggestions:Permissions.Update.t list ->
-
?blocked_path:string ->
+
+
val interrupt : ?unknown:Unknown.t -> unit -> t
+
(** [interrupt ?unknown ()] creates an interrupt request. *)
+
+
val permission :
+
tool_name:string ->
+
input:Jsont.json ->
+
?permission_suggestions:Permissions.Update.t list ->
+
?blocked_path:string ->
+
?unknown:Unknown.t ->
unit -> t
-
(** [permission ~tool_name ~input ?permission_suggestions ?blocked_path ()]
+
(** [permission ~tool_name ~input ?permission_suggestions ?blocked_path ?unknown ()]
creates a permission request. *)
-
-
val initialize : ?hooks:(string * value) list -> unit -> t
-
(** [initialize ?hooks ()] creates an initialize request. *)
-
-
val set_permission_mode : mode:Permissions.Mode.t -> t
-
(** [set_permission_mode ~mode] creates a permission mode change request. *)
-
-
val hook_callback :
-
callback_id:string ->
-
input:value ->
-
?tool_use_id:string ->
+
+
val initialize : ?hooks:(string * Jsont.json) list -> ?unknown:Unknown.t -> unit -> t
+
(** [initialize ?hooks ?unknown ()] creates an initialize request. *)
+
+
val set_permission_mode : mode:Permissions.Mode.t -> ?unknown:Unknown.t -> unit -> t
+
(** [set_permission_mode ~mode ?unknown] creates a permission mode change request. *)
+
+
val hook_callback :
+
callback_id:string ->
+
input:Jsont.json ->
+
?tool_use_id:string ->
+
?unknown:Unknown.t ->
unit -> t
-
(** [hook_callback ~callback_id ~input ?tool_use_id ()] creates a hook callback request. *)
-
-
val mcp_message : server_name:string -> message:value -> t
-
(** [mcp_message ~server_name ~message] creates an MCP message request. *)
+
(** [hook_callback ~callback_id ~input ?tool_use_id ?unknown ()] creates a hook callback request. *)
+
+
val mcp_message : server_name:string -> message:Jsont.json -> ?unknown:Unknown.t -> unit -> t
+
(** [mcp_message ~server_name ~message ?unknown] creates an MCP message request. *)
+
+
val set_model : model:string -> ?unknown:Unknown.t -> unit -> t
+
(** [set_model ~model ?unknown] creates a model change request. *)
-
val set_model : model:string -> t
-
(** [set_model ~model] creates a model change request. *)
+
val get_server_info : ?unknown:Unknown.t -> unit -> t
+
(** [get_server_info ?unknown ()] creates a server info request. *)
-
val get_server_info : unit -> t
-
(** [get_server_info ()] creates a server info request. *)
+
val jsont : t Jsont.t
+
(** [jsont] is the jsont codec for requests. *)
-
val to_json : t -> value
-
(** [to_json t] converts a request to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a request from JSON.
-
@raise Invalid_argument if the JSON is not a valid request. *)
-
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the request. *)
end
···
module Response : sig
(** SDK control response types. *)
-
+
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type success = {
subtype : [`Success];
request_id : string;
-
response : value option;
+
response : Jsont.json option;
+
unknown : Unknown.t;
}
(** Successful response. *)
-
+
type error = {
subtype : [`Error];
request_id : string;
error : string;
+
unknown : Unknown.t;
}
(** Error response. *)
-
+
type t =
| Success of success
| Error of error
(** The type of SDK control responses. *)
-
-
val success : request_id:string -> ?response:value -> unit -> t
-
(** [success ~request_id ?response ()] creates a success response. *)
-
-
val error : request_id:string -> error:string -> t
-
(** [error ~request_id ~error] creates an error response. *)
-
-
val to_json : t -> value
-
(** [to_json t] converts a response to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a response from JSON.
-
@raise Invalid_argument if the JSON is not a valid response. *)
-
+
+
val success : request_id:string -> ?response:Jsont.json -> ?unknown:Unknown.t -> unit -> t
+
(** [success ~request_id ?response ?unknown ()] creates a success response. *)
+
+
val error : request_id:string -> error:string -> ?unknown:Unknown.t -> unit -> t
+
(** [error ~request_id ~error ?unknown] creates an error response. *)
+
+
val jsont : t Jsont.t
+
(** [jsont] is the jsont codec for responses. *)
+
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the response. *)
end
(** {1 Control Messages} *)
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type control_request = {
type_ : [`Control_request];
request_id : string;
request : Request.t;
+
unknown : Unknown.t;
}
(** Control request message. *)
type control_response = {
type_ : [`Control_response];
response : Response.t;
+
unknown : Unknown.t;
}
(** Control response message. *)
+
val control_response_jsont : control_response Jsont.t
+
(** [control_response_jsont] is the jsont codec for control response messages. *)
+
type t =
| Request of control_request
| Response of control_response
(** The type of SDK control messages. *)
-
val create_request : request_id:string -> request:Request.t -> t
-
(** [create_request ~request_id ~request] creates a control request message. *)
+
val create_request : request_id:string -> request:Request.t -> ?unknown:Unknown.t -> unit -> t
+
(** [create_request ~request_id ~request ?unknown ()] creates a control request message. *)
-
val create_response : response:Response.t -> t
-
(** [create_response ~response] creates a control response message. *)
+
val create_response : response:Response.t -> ?unknown:Unknown.t -> unit -> t
+
(** [create_response ~response ?unknown ()] creates a control response message. *)
-
val to_json : t -> value
-
(** [to_json t] converts a control message to JSON. *)
-
-
val of_json : value -> t
-
(** [of_json json] parses a control message from JSON.
-
@raise Invalid_argument if the JSON is not a valid control message. *)
+
val jsont : t Jsont.t
+
(** [jsont] is the jsont codec for control messages. *)
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the control message. *)
···
module Server_info : sig
(** Server information and capabilities. *)
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
type t = {
version : string;
(** Server version string (e.g., "2.0.0") *)
···
output_styles : string list;
(** Supported output formats (e.g., "json", "stream-json") *)
+
+
unknown : Unknown.t;
+
(** Unknown fields for forward compatibility *)
}
(** Server metadata and capabilities.
···
capabilities:string list ->
commands:string list ->
output_styles:string list ->
+
?unknown:Unknown.t ->
+
unit ->
t
-
(** [create ~version ~capabilities ~commands ~output_styles] creates server info. *)
+
(** [create ~version ~capabilities ~commands ~output_styles ?unknown ()] creates server info. *)
val version : t -> string
(** [version t] returns the server version. *)
···
val output_styles : t -> string list
(** [output_styles t] returns available output styles. *)
-
val of_json : value -> t
-
(** [of_json json] parses server info from JSON.
-
@raise Invalid_argument if the JSON is not valid server info. *)
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields. *)
-
val to_json : t -> value
-
(** [to_json t] converts server info to JSON. *)
+
val jsont : t Jsont.t
+
(** [jsont] is the jsont codec for server info. *)
val pp : Format.formatter -> t -> unit
(** [pp fmt t] pretty-prints the server info. *)
+49
claudeio/lib/structured_output.ml
···
+
let src = Logs.Src.create "claude.structured_output" ~doc:"Structured output"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
+
type t = {
+
json_schema : Jsont.json;
+
}
+
+
let json_to_string json =
+
match Jsont_bytesrw.encode_string' Jsont.json json with
+
| Ok str -> str
+
| Error err -> failwith (Jsont.Error.to_string err)
+
+
let of_json_schema schema =
+
Log.debug (fun m -> m "Created output format from JSON schema: %s"
+
(json_to_string schema));
+
{ json_schema = schema }
+
+
let json_schema t = t.json_schema
+
+
(* Codec for serializing structured output format *)
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"StructuredOutput"
+
(fun json_schema -> {json_schema})
+
|> Jsont.Object.mem "jsonSchema" Jsont.json ~enc:(fun t -> t.json_schema)
+
|> Jsont.Object.finish
+
+
let to_json t =
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Structured_output.to_json: " ^ msg)
+
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok t -> t
+
| Error msg -> raise (Invalid_argument ("Structured_output.of_json: " ^ msg))
+
+
let pp fmt t =
+
let schema_str =
+
match Jsont_bytesrw.encode_string' ~format:Jsont.Minify Jsont.json t.json_schema with
+
| Ok s -> s
+
| Error err -> Jsont.Error.to_string err
+
in
+
let truncated =
+
if String.length schema_str > 100 then
+
String.sub schema_str 0 97 ^ "..."
+
else
+
schema_str
+
in
+
Fmt.pf fmt "@[<2>StructuredOutput { schema = %s }@]" truncated
+171
claudeio/lib/structured_output.mli
···
+
(** Structured output configuration using JSON Schema.
+
+
This module provides structured output support for Claude, allowing you to
+
specify the expected output format using JSON schemas. When a structured
+
output format is configured, Claude will return its response in the
+
specified JSON format, validated against your schema.
+
+
{2 Overview}
+
+
Structured outputs ensure that Claude's responses conform to a specific
+
JSON schema, making it easier to parse and use the results programmatically.
+
This is particularly useful for:
+
+
- Extracting structured data from unstructured text
+
- Building APIs that require consistent JSON responses
+
- Integrating Claude into data pipelines
+
- Ensuring type-safe parsing of Claude's outputs
+
+
{2 Creating Output Formats}
+
+
Use {!of_json_schema} to specify a JSON Schema as a {!Jsont.json} value:
+
{[
+
let meta = Jsont.Meta.none in
+
let schema = Jsont.Object ([
+
(("type", meta), Jsont.String ("object", meta));
+
(("properties", meta), Jsont.Object ([
+
(("name", meta), Jsont.Object ([
+
(("type", meta), Jsont.String ("string", meta))
+
], meta));
+
(("age", meta), Jsont.Object ([
+
(("type", meta), Jsont.String ("integer", meta))
+
], meta));
+
], meta));
+
(("required", meta), Jsont.Array ([
+
Jsont.String ("name", meta);
+
Jsont.String ("age", meta)
+
], meta));
+
], meta) in
+
+
let format = Structured_output.of_json_schema schema
+
]}
+
+
{3 Helper Functions for Building Schemas}
+
+
For complex schemas, you can use helper functions to make construction easier:
+
{[
+
let json_object fields =
+
Jsont.Object (fields, Jsont.Meta.none)
+
+
let json_string s =
+
Jsont.String (s, Jsont.Meta.none)
+
+
let json_array items =
+
Jsont.Array (items, Jsont.Meta.none)
+
+
let json_field name value =
+
((name, Jsont.Meta.none), value)
+
+
let person_schema =
+
json_object [
+
json_field "type" (json_string "object");
+
json_field "properties" (json_object [
+
json_field "name" (json_object [
+
json_field "type" (json_string "string")
+
]);
+
json_field "age" (json_object [
+
json_field "type" (json_string "integer")
+
]);
+
]);
+
json_field "required" (json_array [
+
json_string "name";
+
json_string "age"
+
])
+
]
+
+
let format = Structured_output.of_json_schema person_schema
+
]}
+
+
{2 Usage with Claude Client}
+
+
{[
+
let options = Options.default
+
|> Options.with_output_format format
+
+
let client = Client.create ~sw ~process_mgr ~options () in
+
Client.query client "Extract person info from: John is 30 years old";
+
+
let messages = Client.receive_all client in
+
List.iter (function
+
| Message.Result result ->
+
(match Message.Result.structured_output result with
+
| Some json -> (* Process validated JSON *)
+
let json_str = match Jsont_bytesrw.encode_string' Jsont.json json with
+
| Ok s -> s
+
| Error err -> Jsont.Error.to_string err
+
in
+
Printf.printf "Structured output: %s\n" json_str
+
| None -> ())
+
| _ -> ()
+
) messages
+
]}
+
+
{2 JSON Schema Support}
+
+
The module supports standard JSON Schema Draft 7, including:
+
- Primitive types (string, integer, number, boolean, null)
+
- Objects with properties and required fields
+
- Arrays with item schemas
+
- Enumerations
+
- Nested objects and arrays
+
- Complex validation rules
+
+
@see <https://json-schema.org/> JSON Schema specification
+
@see <https://erratique.ch/software/jsont> jsont documentation *)
+
+
(** The log source for structured output operations *)
+
val src : Logs.Src.t
+
+
(** {1 Output Format Configuration} *)
+
+
type t
+
(** The type of structured output format configurations. *)
+
+
val of_json_schema : Jsont.json -> t
+
(** [of_json_schema schema] creates an output format from a JSON Schema.
+
+
The schema should be a valid JSON Schema Draft 7 as a {!Jsont.json} value.
+
+
Example:
+
{[
+
let meta = Jsont.Meta.none in
+
let schema = Jsont.Object ([
+
(("type", meta), Jsont.String ("object", meta));
+
(("properties", meta), Jsont.Object ([
+
(("name", meta), Jsont.Object ([
+
(("type", meta), Jsont.String ("string", meta))
+
], meta));
+
(("age", meta), Jsont.Object ([
+
(("type", meta), Jsont.String ("integer", meta))
+
], meta));
+
], meta));
+
(("required", meta), Jsont.Array ([
+
Jsont.String ("name", meta);
+
Jsont.String ("age", meta)
+
], meta));
+
], meta) in
+
+
let format = Structured_output.of_json_schema schema
+
]} *)
+
+
val json_schema : t -> Jsont.json
+
(** [json_schema t] returns the JSON Schema. *)
+
+
val jsont : t Jsont.t
+
(** Codec for structured output format. *)
+
+
(** {1 Serialization}
+
+
Internal use for encoding/decoding with the CLI. *)
+
+
val to_json : t -> Jsont.json
+
(** [to_json t] converts the output format to its JSON representation.
+
Internal use only. *)
+
+
val of_json : Jsont.json -> t
+
(** [of_json json] parses an output format from JSON.
+
Internal use only.
+
@raise Invalid_argument if the JSON is not a valid output format. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the output format. *)
+15 -9
claudeio/lib/transport.ml
···
let cmd = match Options.output_format options with
| Some format ->
let schema = Structured_output.json_schema format in
-
let schema_str = Ezjsonm.value_to_string schema in
+
let schema_str = match Jsont_bytesrw.encode_string' Jsont.json schema with
+
| Ok s -> s
+
| Error err -> failwith (Jsont.Error.to_string err)
+
in
cmd @ ["--json-schema"; schema_str]
| None -> cmd
in
···
{ process = P process; stdin; stdin_close; stdout; sw }
let send t json =
-
let data = Ezjsonm.value_to_string json in
+
let data = match Jsont_bytesrw.encode_string' Jsont.json json with
+
| Ok s -> s
+
| Error err -> failwith (Jsont.Error.to_string err)
+
in
Log.debug (fun m -> m "Sending: %s" data);
try
Eio.Flow.write t.stdin [Cstruct.of_string (data ^ "\n")]
···
let interrupt t =
Log.info (fun m -> m "Sending interrupt signal");
-
let interrupt_msg =
-
Ezjsonm.dict [
-
"type", Ezjsonm.string "control_response";
-
"response", Ezjsonm.dict [
-
"subtype", Ezjsonm.string "interrupt";
-
"request_id", Ezjsonm.string "";
-
]
+
let interrupt_msg =
+
Jsont.Json.object' [
+
Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_response");
+
Jsont.Json.mem (Jsont.Json.name "response") (Jsont.Json.object' [
+
Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string "interrupt");
+
Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string "");
+
])
]
in
send t interrupt_msg
+2 -2
claudeio/lib/transport.mli
···
type t
-
val create :
+
val create :
sw:Eio.Switch.t ->
process_mgr:_ Eio.Process.mgr ->
options:Options.t ->
unit -> t
-
val send : t -> Ezjsonm.value -> unit
+
val send : t -> Jsont.json -> unit
val receive_line : t -> string option
val interrupt : t -> unit
val close : t -> unit
+165
claudeio/test/advanced_config_demo.ml
···
+
(* Advanced Configuration Demo
+
+
This example demonstrates the advanced configuration options available
+
in the OCaml Claude SDK, including:
+
- Budget limits for cost control
+
- Fallback models for reliability
+
- Settings isolation for CI/CD environments
+
- Custom buffer sizes for large outputs
+
*)
+
+
open Eio.Std
+
open Claude
+
+
let log_setup () =
+
Logs.set_reporter (Logs_fmt.reporter ());
+
Logs.set_level (Some Logs.Info)
+
+
(* Example 1: CI/CD Configuration
+
+
In CI/CD environments, you want isolated, reproducible behavior
+
without any user/project/local settings interfering.
+
*)
+
let ci_cd_config () =
+
Options.default
+
|> Options.with_no_settings (* Disable all settings loading *)
+
|> Options.with_max_budget_usd 0.50 (* 50 cent limit per run *)
+
|> Options.with_fallback_model_string "claude-haiku-4" (* Fast fallback *)
+
|> Options.with_model_string "claude-sonnet-4-5"
+
|> Options.with_permission_mode Permissions.Mode.Bypass_permissions
+
+
(* Example 2: Production Configuration with Fallback
+
+
Production usage with cost controls and automatic fallback
+
to ensure availability.
+
*)
+
let production_config () =
+
Options.default
+
|> Options.with_model_string "claude-sonnet-4-5"
+
|> Options.with_fallback_model_string "claude-sonnet-3-5"
+
|> Options.with_max_budget_usd 10.0 (* $10 limit *)
+
|> Options.with_max_buffer_size 5_000_000 (* 5MB buffer for large outputs *)
+
+
(* Example 3: Development Configuration
+
+
Development with user settings enabled but with cost controls.
+
*)
+
let dev_config () =
+
Options.default
+
|> Options.with_setting_sources [Options.User; Options.Project]
+
|> Options.with_max_budget_usd 1.0 (* $1 limit for dev testing *)
+
|> Options.with_fallback_model_string "claude-haiku-4"
+
+
(* Example 4: Isolated Test Configuration
+
+
For automated testing with no external settings and strict limits.
+
*)
+
let test_config () =
+
Options.default
+
|> Options.with_no_settings
+
|> Options.with_max_budget_usd 0.10 (* 10 cent limit per test *)
+
|> Options.with_model_string "claude-haiku-4" (* Fast, cheap model *)
+
|> Options.with_permission_mode Permissions.Mode.Bypass_permissions
+
|> Options.with_max_buffer_size 1_000_000 (* 1MB buffer *)
+
+
(* Example 5: Custom Buffer Size Demo
+
+
For applications that need to handle very large outputs.
+
*)
+
let _large_output_config () =
+
Options.default
+
|> Options.with_max_buffer_size 10_000_000 (* 10MB buffer *)
+
|> Options.with_model_string "claude-sonnet-4-5"
+
+
(* Helper to run a query with a specific configuration *)
+
let run_query ~sw process_mgr config prompt =
+
print_endline "\n=== Configuration ===";
+
(match Options.max_budget_usd config with
+
| Some budget -> Printf.printf "Budget limit: $%.2f\n" budget
+
| None -> print_endline "Budget limit: None");
+
(match Options.fallback_model config with
+
| Some model -> Printf.printf "Fallback model: %s\n" (Claude.Model.to_string model)
+
| None -> print_endline "Fallback model: None");
+
(match Options.setting_sources config with
+
| Some [] -> print_endline "Settings: Isolated (no settings loaded)"
+
| Some sources ->
+
let source_str = String.concat ", " (List.map (function
+
| Options.User -> "user"
+
| Options.Project -> "project"
+
| Options.Local -> "local"
+
) sources) in
+
Printf.printf "Settings: %s\n" source_str
+
| None -> print_endline "Settings: Default");
+
(match Options.max_buffer_size config with
+
| Some size -> Printf.printf "Buffer size: %d bytes\n" size
+
| None -> print_endline "Buffer size: Default (1MB)");
+
+
print_endline "\n=== Running Query ===";
+
let client = Client.create ~options:config ~sw ~process_mgr () in
+
Client.query client prompt;
+
let messages = Client.receive client in
+
+
Seq.iter (function
+
| Message.Assistant msg ->
+
List.iter (function
+
| Content_block.Text t ->
+
Printf.printf "Response: %s\n" (Content_block.Text.text t)
+
| _ -> ()
+
) (Message.Assistant.content msg)
+
| Message.Result result ->
+
Printf.printf "\n=== Session Complete ===\n";
+
Printf.printf "Duration: %dms\n" (Message.Result.duration_ms result);
+
(match Message.Result.total_cost_usd result with
+
| Some cost -> Printf.printf "Cost: $%.4f\n" cost
+
| None -> ());
+
Printf.printf "Turns: %d\n" (Message.Result.num_turns result)
+
| _ -> ()
+
) messages
+
+
let main () =
+
log_setup ();
+
+
Eio_main.run @@ fun env ->
+
Switch.run @@ fun sw ->
+
let process_mgr = Eio.Stdenv.process_mgr env in
+
+
print_endline "==============================================";
+
print_endline "Claude SDK - Advanced Configuration Examples";
+
print_endline "==============================================";
+
+
(* Example: CI/CD isolated environment *)
+
print_endline "\n\n### Example 1: CI/CD Configuration ###";
+
print_endline "Purpose: Isolated, reproducible environment for CI/CD";
+
let config = ci_cd_config () in
+
run_query ~sw process_mgr config "What is 2+2? Answer in one sentence.";
+
+
(* Example: Production with fallback *)
+
print_endline "\n\n### Example 2: Production Configuration ###";
+
print_endline "Purpose: Production with cost controls and fallback";
+
let config = production_config () in
+
run_query ~sw process_mgr config "Explain OCaml in one sentence.";
+
+
(* Example: Development with settings *)
+
print_endline "\n\n### Example 3: Development Configuration ###";
+
print_endline "Purpose: Development with user/project settings";
+
let config = dev_config () in
+
run_query ~sw process_mgr config "What is functional programming? One sentence.";
+
+
(* Example: Test configuration *)
+
print_endline "\n\n### Example 4: Test Configuration ###";
+
print_endline "Purpose: Automated testing with strict limits";
+
let config = test_config () in
+
run_query ~sw process_mgr config "Say 'test passed' in one word.";
+
+
print_endline "\n\n==============================================";
+
print_endline "All examples completed successfully!";
+
print_endline "=============================================="
+
+
let () =
+
try
+
main ()
+
with
+
| e ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string e);
+
Printexc.print_backtrace stderr;
+
exit 1
+16 -5
claudeio/test/dune
···
+
(library
+
(name test_json_utils)
+
(modules test_json_utils)
+
(libraries jsont jsont.bytesrw))
+
(executable
(public_name camel_jokes)
(name camel_jokes)
···
(public_name permission_demo)
(name permission_demo)
(modules permission_demo)
-
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
+
(libraries test_json_utils claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
(executable
(public_name discovery_demo)
···
(public_name simple_permission_test)
(name simple_permission_test)
(modules simple_permission_test)
-
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
+
(libraries test_json_utils claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
(executable
(public_name hooks_example)
(name hooks_example)
(modules hooks_example)
-
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
+
(libraries test_json_utils claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
(executable
(public_name dynamic_control_demo)
···
(name structured_output_demo)
(modules structured_output_demo)
(flags (:standard -w -33))
-
(libraries claude eio_main logs logs.fmt fmt.tty))
+
(libraries test_json_utils claude eio_main logs logs.fmt fmt.tty))
(executable
(public_name structured_output_simple)
(name structured_output_simple)
(modules structured_output_simple)
(flags (:standard -w -33))
-
(libraries claude eio_main logs logs.fmt fmt.tty))
+
(libraries test_json_utils claude eio_main logs logs.fmt fmt.tty))
+
+
(executable
+
(public_name test_incoming)
+
(name test_incoming)
+
(modules test_incoming)
+
(libraries claude jsont.bytesrw))
+91
claudeio/test/dynamic_control_demo.ml
···
+
open Claude
+
open Eio.Std
+
+
let () = Logs.set_reporter (Logs_fmt.reporter ())
+
let () = Logs.set_level (Some Logs.Info)
+
+
let run env =
+
Switch.run @@ fun sw ->
+
let process_mgr = Eio.Stdenv.process_mgr env in
+
+
(* Create client with default options *)
+
let options = Options.default in
+
let client = Client.create ~options ~sw ~process_mgr () in
+
+
traceln "=== Dynamic Control Demo ===\n";
+
+
(* First query with default model *)
+
traceln "1. Initial query with default model";
+
Client.query client "What model are you?";
+
+
(* Consume initial messages *)
+
let messages = Client.receive_all client in
+
List.iter (function
+
| Message.Assistant msg ->
+
List.iter (function
+
| Content_block.Text t ->
+
traceln "Assistant: %s" (Content_block.Text.text t)
+
| _ -> ()
+
) (Message.Assistant.content msg)
+
| _ -> ()
+
) messages;
+
+
traceln "\n2. Getting server info...";
+
(try
+
let info = Client.get_server_info client in
+
traceln "Server version: %s" (Sdk_control.Server_info.version info);
+
traceln "Capabilities: [%s]"
+
(String.concat ", " (Sdk_control.Server_info.capabilities info));
+
traceln "Commands: [%s]"
+
(String.concat ", " (Sdk_control.Server_info.commands info));
+
traceln "Output styles: [%s]"
+
(String.concat ", " (Sdk_control.Server_info.output_styles info));
+
with
+
| Failure msg -> traceln "Failed to get server info: %s" msg
+
| exn -> traceln "Error getting server info: %s" (Printexc.to_string exn));
+
+
traceln "\n3. Switching to a different model (if available)...";
+
(try
+
Client.set_model_string client "claude-sonnet-4";
+
traceln "Model switched successfully";
+
+
(* Query with new model *)
+
Client.query client "Confirm your model again please.";
+
let messages = Client.receive_all client in
+
List.iter (function
+
| Message.Assistant msg ->
+
List.iter (function
+
| Content_block.Text t ->
+
traceln "Assistant (new model): %s" (Content_block.Text.text t)
+
| _ -> ()
+
) (Message.Assistant.content msg)
+
| _ -> ()
+
) messages;
+
with
+
| Failure msg -> traceln "Failed to switch model: %s" msg
+
| exn -> traceln "Error switching model: %s" (Printexc.to_string exn));
+
+
traceln "\n4. Changing permission mode...";
+
(try
+
Client.set_permission_mode client Permissions.Mode.Accept_edits;
+
traceln "Permission mode changed to Accept_edits";
+
with
+
| Failure msg -> traceln "Failed to change permission mode: %s" msg
+
| exn -> traceln "Error changing permission mode: %s" (Printexc.to_string exn));
+
+
traceln "\n=== Demo Complete ===";
+
()
+
+
let () =
+
Eio_main.run @@ fun env ->
+
try
+
run env
+
with
+
| Transport.CLI_not_found msg ->
+
traceln "Error: %s" msg;
+
traceln "Make sure the 'claude' CLI is installed and authenticated.";
+
exit 1
+
| exn ->
+
traceln "Unexpected error: %s" (Printexc.to_string exn);
+
Printexc.print_backtrace stderr;
+
exit 1
+2 -2
claudeio/test/hooks_example.ml
···
if tool_name = "Bash" then
let tool_input = Claude.Hooks.PreToolUse.tool_input hook in
-
match Ezjsonm.find tool_input ["command"] with
-
| `String command ->
+
match Test_json_utils.get_string tool_input "command" with
+
| Some command ->
if String.length command >= 6 && String.sub command 0 6 = "rm -rf" then begin
Log.app (fun m -> m "🚫 Blocked dangerous command: %s" command);
let output = Claude.Hooks.PreToolUse.deny
+27 -22
claudeio/test/permission_demo.ml
···
Log.app (fun m -> m "Tool: %s" tool_name);
(* Log the full input for debugging *)
-
Log.info (fun m -> m "Full input JSON: %s" (Ezjsonm.value_to_string input));
+
Log.info (fun m -> m "Full input JSON: %s" (Test_json_utils.to_string input));
(* Show input details *)
(* Try to extract key information from the input *)
(try
match tool_name with
| "Read" ->
-
let file_path = Ezjsonm.find input ["file_path"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "File: %s" file_path)
+
(match Test_json_utils.get_string input "file_path" with
+
| Some file_path -> Log.app (fun m -> m "File: %s" file_path)
+
| None -> ())
| "Bash" ->
-
let command = Ezjsonm.find input ["command"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "Command: %s" command)
+
(match Test_json_utils.get_string input "command" with
+
| Some command -> Log.app (fun m -> m "Command: %s" command)
+
| None -> ())
| "Write" | "Edit" ->
-
let file_path = Ezjsonm.find input ["file_path"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "File: %s" file_path)
+
(match Test_json_utils.get_string input "file_path" with
+
| Some file_path -> Log.app (fun m -> m "File: %s" file_path)
+
| None -> ())
| "Glob" ->
-
let pattern = Ezjsonm.find input ["pattern"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "Pattern: %s" pattern);
-
(try
-
let path = Ezjsonm.find input ["path"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "Path: %s" path)
-
with _ -> Log.app (fun m -> m "Path: (current directory)"))
+
(match Test_json_utils.get_string input "pattern" with
+
| Some pattern ->
+
Log.app (fun m -> m "Pattern: %s" pattern);
+
(match Test_json_utils.get_string input "path" with
+
| Some path -> Log.app (fun m -> m "Path: %s" path)
+
| None -> Log.app (fun m -> m "Path: (current directory)"))
+
| None -> ())
| "Grep" ->
-
let pattern = Ezjsonm.find input ["pattern"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "Pattern: %s" pattern);
-
(try
-
let path = Ezjsonm.find input ["path"] |> Ezjsonm.get_string in
-
Log.app (fun m -> m "Path: %s" path)
-
with _ -> Log.app (fun m -> m "Path: (current directory)"))
+
(match Test_json_utils.get_string input "pattern" with
+
| Some pattern ->
+
Log.app (fun m -> m "Pattern: %s" pattern);
+
(match Test_json_utils.get_string input "path" with
+
| Some path -> Log.app (fun m -> m "Path: %s" path)
+
| None -> Log.app (fun m -> m "Path: (current directory)"))
+
| None -> ())
| _ ->
-
Log.app (fun m -> m "Input: %s" (Ezjsonm.value_to_string input))
-
with exn ->
+
Log.app (fun m -> m "Input: %s" (Test_json_utils.to_string input))
+
with exn ->
Log.info (fun m -> m "Failed to parse input details: %s" (Printexc.to_string exn)));
(* Check if already granted *)
···
| _ ->
Granted.deny tool_name;
Log.info (fun m -> m "User denied permission for %s" tool_name);
-
Claude.Permissions.Result.deny ~message:(Printf.sprintf "User denied access to %s" tool_name) ~interrupt:false
+
Claude.Permissions.Result.deny ~message:(Printf.sprintf "User denied access to %s" tool_name) ~interrupt:false ()
end
let process_response client =
+1 -1
claudeio/test/simple_permission_test.ml
···
let auto_allow_callback ~tool_name ~input ~context:_ =
Log.app (fun m -> m "\n🔐 Permission callback invoked!");
Log.app (fun m -> m " Tool: %s" tool_name);
-
Log.app (fun m -> m " Input: %s" (Ezjsonm.value_to_string input));
+
Log.app (fun m -> m " Input: %s" (Test_json_utils.to_string input));
Log.app (fun m -> m " ✅ Auto-allowing");
Claude.Permissions.Result.allow ()
+29 -17
claudeio/test/simulated_permissions.ml
···
Claude.Permissions.Result.allow ()
end else if PermissionState.is_denied tool_name then begin
Log.app (fun m -> m " → Auto-denied (previously denied)");
-
Claude.Permissions.Result.deny
+
Claude.Permissions.Result.deny
~message:(Printf.sprintf "Tool %s is blocked by policy" tool_name)
-
~interrupt:false
+
~interrupt:false ()
end else begin
(* Ask user *)
Printf.printf " Allow %s? [y/n/always/never]: %!" tool_name;
···
Claude.Permissions.Result.allow ()
| "n" | "no" ->
Log.app (fun m -> m " → Denied (one time)");
-
Claude.Permissions.Result.deny
+
Claude.Permissions.Result.deny
~message:(Printf.sprintf "User denied %s" tool_name)
-
~interrupt:false
+
~interrupt:false ()
| "a" | "always" ->
PermissionState.grant tool_name;
Log.app (fun m -> m " → Allowed (always)");
···
| "never" ->
PermissionState.deny tool_name;
Log.app (fun m -> m " → Denied (always)");
-
Claude.Permissions.Result.deny
+
Claude.Permissions.Result.deny
~message:(Printf.sprintf "Tool %s permanently blocked" tool_name)
-
~interrupt:false
+
~interrupt:false ()
| _ ->
Log.app (fun m -> m " → Denied (invalid response)");
-
Claude.Permissions.Result.deny
+
Claude.Permissions.Result.deny
~message:"Invalid permission response"
-
~interrupt:false
+
~interrupt:false ()
end
(* Demonstrate the permission system *)
···
(* Test each tool *)
List.iter (fun tool ->
-
let input = Ezjsonm.dict [
-
"file_path", Ezjsonm.string "/example/path.txt"
-
] in
-
let result = example_permission_callback
+
let input =
+
let open Jsont in
+
Object ([
+
(("file_path", Meta.none), String ("/example/path.txt", Meta.none))
+
], Meta.none)
+
in
+
let result = example_permission_callback
~tool_name:tool ~input ~context in
(* Show result *)
···
let callback = Claude.Permissions.discovery_callback discovered in
(* Simulate some tool requests *)
-
let requests = [
-
("Read", Ezjsonm.dict ["file_path", Ezjsonm.string "test.ml"]);
-
("Bash", Ezjsonm.dict ["command", Ezjsonm.string "ls -la"]);
-
("Write", Ezjsonm.dict ["file_path", Ezjsonm.string "output.txt"]);
-
] in
+
let requests =
+
let open Jsont in
+
[
+
("Read", Object ([
+
(("file_path", Meta.none), String ("test.ml", Meta.none))
+
], Meta.none));
+
("Bash", Object ([
+
(("command", Meta.none), String ("ls -la", Meta.none))
+
], Meta.none));
+
("Write", Object ([
+
(("file_path", Meta.none), String ("output.txt", Meta.none))
+
], Meta.none));
+
]
+
in
Log.app (fun m -> m "Simulating tool requests with discovery callback...\n");
+172
claudeio/test/structured_output_demo.ml
···
+
(* Example demonstrating structured output with JSON Schema *)
+
+
module C = Claude
+
+
let () =
+
(* Configure logging to see what's happening *)
+
Logs.set_reporter (Logs_fmt.reporter ());
+
Logs.set_level (Some Logs.Info);
+
Logs.Src.set_level C.Message.src (Some Logs.Debug)
+
+
let run_codebase_analysis env =
+
Printf.printf "\n=== Codebase Analysis with Structured Output ===\n\n";
+
+
(* Define the JSON Schema for our expected output structure *)
+
let analysis_schema =
+
let open Jsont in
+
Object ([
+
(("type", Meta.none), String ("object", Meta.none));
+
(("properties", Meta.none), Object ([
+
(("file_count", Meta.none), Object ([
+
(("type", Meta.none), String ("integer", Meta.none));
+
(("description", Meta.none), String ("Total number of files analyzed", Meta.none))
+
], Meta.none));
+
(("has_tests", Meta.none), Object ([
+
(("type", Meta.none), String ("boolean", Meta.none));
+
(("description", Meta.none), String ("Whether the codebase has test files", Meta.none))
+
], Meta.none));
+
(("primary_language", Meta.none), Object ([
+
(("type", Meta.none), String ("string", Meta.none));
+
(("description", Meta.none), String ("The primary programming language used", Meta.none))
+
], Meta.none));
+
(("complexity_rating", Meta.none), Object ([
+
(("type", Meta.none), String ("string", Meta.none));
+
(("enum", Meta.none), Array ([
+
String ("low", Meta.none);
+
String ("medium", Meta.none);
+
String ("high", Meta.none)
+
], Meta.none));
+
(("description", Meta.none), String ("Overall complexity rating", Meta.none))
+
], Meta.none));
+
(("key_findings", Meta.none), Object ([
+
(("type", Meta.none), String ("array", Meta.none));
+
(("items", Meta.none), Object ([
+
(("type", Meta.none), String ("string", Meta.none))
+
], Meta.none));
+
(("description", Meta.none), String ("List of key findings from the analysis", Meta.none))
+
], Meta.none));
+
], Meta.none));
+
(("required", Meta.none), Array ([
+
String ("file_count", Meta.none);
+
String ("has_tests", Meta.none);
+
String ("primary_language", Meta.none);
+
String ("complexity_rating", Meta.none);
+
String ("key_findings", Meta.none)
+
], Meta.none));
+
(("additionalProperties", Meta.none), Bool (false, Meta.none))
+
], Meta.none)
+
in
+
+
(* Create structured output format from the schema *)
+
let output_format = C.Structured_output.of_json_schema analysis_schema in
+
+
(* Configure Claude with structured output *)
+
let options = C.Options.default
+
|> C.Options.with_output_format output_format
+
|> C.Options.with_allowed_tools ["Read"; "Glob"; "Grep"]
+
|> C.Options.with_system_prompt
+
"You are a code analysis assistant. Analyze codebases and provide \
+
structured output matching the given JSON Schema."
+
in
+
+
Printf.printf "Structured output format configured\n";
+
Printf.printf "Schema: %s\n\n"
+
(Test_json_utils.to_string ~minify:false analysis_schema);
+
+
(* Create Claude client and query *)
+
Eio.Switch.run @@ fun sw ->
+
let process_mgr = Eio.Stdenv.process_mgr env in
+
let client = C.Client.create ~sw ~process_mgr ~options () in
+
+
let prompt =
+
"Please analyze the current codebase structure. Look at the files, \
+
identify the primary language, count files, check for tests, assess \
+
complexity, and provide key findings. Return your analysis in the \
+
structured JSON format I specified."
+
in
+
+
Printf.printf "Sending query: %s\n\n" prompt;
+
C.Client.query client prompt;
+
+
(* Process responses *)
+
let messages = C.Client.receive client in
+
Seq.iter (function
+
| C.Message.Assistant msg ->
+
Printf.printf "\nAssistant response:\n";
+
List.iter (function
+
| C.Content_block.Text text ->
+
Printf.printf " Text: %s\n" (C.Content_block.Text.text text)
+
| C.Content_block.Tool_use tool ->
+
Printf.printf " Using tool: %s\n" (C.Content_block.Tool_use.name tool)
+
| _ -> ()
+
) (C.Message.Assistant.content msg)
+
+
| C.Message.Result result ->
+
Printf.printf "\n=== Result ===\n";
+
Printf.printf "Duration: %dms\n" (C.Message.Result.duration_ms result);
+
Printf.printf "Cost: $%.4f\n"
+
(Option.value (C.Message.Result.total_cost_usd result) ~default:0.0);
+
+
(* Extract and display structured output *)
+
(match C.Message.Result.structured_output result with
+
| Some output ->
+
Printf.printf "\n=== Structured Output ===\n";
+
Printf.printf "%s\n\n" (Test_json_utils.to_string ~minify:false output);
+
+
(* Parse the structured output *)
+
let file_count = Test_json_utils.get_int output "file_count" |> Option.value ~default:0 in
+
let has_tests = Test_json_utils.get_bool output "has_tests" |> Option.value ~default:false in
+
let language = Test_json_utils.get_string output "primary_language" |> Option.value ~default:"unknown" in
+
let complexity = Test_json_utils.get_string output "complexity_rating" |> Option.value ~default:"unknown" in
+
let findings =
+
match Test_json_utils.get_array output "key_findings" with
+
| Some items ->
+
List.filter_map (fun json ->
+
Test_json_utils.as_string json
+
) items
+
| None -> []
+
in
+
+
Printf.printf "=== Parsed Analysis ===\n";
+
Printf.printf "File Count: %d\n" file_count;
+
Printf.printf "Has Tests: %b\n" has_tests;
+
Printf.printf "Primary Language: %s\n" language;
+
Printf.printf "Complexity: %s\n" complexity;
+
Printf.printf "Key Findings:\n";
+
List.iter (fun finding ->
+
Printf.printf " - %s\n" finding
+
) findings
+
+
| None ->
+
Printf.printf "No structured output received\n";
+
(match C.Message.Result.result result with
+
| Some text -> Printf.printf "Text result: %s\n" text
+
| None -> ()))
+
+
| C.Message.System sys ->
+
(match C.Message.System.subtype sys with
+
| "init" ->
+
Printf.printf "Session initialized\n"
+
| _ -> ())
+
+
| _ -> ()
+
) messages;
+
+
Printf.printf "\nDone!\n"
+
+
let () =
+
Eio_main.run @@ fun env ->
+
try
+
run_codebase_analysis env
+
with
+
| C.Transport.CLI_not_found msg ->
+
Printf.eprintf "Error: Claude CLI not found\n%s\n" msg;
+
Printf.eprintf "Make sure 'claude' is installed and in your PATH\n";
+
exit 1
+
| C.Transport.Connection_error msg ->
+
Printf.eprintf "Connection error: %s\n" msg;
+
exit 1
+
| exn ->
+
Printf.eprintf "Unexpected error: %s\n" (Printexc.to_string exn);
+
Printexc.print_backtrace stderr;
+
exit 1
+72
claudeio/test/structured_output_simple.ml
···
+
(* Simple example showing structured output with explicit JSON Schema *)
+
+
module C = Claude
+
+
let () =
+
Logs.set_reporter (Logs_fmt.reporter ());
+
Logs.set_level (Some Logs.Info)
+
+
let simple_example env =
+
Printf.printf "\n=== Simple Structured Output Example ===\n\n";
+
+
(* Define a simple schema for a person's info *)
+
let person_schema =
+
let open Jsont in
+
Object ([
+
(("type", Meta.none), String ("object", Meta.none));
+
(("properties", Meta.none), Object ([
+
(("name", Meta.none), Object ([
+
(("type", Meta.none), String ("string", Meta.none))
+
], Meta.none));
+
(("age", Meta.none), Object ([
+
(("type", Meta.none), String ("integer", Meta.none))
+
], Meta.none));
+
(("occupation", Meta.none), Object ([
+
(("type", Meta.none), String ("string", Meta.none))
+
], Meta.none));
+
], Meta.none));
+
(("required", Meta.none), Array ([
+
String ("name", Meta.none);
+
String ("age", Meta.none);
+
String ("occupation", Meta.none)
+
], Meta.none))
+
], Meta.none)
+
in
+
+
let output_format = C.Structured_output.of_json_schema person_schema in
+
+
let options = C.Options.default
+
|> C.Options.with_output_format output_format
+
|> C.Options.with_max_turns 1
+
in
+
+
Printf.printf "Asking Claude to provide structured data...\n\n";
+
+
Eio.Switch.run @@ fun sw ->
+
let process_mgr = Eio.Stdenv.process_mgr env in
+
let client = C.Client.create ~sw ~process_mgr ~options () in
+
+
C.Client.query client
+
"Tell me about a famous computer scientist. Provide their name, age, \
+
and occupation in the exact JSON structure I specified.";
+
+
let messages = C.Client.receive_all client in
+
List.iter (function
+
| C.Message.Result result ->
+
Printf.printf "Response received!\n";
+
(match C.Message.Result.structured_output result with
+
| Some json ->
+
Printf.printf "\nStructured Output:\n%s\n"
+
(Test_json_utils.to_string ~minify:false json)
+
| None ->
+
Printf.printf "No structured output\n")
+
| _ -> ()
+
) messages
+
+
let () =
+
Eio_main.run @@ fun env ->
+
try
+
simple_example env
+
with exn ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
+
exit 1
+78
claudeio/test/test_incoming.ml
···
+
(** Test the Incoming message codec *)
+
+
open Claude
+
+
let test_decode_user_message () =
+
let json_str = {|{"type":"user","content":"Hello"}|} in
+
match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
+
| Ok (Incoming.Message (Message.User _)) ->
+
print_endline "✓ Decoded user message successfully"
+
| Ok _ ->
+
print_endline "✗ Wrong message type decoded"
+
| Error err ->
+
Printf.printf "✗ Failed to decode user message: %s\n" (Jsont.Error.to_string err)
+
+
let test_decode_assistant_message () =
+
let json_str = {|{"type":"assistant","model":"claude-sonnet-4","content":[{"type":"text","text":"Hi"}]}|} in
+
match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
+
| Ok (Incoming.Message (Message.Assistant _)) ->
+
print_endline "✓ Decoded assistant message successfully"
+
| Ok _ ->
+
print_endline "✗ Wrong message type decoded"
+
| Error err ->
+
Printf.printf "✗ Failed to decode assistant message: %s\n" (Jsont.Error.to_string err)
+
+
let test_decode_system_message () =
+
let json_str = {|{"type":"system","subtype":"init","data":{"session_id":"test-123"}}|} in
+
match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
+
| Ok (Incoming.Message (Message.System _)) ->
+
print_endline "✓ Decoded system message successfully"
+
| Ok _ ->
+
print_endline "✗ Wrong message type decoded"
+
| Error err ->
+
Printf.printf "✗ Failed to decode system message: %s\n" (Jsont.Error.to_string err)
+
+
let test_decode_control_response () =
+
let json_str = {|{"type":"control_response","response":{"subtype":"success","request_id":"test-req-1"}}|} in
+
match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
+
| Ok (Incoming.Control_response resp) ->
+
(match resp.response with
+
| Sdk_control.Response.Success s ->
+
if s.request_id = "test-req-1" then
+
print_endline "✓ Decoded control response successfully"
+
else
+
Printf.printf "✗ Wrong request_id: %s\n" s.request_id
+
| Sdk_control.Response.Error _ ->
+
print_endline "✗ Got error response instead of success")
+
| Ok _ ->
+
print_endline "✗ Wrong message type decoded"
+
| Error err ->
+
Printf.printf "✗ Failed to decode control response: %s\n" (Jsont.Error.to_string err)
+
+
let test_decode_control_response_error () =
+
let json_str = {|{"type":"control_response","response":{"subtype":"error","request_id":"test-req-2","error":"Something went wrong"}}|} in
+
match Jsont_bytesrw.decode_string' Incoming.jsont json_str with
+
| Ok (Incoming.Control_response resp) ->
+
(match resp.response with
+
| Sdk_control.Response.Error e ->
+
if e.request_id = "test-req-2" && e.error = "Something went wrong" then
+
print_endline "✓ Decoded control error response successfully"
+
else
+
Printf.printf "✗ Wrong error content\n"
+
| Sdk_control.Response.Success _ ->
+
print_endline "✗ Got success response instead of error")
+
| Ok _ ->
+
print_endline "✗ Wrong message type decoded"
+
| Error err ->
+
Printf.printf "✗ Failed to decode control error response: %s\n" (Jsont.Error.to_string err)
+
+
let () =
+
print_endline "Testing Incoming message codec...";
+
print_endline "";
+
test_decode_user_message ();
+
test_decode_assistant_message ();
+
test_decode_system_message ();
+
test_decode_control_response ();
+
test_decode_control_response_error ();
+
print_endline "";
+
print_endline "All tests completed!"
+41
claudeio/test/test_json_utils.ml
···
+
(* Helper functions for JSON operations in tests *)
+
+
let to_string ?(minify=false) json =
+
let format = if minify then Jsont.Minify else Jsont.Indent in
+
match Jsont_bytesrw.encode_string' ~format Jsont.json json with
+
| Ok s -> s
+
| Error err -> Jsont.Error.to_string err
+
+
let get_field json key =
+
match json with
+
| Jsont.Object (members, _) ->
+
List.find_map (fun ((name, _), value) ->
+
if name = key then Some value else None
+
) members
+
| _ -> None
+
+
let get_string json key =
+
match get_field json key with
+
| Some (Jsont.String (s, _)) -> Some s
+
| _ -> None
+
+
let get_int json key =
+
match get_field json key with
+
| Some (Jsont.Number (f, _)) ->
+
let i = int_of_float f in
+
if float_of_int i = f then Some i else None
+
| _ -> None
+
+
let get_bool json key =
+
match get_field json key with
+
| Some (Jsont.Bool (b, _)) -> Some b
+
| _ -> None
+
+
let get_array json key =
+
match get_field json key with
+
| Some (Jsont.Array (items, _)) -> Some items
+
| _ -> None
+
+
let as_string = function
+
| Jsont.String (s, _) -> Some s
+
| _ -> None