Library
Custom OCaml tools
Register tools, observe transient progress and nested traces, and run the tracked offline example.
View Markdown source ↗Use ochat.ochat_function to combine model-visible metadata with an OCaml
implementation. ChatMD authors normally select built-ins
or other tool kinds; this API is for library authors.
Definition and registration
Section titled “Definition and registration”Def supplies a decoded input type, name, type_, optional description,
parameters, and input_of_string. For type_ = "function", parameters describe
JSON arguments; custom tools use a format/grammar object and raw input instead.
The decoder may reject input. Registration is not local schema validation or
authorization.
module Echo : Ochat_function.Def with type input = string = struct type input = string let name = "echo" let type_ = "function" let description = Some "Return the supplied text" let parameters = Jsonaf.of_string {|{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false}|} let input_of_string input = Jsonaf.of_string input |> Jsonaf.member_exn "text" |> Jsonaf.string_exnend
let echo = Ochat_function.create_function (module Echo) (fun text -> Openai.Responses.Tool_output.Output.Text text)create_function defaults strict to true and forwards it as provider metadata.
It does not enforce schemas, create confinement, request approval, catch
implementation failures or add retries. The host owns these policies.
Use Core/Eio capabilities in implementations that perform I/O.
Results and dispatch
Section titled “Results and dispatch”Results are Openai.Responses.Tool_output.Output.t, not bare strings:
Text textis ordinary textual output.Content partscarries ordered text/image parts;import_imageuses this.
Ochat_function.functions returns metadata and a table of runners:
let invoke_echo () = let _metadata, dispatch = Ochat_function.functions [ echo ] in let run = Core.Hashtbl.find_exn dispatch "echo" in run ~invocation:Ochat_function.Invocation.silent {|{"text":"Hello"}|}Each runner requires ~invocation; alternatively echo.run input invokes
silently. Names must be unique: duplicates raise at table construction.
Metadata order is not a stable ordering contract for callers.
Progress-capable tools
Section titled “Progress-capable tools”create_streaming_function passes ~invocation to the decoded implementation.
Emit transient progress with Invocation.emit; return exactly one final output.
Progress does not replace that output and must not become canonical history.
let observed_echo = Ochat_function.create_streaming_function (module Echo) (fun ~invocation text -> Ochat_function.Invocation.emit invocation { channel = `Activity; update = Replace "Preparing response" }; Openai.Responses.Tool_output.Output.Text text)Channels are Assistant, Reasoning, Stdout, Stderr, and Activity.
Append text extends a channel; Replace text replaces its latest replaceable
update. Each payload must independently be valid UTF-8. This layer does not
sanitize, redact or bound custom progress: implementations must respect the
host’s disclosure policy before notifying observers.
Use Invocation.create callback to observe progress, Invocation.silent to
discard it, or create_with_trace ~progress ~trace to also observe nested tools.
Callbacks run synchronously, must return promptly, and must be concurrency-safe
if shared across invocations. Observer exceptions are suppressed/logged by the
adapter so they do not change final output. Do not use observer exceptions
as cancellation or authorization signals.
Nested traces
Section titled “Nested traces”Invocation.emit_trace accepts Trace.Tool_started, Tool_progress, and
Tool_finished. Start includes call ID, name, function/custom kind and payload;
finish includes Returned, Raised or Cancelled and optional output. These
are transient display data, not additional authoritative tool results.
Apply the same disclosure policy as for progress. The constructors do not
automatically execute nested tools.
Invocation.is_observed allows skipping expensive display-only work when no
observer is installed. run and silent run_with_progress retain identical
final-result semantics.
Runnable offline example
Section titled “Runnable offline example”The complete example is compiled and executed by the opt-in documentation check. It verifies silent/observed dispatch, progress delivery, structured output, duplicate names and malformed input without a provider key or network request:
dune exec docs-src/examples/tools/custom_tool.exedune build @agent-docs-checkThe check does not execute every historical Markdown snippet. Exact types are in the interface; implementation is here. See also packaged registrations and definition catalog.
View source · Custom OCaml tool3 files
Complete source files. Open a filename to read it here.
custom_tool.mlStart here
open! Core
module Output = Openai.Responses.Tool_output.Output
module Echo : Ochat_function.Def with type input = string = struct
type input = string
let name = "echo"
let type_ = "function"
let description = Some "Return the supplied text"
let parameters =
Jsonaf.of_string
{|{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false}|}
;;
let input_of_string input =
Jsonaf.of_string input |> Jsonaf.member_exn "text" |> Jsonaf.string_exn
;;
end
let echo = Ochat_function.create_function (module Echo) (fun text -> Output.Text text)
let observed_echo =
Ochat_function.create_streaming_function
(module Echo)
(fun ~invocation text ->
Ochat_function.Invocation.emit
invocation
{ channel = `Activity; update = Replace "Preparing response" };
Output.Text text)
;;
let require predicate message = if not predicate then failwith message
let is_text expected = function
| Output.Text actual -> String.equal expected actual
| Content _ -> false
;;
let check_dispatch () =
let metadata, dispatch = Ochat_function.functions [ echo ] in
require (List.length metadata = 1) "missing metadata";
let run = Hashtbl.find_exn dispatch "echo" in
let output = run ~invocation:Ochat_function.Invocation.silent {|{"text":"Hello"}|} in
require (is_text "Hello" output) "incorrect dispatch output";
require
(Result.is_error (Result.try_with (fun () -> echo.run {|{"missing":true}|})))
"malformed arguments were accepted";
require
(Result.is_error
(Result.try_with (fun () -> Ochat_function.functions [ echo; echo ])))
"duplicate tool names were accepted"
;;
let check_progress () =
let updates = ref [] in
let invocation =
Ochat_function.Invocation.create (fun update -> updates := update :: !updates)
in
let output = observed_echo.run_with_progress ~invocation {|{"text":"Hello"}|} in
require (is_text "Hello" output) "progress replaced the final result";
require
(match !updates with
| [ { channel = `Activity; update = Replace "Preparing response" } ] -> true
| _ -> false)
"incorrect progress";
require
(is_text "Hello" (observed_echo.run {|{"text":"Hello"}|}))
"silent output changed";
require (List.length !updates = 1) "silent dispatch emitted observed progress"
;;
let check_content () =
let tool =
Ochat_function.create_function
(module Echo)
(fun text -> Output.Content [ Input_text { text } ])
in
require
(match tool.run {|{"text":"Hello"}|} with
| Output.Content [ Input_text { text = "Hello"; _ } ] -> true
| _ -> false)
"structured output was lost"
;;
let check_trace () =
let traces = ref [] in
let invocation =
Ochat_function.Invocation.create_with_trace ~progress:ignore ~trace:(fun trace ->
traces := trace :: !traces)
in
Ochat_function.Invocation.emit_trace
invocation
(Tool_started { call_id = "child"; name = "echo"; kind = `Function; payload = "{}" });
Ochat_function.Invocation.emit_trace
invocation
(Tool_finished
{ call_id = "child"; outcome = Returned; output = Some (Output.Text "ok") });
require (List.length !traces = 2) "nested trace was lost";
require (Ochat_function.Invocation.is_observed invocation) "observer was not detected";
require
(not (Ochat_function.Invocation.is_observed Ochat_function.Invocation.silent))
"silent invocation reported an observer"
;;
let () =
check_dispatch ();
check_progress ();
check_content ();
check_trace ();
print_endline "Custom-tool documentation example passed (offline)"
;;
dunebuild
(executable
(name custom_tool)
(libraries core jsonaf ochat.ochat_function ochat.openai)
(preprocess (pps ppx_jane)))
LICENSE.txtnotice
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.