toolnexus.content/attach
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §1B · clojure/src/toolnexus/content.cljc
;; There is no ContentPart type. A part is a plain map, and the keys ARE the wire;; keys — :mimeType, not :mime-type, the same rule that puts :isError in;; toolnexus.tool and :input_schema in toolnexus.adapter.{:type "text" :text "What is in this image?"}{:type "image" :mimeType "image/png" :data "iVBORw0KGgo…"} ; base64, standard + padded{:type "image" :mimeType "image/webp" :url "https://…/diagram.webp"}{:type "file" :mimeType "application/pdf" :data "JVBERi0x…" :name "report.pdf"}{:type "audio" :mimeType "audio/wav" :data "UklGRi…"};; exactly one of :data / :url — both, or neither, is a construction error
;; the edge constructors — the path and the raw bytes never enter the part(toolnexus.content/attach source) ; path | data: URL | http(s) URL | bytes(toolnexus.content/attach source opts) ; opts = {:mimeType "…" :name "…" :max-part-bytes n}(toolnexus.content/from-file path) ; read now, base64 now; mime from the fixed table(toolnexus.content/image-file path) ; from-file, pinned to an "image" part(toolnexus.content/from-bytes bs mime) ; byte array | seq of byte values | string (UTF-8)(toolnexus.content/from-data-url u) ; data:<mime>;base64,<b64> -> {:mimeType :data}(toolnexus.content/from-url u) ; http(s) -> {:url …}(toolnexus.content/text-part s)
;; a failure is a VALUE with a stable :code — never a throw{:type "error" :code "unsupported-source" :error "unsupported byte source: …"}
;; reading a part back(toolnexus.content/validate-part part) ; part, or an error part(toolnexus.content/validate-part part max-bytes)(toolnexus.content/error-part? x)(toolnexus.content/part-bytes part) ; DECODED bytes, never the +33% base64(toolnexus.content/describe-part part) ; {:type :mimeType :bytes} — never :data(toolnexus.content/summarize-part part) ; "image (image/png, 82 bytes)"The non-text half of a message: text | image | file | audio, carrying base64 bytes or a URL plus
a mimeType — never a path. One union covers all three directions: a part you attach to a prompt,
a part a tool hands back under :parts, and a part that arrives
from an MCP server.
When to use it
Section titled “When to use it”- Attaching an image, a PDF or an audio clip to a run. A prompt is a string or a vector of
parts:
(client/run c [(content/text-part "What changed?") (content/attach "shot.png")] {…}). The vector is the canonical transcript shape; the provider blocks are built on the way out, per style, bytoolnexus.content/build-wire. - Returning one from a tool. A screenshot tool sets
:outputto a description and the image goes in:partsviatoolnexus.tool/with-parts. - Reading one back —
part-bytesfor size,summarize-partfor the one-line, payload-free rendering §1B pins byte-identically across all seven ports.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. A text part and an image part
Section titled “1. A text part and an image part”(require '[toolnexus.content :as content] '[toolnexus.client :as client] '[clojure.string :as str] '[koine.fs :as fs])
;; The shared cross-language fixture lives at the repo root. Walk up to it so the;; example runs from wherever you happen to be 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"));; the COMMITTED golden — trimmed, because a text file ends in a newline and base64 does not(def golden (str/trim (str (fs/read-file (find-up "examples/media/fixture.png.base64")))))
(def prompt [(content/text-part "What is in this image?") (content/attach png)])
(def img (second prompt))(println "part:" (content/summarize-part img))
(assert (= "image" (:type img)))(assert (= "image/png" (:mimeType img)))(assert (= golden (:data img))) ; base64d at construction, byte-for-byte the golden(assert (nil? (:url img))) ; exactly one of :data / :url(assert (not (str/includes? (pr-str img) "fixture.png"))) ; the PATH is gone
;; §1B pins this string byte-identically in all seven ports; <bytes> is DECODED(assert (= "image (image/png, 82 bytes)" (content/summarize-part img)))(assert (= 82 (content/part-bytes img)));; a log line or a §9 event renders a part WITHOUT its payload(assert (= {:type "image" :mimeType "image/png" :bytes 82} (content/describe-part img)))
;; that vector is a prompt — this is the shape client/run turns it into(def msg (client/user-message prompt))(assert (= "user" (:role msg)))(assert (content/part-array? (:content msg)))
(println "OK")2. Every source this port accepts folds to the same bytes
Section titled “2. Every source this port accepts folds to the same bytes”(require '[toolnexus.content :as content] '[toolnexus.tool :as tool] '[clojure.string :as str] '[koine.fs :as fs])
(defn find-up [rel] (->> (iterate (fn [p] (str "../" p)) rel) (take 8) (filter fs/exists?) first))
(def png (find-up "examples/media/fixture.png"))(def golden (str/trim (str (fs/read-file (find-up "examples/media/fixture.png.base64")))))
;; 1. a path — read now, base64 now(def from-path (content/attach png))
;; 2. a native byte array: JVM byte[] on one host, Go []byte on the other, and;; exactly what koine.fs/read-bytes hands back on both.(def raw (fs/read-bytes png))(def from-array (content/from-bytes raw "image/png"))
;; 3. a SEQ of byte values — the shape ordinary Clojure code produces. Signed;; (straight off a JVM array) and unsigned (0-255) fold to the same base64.(def signed (vec (seq raw)))(def unsigned (map (fn [b] (bit-and (long b) 255)) (seq raw)))(def from-vec (content/from-bytes signed "image/png"))(def from-lazy (content/from-bytes unsigned "image/png"))
;; 4. a data: URL — parsed into {:mimeType :data}, NEVER kept as a :url(def from-data-url (content/attach (str "data:image/png;base64," golden)))
(assert (= golden (:data from-path) (:data from-array) (:data from-vec) (:data from-lazy) (:data from-data-url)))(assert (= "image/png" (:mimeType from-data-url)))(assert (nil? (:url from-data-url)))
;; An http(s) URL is the one source kept AS a url — nothing is fetched here.(def remote (content/from-url "https://example.com/diagram.webp"))(assert (= {:type "image" :mimeType "image/webp" :url "https://example.com/diagram.webp"} remote))(assert (= 0 (content/part-bytes remote))) ; a url part carries no bytes
;; The table drives the part KIND, not just the mime: pdf is a "file", wav is "audio".(assert (= "file" (:type (content/from-url "https://example.com/spec.pdf"))))(assert (= "audio" (:type (content/from-url "https://example.com/clip.wav"))))
;; Returning one from a tool: :output stays the text every transcript sees.(def shot (tool/tool {:name "screenshot" :description "Take a screenshot" :execute (fn [_] (let [p (content/image-file png)] (tool/with-parts (tool/success (content/summarize-part p)) [p])))}))
(def r ((:execute shot) {}))(println "tool output:" (:output r))(assert (= "image (image/png, 82 bytes)" (:output r)))(assert (= [from-path] (:parts r)))
(println "OK")3. Every failure is a value with a name
Section titled “3. Every failure is a value with a name”(require '[toolnexus.content :as content] '[clojure.string :as str])
;; --- both :data and :url, and neither -------------------------------------(def conflict (content/validate-part {:type "image" :mimeType "image/png" :data "AQ==" :url "https://x/y.png"}))(println "conflict =>" (:code conflict) "-" (:error conflict))(assert (content/error-part? conflict))(assert (= "source-conflict" (:code conflict)))
(assert (= "source-missing" (:code (content/validate-part {:type "image" :mimeType "image/png"}))))
;; --- an unknown extension is refused BY NAME, never sniffed ----------------(def unknown (content/from-file "notes.xyz"))(println "unknown =>" (:code unknown) "-" (:error unknown))(assert (= "unknown-extension" (:code unknown)))(assert (str/includes? (:error unknown) "xyz"));; …and an explicit :mimeType is the way through, exactly as the message says(assert (nil? (:code (content/from-url "https://x/y.bin" {:mimeType "application/octet-stream"}))))
;; --- a handle-shaped source: this port says so instead of pretending -------(def handle (content/from-bytes {:a :handle} "image/png"))(println "handle =>" (:code handle))(assert (= "unsupported-source" (:code handle)))(assert (str/includes? (:error handle) "java.io.File")) ; names what you are likely holding(assert (str/includes? (:error handle) "koine.fs/read-bytes")) ; and names the way out;; the SOURCE is reported before a missing mime — a mime would not have fixed it(assert (= "unsupported-source" (:code (content/from-bytes {:a :handle} ""))));; attach refuses it identically rather than throwing(assert (= "unsupported-source" (:code (content/attach {:a :handle} {:mimeType "image/png"}))))
;; --- :max-part-bytes — DECODED bytes, enforced at request assembly ---------(def big (content/from-bytes (repeat 4096 65) "image/png"))(assert (= 4096 (content/part-bytes big))) ; the base64 is 5464 chars; the limit is not that
;; A part the CALLER attached that busts the limit stops the run before any HTTP call.(def stopped (content/encode-parts [big] {:style "openai" :provenance "attached" :max-part-bytes 1024}))(println "assembly =>" (:code stopped) "-" (:error stopped))(assert (= "too-large" (:code stopped)));; That :error / :code pair is what client/run reports as;; {:status "incomplete" :limit "contentPart"} — a stopped RunResult, not an exception.
;; A part DERIVED from a tool or MCP result degrades to a placeholder and warns;; once instead, so a remote server cannot fail a run you never asked to risk.(content/reset-unsupported-warnings!)(def degraded (content/encode-parts [big] {:style "openai" :provenance "derived" :max-part-bytes 1024}))(assert (nil? (:error degraded)))(assert (= [{:type "text" :text "[unsupported image part (image/png, 4096 bytes)]"}] (:blocks degraded)))
;; Under the limit, it is an ordinary image block.(def fine (content/encode-parts [big] {:style "openai" :provenance "attached" :max-part-bytes 8192}))(assert (= "image_url" (:type (first (:blocks fine)))))
;; Nothing above threw. That is the contract, not a coincidence.(println "OK")See also
Section titled “See also”toolnexus.tool/success— the result envelope whose:partscarries thesetoolnexus.tool/tool— The uniform shape every tool source collapses to: name, description, JSON-Schema parameters, execute.toolnexus.builtin/builtin-tools— thereadbuiltin returns a part for a known media file