Skip to content

attach

Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §1B · clojure/src/toolnexus/content.cljc

(require '[toolnexus.content :as content])
;; the edge: dispatches on the SHAPE of what you hand it
(content/attach source) ; a data: URL | an http(s) URL | else: a filesystem path
(content/attach source opts) ; opts = {:mimeType "…" :name "…" :max-part-bytes n}
;; the named constructors attach dispatches to internally
(content/from-file path) ; read now, base64 now; mime from the fixed extension table
(content/from-file path opts)
(content/image-file path) ; from-file, but errors if the result is not an "image" part
(content/from-bytes bs mime) ; byte array | seq of byte values | string (its UTF-8 bytes)
(content/from-bytes bs mime opts)
(content/from-data-url u) ; "data:<mime>;base64,<b64>" -> {:mimeType … :data …}
(content/from-url u) ; "http(s)://…" -> {:mimeType … :url …}
(content/text-part s) ; {:type "text" :text s} — not attach; there is no non-text part for text
;; a failure is a VALUE, never a throw
{:type "error" :code "unsupported-source" :error "unsupported byte source: …"}
;; on the CLIENT: what to do when a part's style can't represent it
(client/create-client {:on-unsupported-part "error"}) ; or "text" — see below

The authoring side of multimodal content: constructors that turn a path, bytes, a blob, a data URL, or a remote URL into the ContentPart a prompt or tool result carries — the write half of the read-only ContentPart shape that page documents in full. This page is the dedicated reference for the constructors themselves — attach and its five named siblings — while content-part.mdx covers the shape every one of them produces.

attach never carries the path or the raw bytes forward into the part — a path is read and base64’d now, at construction, so the resulting part survives a persisted transcript, an MCP hop, or the process boundary of a durable resume, none of which can dereference a filesystem path that made sense only on the machine that built it.

  • Attaching a file, image or audio clip to a prompt — (client/run c [(content/text-part "…") (content/attach "diagram.png")] {…}). A prompt is a string or a vector of parts; attach is how you build the non-text elements of that vector.
  • You already have bytes in hand (read from a database blob, downloaded, generated) rather than a path on disk — use from-bytes directly with an explicit mime type, since bytes carry no extension to read one from.
  • You have a remote URL and want the provider to fetch it rather than downloading and re-uploading the bytes yourself — from-url keeps it as a :url part.
  • Not for turning a string into its own UTF-8 bytes — attach on a bare string always reads it as either a data:/http(s): URL or a filesystem path, never as text-to-encode. Call from-bytes directly for that, since attach cannot tell a payload from a filename apart.

1. The smallest useful call — attach a local file to a prompt

Section titled “1. The smallest useful call — attach a local file to a prompt”
(require '[toolnexus.client :as client]
'[toolnexus.content :as content]
'[toolnexus.tool :as tool]
'[koine.fs :as fs]
'[koine.json :as json])
(println)
;; the shared cross-language fixture lives at the repo root; walk up to it so
;; the example runs from wherever it happens to sit inside the tree
(defn find-up [rel]
(->> (iterate (fn [p] (str "../" p)) rel) (take 8) (filter fs/exists?) first))
(def png (find-up "examples/media/fixture.png"))
(defn- text-response [t]
{:choices [{:message {:role "assistant" :content t}}]
:usage {:prompt_tokens 10 :completion_tokens 5 :total_tokens 15}})
(def llm
(client/create-client
{:base-url "http://mock.local" :style "openai" :model "gpt-4o-mini"
:http-client (fn [_url _headers _body]
{:status 200 :headers {} :body (json/write-str (text-response "a diagram"))})}))
(def rr
(client/run llm
[(content/text-part "What is in this image?")
(content/attach png)]
{:toolkit (tool/toolkit [])}))
(assert (= "a diagram" (:text rr)))
(println "OK")

attach sees a string that is neither a data: nor an http(s): URL, so it reads the file off disk now, base64-encodes it, and looks its mime type up in the fixed extension table (image/png) — the path itself never reaches the resulting part.

2. The realistic case — bytes already in hand, from a database or a generated buffer

Section titled “2. The realistic case — bytes already in hand, from a database or a generated buffer”
(require '[toolnexus.content :as content])
(println)
(defn attach-generated-chart [png-bytes]
;; bytes carry no extension, so the mime type is required
(content/from-bytes png-bytes "image/png"))
;; a byte array, a seq of byte values, or a string are all accepted, and a
;; byte array and its own seq encode identically:
(def p1 (content/from-bytes (byte-array [137 80 78 71]) "image/png"))
(def p2 (content/from-bytes (seq (byte-array [137 80 78 71])) "image/png"))
(def p3 (content/from-bytes "raw text payload" "text/plain")) ; taken as its UTF-8 bytes
(assert (= "image" (:type p1)))
(assert (= (:data p1) (:data p2)))
(assert (= "text/plain" (:mimeType p3)))
(assert (= "image" (:type (attach-generated-chart (byte-array [137 80 78 71])))))
(println "OK")

3. The full surface — every constructor, plus the unsupported-part policy on the client

Section titled “3. The full surface — every constructor, plus the unsupported-part policy on the client”
(require '[toolnexus.client :as client]
'[toolnexus.content :as content]
'[koine.fs :as fs])
(println)
(defn find-up [rel]
(->> (iterate (fn [p] (str "../" p)) rel) (take 8) (filter fs/exists?) first))
(def png (find-up "examples/media/fixture.png"))
;; every path attach can take, named explicitly
(def f1 (content/from-file png)) ; read + base64'd now
(def f2 (content/image-file png)) ; from-file, but errors if not an image
(def f3 (content/from-data-url "data:image/png;base64,iVBORw0KGgo="))
(def f4 (content/from-url "https://example.com/diagram.webp")) ; kept as :url, never downloaded here
(def f5 (content/attach {:a 1} {:mimeType "image/png"})) ; unsupported shape -> error part, no throw
(assert (= "image" (:type f1)))
(assert (= "image" (:type f2)))
(assert (= "image/png" (:mimeType f3)))
(assert (= "https://example.com/diagram.webp" (:url f4)))
(assert (= "error" (:type f5)))
(assert (= "unsupported-source" (:code f5)))
;; a client that DECLARES how to handle a part its style cannot send at all
(def llm
(client/create-client
{:base-url "https://api.openai.com/v1"
:style "openai"
:model "gpt-4.1"
:max-part-bytes 5000000 ; §1B — reject a part over this many DECODED bytes
:on-unsupported-part "text"})) ; degrade to a text placeholder + warn once, instead of stopping the run
(assert (= "text" (:on-unsupported-part llm)))
(println "OK")

:on-unsupported-part governs the provenance rule uniformly: unset, an attached part the style genuinely cannot represent (say, an OpenAI Chat Completions file part with a :url rather than inline data) stops the run; a tool-derived part degrades to a text placeholder and warns once either way. Setting "error" or "text" overrides that default in both directions rather than leaving the provenance-dependent behavior in place.

  • toolnexus.content/attach — The non-text half of a message: text | image | file | audio, carrying base64 bytes or a URL plus a mimeType — never a path. Read this first for the full union-type shape these constructors build.
  • toolnexus.tool/tool — The uniform shape every tool source collapses to: name, description, JSON-Schema parameters, execute.
  • toolnexus.tool/success — The result envelope a tool returns; its :parts carries any non-text ContentPart a tool hands back, built the same way as a prompt-side attachment.