Skip to content

ofFile

Java · package io.github.muthuishere:toolnexus · SPEC §1B · ContentPart.java

// Edge constructors — read now, base64 now, store no handle.
public static ContentPart ofFile(Path path); // + (Path, String mimeType)
public static ContentPart ofFile(java.io.File file); // + (File, String mimeType)
public static ContentPart ofStream(InputStream in, String mimeType);
public static ContentPart ofStream(InputStream in, String name, String mimeType);
public static ContentPart ofBytes(String type, String mimeType, byte[] bytes);
public static ContentPart ofBase64(String type, String mimeType, String base64);
public static ContentPart ofUrl(String type, String url); // + (type, mimeType, url)
// Checked siblings, for callers who want IOException on the signature.
public static ContentPart ofFileChecked(Path path) throws IOException; // + File, + mimeType
public static ContentPart ofStreamChecked(InputStream in, String mimeType) throws IOException;
// Named shorthands, from raw bytes.
public static ContentPart image(String mimeType, byte[] bytes);
public static ContentPart audio(String mimeType, byte[] bytes);
public static ContentPart file(String mimeType, byte[] bytes, String name);
public long bytes(); // DECODED byte length — what .text() is to a text part
// LlmClient.Options — what happens when a style can't represent a part
public Options onUnsupportedPart(String v); // "error" | "text" | null (by provenance)

The authoring side of multimodal content: constructors that turn a path, bytes, a blob (an InputStream), a data: URL, or a remote https: URL into the ContentPart a prompt or tool result carries — the write half of the read-only ContentPart shape. Every edge constructor accepts broadly and stores narrowly: ofFile/ofStream read the source eagerly, right now, and the resulting part holds only mimeType + base64 data — never a Path, a File, or an open stream. A path or a half-read stream does not survive a persisted transcript, a subagent handoff, or an A2A boundary; base64 bytes do.

Mime type comes from a fixed extension table (png jpg jpeg gif webp pdf mp3 wav) — never sniffed from content, never read from a platform mime database (/etc/mime.types varies per machine, which would break cross-port parity). An extension outside that table is an IllegalArgumentException naming the extension; the escape hatch is passing an explicit mimeType, never silent application/octet-stream.

There are always two spellings of each reader: ofFile/ofStream throw UncheckedIOException (so a literal-argument call like ContentPart.ofFile(Path.of("shot.png")) doesn’t force a try/catch, matching the precedent Files.lines/Files.walk already set), and ofFileChecked/ofStreamChecked are the identical calls with throws IOException on the signature, for code that wants the checked form.

Two places, and they are the same ContentPart type on both ends:

  • Attaching an image, a PDF, or an audio clip to a run — LlmClient takes a List<ContentPart> wherever it takes a String prompt (run, stream, ask, send). Reach for ofFile/ofFile(File) for something already on disk, ofStream for an in-memory or network source you’re already holding as a stream, ofBytes/image/audio/file for raw byte[] you already have, and ofUrl for a remote asset the provider itself should fetch.
  • Returning one from a tool — ToolResult has a parts list, so a screenshot tool hands back the image itself rather than a description of it.

Reach for Options.onUnsupportedPart on the client side when a provider style (e.g. one that can’t represent audio) meets a part it cannot send: leave it null for the default provenance-based behaviour, set "error" to always fail fast at request assembly, or "text" to always degrade to a placeholder instead of ever throwing.

1. The smallest useful call — a file path becomes an attachable ContentPart

Section titled “1. The smallest useful call — a file path becomes an attachable ContentPart”
import io.github.muthuishere.toolnexus.ContentPart;
import java.nio.file.Path;
import java.util.List;
public class Example {
public static void main(String[] args) {
Path fixture = Path.of("examples/media/fixture.png");
// This is the shape LlmClient.run(List<ContentPart>, toolkit) takes.
List<ContentPart> prompt = List.of(
ContentPart.text("What colour is the top-left quadrant?"),
ContentPart.ofFile(fixture) // read now, base64 now — the part holds no path
);
ContentPart image = prompt.get(1);
if (!image.type().equals("image")) throw new AssertionError(image.type());
if (!image.mimeType().equals("image/png")) throw new AssertionError(image.mimeType());
if (image.bytes() <= 0) throw new AssertionError("expected decoded bytes: " + image.bytes());
System.out.println("ok: " + image.describe());
}
}

2. The realistic case — a tool returns a screenshot it captured in memory

Section titled “2. The realistic case — a tool returns a screenshot it captured in memory”

A tool that never touches the filesystem hands back the bytes it already has, via the raw-byte[] shorthand, rather than round-tripping through a temp file just to call ofFile.

import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
static byte[] fakeScreenshotBytes() {
return new byte[]{(byte) 0x89, 'P', 'N', 'G'}; // stands in for a captured PNG
}
public static void main(String[] args) {
Tool screenshot = new Tool() {
@Override public String name() { return "screenshot"; }
@Override public String description() { return "Capture the current screen."; }
@Override public Map<String, Object> inputSchema() {
return Map.of("type", "object", "properties", Map.of());
}
@Override public String source() { return "custom"; }
@Override public ToolResult execute(Map<String, Object> args, ToolContext ctx) {
ContentPart shot = ContentPart.image("image/png", fakeScreenshotBytes());
return new ToolResult("screenshot captured", false, null, List.of(shot));
}
};
ToolResult result = screenshot.execute(Map.of(), null);
if (result.isError()) throw new AssertionError(result);
if (result.parts() == null || result.parts().size() != 1) throw new AssertionError(result.parts());
if (!result.parts().get(0).type().equals("image")) throw new AssertionError(result.parts());
System.out.println("ok: tool returned " + result.parts().get(0).describe());
}
}

3. The full surface — every source normalises the same, and unknown extensions are refused

Section titled “3. The full surface — every source normalises the same, and unknown extensions are refused”
import io.github.muthuishere.toolnexus.ContentPart;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class Example {
static String rejects(Runnable r) {
try {
r.run();
} catch (IllegalArgumentException e) {
return e.getMessage();
}
throw new AssertionError("expected a rejection");
}
public static void main(String[] args) throws Exception {
Path fixture = Path.of("examples/media/fixture.png");
byte[] bytes = Files.readAllBytes(fixture);
ContentPart fromPath = ContentPart.ofFile(fixture);
ContentPart fromBytes = ContentPart.image("image/png", bytes);
ContentPart fromStream = ContentPart.ofStream(new ByteArrayInputStream(bytes), "image/png");
ContentPart fromDataUrl = ContentPart.ofUrl("image",
"data:image/png;base64," + java.util.Base64.getEncoder().encodeToString(bytes));
ContentPart fromUrl = ContentPart.ofUrl("image", "image/png", "https://example.com/a.png");
// Four in-process sources normalise to the identical bytes; the fifth stays a url.
for (ContentPart p : java.util.List.of(fromPath, fromBytes, fromStream, fromDataUrl)) {
if (!p.data().equals(fromPath.data())) throw new AssertionError("differs: " + p);
if (p.url() != null) throw new AssertionError("data and url are exclusive");
}
if (fromUrl.data() != null) throw new AssertionError("a remote https: URL keeps url(), not data()");
if (!fromUrl.url().equals("https://example.com/a.png")) throw new AssertionError(fromUrl.url());
// An unknown extension is refused BY NAME — never a silent application/octet-stream.
// (A real file, so the with-mimeType call below has bytes to read.)
Path notes = Files.createTempFile("notes", ".xyz");
Files.writeString(notes, "just some notes");
try {
String unknown = rejects(() -> ContentPart.ofFile(notes));
if (!unknown.contains("\"xyz\"")) throw new AssertionError(unknown);
// The escape hatch is an explicit mimeType, not sniffing:
ContentPart withMime = ContentPart.ofFile(notes, "text/plain");
if (!withMime.mimeType().equals("text/plain")) throw new AssertionError(withMime.mimeType());
} finally {
Files.deleteIfExists(notes);
}
System.out.println("ok: 4 in-process sources identical, url kept as url, unknown ext named");
}
}
  • ContentPart — The read-only shape these constructors produce: text | image | file | audio, carrying base64 bytes or a URL plus a mimeType.
  • Tool — The uniform shape every tool source collapses to: name, description, JSON-Schema parameters, execute.
  • ToolResult — The result envelope: output text, optional error flag, optional non-text parts, and optional metadata that can carry a suspension.
  • ToolContext — Optional per-call context handed to execute: cancellation, identity, and host-supplied state.