CookbookTool loop in Java

Tool loop in Java

The model can call two functions, find_book and availability, as many times as it needs; the loop ends when it answers in text. It uses plain HTTP, so there is the only dependency is Jackson (com.fasterxml.jackson.core:jackson-databind) for JSON.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
 
public class ToolLoop {
    static final ObjectMapper M = new ObjectMapper();
    static final HttpClient HTTP = HttpClient.newHttpClient();
    record Book(String title, int copies, int onLoan, String dueBack) {}
    static final Map<String, Book> LIBRARY = Map.of(
        "978-0141439518", new Book("Pride and Prejudice", 3, 3, "2026-10-19"),
        "978-0007119318", new Book("The Hobbit", 2, 1, null));
 
    static ObjectNode runTool(String name, String rawArgs) {
        ObjectNode out = M.createObjectNode();
        try {
            JsonNode args = M.readTree(rawArgs);
            switch (name) {
                case "find_book" -> {
                    ArrayNode hits = out.putArray("results");
                    String q = args.path("query").asText().toLowerCase();
                    LIBRARY.forEach((isbn, b) -> {
                        if (b.title().toLowerCase().contains(q)) hits.addObject().put("isbn", isbn).put("title", b.title());
                    });
                }
                case "availability" -> {
                    Book b = LIBRARY.get(args.path("isbn").asText());
                    if (b == null) return out.put("error", "no book with that ISBN");
                    int free = b.copies() - b.onLoan();
                    out.put("available", free);
                    out.put("next_due_back", free == 0 ? b.dueBack() : null);
                }
                default -> out.put("error", "unknown tool " + name);
            }
        } catch (Exception e) {
            out.put("error", e.getMessage());
        }
        return out;
    }
 
    public static void main(String[] args) throws Exception {
        JsonNode tools = M.readTree("""
            [
              {"type": "function", "function": {"name": "find_book", "description": "Search the catalogue by title words. Returns ISBNs.",
                "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}},
              {"type": "function", "function": {"name": "availability", "description": "Copies available now for an ISBN, and when the next one is due back.",
                "parameters": {"type": "object", "properties": {"isbn": {"type": "string"}}, "required": ["isbn"]}}}
            ]
            """);
        ArrayNode messages = M.createArrayNode();
        messages.addObject().put("role", "system").put("content", "You help members of a town library. Use the tools; never guess availability.");
        messages.addObject().put("role", "user").put("content", "Can I borrow Pride and Prejudice today? If not, when?");
 
        for (int round = 0; round < 8; round++) {
            ObjectNode body = M.createObjectNode().put("model", "Amara S1.2").put("stream", false).put("max_tokens", 800);
            body.set("tools", tools);
            body.set("messages", messages);
            HttpRequest req = HttpRequest.newBuilder(URI.create("https://app.syntic.ai/v1/chat/completions"))
                .header("Authorization", "Bearer " + System.getenv("SYNTIC_API_KEY"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body))).build();
            HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
            JsonNode json = M.readTree(res.body());
            if (res.statusCode() >= 300) throw new RuntimeException(json.path("error").toString());
 
            JsonNode msg = json.path("choices").get(0).path("message");
            if (!msg.has("tool_calls") || msg.path("tool_calls").isEmpty()) {
                System.out.println(msg.path("content").asText());
                return;
            }
            messages.add(msg);
            for (JsonNode call : msg.path("tool_calls")) {
                ObjectNode result = runTool(call.path("function").path("name").asText(), call.path("function").path("arguments").asText("{}"));
                messages.addObject().put("role", "tool").put("tool_call_id", call.path("id").asText()).put("content", M.writeValueAsString(result));
            }
        }
        System.out.println("Sorry, I couldn't finish that request.");
    }
}

The three rules

  1. Append the assistant message with its tool_calls exactly as received.
  2. Add one tool message per call, with the matching tool_call_id.
  3. Send the same tools list on every round.

Errors from your functions are returned to the model as results so it can recover, and the round limit stops a runaway loop.

Was this page helpful?