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

sync

+98 -164
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 *)
+
(** Control response builders using jsont *)
+
module Control_response = struct
+
let success ~request_id ~response =
+
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 "success");
+
Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string request_id);
+
Jsont.Json.mem (Jsont.Json.name "response") response;
+
]);
+
]
+
+
let error ~request_id ~message =
+
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 "error");
+
Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string request_id);
+
Jsont.Json.mem (Jsont.Json.name "error") (Jsont.Json.string message);
+
]);
+
]
+
end
+
+
(* Helper functions for JSON manipulation using jsont *)
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
+
(* JSON construction helpers using jsont *)
+
let json_string s = Jsont.Json.string s
+
let json_null () = Jsont.Json.null ()
-
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)
+
let json_object pairs =
+
Jsont.Json.object' (List.map (fun (k, v) -> Jsont.Json.mem (Jsont.Json.name k) v) pairs)
type t = {
transport : Transport.t;
···
control_condition : Eio.Condition.t;
}
-
let handle_control_request t control_msg =
-
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" (json_to_string data));
-
match find_string data ["request"; "subtype"] with
-
| "can_use_tool" ->
-
let tool_name = find_string data ["request"; "tool_name"] in
-
let input = find data ["request"; "input"] in
+
let handle_control_request t (ctrl_req : Incoming.Control_request.t) =
+
let request_id = Incoming.Control_request.request_id ctrl_req in
+
Log.info (fun m -> m "Handling control request: %s" (Incoming.Control_request.subtype ctrl_req));
+
+
match Incoming.Control_request.request ctrl_req with
+
| Incoming.Control_request.Can_use_tool req ->
+
let tool_name = Incoming.Control_request.Can_use_tool.tool_name req in
+
let input = Incoming.Control_request.Can_use_tool.input req in
Log.info (fun m -> m "Permission request for tool '%s' with input: %s"
tool_name (json_to_string input));
-
let suggestions =
-
try
-
let sugg_json = find data ["request"; "permission_suggestions"] in
-
match sugg_json with
-
| Jsont.Array _ ->
-
(* TODO: Parse permission suggestions *)
-
[]
-
| _ -> []
-
with Not_found -> []
-
in
-
let context = Permissions.Context.create ~suggestions () in
+
(* TODO: Parse permission_suggestions properly *)
+
let context = Permissions.Context.create ~suggestions:[] () in
Log.info (fun m -> m "Invoking permission callback for tool: %s" tool_name);
let result = match t.permission_callback with
···
| Permissions.Result.Allow _ -> "ALLOW"
| Permissions.Result.Deny _ -> "DENY"));
-
(* Convert permission result to CLI format: {"behavior": "allow", "updatedInput": ...} or {"behavior": "deny", "message": ...} *)
+
(* Convert permission result to CLI format *)
let response_data = match result with
| 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
-
json_dict [
+
let updated_input = Option.value updated_input ~default:input in
+
json_object [
("behavior", json_string "allow");
("updatedInput", updated_input);
]
| Permissions.Result.Deny { message; interrupt = _; unknown = _ } ->
-
json_dict [
+
json_object [
("behavior", json_string "deny");
("message", json_string message);
]
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", response_data
-
]
-
] in
+
let response = Control_response.success ~request_id ~response:response_data in
Log.info (fun m -> m "Sending control response: %s" (json_to_string response));
Transport.send t.transport response
-
-
| "hook_callback" ->
-
let callback_id = find_string data ["request"; "callback_id"] in
-
let input = find data ["request"; "input"] in
-
let tool_use_id =
-
try Some (find_string data ["request"; "tool_use_id"])
-
with Not_found -> None
-
in
+
+
| Incoming.Control_request.Hook_callback req ->
+
let callback_id = Incoming.Control_request.Hook_callback.callback_id req in
+
let input = Incoming.Control_request.Hook_callback.input req in
+
let tool_use_id = Incoming.Control_request.Hook_callback.tool_use_id req in
Log.info (fun m -> m "Hook callback request for callback_id: %s" callback_id);
(try
···
| 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
+
let response = Control_response.success ~request_id ~response:result_json in
Log.info (fun m -> m "Hook callback succeeded, sending response");
Transport.send t.transport response
with
| 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 = 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
+
Transport.send t.transport (Control_response.error ~request_id ~message:error_msg)
| 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 = 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)
+
Transport.send t.transport (Control_response.error ~request_id ~message:error_msg))
-
| subtype ->
-
(* Respond with error for unknown control requests *)
-
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
+
| Incoming.Control_request.Unknown (subtype, _) ->
+
let error_msg = Printf.sprintf "Unsupported control request: %s" subtype in
+
Transport.send t.transport (Control_response.error ~request_id ~message:error_msg)
let handle_control_response t control_resp =
let request_id = match control_resp.Sdk_control.response with
···
Log.debug (fun m -> m "Handle messages: EOF received");
Seq.Nil
| Some line ->
-
try
-
(* 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:(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)"
-
(Control.subtype control_msg) (Control.request_id control_msg));
-
handle_control_request t control_msg;
-
loop ()
+
(* Use unified Incoming codec for all message types *)
+
match Jsont_bytesrw.decode_string' Incoming.jsont line with
+
| Ok (Incoming.Message msg) ->
+
Log.info (fun m -> m "← %a" Message.pp msg);
-
| _ ->
-
(* 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 ->
+
(match Message.System.Data.session_id (Message.System.data sys) with
+
| Some session_id ->
+
t.session_id <- Some session_id;
+
Log.debug (fun m -> m "Stored session ID: %s" session_id)
+
| None -> ())
+
| _ -> ());
-
(* 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)
-
Seq.Cons (msg, loop)
+
| Ok (Incoming.Control_response resp) ->
+
handle_control_response t resp;
+
loop ()
-
| Ok (Incoming.Control_response resp) ->
-
handle_control_response t resp;
-
loop ()
+
| Ok (Incoming.Control_request ctrl_req) ->
+
Log.info (fun m -> m "Received control request: %s (request_id: %s)"
+
(Incoming.Control_request.subtype ctrl_req)
+
(Incoming.Control_request.request_id ctrl_req));
+
handle_control_request t ctrl_req;
+
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"
-
(Printexc.to_string exn) line);
-
loop ()
+
| Error err ->
+
Log.err (fun m -> m "Failed to decode incoming message: %s\nLine: %s"
+
(Jsont.Error.to_string err) line);
+
loop ()
in
Log.debug (fun m -> m "Starting message handler");
loop
···
Log.debug (fun m -> m "Registered callback: %s for event: %s" callback_id event_name);
callback_id
) matcher.Hooks.callbacks in
-
json_dict [
+
json_object [
"matcher", (match matcher.Hooks.matcher with
| Some p -> json_string p
-
| None -> json_null);
-
"hookCallbackIds", Jsont.Array (List.map (fun id -> json_string id) callback_ids, Jsont.Meta.none);
+
| None -> json_null ());
+
"hookCallbackIds", Jsont.Json.list (List.map (fun id -> json_string id) callback_ids);
]
) matchers in
-
(event_name, Jsont.Array (matchers_json, Jsont.Meta.none)) :: acc
+
(event_name, Jsont.Json.list matchers_json) :: acc
) [] hooks_config in
(* Send initialize control request *)
-
let initialize_msg = json_dict [
+
let initialize_msg = json_object [
"type", json_string "control_request";
"request_id", json_string "init_hooks";
-
"request", json_dict [
+
"request", json_object [
"subtype", json_string "initialize";
-
"hooks", json_dict hooks_json;
+
"hooks", json_object hooks_json;
]
] in
Log.info (fun m -> m "Sending hooks initialize request");
···
let response_json = wait_for_response () in
Log.debug (fun m -> m "Received control response: %s" (json_to_string response_json));
-
(* Parse the response *)
-
let response_data = find response_json ["response"] in
+
(* Parse the response - extract the "response" field using jsont codec *)
+
let response_field_codec = Jsont.Object.map ~kind:"ResponseField" Fun.id
+
|> Jsont.Object.mem "response" Jsont.json ~enc:Fun.id
+
|> Jsont.Object.finish
+
in
+
let response_data = match Jsont.Json.decode response_field_codec response_json with
+
| Ok r -> r
+
| Error msg -> raise (Invalid_argument ("Failed to extract response field: " ^ msg))
+
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))
+19 -38
claudeio/lib/content_block.ml
···
module Tool_use = struct
module Input = struct
+
(* Dynamic JSON data for tool inputs with typed accessors using jsont decoders *)
type t = Jsont.json
let jsont = Jsont.json
let of_string_pairs pairs =
-
Jsont.Object (
-
List.map (fun (k, v) ->
-
((Jsont.Json.name k), Jsont.String (v, Jsont.Meta.none))
-
) pairs,
-
Jsont.Meta.none
-
)
+
Jsont.Json.object' (List.map (fun (k, v) ->
+
Jsont.Json.mem (Jsont.Json.name k) (Jsont.Json.string v)
+
) pairs)
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
-
)
+
Jsont.Json.object' (List.map (fun (k, v) -> Jsont.Json.mem (Jsont.Json.name k) v) assoc)
-
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
+
(* Helper to decode an optional field with a given codec *)
+
let get_opt (type a) (codec : a Jsont.t) t key : a option =
+
let field_codec = Jsont.Object.map ~kind:"field" (fun v -> v)
+
|> Jsont.Object.opt_mem key codec ~enc:Fun.id
+
|> Jsont.Object.finish
+
in
+
match Jsont.Json.decode field_codec t with
+
| Ok v -> v
+
| Error _ -> 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_string t key = get_opt Jsont.string t key
+
let get_int t key = get_opt Jsont.int t key
+
let get_bool t key = get_opt Jsont.bool t key
+
let get_float t key = get_opt Jsont.number t key
let keys t =
+
(* Decode as object with all members captured as unknown *)
match t with
| Jsont.Object (members, _) -> List.map (fun ((name, _), _) -> name) members
| _ -> []
+25 -166
claudeio/lib/hooks.ml
···
unknown : Input_unknown.t;
}
-
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 =
-
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
···
|> Jsont.Object.mem "tool_input" Jsont.json ~enc:tool_input
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+
+
let of_json json =
+
match Jsont.Json.decode input_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("PreToolUse: " ^ msg))
type permission_decision = [ `Allow | `Deny | `Ask ]
···
unknown : Input_unknown.t;
}
-
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 =
-
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
···
|> Jsont.Object.mem "tool_response" Jsont.json ~enc:tool_response
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+
+
let of_json json =
+
match Jsont.Json.decode input_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("PostToolUse: " ^ msg))
module Output_unknown = struct
type t = Jsont.json
···
unknown : Input_unknown.t;
}
-
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 =
-
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
···
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+
let of_json json =
+
match Jsont.Json.decode input_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("UserPromptSubmit: " ^ msg))
+
module Output_unknown = struct
type t = Jsont.json
let empty = Jsont.Object ([], Jsont.Meta.none)
···
unknown : Input_unknown.t;
}
-
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 =
-
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
···
|> Jsont.Object.mem "stop_hook_active" Jsont.bool ~enc:stop_hook_active
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+
+
let of_json json =
+
match Jsont.Json.decode input_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Stop: " ^ msg))
module Output_unknown = struct
type t = Jsont.json
···
(** {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 =
-
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} *)
···
unknown : Input_unknown.t;
}
-
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 =
-
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
···
|> Jsont.Object.mem "transcript_path" Jsont.string ~enc:transcript_path
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+
+
let of_json json =
+
match Jsont.Json.decode input_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("PreCompact: " ^ msg))
type output = unit (* No specific output for PreCompact *)
+151 -18
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)
+
(** Control request types for incoming control_request messages *)
+
module Control_request = struct
+
(** Can use tool permission request *)
+
module Can_use_tool = struct
+
type t = {
+
tool_name : string;
+
input : Jsont.json;
+
permission_suggestions : Jsont.json list;
+
}
+
+
let tool_name t = t.tool_name
+
let input t = t.input
+
let permission_suggestions t = t.permission_suggestions
+
+
let jsont : t Jsont.t =
+
let make tool_name input permission_suggestions =
+
{ tool_name; input; permission_suggestions = Option.value permission_suggestions ~default:[] }
+
in
+
Jsont.Object.map ~kind:"CanUseTool" make
+
|> Jsont.Object.mem "tool_name" Jsont.string ~enc:tool_name
+
|> Jsont.Object.mem "input" Jsont.json ~enc:input
+
|> Jsont.Object.opt_mem "permission_suggestions" (Jsont.list Jsont.json)
+
~enc:(fun t -> if t.permission_suggestions = [] then None else Some t.permission_suggestions)
+
|> Jsont.Object.finish
+
end
+
+
(** Hook callback request *)
+
module Hook_callback = struct
+
type t = {
+
callback_id : string;
+
input : Jsont.json;
+
tool_use_id : string option;
+
}
+
+
let callback_id t = t.callback_id
+
let input t = t.input
+
let tool_use_id t = t.tool_use_id
+
+
let jsont : t Jsont.t =
+
let make callback_id input tool_use_id = { callback_id; input; tool_use_id } in
+
Jsont.Object.map ~kind:"HookCallback" make
+
|> Jsont.Object.mem "callback_id" Jsont.string ~enc:callback_id
+
|> Jsont.Object.mem "input" Jsont.json ~enc:input
+
|> Jsont.Object.opt_mem "tool_use_id" Jsont.string ~enc:tool_use_id
+
|> Jsont.Object.finish
+
end
+
+
(** Request payload - discriminated by subtype *)
+
type request =
+
| Can_use_tool of Can_use_tool.t
+
| Hook_callback of Hook_callback.t
+
| Unknown of string * Jsont.json
+
+
let request_of_json json =
+
let subtype_codec = Jsont.Object.map ~kind:"Subtype" Fun.id
+
|> Jsont.Object.mem "subtype" Jsont.string ~enc:Fun.id
+
|> Jsont.Object.finish
+
in
+
match Jsont.Json.decode subtype_codec json with
+
| Error _ -> Unknown ("unknown", json)
+
| Ok subtype ->
+
match subtype with
+
| "can_use_tool" ->
+
(match Jsont.Json.decode Can_use_tool.jsont json with
+
| Ok r -> Can_use_tool r
+
| Error _ -> Unknown (subtype, json))
+
| "hook_callback" ->
+
(match Jsont.Json.decode Hook_callback.jsont json with
+
| Ok r -> Hook_callback r
+
| Error _ -> Unknown (subtype, json))
+
| _ -> Unknown (subtype, json)
+
+
(** Full control request message *)
+
type t = {
+
request_id : string;
+
request : request;
+
}
+
+
let request_id t = t.request_id
+
let request t = t.request
+
+
let subtype t =
+
match t.request with
+
| Can_use_tool _ -> "can_use_tool"
+
| Hook_callback _ -> "hook_callback"
+
| Unknown (s, _) -> s
+
+
let jsont : t Jsont.t =
+
let dec json =
+
let envelope_codec =
+
Jsont.Object.map ~kind:"ControlRequestEnvelope" (fun request_id request_json -> (request_id, request_json))
+
|> Jsont.Object.mem "request_id" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "request" Jsont.json ~enc:snd
+
|> Jsont.Object.finish
+
in
+
match Jsont.Json.decode envelope_codec json with
+
| Error err -> failwith ("Failed to decode control_request envelope: " ^ err)
+
| Ok (request_id, request_json) ->
+
{ request_id; request = request_of_json request_json }
+
in
+
let enc t =
+
let request_json = match t.request with
+
| Can_use_tool r ->
+
(match Jsont.Json.encode Can_use_tool.jsont r with
+
| Ok j -> j
+
| Error err -> failwith ("Failed to encode Can_use_tool: " ^ err))
+
| Hook_callback r ->
+
(match Jsont.Json.encode Hook_callback.jsont r with
+
| Ok j -> j
+
| Error err -> failwith ("Failed to encode Hook_callback: " ^ err))
+
| Unknown (_, j) -> j
+
in
+
Jsont.Json.object' [
+
Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "control_request");
+
Jsont.Json.mem (Jsont.Json.name "request_id") (Jsont.Json.string t.request_id);
+
Jsont.Json.mem (Jsont.Json.name "request") request_json;
+
]
+
in
+
Jsont.map ~kind:"ControlRequest" ~dec ~enc Jsont.json
+
end
+
type t =
| Message of Message.t
| Control_response of Sdk_control.control_response
+
| Control_request of Control_request.t
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
+
"system", "result"), while control_response and control_request have single type values.
+
Jsont's case_mem discriminator doesn't support multiple tags per case, so we implement
a custom decoder/encoder. *)
+
let type_field_codec = Jsont.Object.map ~kind:"type_field" Fun.id
+
|> Jsont.Object.opt_mem "type" Jsont.string ~enc:Fun.id
+
|> Jsont.Object.finish
+
in
+
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 type_field_codec json with
+
| Error _ | Ok None ->
+
(* No type field, try as message *)
+
(match Jsont.Json.decode Message.jsont json with
+
| Ok msg -> Message msg
+
| Error err -> failwith ("Failed to decode message: " ^ err))
+
| Ok (Some typ) ->
+
match typ with
+
| "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 *)
+
| "control_request" ->
+
(match Jsont.Json.decode Control_request.jsont json with
+
| Ok req -> Control_request req
+
| Error err -> failwith ("Failed to decode control_request: " ^ err))
+
| "user" | "assistant" | "system" | "result" | _ ->
+
(* Message types *)
(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"
+
| Error err -> failwith ("Failed to decode message: " ^ err))
in
let enc = function
···
(match Jsont.Json.encode Sdk_control.control_response_jsont resp with
| Ok json -> json
| Error err -> failwith ("Failed to encode control response: " ^ err))
+
| Control_request req ->
+
(match Jsont.Json.encode Control_request.jsont req with
+
| Ok json -> json
+
| Error err -> failwith ("Failed to encode control request: " ^ 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)
+
| Control_request req -> Format.fprintf fmt "@[<2>ControlRequest@ { request_id=%S; subtype=%S }@]"
+
(Control_request.request_id req) (Control_request.subtype req)
+39 -1
claudeio/lib/incoming.mli
···
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)
+
- "control_request" -> Control_request variant
This provides a clean, type-safe way to decode incoming messages in a single
operation, avoiding the parse-then-switch-then-parse pattern. *)
+
(** Control request types for incoming control_request messages *)
+
module Control_request : sig
+
(** Can use tool permission request *)
+
module Can_use_tool : sig
+
type t
+
+
val tool_name : t -> string
+
val input : t -> Jsont.json
+
val permission_suggestions : t -> Jsont.json list
+
val jsont : t Jsont.t
+
end
+
+
(** Hook callback request *)
+
module Hook_callback : sig
+
type t
+
+
val callback_id : t -> string
+
val input : t -> Jsont.json
+
val tool_use_id : t -> string option
+
val jsont : t Jsont.t
+
end
+
+
(** Request payload - discriminated by subtype *)
+
type request =
+
| Can_use_tool of Can_use_tool.t
+
| Hook_callback of Hook_callback.t
+
| Unknown of string * Jsont.json
+
+
(** Full control request message *)
+
type t
+
+
val request_id : t -> string
+
val request : t -> request
+
val subtype : t -> string
+
val jsont : t Jsont.t
+
end
+
type t =
| Message of Message.t
| Control_response of Sdk_control.control_response
+
| Control_request of Control_request.t
val jsont : t Jsont.t
(** Codec for incoming messages. Uses the "type" field to discriminate. *)
+261 -233
claudeio/lib/message.ml
···
], Jsont.Meta.none));
], Jsont.Meta.none)
+
(* Jsont codec for parsing incoming user messages from CLI *)
+
let incoming_jsont : t Jsont.t =
+
let message_jsont =
+
Jsont.Object.map ~kind:"UserMessage" (fun json_content ->
+
let content = decode_content json_content in
+
{ content; unknown = Unknown.empty }
+
)
+
|> Jsont.Object.mem "content" Jsont.json ~enc:(fun t -> encode_content (content t))
+
|> Jsont.Object.finish
+
in
+
Jsont.Object.map ~kind:"UserEnvelope" Fun.id
+
|> Jsont.Object.mem "message" message_jsont ~enc:Fun.id
+
|> Jsont.Object.finish
+
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
-
| 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; unknown = Unknown.empty }
-
| _ -> raise (Invalid_argument "User.of_json: expected object")
+
match Jsont.Json.decode incoming_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("User.of_json: " ^ msg))
let pp fmt t =
match t.content with
···
(Jsont.Json.name "message", Jsont.Object (msg_fields, Jsont.Meta.none));
], Jsont.Meta.none)
+
(* Jsont codec for parsing incoming assistant messages from CLI *)
+
let incoming_jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"AssistantEnvelope" Fun.id
+
|> Jsont.Object.mem "message" jsont ~enc:Fun.id
+
|> Jsont.Object.finish
+
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
-
| Jsont.Object (msg_fields, _) ->
-
let content =
-
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 = 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 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; unknown = Unknown.empty }
-
| _ -> raise (Invalid_argument "Assistant.of_json: expected object")
+
match Jsont.Json.decode incoming_jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Assistant.of_json: " ^ msg))
let pp fmt t =
let text_count = List.length (get_text_blocks t) in
···
end
module System = struct
-
module Data = struct
-
(* Opaque JSON type with typed accessors *)
-
type t = Jsont.json
+
(** Typed data for system init messages *)
+
module Init = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let _jsont = Jsont.json
+
end
+
+
type t = {
+
session_id : string option;
+
model : string option;
+
cwd : string option;
+
unknown : Unknown.t;
+
}
+
+
let make session_id model cwd unknown = { session_id; model; cwd; unknown }
-
let jsont = Jsont.json
+
let create ?session_id ?model ?cwd () =
+
{ session_id; model; cwd; unknown = Unknown.empty }
+
+
let session_id t = t.session_id
+
let model t = t.model
+
let cwd t = t.cwd
+
let unknown t = t.unknown
-
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"SystemInit" make
+
|> Jsont.Object.opt_mem "session_id" Jsont.string ~enc:session_id
+
|> Jsont.Object.opt_mem "model" Jsont.string ~enc:model
+
|> Jsont.Object.opt_mem "cwd" Jsont.string ~enc:cwd
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
end
-
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
-
)
+
(** Typed data for system error messages *)
+
module Error = struct
+
module Unknown = struct
+
type t = Jsont.json
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let _jsont = Jsont.json
+
end
+
+
type t = {
+
error : string;
+
unknown : Unknown.t;
+
}
+
+
let make error unknown = { error; unknown }
+
+
let create ~error = { error; unknown = Unknown.empty }
+
+
let error t = t.error
+
let unknown t = t.unknown
+
+
let jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"SystemError" make
+
|> Jsont.Object.mem "error" Jsont.string ~enc:error
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
+
end
-
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
+
(** Sum type for system message data *)
+
module Data = struct
+
type t =
+
| Init of Init.t
+
| Error of Error.t
+
| Other of Jsont.json (** Unknown subtypes preserve raw JSON *)
-
let get_string t key =
-
match get_field t key with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
+
let init ?session_id ?model ?cwd () = Init (Init.create ?session_id ?model ?cwd ())
+
let error ~error = Error (Error.create ~error)
+
let other json = Other json
-
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
+
let session_id = function
+
| Init i -> Init.session_id i
| _ -> None
-
let get_bool t key =
-
match get_field t key with
-
| Some (Jsont.Bool (b, _)) -> Some b
+
let model = function
+
| Init i -> Init.model i
| _ -> None
-
let get_float t key =
-
match get_field t key with
-
| Some (Jsont.Number (f, _)) -> Some f
+
let cwd = function
+
| Init i -> Init.cwd i
| _ -> None
-
let get_list t key =
-
match get_field t key with
-
| Some (Jsont.Array (items, _)) -> Some items
+
let error_msg = function
+
| Error e -> Some (Error.error e)
| _ -> None
-
let raw_json t = t
+
let to_json = function
+
| Init i ->
+
(match Jsont.Json.encode Init.jsont i with
+
| Ok json -> json
+
| Error msg -> failwith ("Init.to_json: " ^ msg))
+
| Error e ->
+
(match Jsont.Json.encode Error.jsont e with
+
| Ok json -> json
+
| Error msg -> failwith ("Error.to_json: " ^ msg))
+
| Other json -> json
-
let to_json t = t
-
let of_json json = json
+
let of_json ~subtype json =
+
match subtype with
+
| "init" ->
+
(match Jsont.Json.decode Init.jsont json with
+
| Ok i -> Init i
+
| Error _ -> Other json)
+
| "error" ->
+
(match Jsont.Json.decode Error.jsont json with
+
| Ok e -> Error e
+
| Error _ -> Other json)
+
| _ -> Other json
end
module Unknown = struct
···
}
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
+
(** Create a system init message *)
+
let init ?session_id ?model ?cwd () =
+
{ subtype = "init";
+
data = Data.init ?session_id ?model ?cwd ();
+
unknown = Unknown.empty }
+
+
(** Create a system error message *)
+
let error ~error =
+
{ subtype = "error";
+
data = Data.error ~error;
+
unknown = Unknown.empty }
+
(* Custom jsont that handles both formats:
- Old format: {"type":"system","subtype":"init","data":{...}}
- New format: {"type":"system","subtype":"init","cwd":"...","session_id":"...",...}
When data field is not present, we use the entire object as data *)
let jsont : t Jsont.t =
-
let make_with_optional_data subtype opt_data unknown_json =
-
let data = match opt_data with
-
| Some d -> d
-
| None -> unknown_json (* Use the full unknown object as data *)
+
let dec json =
+
(* First decode just the subtype *)
+
let subtype_codec = Jsont.Object.map ~kind:"SystemSubtype" Fun.id
+
|> Jsont.Object.mem "subtype" Jsont.string ~enc:Fun.id
+
|> Jsont.Object.finish
in
-
make subtype data Unknown.empty
+
match Jsont.Json.decode subtype_codec json with
+
| Error msg -> failwith ("System.jsont: " ^ msg)
+
| Ok subtype ->
+
(* Try to get data field, otherwise use full object *)
+
let data_codec = Jsont.Object.map ~kind:"SystemDataField" Fun.id
+
|> Jsont.Object.opt_mem "data" Jsont.json ~enc:Fun.id
+
|> Jsont.Object.finish
+
in
+
let data_json = match Jsont.Json.decode data_codec json with
+
| Ok (Some d) -> d
+
| _ -> json
+
in
+
let data = Data.of_json ~subtype data_json in
+
{ subtype; data; unknown = Unknown.empty }
in
-
Jsont.Object.map ~kind:"System" make_with_optional_data
-
|> Jsont.Object.mem "subtype" Jsont.string ~enc:subtype
-
|> Jsont.Object.opt_mem "data" Data.jsont ~enc:(fun t -> Some (data t))
-
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun _ -> Unknown.empty)
-
|> Jsont.Object.finish
+
let enc t =
+
Jsont.Json.object' [
+
Jsont.Json.mem (Jsont.Json.name "type") (Jsont.Json.string "system");
+
Jsont.Json.mem (Jsont.Json.name "subtype") (Jsont.Json.string t.subtype);
+
Jsont.Json.mem (Jsont.Json.name "data") (Data.to_json t.data);
+
]
+
in
+
Jsont.map ~kind:"System" ~dec ~enc Jsont.json
let to_json t =
-
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)
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("System.to_json: " ^ msg)
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")
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("System.of_json: " ^ msg))
let pp fmt t =
-
match t.subtype with
-
| "init" ->
-
let session_id = Data.get_string t.data "session_id" in
-
let model = Data.get_string t.data "model" in
-
let cwd = Data.get_string t.data "cwd" in
+
match t.data with
+
| Data.Init i ->
Fmt.pf fmt "@[<2>System.init@ { session_id = %a;@ model = %a;@ cwd = %a }@]"
-
Fmt.(option string) session_id
-
Fmt.(option string) model
-
Fmt.(option string) cwd
-
| "error" ->
-
let error = Data.get_string t.data "error" in
-
Fmt.pf fmt "@[<2>System.error@ { error = %a }@]"
-
Fmt.(option string) error
-
| _ ->
+
Fmt.(option string) (Init.session_id i)
+
Fmt.(option string) (Init.model i)
+
Fmt.(option string) (Init.cwd i)
+
| Data.Error e ->
+
Fmt.pf fmt "@[<2>System.error@ { error = %s }@]" (Error.error e)
+
| Data.Other _ ->
Fmt.pf fmt "@[<2>System.%s@ { ... }@]" t.subtype
end
module Result = struct
module Usage = struct
-
(* Opaque JSON type with typed accessors *)
-
type t = Jsont.json
+
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
-
let jsont = Jsont.json
+
type t = {
+
input_tokens : int option;
+
output_tokens : int option;
+
total_tokens : int option;
+
cache_creation_input_tokens : int option;
+
cache_read_input_tokens : int option;
+
unknown : Unknown.t;
+
}
+
+
let make input_tokens output_tokens total_tokens
+
cache_creation_input_tokens cache_read_input_tokens unknown =
+
{ input_tokens; output_tokens; total_tokens;
+
cache_creation_input_tokens; cache_read_input_tokens; unknown }
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 -> (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 -> (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 -> (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 -> (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 -> (Jsont.Json.name "cache_read_input_tokens", Jsont.Number (float_of_int n, Jsont.Meta.none)) :: fields
-
| None -> fields in
-
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
+
{ input_tokens; output_tokens; total_tokens;
+
cache_creation_input_tokens; cache_read_input_tokens;
+
unknown = Unknown.empty }
-
let input_tokens t = get_int t "input_tokens"
+
let input_tokens t = t.input_tokens
+
let output_tokens t = t.output_tokens
+
let total_tokens t = t.total_tokens
+
let cache_creation_input_tokens t = t.cache_creation_input_tokens
+
let cache_read_input_tokens t = t.cache_read_input_tokens
+
let unknown t = t.unknown
-
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 jsont : t Jsont.t =
+
Jsont.Object.map ~kind:"Usage" make
+
|> Jsont.Object.opt_mem "input_tokens" Jsont.int ~enc:input_tokens
+
|> Jsont.Object.opt_mem "output_tokens" Jsont.int ~enc:output_tokens
+
|> Jsont.Object.opt_mem "total_tokens" Jsont.int ~enc:total_tokens
+
|> Jsont.Object.opt_mem "cache_creation_input_tokens" Jsont.int ~enc:cache_creation_input_tokens
+
|> Jsont.Object.opt_mem "cache_read_input_tokens" Jsont.int ~enc:cache_read_input_tokens
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
+
|> Jsont.Object.finish
let effective_input_tokens t =
-
match input_tokens t with
+
match t.input_tokens with
| None -> 0
| Some input ->
-
let cached = Option.value (cache_read_input_tokens t) ~default:0 in
+
let cached = Option.value t.cache_read_input_tokens ~default:0 in
max 0 (input - cached)
let total_cost_estimate t ~input_price ~output_price =
-
match input_tokens t, output_tokens t with
+
match t.input_tokens, t.output_tokens with
| Some input, Some output ->
let input_cost = float_of_int input *. input_price /. 1_000_000. in
let output_cost = float_of_int output *. output_price /. 1_000_000. in
···
let pp fmt t =
Fmt.pf fmt "@[<2>Usage@ { input = %a;@ output = %a;@ total = %a;@ \
cache_creation = %a;@ cache_read = %a }@]"
-
Fmt.(option int) (input_tokens t)
-
Fmt.(option int) (output_tokens t)
-
Fmt.(option int) (total_tokens t)
-
Fmt.(option int) (cache_creation_input_tokens t)
-
Fmt.(option int) (cache_read_input_tokens t)
+
Fmt.(option int) t.input_tokens
+
Fmt.(option int) t.output_tokens
+
Fmt.(option int) t.total_tokens
+
Fmt.(option int) t.cache_creation_input_tokens
+
Fmt.(option int) t.cache_read_input_tokens
-
let to_json t = t
-
let of_json json = json
+
let to_json t =
+
match Jsont.Json.encode jsont t with
+
| Ok json -> json
+
| Error msg -> failwith ("Usage.to_json: " ^ msg)
+
+
let of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Usage.of_json: " ^ msg))
end
module Unknown = struct
···
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; unknown = Unknown.empty }
-
| _ -> raise (Invalid_argument "Result.of_json: expected object")
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Result.of_json: " ^ msg))
let pp fmt t =
if t.is_error then
···
let system ~subtype ~data = System (System.create ~subtype ~data)
let system_init ~session_id =
-
let data = System.Data.of_assoc [(("session_id", Jsont.String (session_id, Jsont.Meta.none)))] in
-
System (System.create ~subtype:"init" ~data)
+
System (System.init ~session_id ())
let system_error ~error =
-
let data = System.Data.of_assoc [(("error", Jsont.String (error, Jsont.Meta.none)))] in
-
System (System.create ~subtype:"error" ~data)
+
System (System.error ~error)
let result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
~session_id ?total_cost_usd ?usage ?result ?structured_output () =
···
| System t -> System.to_json t
| Result t -> Result.to_json t
-
let of_json json =
-
match json with
-
| 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")
-
(* Jsont codec for the main Message variant type.
-
Uses a custom decoder to handle both old and new formats. *)
+
Uses case_mem for discriminated union based on "type" field. *)
let jsont : t Jsont.t =
-
Jsont.map ~kind:"Message" ~dec:of_json ~enc:to_json Jsont.json
+
let case_map kind obj dec = Jsont.Object.Case.map kind obj ~dec in
+
let case_user = case_map "user" User.incoming_jsont (fun v -> User v) in
+
let case_assistant = case_map "assistant" Assistant.incoming_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 of_json json =
+
match Jsont.Json.decode jsont json with
+
| Ok v -> v
+
| Error msg -> raise (Invalid_argument ("Message.of_json: " ^ msg))
let pp fmt = function
| User t -> User.pp fmt t
···
| _ -> []
let get_session_id = function
-
| System s when System.subtype s = "init" ->
-
System.Data.get_string (System.data s) "session_id"
+
| System s -> System.Data.session_id (System.data s)
| Result r -> Some (Result.session_id r)
| _ -> None
+73 -29
claudeio/lib/message.mli
···
module System : sig
(** System control and status messages. *)
-
module Data : sig
-
(** System message data. *)
+
(** Typed data for system init messages *)
+
module Init : sig
+
type t
+
(** Type of init message data. *)
-
type t = Jsont.json
-
(** Opaque type for system message data. Contains the raw JSON
-
with typed accessors for common fields. *)
+
val create : ?session_id:string -> ?model:string -> ?cwd:string -> unit -> t
+
(** [create ?session_id ?model ?cwd ()] creates init data. *)
+
+
val session_id : t -> string option
+
(** [session_id t] returns the session ID if present. *)
+
+
val model : t -> string option
+
(** [model t] returns the model name if present. *)
+
+
val cwd : t -> string option
+
(** [cwd t] returns the current working directory if present. *)
val jsont : t Jsont.t
-
(** [jsont] is the Jsont codec for system data. *)
+
(** [jsont] is the Jsont codec for init data. *)
+
end
-
val empty : t
-
(** [empty] creates empty data. *)
+
(** Typed data for system error messages *)
+
module Error : sig
+
type t
+
(** Type of error message data. *)
-
val of_assoc : (string * Jsont.json) list -> t
-
(** [of_assoc assoc] creates data from an association list. *)
+
val create : error:string -> t
+
(** [create ~error] creates error data. *)
-
val get_string : t -> string -> string option
-
(** [get_string t key] returns the string value for [key], if present. *)
+
val error : t -> string
+
(** [error t] returns the error message. *)
-
val get_int : t -> string -> int option
-
(** [get_int t key] returns the integer value for [key], if present. *)
+
val jsont : t Jsont.t
+
(** [jsont] is the Jsont codec for error data. *)
+
end
-
val get_bool : t -> string -> bool option
-
(** [get_bool t key] returns the boolean value for [key], if present. *)
+
(** System message data variants *)
+
module Data : sig
+
type t =
+
| Init of Init.t (** Init message data *)
+
| Error of Error.t (** Error message data *)
+
| Other of Jsont.json (** Unknown subtype data *)
+
(** Variant type for system message data. *)
-
val get_float : t -> string -> float option
-
(** [get_float t key] returns the float value for [key], if present. *)
+
val init : ?session_id:string -> ?model:string -> ?cwd:string -> unit -> t
+
(** [init ?session_id ?model ?cwd ()] creates init data. *)
-
val get_list : t -> string -> Jsont.json list option
-
(** [get_list t key] returns the list value for [key], if present. *)
+
val error : error:string -> t
+
(** [error ~error] creates error data. *)
-
val get_field : t -> string -> Jsont.json option
-
(** [get_field t key] returns the raw JSON value for [key], if present. *)
+
val other : Jsont.json -> t
+
(** [other json] creates data for unknown subtypes. *)
-
val raw_json : t -> Jsont.json
-
(** [raw_json t] returns the full underlying JSON data. *)
+
val session_id : t -> string option
+
(** [session_id t] extracts session_id from Init data, None otherwise. *)
+
+
val model : t -> string option
+
(** [model t] extracts model from Init data, None otherwise. *)
+
+
val cwd : t -> string option
+
(** [cwd t] extracts cwd from Init data, None otherwise. *)
+
+
val error_msg : t -> string option
+
(** [error_msg t] extracts error from Error data, None otherwise. *)
val to_json : t -> Jsont.json
-
(** [to_json t] converts to JSON representation. Internal use only. *)
+
(** [to_json t] converts to JSON representation. *)
-
val of_json : Jsont.json -> t
-
(** [of_json json] parses from JSON. Internal use only. *)
+
val of_json : subtype:string -> Jsont.json -> t
+
(** [of_json ~subtype json] parses data based on subtype. *)
end
module Unknown : sig
···
@param subtype The subtype of the system message
@param data Additional data for the message *)
+
val init : ?session_id:string -> ?model:string -> ?cwd:string -> unit -> t
+
(** [init ?session_id ?model ?cwd ()] creates a system init message. *)
+
+
val error : error:string -> t
+
(** [error ~error] creates a system error message. *)
+
val subtype : t -> string
(** [subtype t] returns the subtype of the system message. *)
···
module Usage : sig
(** Usage statistics for API calls. *)
-
type t = Jsont.json
-
(** Opaque type for usage statistics. *)
+
module Unknown : sig
+
type t = Jsont.json
+
val empty : t
+
val is_empty : t -> bool
+
val jsont : t Jsont.t
+
end
+
+
type t
+
(** Type for usage statistics. *)
val jsont : t Jsont.t
(** [jsont] is the Jsont codec for usage statistics. *)
···
val cache_read_input_tokens : t -> int option
(** [cache_read_input_tokens t] returns cache read input tokens. *)
+
+
val unknown : t -> Unknown.t
+
(** [unknown t] returns the unknown fields preserved from JSON. *)
val effective_input_tokens : t -> int
(** [effective_input_tokens t] returns input tokens minus cached tokens, or 0 if not available. *)
+11 -10
claudeio/lib/options.ml
···
~enc:Model.to_string
Jsont.string
-
(* Helper codec for env - list of string pairs encoded as object *)
+
(* Helper codec for env - list of string pairs encoded as object.
+
Env is a dynamic object where all values should be strings.
+
Uses pattern matching to extract object members, then jsont for string decoding. *)
let env_jsont : (string * string) list Jsont.t =
Jsont.map ~kind:"Env"
-
~dec:(fun obj ->
-
match obj with
+
~dec:(fun json ->
+
match json with
| Jsont.Object (members, _) ->
-
List.map (fun ((name, _), value) ->
-
match value with
-
| Jsont.String (s, _) -> (name, s)
-
| _ -> (name, "")
+
List.filter_map (fun ((name, _), value) ->
+
match Jsont.Json.decode Jsont.string value with
+
| Ok s -> Some (name, s)
+
| Error _ -> None
) members
| _ -> [])
~enc:(fun pairs ->
-
let mems = List.map (fun (k, v) ->
+
Jsont.Json.object' (List.map (fun (k, v) ->
Jsont.Json.mem (Jsont.Json.name k) (Jsont.Json.string v)
-
) pairs in
-
Jsont.Json.object' mems)
+
) pairs))
Jsont.json
(* Helper codec for headers - list of string pairs encoded as object *)
+18 -30
claudeio/test/test_json_utils.ml
···
-
(* Helper functions for JSON operations in tests *)
+
(* Helper functions for JSON operations in tests using jsont codecs *)
let to_string ?(minify=false) json =
let format = if minify then Jsont.Minify else Jsont.Indent in
···
| 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
+
(* Helper to decode an optional field with a given codec *)
+
let get_opt (type a) (codec : a Jsont.t) json key : a option =
+
let field_codec = Jsont.Object.map ~kind:"field" (fun v -> v)
+
|> Jsont.Object.opt_mem key codec ~enc:Fun.id
+
|> Jsont.Object.finish
+
in
+
match Jsont.Json.decode field_codec json with
+
| Ok v -> v
+
| Error _ -> None
-
let get_bool json key =
-
match get_field json key with
-
| Some (Jsont.Bool (b, _)) -> Some b
-
| _ -> None
+
let get_string json key = get_opt Jsont.string json key
+
let get_int json key = get_opt Jsont.int json key
+
let get_bool json key = get_opt Jsont.bool json key
let get_array json key =
-
match get_field json key with
-
| Some (Jsont.Array (items, _)) -> Some items
-
| _ -> None
+
get_opt (Jsont.list Jsont.json) json key
-
let as_string = function
-
| Jsont.String (s, _) -> Some s
-
| _ -> None
+
let as_string json =
+
match Jsont.Json.decode Jsont.string json with
+
| Ok s -> Some s
+
| Error _ -> None