Skip to content

Tools.fromObject

Java · package io.github.muthuishere:toolnexus · SPEC §6 · Tools.java

public static List<Tool> fromObject(Object target)

Scans every public method on target’s class for the @ToolMethod annotation and turns each match into a Tool (source "native") — the object itself supplies the method bodies, fromObject supplies the discovery + schema inference + Tool wrapping.

When you have a plain object — a service, a small “tools” class — with one or more @ToolMethod-annotated methods, and want every one of them as Tools in a single call, rather than wrapping each with NativeTool.of by hand.

Toolkit.Options.annotatedObjects calls fromObject on every object you add there internally — use fromObject directly when you want the List<Tool> itself (to register selectively, filter, or hand to an adapter) rather than a fully assembled Toolkit.

1. Sweeping a class with two annotated methods

Section titled “1. Sweeping a class with two annotated methods”
import io.github.muthuishere.toolnexus.*;
import io.github.muthuishere.toolnexus.annotations.Param;
import io.github.muthuishere.toolnexus.annotations.ToolMethod;
import java.util.List;
import java.util.Map;
public class Example {
static final class MathTools {
@ToolMethod(name = "add", description = "Add two numbers")
public String add(@Param(name = "a") double a, @Param(name = "b") double b) {
return String.valueOf(a + b);
}
@ToolMethod(name = "negate", description = "Negate a number")
public String negate(@Param(name = "a") double a) {
return String.valueOf(-a);
}
// Not annotated — never collected.
public String helper() {
return "not a tool";
}
}
public static void main(String[] args) {
List<Tool> tools = Tools.fromObject(new MathTools());
if (tools.size() != 2) throw new AssertionError("expected 2, got " + tools.size());
List<String> names = tools.stream().map(Tool::name).sorted().toList();
if (!names.equals(List.of("add", "negate"))) throw new AssertionError(names);
System.out.println("ok: " + names);
}
}

2. Instance state — the object is captured, not just its class

Section titled “2. Instance state — the object is captured, not just its class”

fromObject binds to a specific instance: methods run against that instance’s fields, so two objects of the same class produce independent tools.

import io.github.muthuishere.toolnexus.*;
import io.github.muthuishere.toolnexus.annotations.Param;
import io.github.muthuishere.toolnexus.annotations.ToolMethod;
import java.util.List;
import java.util.Map;
public class Example {
static final class Counter {
private int count;
@ToolMethod(name = "increment", description = "Increment and return the counter")
public String increment(@Param(name = "by", required = false) Integer by) {
count += (by == null ? 1 : by);
return String.valueOf(count);
}
}
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
Tool toolA = Tools.fromObject(a).get(0);
Tool toolB = Tools.fromObject(b).get(0);
toolA.execute(Map.of(), new ToolContext());
toolA.execute(Map.of(), new ToolContext());
ToolResult resA = toolA.execute(Map.of("by", 5), new ToolContext());
ToolResult resB = toolB.execute(Map.of(), new ToolContext());
if (!resA.output().equals("7")) throw new AssertionError(resA.output()); // 1 + 1 + 5
if (!resB.output().equals("1")) throw new AssertionError(resB.output()); // independent state
System.out.println("ok: a=" + resA.output() + " b=" + resB.output());
}
}

3. Registering collected tools alongside every other source

Section titled “3. Registering collected tools alongside every other source”

The result is an ordinary List<Tool> — mix it into a Toolkit via register, exactly like a tool from any other source.

import io.github.muthuishere.toolnexus.*;
import io.github.muthuishere.toolnexus.annotations.Param;
import io.github.muthuishere.toolnexus.annotations.ToolMethod;
import java.util.List;
import java.util.Map;
public class Example {
static final class TimeTools {
@ToolMethod(name = "epoch_zero", description = "Return the fixed epoch, for a deterministic doc example")
public String epochZero() {
return "1970-01-01T00:00:00Z";
}
}
public static void main(String[] args) {
try (Toolkit tk = Toolkit.create(new Toolkit.Options())) {
for (Tool t : Tools.fromObject(new TimeTools())) {
tk.register(t);
}
Tool found = tk.get("epoch_zero");
if (found == null) throw new AssertionError("not registered");
if (!found.source().equals("native")) throw new AssertionError(found.source());
ToolResult res = found.execute(Map.of(), new ToolContext());
if (!res.output().equals("1970-01-01T00:00:00Z")) throw new AssertionError(res.output());
System.out.println("ok: " + found.name() + " -> " + res.output());
}
}
}
Member Type What it is
fromObject(target) List<Tool> One Tool per @ToolMethod-annotated public method on target’s class, in declaration order.
Unannotated methods Silently skipped — never collected, never an error.
Binding Each returned Tool.execute invokes the method against the same target instance passed in.
  • @ToolMethod annotation — what marks a method as collectible, and how its schema is inferred.
  • NativeTool.of — build a single Tool by hand when you don’t want annotation-driven inference.
  • Toolkit.createannotatedObjects(...) calls fromObject for you.