forked from
anil.recoil.org/monopam-myspace
My aggregated monorepo of OCaml code, automaintained
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Client interface for interacting with Claude.
7
8 This module provides the high-level client API for sending messages to
9 Claude and receiving responses. It handles the bidirectional streaming
10 protocol, permission callbacks, and hooks.
11
12 {2 Basic Usage}
13
14 {[
15 Eio.Switch.run @@ fun sw ->
16 let client = Client.create ~sw ~process_mgr ~clock () in
17 Client.query client "What is 2+2?";
18
19 let messages = Client.receive_all client in
20 List.iter
21 (function
22 | Message.Assistant msg ->
23 Printf.printf "Claude: %s\n" (Message.Assistant.text msg)
24 | _ -> ())
25 messages
26 ]}
27
28 {2 Features}
29
30 - {b Message Streaming}: Messages are streamed lazily via {!Seq.t}
31 - {b Permission Control}: Custom permission callbacks for tool usage
32 - {b Hooks}: Intercept and modify tool execution
33 - {b Dynamic Control}: Change settings mid-conversation
34 - {b Resource Management}: Automatic cleanup via Eio switches
35
36 {2 Message Flow}
37
38 1. Create a client with {!create} 2. Send messages with {!query} or
39 {!Advanced.send_message} 3. Receive responses with {!receive} or {!receive_all} 4.
40 Continue multi-turn conversations by sending more messages 5. Client
41 automatically cleans up when the switch exits
42
43 {2 Advanced Features}
44
45 - Permission discovery mode for understanding required permissions
46 - Mid-conversation model switching and permission mode changes
47 - Server capability introspection *)
48
49val src : Logs.Src.t
50(** The log source for client operations *)
51
52type t
53(** The type of Claude clients. *)
54
55val session_id : t -> string option
56(** [session_id t] returns the session ID if one has been received from Claude.
57 The session ID is provided in system init messages and uniquely identifies
58 the current conversation session. *)
59
60val create :
61 ?options:Options.t ->
62 sw:Eio.Switch.t ->
63 process_mgr:_ Eio.Process.mgr ->
64 clock:float Eio.Time.clock_ty Eio.Resource.t ->
65 unit ->
66 t
67(** [create ?options ~sw ~process_mgr ~clock ()] creates a new Claude client.
68
69 @param options Configuration options (defaults to {!Options.default})
70 @param sw Eio switch for resource management
71 @param process_mgr Eio process manager for spawning the Claude CLI
72 @param clock Eio clock for time operations *)
73
74(** {1 Simple Query Interface} *)
75
76val query : t -> string -> unit
77(** [query t prompt] sends a text message to Claude.
78
79 This is a convenience function for simple string messages. For more complex
80 messages with tool results or multiple content blocks, use
81 {!Advanced.send_message} instead. *)
82
83val respond_to_tool :
84 t -> tool_use_id:string -> content:Jsont.json -> ?is_error:bool -> unit -> unit
85(** [respond_to_tool t ~tool_use_id ~content ?is_error ()] responds to a tool
86 use request.
87
88 {b Duplicate protection:} If the same [tool_use_id] has already been
89 responded to, this call is silently skipped with a warning log. This
90 prevents API errors from duplicate tool responses.
91
92 @param tool_use_id The ID from the {!Response.Tool_use.t} event
93 @param content The result content (can be a string or array of content blocks)
94 @param is_error Whether this is an error response (default: false) *)
95
96val respond_to_tools : t -> (string * Jsont.json * bool option) list -> unit
97(** [respond_to_tools t responses] responds to multiple tool use requests at
98 once.
99
100 {b Duplicate protection:} Any [tool_use_id] that has already been
101 responded to is filtered out with a warning log.
102
103 Each tuple is [(tool_use_id, content, is_error option)] where content
104 can be a string or array of content blocks.
105
106 Example:
107 {[
108 Client.respond_to_tools client
109 [
110 ("tool_use_123", Jsont.string "Success", None);
111 ("tool_use_456", Jsont.string "Error occurred", Some true);
112 ]
113 ]} *)
114
115val clear_tool_response_tracking : t -> unit
116(** [clear_tool_response_tracking t] clears the internal tracking of which
117 tool_use_ids have been responded to.
118
119 This is useful when starting a new conversation or turn where you want
120 to allow responses to previously-seen tool IDs. Normally this is not
121 needed as tool IDs are unique per conversation turn. *)
122
123(** {1 Response Handling} *)
124
125val run : t -> handler:#Handler.handler -> unit
126(** [run t ~handler] processes all responses using the given handler.
127
128 This is the recommended way to handle responses in an event-driven style.
129 The handler's methods will be called for each response event as it arrives.
130
131 Example:
132 {[
133 let my_handler = object
134 inherit Claude.Handler.default
135 method! on_text t = print_endline (Response.Text.content t)
136 method! on_complete c =
137 Printf.printf "Cost: $%.4f\n"
138 (Option.value ~default:0.0 (Response.Complete.total_cost_usd c))
139 end in
140 Client.query client "Hello";
141 Client.run client ~handler:my_handler
142 ]} *)
143
144val receive : t -> Response.t Seq.t
145(** [receive t] returns a lazy sequence of responses from Claude.
146
147 The sequence yields response events as they arrive from Claude, including:
148 - {!constructor:Response.Text} - Text content from assistant
149 - {!constructor:Response.Tool_use} - Tool invocation requests
150 - {!constructor:Response.Thinking} - Internal reasoning
151 - {!constructor:Response.Init} - Session initialization
152 - {!constructor:Response.Error} - Error events
153 - {!constructor:Response.Complete} - Final result with usage statistics
154
155 Control messages (permission requests, hook callbacks) are handled
156 internally and not yielded to the sequence.
157
158 For simple cases, prefer {!run} with a handler instead. *)
159
160val receive_all : t -> Response.t list
161(** [receive_all t] collects all responses into a list.
162
163 This is a convenience function that consumes the {!receive} sequence. Use
164 this when you want to process all responses at once rather than streaming
165 them.
166
167 For most cases, prefer {!run} with a handler instead. *)
168
169val interrupt : t -> unit
170(** [interrupt t] sends an interrupt signal to stop Claude's execution. *)
171
172(** {1 Dynamic Control}
173
174 These methods allow you to change Claude's behavior mid-conversation without
175 recreating the client. This is useful for:
176
177 - Adjusting permission strictness based on user feedback
178 - Switching to faster/cheaper models for simple tasks
179 - Adapting to changing requirements during long conversations
180 - Introspecting server capabilities
181
182 {2 Example: Adaptive Permission Control}
183
184 {[
185 (* Start with strict permissions *)
186 let client = Client.create ~sw ~process_mgr ~clock
187 ~options:(Options.default
188 |> Options.with_permission_mode Permissions.Mode.Default) ()
189 in
190
191 Client.query client "Analyze this code";
192 let _ = Client.receive_all client in
193
194 (* User approves, switch to auto-accept edits *)
195 Client.set_permission_mode client Permissions.Mode.Accept_edits;
196
197 Client.query client "Now refactor it";
198 let _ = Client.receive_all client in
199 ]}
200
201 {2 Example: Model Switching for Efficiency}
202
203 {[
204 (* Use powerful model for complex analysis *)
205 let client = Client.create ~sw ~process_mgr ~clock
206 ~options:(Options.default |> Options.with_model "claude-sonnet-4-5") ()
207 in
208
209 Client.query client "Design a new architecture for this system";
210 let _ = Client.receive_all client in
211
212 (* Switch to faster model for simple tasks *)
213 Client.set_model client "claude-haiku-4";
214
215 Client.query client "Now write a README";
216 let _ = Client.receive_all client in
217 ]}
218
219 {2 Example: Server Introspection}
220
221 {[
222 let info = Client.get_server_info client in
223 Printf.printf "Claude CLI version: %s\n"
224 (Sdk_control.Server_info.version info);
225 Printf.printf "Capabilities: %s\n"
226 (String.concat ", " (Sdk_control.Server_info.capabilities info))
227 ]} *)
228
229val set_permission_mode : t -> Permissions.Mode.t -> unit
230(** [set_permission_mode t mode] changes the permission mode mid-conversation.
231
232 This allows switching between permission modes without recreating the
233 client:
234 - {!Permissions.Mode.Default} - Prompt for all permissions
235 - {!Permissions.Mode.Accept_edits} - Auto-accept file edits
236 - {!Permissions.Mode.Plan} - Planning mode with restricted execution
237 - {!Permissions.Mode.Bypass_permissions} - Skip all permission checks
238
239 @raise Failure if the server returns an error *)
240
241val set_model : t -> Model.t -> unit
242(** [set_model t model] switches to a different AI model mid-conversation.
243
244 Common models:
245 - [`Sonnet_4_5] - Most capable, balanced performance
246 - [`Opus_4] - Maximum capability for complex tasks
247 - [`Haiku_4] - Fast and cost-effective
248
249 @raise Failure if the model is invalid or unavailable *)
250
251val get_server_info : t -> Server_info.t
252(** [get_server_info t] retrieves server capabilities and metadata.
253
254 Returns information about:
255 - Server version string
256 - Available capabilities
257 - Supported commands
258 - Available output styles
259
260 Useful for feature detection and debugging.
261
262 @raise Failure if the server returns an error *)
263
264(** {1 Permission Discovery} *)
265
266val enable_permission_discovery : t -> unit
267(** [enable_permission_discovery t] enables permission discovery mode.
268
269 In discovery mode, all tool usage is logged but allowed. Use
270 {!discovered_permissions} to retrieve the list of permissions that were
271 requested during execution.
272
273 This is useful for understanding what permissions your prompt requires. *)
274
275val discovered_permissions : t -> Permissions.Rule.t list
276(** [discovered_permissions t] returns permissions discovered during execution.
277
278 Only useful after enabling {!enable_permission_discovery}. *)
279
280(** {1 Advanced Interface}
281
282 Low-level access to the protocol for advanced use cases. *)
283
284module Advanced : sig
285 val send_message : t -> Message.t -> unit
286 (** [send_message t msg] sends a message to Claude.
287
288 Supports all message types including user messages with tool results. *)
289
290 val send_user_message : t -> Message.User.t -> unit
291 (** [send_user_message t msg] sends a user message to Claude. *)
292
293 val send_raw : t -> Sdk_control.t -> unit
294 (** [send_raw t control] sends a raw SDK control message.
295
296 This is for advanced use cases that need direct control protocol access. *)
297
298 val send_json : t -> Jsont.json -> unit
299 (** [send_json t json] sends raw JSON to Claude.
300
301 This is the lowest-level send operation. Use with caution. *)
302
303 val receive_raw : t -> Incoming.t Seq.t
304 (** [receive_raw t] returns a lazy sequence of raw incoming messages.
305
306 This includes all message types before Response conversion:
307 - {!Proto.Incoming.t.constructor-Message} - Regular messages
308 - {!Proto.Incoming.t.constructor-Control_response} - Control responses (normally handled
309 internally)
310 - {!Proto.Incoming.t.constructor-Control_request} - Control requests (normally handled
311 internally)
312
313 Most users should use {!receive} or {!run} instead. *)
314end