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 belowThe 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.
When to use it
Section titled “When to use 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;attachis 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-bytesdirectly 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-urlkeeps it as a:urlpart. - Not for turning a string into its own UTF-8 bytes —
attachon a bare string always reads it as either adata:/http(s):URL or a filesystem path, never as text-to-encode. Callfrom-bytesdirectly for that, sinceattachcannot tell a payload from a filename apart.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”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.
See also
Section titled “See also”toolnexus.content/attach— The non-text half of a message:text | image | file | audio, carrying base64 bytes or a URL plus amimeType— 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:partscarries any non-textContentParta tool hands back, built the same way as a prompt-side attachment.