AgentScope Agent Oriented Framework

AgentScope is an agent oriented programming framework for building LLM powered applications that has components for ReAct reasoning, tool calling, memory management, and multi agent collaboration.

Here we only write Clojure examples using a subset of the Java implementation of AgentScope. For reference this is the home web page for Agentscope.

We develop two parallel implementations of simple text generation and tool use examples in this chapter:

  • Using the local model nemotron-3-nano:4b running with a local Ollama server. Source code: Clojure-AI-Book/source-code/AgentScope_ollama.
  • Using the Google Gemini model gemini-3-flash-preview. Source code: Clojure-AI-Book/source-code/AgentScope_gemini.

These implementations are similar and could have been generalized into a single code base. To reduce the lines of code listings here, we will first look at the Gemini implementation of a simple text generation example and look at the local Ollama implementation of a multiple tool use example. You can read through the Gemini tool use and the Ollama simple generative text examples in the GitHub repository.

In case this is confusing, here are the parallel files:

 1 source-code $ tree AgentScope_gemini/src AgentScope_ollama/src
 2 AgentScope_gemini/src
 3 └── agentscope
 4     ├── main.clj
 5     └── tool_use.clj
 6 AgentScope_ollama/src
 7 └── agentscope
 8     ├── main.clj
 9     └── tool_use.clj
10 
11 4 directories, 4 files

Overview of AgentScope

AgentScope is a developer friendly and production ready framework for building LLM powered agent applications. While the original AgentScope SDK is written in Python, it also provides a Java implementation that we can call directly from Clojure via Java interop. The key abstractions in AgentScope are:

  • ReActAgent — an agent that implements the ReAct (Reason + Act) loop. Given a user message, the agent reasons about what to do, optionally calls tools, observes the results, and continues reasoning until it can produce a final answer. This is the core agent type we use in both examples.
  • Model — a pluggable chat model interface. AgentScope ships with built-in model implementations including the two we use here: GeminiChatModel (for Google Gemini) and OllamaChatModel (for any model served by a local Ollama instance). You construct a model using its builder, then hand it to an agent.
  • Msg — the message abstraction. You build a Msg with a text content (and optionally images or other modalities), pass it to the agent via .call(), and receive a response Msg back. The .block() call unwraps the reactive (Project Reactor Mono) return value into a synchronous result.
  • AgentTool and Toolkit — the tool-use subsystem. Each tool implements the AgentTool interface with four methods: getName, getDescription, getParameters (a JSON-schema map), and callAsync (which returns a Reactor Mono<ToolResultBlock>). Tools are registered into a Toolkit, which is then attached to a ReActAgent. When the LLM decides it needs a tool, the agent framework handles the function-call lifecycle automatically.

The beauty of this design is that tool definitions live entirely in Clojure — we use reify to implement the AgentTool interface inline, with no companion Java classes or annotation processing required. The LLM sees the tool names, descriptions, and parameter schemas; the agent runtime dispatches to our Clojure functions when the LLM requests a tool call.

For more information on AgentScope, see the AgentScope documentation and the AgentScope GitHub repository.

Generating Completions With AgentScope: Gemini Example

Our first example demonstrates the simplest use case: creating a GeminiChatModel, wrapping it in a ReActAgent, and sending a single prompt. This is the “Hello World” of AgentScope — no tools, just a straight question-and-response cycle.

The flow is straightforward:

  1. Read the GEMINI_API_KEY from the environment and exit with an error if it is missing.
  2. Build a GeminiChatModel using its builder, specifying the API key and the model name gemini-2.5-flash.
  3. Build a ReActAgent with a name, a system prompt, and the model.
  4. Construct a Msg with the user’s text content, call .call() on the agent, and wait for the result with .block().
  5. Print the text content of the response Msg.

You will need a Google Gemini API key, which you can obtain from Google AI Studio. Set it as an environment variable before running:

1 export GEMINI_API_KEY=your-key-here

The project depends on three Leiningen artifacts:

Artifact Version Purpose
io.agentscope/agentscope 1.0.9 AgentScope core (agents, messaging, tools)
com.google.genai/google-genai 1.44.0 Google GenAI SDK (Gemini models)
org.slf4j/slf4j-simple 2.0.13 Logging

Here is a listing of Clojure-AI-Book/source-code/AgentScope_gemini/src/agentscope/main.clj:

 1 (ns agentscope.main
 2   "AgentScope ReActAgent demo using Google Gemini (gemini-2.5-flash).
 3 
 4    Set the environment variable GEMINI_API_KEY before running:
 5      export GEMINI_API_KEY=your_key_here
 6      lein run"
 7   (:import [io.agentscope.core ReActAgent]
 8            [io.agentscope.core.message Msg]
 9            [io.agentscope.core.model GeminiChatModel])
10   (:gen-class))
11 
12 (defn -main [& _args]
13   (let [api-key (System/getenv "GEMINI_API_KEY")]
14     (when (or (nil? api-key) (clojure.string/blank? api-key))
15       (binding [*out* *err*]
16         (println "ERROR: GEMINI_API_KEY environment variable is not set."))
17       (System/exit 1))
18 
19     ;; Build the Gemini chat model
20     (let [model (-> (GeminiChatModel/builder)
21                     (.apiKey api-key)
22                     (.modelName "gemini-2.5-flash")
23                     (.build))
24 
25           ;; Build the ReActAgent
26           agent (-> (ReActAgent/builder)
27                     (.name "Assistant")
28                     (.sysPrompt "You are a helpful AI assistant.")
29                     (.model model)
30                     (.build))
31 
32           ;; Send a message and block for the response
33           response (-> (.call agent
34                               (-> (Msg/builder)
35                                   (.textContent
36                                    "Hello. Fun fact about Java programming.")
37                                   (.build)))
38                        (.block))]
39 
40       (println "Agent response:")
41       (println (.getTextContent response)))))

Sample output looks like:

 1 $ make hello
 2 lein run
 3 Compiling agentscope.main
 4 Compiling agentscope.tool-use
 5 Mar 26, 2026 1:24:04 PM com.google.genai.ApiClient getApiKeyFromEnv
 6 WARNING: Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.
 7 Agent response:
 8 Hello there!
 9 
10 Here's a fun fact about Java:
11 
12 The programming language wasn't originally named Java! It was initially called **Oak\
13 **, after an oak tree outside James Gosling's office. However, due to a trademark co\
14 nflict, they had to change it.
15 
16 The team then renamed it **Java**, inspired by Java coffee, which was a favorite bev\
17 erage of the developers (and the name of the Indonesian island where the coffee orig\
18 inates). This is why the Java logo is a steaming cup of coffee! ☕

Multiple Tool Use with AgentScope: Ollama Example

Our second example is far more interesting: we give the agent five tools and let it decide which ones to call based on the user’s question. This example uses the OllamaChatModel with the small local model nemotron-3-nano:4b, so no API key is needed — just a locally running Ollama server on http://localhost:11434.

Before running the example, start Ollama after pulling the model:

1 ollama pull nemotron-3-nano:4b
2 ollama serve

The project dependencies are simpler than the Gemini version since we do not need the Google GenAI SDK:

Artifact Version Purpose
io.agentscope/agentscope 1.0.9 AgentScope core (agents, messaging, tools)
org.slf4j/slf4j-simple 2.0.13 Logging

We define five tools, each implemented as a Clojure function that returns a reify of the AgentTool interface:

  1. getWeather — a stub that returns “Sunny, 25°C” for any city. In a real application you would call a weather API.
  2. list_dir — lists files and subdirectories at a given path using java.io.File.
  3. read_file — reads the contents of a file, with an optional max_lines parameter to limit output.
  4. recursive-file-search — recursively searches for files whose names contain a search string.
  5. math-eval — evaluates simple arithmetic expressions (integers with +, *, /) using a safe left-to-right evaluator.

Each tool follows the same pattern: implement getName, getDescription, getParameters (a JSON-schema object), and callAsync (which receives the parameters and returns a Mono<ToolResultBlock>). The callAsync method extracts the input parameters from the param object via .getInput, performs its logic in pure Clojure, and wraps the result string with ToolResultBlock/text inside Mono/just.

All five tools are registered into a Toolkit, which is then attached to the ReActAgent via its builder. When the agent receives a prompt, the ReAct loop inspects the available tools and their descriptions, decides which tools to call (if any), calls them, observes the results, and iterates until it can produce a final answer. The agent may call multiple tools in a single turn — for example, when asked about weather in two cities, it calls getWeather twice.

The -main function runs four example queries in sequence: a weather query for two cities, a request to list and read markdown files, a recursive file search, and a math evaluation.

Here is a listing of Clojure-AI-Book/source-code/AgentScope_ollama/src/agentscope/tool_use.clj:

  1 (ns agentscope.tool-use
  2   "Demonstrates AgentScope tool use with five tools:
  3      - getWeather  – stub weather lookup
  4      - list_dir    – list files in a directory
  5      - read_file   – read a file's contents (with optional line limit)
  6      - recursive-file-search – recursively search for files matching a string
  7      - math-eval   – evaluate arithmetic expressions (+, *, /, integers)
  8 
  9    Tools are defined entirely in Clojure by implementing the AgentTool
 10    interface with reify.  No companion Java class is needed.
 11 
 12    AgentTool requires four methods:
 13      getName        – the function name the LLM will call
 14      getDescription – natural-language description for the LLM
 15      getParameters  – JSON-schema map describing the input parameters
 16      callAsync      – executes the tool, returns Mono<ToolResultBlock>
 17 
 18    Ensure Ollama is running locally on http://localhost:11434 before running:
 19      lein run -m agentscope.tool-use"
 20   (:import [io.agentscope.core ReActAgent]
 21            [io.agentscope.core.message Msg ToolResultBlock]
 22            [io.agentscope.core.model OllamaChatModel]
 23            [io.agentscope.core.tool AgentTool Toolkit]
 24            [reactor.core.publisher Mono]
 25            [java.io File])
 26   (:gen-class))
 27 
 28 (defn- weather-tool
 29   "Returns an AgentTool implementation for a stub weather lookup."
 30   []
 31   (reify AgentTool
 32     (getName [_] "getWeather")
 33     (getDescription [_] "Get the current weather for a specified city")
 34     (getParameters [_]
 35       {"type"       "object"
 36        "properties" {"city" {"type"        "string"
 37                              "description" "The name of the city"}}
 38        "required"   ["city"]})
 39     (callAsync [_ param]
 40       (let [city (get (.getInput param) "city")]
 41         (Mono/just (ToolResultBlock/text (str city " weather: Sunny, 25°C")))))))
 42 
 43 (defn- list-dir-tool
 44   "Returns an AgentTool that lists entries in a directory."
 45   []
 46   (reify AgentTool
 47     (getName [_] "list_dir")
 48     (getDescription [_] "List files and subdirectories at the given path. Defaults t\
 49 o the current working directory when no path is supplied.")
 50     (getParameters [_]
 51       {"type"       "object"
 52        "properties" {"path" {"type"        "string"
 53                              "description" "Directory path to list (defaults to curr\
 54 ent directory)"}}
 55        "required"   []})
 56     (callAsync [_ param]
 57       (let [path  (or (get (.getInput param) "path") ".")
 58             dir   (File. path)
 59             names (if (.isDirectory dir)
 60                     (->> (.listFiles dir)
 61                          (sort-by #(.getName %))
 62                          (map #(if (.isDirectory %) (str (.getName %) "/") (.getName\
 63  %)))
 64                          (clojure.string/join "\n"))
 65                     (str "Error: not a directory: " path))]
 66         (Mono/just (ToolResultBlock/text names))))))
 67 
 68 (defn- read-file-tool
 69   "Returns an AgentTool that reads a file, optionally limiting output to N lines."
 70   []
 71   (reify AgentTool
 72     (getName [_] "read_file")
 73     (getDescription [_] "Read the contents of a file. Optionally restrict output to \
 74 the first max_lines lines.")
 75     (getParameters [_]
 76       {"type"       "object"
 77        "properties" {"path"      {"type"        "string"
 78                                   "description" "Path to the file to read"}
 79                      "max_lines" {"type"        "integer"
 80                                   "description" "Maximum number of lines to return (\
 81 optional, returns all lines when omitted)"}}
 82        "required"   ["path"]})
 83     (callAsync [_ param]
 84       (let [input     (.getInput param)
 85             path      (get input "path")
 86             max-lines (get input "max_lines")
 87             content   (try
 88                         (let [lines (clojure.string/split-lines (slurp path))]
 89                           (clojure.string/join "\n" (if max-lines (take max-lines li\
 90 nes) lines)))
 91                         (catch Exception e
 92                           (str "Error reading file: " (.getMessage e))))]
 93         (Mono/just (ToolResultBlock/text content))))))
 94 
 95 (defn- recursive-file-search-tool
 96   "Returns an AgentTool that recursively searches for files matching a search string\
 97 ."
 98   []
 99   (reify AgentTool
100     (getName [_] "recursive-file-search")
101     (getDescription [_] "Recursively search for files whose names contain the search\
102  string. Start search from the current working directory by default, or from an opti\
103 onal start_path.")
104     (getParameters [_]
105       {"type"       "object"
106        "properties" {"search_string" {"type"        "string"
107                                        "description" "String to search for in file n\
108 ames"}
109                      "start_path"    {"type"        "string"
110                                        "description" "Directory path to start the se\
111 arch from (defaults to current directory)"}}
112        "required"   ["search_string"]})
113     (callAsync [_ param]
114       (let [input      (.getInput param)
115             search-str (get input "search_string")
116             start-path (or (get input "start_path") ".")
117             matches    (fn matches [dir]
118                          (when (.isDirectory dir)
119                            (->> (.listFiles dir)
120                                 (mapcat (fn [f]
121                                           (if (.isDirectory f)
122                                             (matches f)
123                                             (when (.contains (.getName f) search-str)
124                                               [(.getPath f)])))))))]
125         (try
126           (let [result (matches (File. start-path))]
127             (Mono/just (ToolResultBlock/text (if (empty? result)
128                                                (str "No files matching '" search-str\
129  "' found.")
130                                                (clojure.string/join "\n" result)))))
131           (catch Exception e
132             (Mono/just (ToolResultBlock/text (str "Error searching: " (.getMessage e\
133 ))))))))))
134 
135 (defn- math-eval-tool
136   "Returns an AgentTool that evaluates a simple arithmetic expression."
137   []
138   (reify AgentTool
139     (getName [_] "math-eval")
140     (getDescription [_] "Evaluate a simple arithmetic expression consisting of integ\
141 ers and operators +, *, /. Example: \"2 + 3 * 4\"")
142     (getParameters [_]
143       {"type"       "object"
144        "properties" {"expression" {"type"        "string"
145                                     "description" "Arithmetic expression with intege\
146 rs and operators +, *, /"}}
147        "required"   ["expression"]})
148     (callAsync [_ param]
149       (let [expr (get (.getInput param) "expression")
150             result (try
151                      (let [;; Tokenize: extract numbers and operators
152                            tokens (re-seq #"\d+|[+*/]" expr)
153                            ;; Validate: ensure only valid tokens
154                            valid? (every? #(or (re-matches #"\d+" %) (re-matches #"[\
155 +*/]" %)) tokens)]
156                        (if-not valid?
157                          (str "Error: invalid expression '" expr "'")
158                          (let [;; Parse and evaluate left-to-right (same precedence \
159 as clojure core math)
160                                eval-expr (fn eval-expr [tokens]
161                                            (loop [tok tokens
162                                                   result (Long/parseLong (first toke\
163 ns))
164                                                   remaining (rest tokens)]
165                                              (if (empty? remaining)
166                                                result
167                                                (let [op (first remaining)
168                                                      next-val (Long/parseLong (secon\
169 d remaining))
170                                                      new-result (case op
171                                                                    "+" (+ result nex\
172 t-val)
173                                                                    "*" (* result nex\
174 t-val)
175                                                                    "/" (quot result \
176 next-val))]
177                                                  (recur (drop 2 remaining) new-resul\
178 t (drop 2 remaining))))))]
179                            (str (eval-expr tokens)))))
180                      (catch Exception e
181                        (str "Error evaluating expression: " (.getMessage e))))]
182         (Mono/just (ToolResultBlock/text result))))))
183 
184 (defn -main [& _args]
185   ;; Build the Ollama chat model
186   (let [model (-> (OllamaChatModel/builder)
187                   (.modelName "nemotron-3-nano:4b")
188                   (.baseUrl "http://localhost:11434")
189                   (.build))
190 
191           ;; Register all five tools via the AgentTool interface —
192           ;; no @Tool / @ToolParam annotations required.
193           toolkit (doto (Toolkit.)
194                     (.registerAgentTool (weather-tool))
195                     (.registerAgentTool (list-dir-tool))
196                     (.registerAgentTool (read-file-tool))
197                     (.registerAgentTool (recursive-file-search-tool))
198                     (.registerAgentTool (math-eval-tool)))
199 
200           ;; Build the ReActAgent with the toolkit attached
201           agent (-> (ReActAgent/builder)
202                     (.name "AssistantAgent")
203                     (.sysPrompt "You are a helpful assistant with tools to look up w\
204 eather, list directory contents, read files, search for files by name, and evaluate \
205 math expressions.")
206                     (.model model)
207                     (.toolkit toolkit)
208                     (.build))
209 
210           ;; Example 1: weather – agent calls getWeather for each city
211           weather-response (-> (.call agent
212                                       (-> (Msg/builder)
213                                           (.textContent "What is the weather like in\
214  Tokyo and Paris?")
215                                           (.build)))
216                                (.block))
217 
218           ;; Example 2: list the current directory, then show the first 5 lines
219           ;;            of every .md file found there
220           files-response   (-> (.call agent
221                                       (-> (Msg/builder)
222                                           (.textContent "List files in the current d\
223 irectory and for each .md markdown file, show me the first 5 lines.")
224                                           (.build)))
225                                (.block))
226 
227           ;; Example 3: recursive file search – find all .clj files
228           search-response  (-> (.call agent
229                                       (-> (Msg/builder)
230                                           (.textContent "Search for all .clj files i\
231 n the current directory and its subdirectories.")
232                                           (.build)))
233                                (.block))
234 
235           ;; Example 4: math evaluation
236           math-response    (-> (.call agent
237                                       (-> (Msg/builder)
238                                           (.textContent "Calculate the value of:  15\
239  + 27 * 3")
240                                           (.build)))
241                                (.block))]
242 
243       (println "=== Weather Query ===")
244       (println (.getTextContent weather-response))
245       (println)
246       (println "=== Markdown Files Query ===")
247       (println (.getTextContent files-response))
248       (println)
249       (println "=== File Search Query (.clj files) ===")
250       (println (.getTextContent search-response))
251       (println)
252       (println "=== Math Eval Query ===")
253       (println (.getTextContent math-response))))

Sample output looks like:

 1 $ make run-tools
 2 lein run -m agentscope.tool-use
 3 Compiling agentscope.tool-use
 4 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'getWeather' in group \
 5 'ungrouped'
 6 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'list_dir' in group 'u\
 7 ngrouped'
 8 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'read_file' in group '\
 9 ungrouped'
10 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'recursive-file-search\
11 ' in group 'ungrouped'
12 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'math-eval' in group '\
13 ungrouped'
14 === Weather Query ===
15 The weather in Tokyo is sunny with a temperature of 25°C. The weather in Paris is al\
16 so sunny with a temperature of 25°C.
17 
18 === Markdown Files Query ===
19 I found 1 markdown file in the current directory: **README.md**
20 
21 Here are the first 5 lines of README.md:
22 
23 ``
24  # AgentScope + Gemini  Clojure Edition
25 This directory contains Clojure examples for using the **AgentScope SDK** directly v\
26 ia Java interop.
27 > See [`README.md`](README.md) for background on AgentScope and the Gemini model.
28 ``
29 
30 Other files and directories in the current directory are: `.DS_Store`, `Makefile`, `\
31 project.clj`, `src/`, and `target/`.
32 
33 === File Search Query (.clj files) ===
34 I found a total of **3 .clj files** in the current directory and its subdirectories:
35 
36 1. `./project.clj`
37 2. `./src/agentscope/main.clj`
38 3. `./src/agentscope/tool_use.clj`
39 
40 These appear to be Clojure configuration, project management, and module file extens\
41 ions.
42 
43 === Math Eval Query ===
44 The value of `15 + 27 * 3` is **126**. (Multiplication takes precedence over additio\
45 n.)
46 [HttpTransportFactory-ShutdownHook] INFO io.agentscope.core.model.transport.HttpTran\
47 sportFactory - Shutting down 1 managed HttpTransport(s)

Notice how the agent autonomously chains tool calls. For the markdown files query, it first calls list_dir to discover the files, then calls read_file with max_lines=5 for each .md file it finds. For the weather query it calls getWeather twice (once for Tokyo, once for Paris). The ReAct loop handles all of this orchestration — your code simply registers the tools and sends the prompt.

Summary

In this chapter we used the AgentScope Java SDK from Clojure to build two kinds of LLM-powered agents:

  • A simple completion agent that wraps a Gemini model in a ReActAgent and sends a prompt with no tools.
  • A tool-using agent that wraps an Ollama model, registers five tools written in Clojure via the AgentTool interface and Toolkit, and lets the ReAct loop decide which tools to call.

The key takeaway is that AgentScope’s builder pattern and reify based tool definitions map naturally to Clojure idioms. You get the full power of the ReAct reasoning loop with automatic tool selection, multi-turn tool calling, and result synthesis without writing any orchestration code yourself. The same pattern scales to more tools, different models, or multi-agent workflows using AgentScope’s MsgHub for agent-to-agent communication.