Skip to content
ochat
Search documentation

Use quotes for an exact phrase.

Search by topic, command, or code identifier.

    GitHub ↗

    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.

    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_exn
    end
    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 are Openai.Responses.Tool_output.Output.t, not bare strings:

    • Text text is ordinary textual output.
    • Content parts carries ordered text/image parts; import_image uses 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.

    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.

    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.

    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:

    Terminal window
    dune exec docs-src/examples/tools/custom_tool.exe
    dune build @agent-docs-check

    The 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)"
    ;;
    

    Link to this fileDownload this file

    dunebuild
    (executable
     (name custom_tool)
     (libraries core jsonaf ochat.ochat_function ochat.openai)
     (preprocess (pps ppx_jane)))
    

    Link to this fileDownload this file

    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.

    Link to this fileDownload this file