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:4brunning 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) andOllamaChatModel(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
Msgwith a text content (and optionally images or other modalities), pass it to the agent via.call(), and receive a responseMsgback. The.block()call unwraps the reactive (Project ReactorMono) return value into a synchronous result.AgentTool and Toolkit — the tool-use subsystem. Each tool implements the
AgentToolinterface with four methods:getName,getDescription,getParameters(a JSON-schema map), andcallAsync(which returns a ReactorMono<ToolResultBlock>). Tools are registered into aToolkit, which is then attached to aReActAgent. 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:
- Read the
GEMINI_API_KEYfrom the environment and exit with an error if it is missing. - Build a
GeminiChatModelusing its builder, specifying the API key and the model namegemini-2.5-flash. - Build a
ReActAgentwith a name, a system prompt, and the model. - Construct a
Msgwith the user’s text content, call.call()on the agent, and wait for the result with.block(). - 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**, after an oak tree outside James Gosling's office. However, due to a trademark conflict, they had to change it.
13
14 The team then renamed it **Java**, inspired by Java coffee, which was a favorite beverage of the developers (and the name of the Indonesian island where the coffee originates). 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:
- getWeather — a stub that returns “Sunny, 25°C” for any city. In a real application you would call a weather API.
- list_dir — lists files and subdirectories at a given path using
java.io.File. - read_file — reads the contents of a file, with an optional
max_linesparameter to limit output. - recursive-file-search — recursively searches for files whose names contain a search string.
- 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 to the current working directory when no path is supplied.")
49 (getParameters [_]
50 {"type" "object"
51 "properties" {"path" {"type" "string"
52 "description" "Directory path to list (defaults to current directory)"}}
53 "required" []})
54 (callAsync [_ param]
55 (let [path (or (get (.getInput param) "path") ".")
56 dir (File. path)
57 names (if (.isDirectory dir)
58 (->> (.listFiles dir)
59 (sort-by #(.getName %))
60 (map #(if (.isDirectory %) (str (.getName %) "/") (.getName %)))
61 (clojure.string/join "
62 "))
63 (str "Error: not a directory: " path))]
64 (Mono/just (ToolResultBlock/text names))))))
65
66 (defn- read-file-tool
67 "Returns an AgentTool that reads a file, optionally limiting output to N lines."
68 []
69 (reify AgentTool
70 (getName [_] "read_file")
71 (getDescription [_] "Read the contents of a file. Optionally restrict output to the first max_lines lines.")
72 (getParameters [_]
73 {"type" "object"
74 "properties" {"path" {"type" "string"
75 "description" "Path to the file to read"}
76 "max_lines" {"type" "integer"
77 "description" "Maximum number of lines to return (optional, returns all lines when omitted)"}}
78 "required" ["path"]})
79 (callAsync [_ param]
80 (let [input (.getInput param)
81 path (get input "path")
82 max-lines (get input "max_lines")
83 content (try
84 (let [lines (clojure.string/split-lines (slurp path))]
85 (clojure.string/join "
86 " (if max-lines (take max-lines lines) lines)))
87 (catch Exception e
88 (str "Error reading file: " (.getMessage e))))]
89 (Mono/just (ToolResultBlock/text content))))))
90
91 (defn- recursive-file-search-tool
92 "Returns an AgentTool that recursively searches for files matching a search string."
93 []
94 (reify AgentTool
95 (getName [_] "recursive-file-search")
96 (getDescription [_] "Recursively search for files whose names contain the search string. Start search from the current working directory by default, or from an optional start_path.")
97 (getParameters [_]
98 {"type" "object"
99 "properties" {"search_string" {"type" "string"
100 "description" "String to search for in file names"}
101 "start_path" {"type" "string"
102 "description" "Directory path to start the search from (defaults to current directory)"}}
103 "required" ["search_string"]})
104 (callAsync [_ param]
105 (let [input (.getInput param)
106 search-str (get input "search_string")
107 start-path (or (get input "start_path") ".")
108 matches (fn matches [dir]
109 (when (.isDirectory dir)
110 (->> (.listFiles dir)
111 (mapcat (fn [f]
112 (if (.isDirectory f)
113 (matches f)
114 (when (.contains (.getName f) search-str)
115 [(.getPath f)])))))))]
116 (try
117 (let [result (matches (File. start-path))]
118 (Mono/just (ToolResultBlock/text (if (empty? result)
119 (str "No files matching '" search-str "' found.")
120 (clojure.string/join "
121 " result)))))
122 (catch Exception e
123 (Mono/just (ToolResultBlock/text (str "Error searching: " (.getMessage e))))))))))
124
125 (defn- math-eval-tool
126 "Returns an AgentTool that evaluates a simple arithmetic expression."
127 []
128 (reify AgentTool
129 (getName [_] "math-eval")
130 (getDescription [_] "Evaluate a simple arithmetic expression consisting of integers and operators +, *, /. Example: \"2 + 3 * 4\"")
131 (getParameters [_]
132 {"type" "object"
133 "properties" {"expression" {"type" "string"
134 "description" "Arithmetic expression with integers and operators +, *, /"}}
135 "required" ["expression"]})
136 (callAsync [_ param]
137 (let [expr (get (.getInput param) "expression")
138 result (try
139 (let [;; Tokenize: extract numbers and operators
140 tokens (re-seq #"\d+|[+*/]" expr)
141 ;; Validate: ensure only valid tokens
142 valid? (every? #(or (re-matches #"\d+" %) (re-matches #"[+*/]" %)) tokens)]
143 (if-not valid?
144 (str "Error: invalid expression '" expr "'")
145 (let [;; Parse and evaluate left-to-right (same precedence as clojure core math)
146 eval-expr (fn eval-expr [tokens]
147 (loop [tok tokens
148 result (Long/parseLong (first tokens))
149 remaining (rest tokens)]
150 (if (empty? remaining)
151 result
152 (let [op (first remaining)
153 next-val (Long/parseLong (second remaining))
154 new-result (case op
155 "+" (+ result next-val)
156 "*" (* result next-val)
157 "/" (quot result next-val))]
158 (recur (drop 2 remaining) new-result (drop 2 remaining))))))]
159 (str (eval-expr tokens)))))
160 (catch Exception e
161 (str "Error evaluating expression: " (.getMessage e))))]
162 (Mono/just (ToolResultBlock/text result))))))
163
164 (defn -main [& _args]
165 ;; Build the Ollama chat model
166 (let [model (-> (OllamaChatModel/builder)
167 (.modelName "nemotron-3-nano:4b")
168 (.baseUrl "http://localhost:11434")
169 (.build))
170
171 ;; Register all five tools via the AgentTool interface —
172 ;; no @Tool / @ToolParam annotations required.
173 toolkit (doto (Toolkit.)
174 (.registerAgentTool (weather-tool))
175 (.registerAgentTool (list-dir-tool))
176 (.registerAgentTool (read-file-tool))
177 (.registerAgentTool (recursive-file-search-tool))
178 (.registerAgentTool (math-eval-tool)))
179
180 ;; Build the ReActAgent with the toolkit attached
181 agent (-> (ReActAgent/builder)
182 (.name "AssistantAgent")
183 (.sysPrompt "You are a helpful assistant with tools to look up weather, list directory contents, read files, search for files by name, and evaluate math expressions.")
184 (.model model)
185 (.toolkit toolkit)
186 (.build))
187
188 ;; Example 1: weather – agent calls getWeather for each city
189 weather-response (-> (.call agent
190 (-> (Msg/builder)
191 (.textContent "What is the weather like in Tokyo and Paris?")
192 (.build)))
193 (.block))
194
195 ;; Example 2: list the current directory, then show the first 5 lines
196 ;; of every .md file found there
197 files-response (-> (.call agent
198 (-> (Msg/builder)
199 (.textContent "List files in the current directory and for each .md markdown file, show me the first 5 lines.")
200 (.build)))
201 (.block))
202
203 ;; Example 3: recursive file search – find all .clj files
204 search-response (-> (.call agent
205 (-> (Msg/builder)
206 (.textContent "Search for all .clj files in the current directory and its subdirectories.")
207 (.build)))
208 (.block))
209
210 ;; Example 4: math evaluation
211 math-response (-> (.call agent
212 (-> (Msg/builder)
213 (.textContent "Calculate the value of: 15 + 27 * 3")
214 (.build)))
215 (.block))]
216
217 (println "=== Weather Query ===")
218 (println (.getTextContent weather-response))
219 (println)
220 (println "=== Markdown Files Query ===")
221 (println (.getTextContent files-response))
222 (println)
223 (println "=== File Search Query (.clj files) ===")
224 (println (.getTextContent search-response))
225 (println)
226 (println "=== Math Eval Query ===")
227 (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 'ungrouped'
5 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'list_dir' in group 'ungrouped'
6 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'read_file' in group 'ungrouped'
7 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'recursive-file-search' in group 'ungrouped'
8 [main] INFO io.agentscope.core.tool.Toolkit - Registered tool 'math-eval' in group 'ungrouped'
9 === Weather Query ===
10 The weather in Tokyo is sunny with a temperature of 25°C. The weather in Paris is also sunny with a temperature of 25°C.
11
12 === Markdown Files Query ===
13 I found 1 markdown file in the current directory: **README.md**
14
15 Here are the first 5 lines of README.md:
16
17 ``
18 # AgentScope + Gemini — Clojure Edition
19 This directory contains Clojure examples for using the **AgentScope SDK** directly via Java interop.
20 > See [`README.md`](README.md) for background on AgentScope and the Gemini model.
21 ``
22
23 Other files and directories in the current directory are: `.DS_Store`, `Makefile`, `project.clj`, `src/`, and `target/`.
24
25 === File Search Query (.clj files) ===
26 I found a total of **3 .clj files** in the current directory and its subdirectories:
27
28 1. `./project.clj`
29 2. `./src/agentscope/main.clj`
30 3. `./src/agentscope/tool_use.clj`
31
32 These appear to be Clojure configuration, project management, and module file extensions.
33
34 === Math Eval Query ===
35 The value of `15 + 27 * 3` is **126**. (Multiplication takes precedence over addition.)
36 [HttpTransportFactory-ShutdownHook] INFO io.agentscope.core.model.transport.HttpTransportFactory - 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
ReActAgentand sends a prompt with no tools. - A tool-using agent that wraps an Ollama model, registers five tools written in Clojure via the
AgentToolinterface andToolkit, 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.
Optional Practice Problems
- Multi-Agent Debate: Build a two-agent debate system in
source-code/AgentScope_geminiorsource-code/AgentScope_ollamawhere one agent argues for functional programming and another argues for object-oriented programming. - Human-in-the-Loop Feedback: Add a step where a human agent reviews the discussion and provides feedback before the final answer is generated.
- State Serialization: Implement state saving/loading so that conversation logs can be saved to disk and resumed at a later point.