
This is a simple Generative AI example based on the VertexAI agent framework. This initial experiment controls the workflow on the basis of a history (in JSON) of operations. This simple agents decides between two sequential tasks on the basis of the history. The history is stored and managed outside the agents. The keywords of the agents determine the next step. The next step updates the history.
This represents a migration from ADK Agents to VertexAI agents. This is an initial experiment with the use of VertexAI, basically illustrating the setup of a simple agent. It is a simple example based on an example.
The basic flow originates from the client. The client calls the top agent which delegates flow to further subagents. The final subagent (could be several levels deep) gives a response to tell the client what to do next. Function calls, for example to the backend, does not occur in the agents but in the JAVA code after the agents or in the client. If user interaction through the client is required, the result of the agent is a set of keywords to tell the client what actions to perform. Then the cycle begins again.
Some useful links are:
- java-genai: This is the github page with examples for the use of vertexai agents
- java-genai API:
- prompts examples:
- Vertex AI SDK migration guide
- Generative AI prompt samples
- Optimize prompts
- Overview of prompting strategies
- Vertex AI quickstart
- Generative AI beginner’s guide: VertexAI
BaseExecuteAgentToSimpleResponse
This class is the basis of a simple VertexAI call.
The agent is setup with GenerateContentConfig:
GenerateContentConfig config =
GenerateContentConfig.builder()
.candidateCount(1)
.maxOutputTokens(1024)
.systemInstruction(systemInstruction)
.tools(ImmutableList.of())
.temperature(0.0f)
.build();
This sets up the definition of the agent. For this simple agent, the most important are the instructions given to the agent:
systemInstruction(systemInstruction)
The instructions (as with all information with the agent) are of the type Content. The instruction sent to this class are in (human readable) string form, but are converted through:
Content systemInstruction = Content.fromParts(Part.fromText(instructions));
Another important keyword in this configuration is the
candidateCount(1)
The response generated by the agent is a a GenerateContentResponse and within the response is the Candidate, which holds the results we are interested in. By setting candidate count to one means we want only the result.
Since the task is very deterministic, the lowest temperature is given to force no guessing:
.temperature(0.0f)
To generate the response, the config and the history (contents) is used:
GenerateContentResponse response =
client.models.generateContent(modelId, contents, config);
This history is given as:
List<Content> contents
This is a translation of an array of JSON objects, where each object is a task or event in the history,
GenerateContentResponse
The GenerateContentResponse is the response of the agent. We are interested in the text response of the agent found within the GenerateContentResponse. Basically it is found within the following JSON hierarchy:
Candidates -> Content -> Parts -> String
Each of these is an Optional argument. For example, the candidates within the GenerateContentResponse object is defined as:
Thus, first to see if there are candidates, you can ask whether it is present:
response.candidates().isPresent()
If it is present, we can access the list through
response.candidates().get()
And since we want only the first element, we get the zeroth element of the array:
response.candidates().get().get(0)
The same pattern goes for the Content:
firstCandidate.content().isPresent() -> firstCandidate.content().get()
And for Parts (to get the first part in the list of parts):
content.parts().isPresent() -> content.parts().get().get(0)
And of the Part, the text string of the part is retrieved with:
part.text().isPresent() -> part.text().get()
This text part is what we are after as the response to our agent.
package esblurock.info.JavaAIAgent.vertexai.version1;
import java.util.List;
import com.google.common.collect.ImmutableList;
import com.google.genai.Client;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.GenerateContentResponse;
import com.google.genai.types.Part;
public class BaseExecuteAgentToSimpleResponse {
public static String executeAgent(String instructions, Client client, List<Content> contents) {
final String modelId = "gemini-2.5-flash";
Content systemInstruction = Content.fromParts(Part.fromText(instructions));
GenerateContentConfig config =
GenerateContentConfig.builder()
.candidateCount(1)
.maxOutputTokens(1024)
.systemInstruction(systemInstruction)
.tools(ImmutableList.of())
.temperature(0.0f)
.build();
GenerateContentResponse response =
client.models.generateContent(modelId, contents, config);
String generatedTextString = "ERROR";
// 1. Check if candidates exist
if (response.candidates().isPresent() && !response.candidates().get().isEmpty()) {
var firstCandidate = response.candidates().get().get(0);
// 2. Check if content exists
if (firstCandidate.content().isPresent()) {
var content = firstCandidate.content().get();
// 3. Check if parts exist
if (content.parts().isPresent() && !content.parts().get().isEmpty()) {
generatedTextString = content.parts().get().get(0).text().get();
}
} else {
// Handle cases where the model stopped early (e.g., SAFETY, OTHER)
System.out.println("No content. Finish Reason: " + firstCandidate.finishReason().orElse(null));
}
} else {
System.out.println("No candidates returned by the model.");
}
System.out.println("Model Output: " + generatedTextString);
int promptTokens = response.usageMetadata().get().promptTokenCount().get();
String finishString = response.finishReason().toString();
System.out.println("Prompt Tokens Used: " + promptTokens);
System.out.println("Finish Reason: " + finishString);
return generatedTextString;
}
}
GenerateContentResponse: Example
The structure shown above can be seen in this example output:
{
"sdkHttpResponse": {
"headers": {
"date": "Sun, 22 Feb 2026 10:23:14 GMT",
"server": "scaffolding on HTTPServer2",
"x-content-type-options": "nosniff",
"x-xss-protection": "0",
"vary": "Referer",
"x-frame-options": "SAMEORIGIN",
"content-type": "application/json; charset=UTF-8",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
}
},
"candidates": [
{
"content": {
"parts": [
{
"text": "DELEGATE: InitialParameters"
}
],
"role": "model"
},
"finishReason": "STOP",
"avgLogprobs": -1.6606218020121257
}
],
"createTime": "2026-02-22T10:23:14.130807Z",
"modelVersion": "gemini-2.5-flash",
"responseId": "Etmaaff9B8OY-eQP3L-G6Ak",
"usageMetadata": {
"candidatesTokenCount": 6,
"candidatesTokensDetails": [
{
"modality": "TEXT",
"tokenCount": 6
}
],
"promptTokenCount": 320,
"promptTokensDetails": [
{
"modality": "TEXT",
"tokenCount": 320
}
],
"thoughtsTokenCount": 95,
"totalTokenCount": 421,
"trafficType": "ON_DEMAND"
}
}Agent Instructions
The agent instructions are following an example in the Google Docs:
As stated in the OBJECTIVE_AND_PERSONA section, the agent is an orchestrator. It is meant to represent the workflow of the execution of two tasks (as specified in the first part of the instructions:
- **InitialParameters**: Specifying the data type of the dataset.
- **DatasetFlowIdentifier**: The user should specify a unique name to the dataset flow.
These are to be consecutive tasks in the order given:
Run the tasks in the given order
Do not run the next task until the previous task is complete
To determine the which task to run, the history (a List<Content> as given in the generation call):
DO NOT use any internal knowledge or external information
Analyze only the JSON history content
The statement only to use the history information turned out to be important. Otherwise, past knowledge involving other runs was used.
Of course, the definition of a complete task is important:
- A task is complete when TaskName: {status: Complete} is found in the history
- If the **task**: {status: Complete} is NOT found in the history, then **task** is incomplete
- Identify the first uncompleted task and output ‘DELEGATE: TaskName’
The second line was necessary, otherwise the agent associated any complete with the whole set of tasks.
Then there are CONSTRAINTS:
Dos and don’ts for the following aspects
1. Your only task is to specify which task to perform next or say TASK_COMPLETE
2. Do not perform any task yourself, delegate it to subtasks
<OBJECTIVE_AND_PERSONA>
You are the **Lead Orchestrator** for the Dataset Setup Workflow. Your task is to perform the workflow steps
</OBJECTIVE_AND_PERSONA>
<INSTRUCTIONS>
To complete the task, you need to follow these steps:
1. **InitialParameters**: Specifying the data type of the dataset.
2. **DatasetFlowIdentifier**: The user should specify a unique name to the dataset flow.
Run the tasks in the given order
Do not run the next task until the previous task is complete
DO NOT use any internal knowledge or external information
Analyze only the JSON history content
A task is complete when TaskName: {status: Complete} is found in the history
If the **task**: {status: Complete} is NOT found in the history, then **task** is incomplete Identify the first uncompleted task and output ‘DELEGATE: TaskName’
</INSTRUCTIONS>
<CONSTRAINTS>
Dos and don’ts for the following aspects
1. Your only task is to specify which task to perform next or say TASK_COMPLETE
2. Do not perform any task yourself, delegate it to subtasks
</CONSTRAINTS>
<OUTPUT_FORMAT>
The output format must be either
1. TASK_COMPLETE
2. DELEGATE: TaskName
</OUTPUT_FORMAT>
DatasetAgentOrchestration
The DatasetAgentOrchestration class defines the agent and the corresponding logic interpreting the result of the agent.The purpose of the agent is to determine what to do next in the workflow. It produces keyword phrases that dictate what is to be done, or if the workflow is complete. The JAVA logic after the agent call determines the actions to be taken.
Using the JAVA logic instead of the agent itself calling the subagents (within its own evaluation) is a paradigm shift. The main reason what to simplify the outputs of the defined agents to simple keywords. The complex data structures, information and actual function calls, for example to a backend, occurs outside the agent.
The response of the agent is formed with the command:
BaseExecuteAgentToSimpleResponse.executeAgent(..)
There are three possible responses:
- DELEGATE: InitialParameters
- DELEGATE: DatasetFlowIdentifier
- TASK_COMPLETE
The delegate keyword says which agent to call next. The history would be transferred to the corresponding agent.This is done through basically a switch:
if (responseString.contains(“DELEGATE:”)) {
String target = extractTarget(responseString);
switch (target) {
case “InitialParameters”:
outputString = executeInitialParameters(contents,client);
case “DatasetFlowIdentifier”:
outputString = executeDatasetFlowIdentifier(contents,client);
default:
outputString = “Error: Unknown specialist requested.”;
}} else if(responseString.contains(“TASK_COMPLETE”)){
outputString = “TASK_COMPLETE”;
}
The function calls set up the subagents. The final result is then sent back to the client. This could be some user interface interaction that is needed. This would be sent through the outputString. The outputString information is then appended to the history and the history is sent back to the client.
For this example the subagents are dummies. In the real case, it will be determined what is done by the subagent routines (not the agent but the JAVA logic around the agent).
Conversion of History:
JSON array to List<Content>
To generate content from a model (client.models.generateContent) we need to map a JSON array (which is a default datatype for the whole system) to a List<Content>.
Each JsonObject in the array is mapped to a single Content with:
Content.builder()
.role(role)
.parts(parts)
where the role comes from the JsonObject (element of array), every event has a role, meaning from where it came:
JsonObject msg = element.getAsJsonObject();
String role = msg.get(“role”).getAsString();
The parts are the individual elements of the json object. For example, JsonObject only consists of a role and text, the text is converted to a part:
Part.fromText(msg.get(“text”).getAsString())
The HistoryMapper.mapJsonArrayToContentList
was written with the following JSON array in mind:
{ "role": "user", "text": "Setup repo Alpha" },
{ "role": "model", "functionCall": { "name": "request_human_input", "args": { "param": "type" } } },
{ "role": "tool", "functionResponse": { "name": "request_human_input", "response": { "status": "ok" } } },
{ "role": "user", "text": "It is a Private repo" }
package esblurock.info.JavaAIAgent.vertexai.version1;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.google.genai.types.Content;
import com.google.genai.types.FunctionCall;
import com.google.genai.types.FunctionResponse;
import com.google.genai.types.Part;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
public class HistoryMapper {
/*
* [
{ "role": "user", "text": "Setup repo Alpha" },
{ "role": "model", "functionCall": { "name": "request_human_input", "args": { "param": "type" } } },
{ "role": "tool", "functionResponse": { "name": "request_human_input", "response": { "status": "ok" } } },
{ "role": "user", "text": "It is a Private repo" }
]
*/
public static List<Content> mapJsonArrayToContentList(JsonArray historyArray) {
List<Content> contents = new ArrayList<Content>();
for (JsonElement element : historyArray) {
JsonObject msg = element.getAsJsonObject();
String role = msg.get("role").getAsString();
List<Part> parts = new ArrayList<Part>();
// 1. Text
if (msg.has("text")) {
parts.add(Part.fromText(msg.get("text").getAsString()));
}
// 2. Function Call (Model)
if (msg.has("functionCall")) {
JsonObject fc = msg.getAsJsonObject("functionCall");
// In the new SDK, we use FunctionCall.builder()
Map<String, Object> args = new Gson().fromJson(fc.get("args"), Map.class);
parts.add(Part.builder().functionCall(
FunctionCall.builder()
.name(fc.get("name").getAsString())
.args(args)
.build()
).build());
}
// 3. Function Response (Tool)
if (msg.has("functionResponse")) {
JsonObject fr = msg.getAsJsonObject("functionResponse");
Map<String, Object> response = new Gson().fromJson(fr.get("response"), Map.class);
parts.add(Part.builder().functionResponse(
FunctionResponse.builder()
.name(fr.get("name").getAsString())
.response(response)
.build()
).build());
}
// Assemble into a Content object
contents.add(Content.builder()
.role(role)
.parts(parts)
.build());
}
return contents;
}
}Example Call
This agent set is called through a JAVA test:
package esblurock.info.JavaAIAgent;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import com.google.genai.Client;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import esblurock.info.JavaAIAgent.vertexai.version1.DatasetAgentOrchestration;
class simpleTestOrchestrator {
@Test
void test() {
Client client = new Client();
String historyString = "[\n"
+ " { \"role\": \"user\", \"text\": \"Read in a dataset of vibrational modes\" },\n"
+ " { \"role\": \"model\", \"text\": \"InitialParameters: {status: Complete}\" }\n"
+ "]";
System.out.println(historyString);
JsonArray historyArray = JsonParser.parseString(historyString).getAsJsonArray();
System.out.println("the size of the array is: " + historyArray.size());
System.out.println("The history: " + historyArray);
DatasetAgentOrchestration.runWorkflow(historyArray, client);
}
}History Call
[
{
“role”: “user”,
“text”: “Read in a dataset of vibrational modes”
},
{
“role”: “model”,
“text”: “InitialParameters: {status: Complete}”
}
]
This history call simulates when the InitialParameters task is complete. The result of the call is:
DELEGATE: DatasetFlowIdentifier
meaning that the DatasetFlowIdentifier is the next task to be completed. The full response of the agent is show in the ‘GenerateContentResponse: Example’ section above.