Skip to content

Remote Tools

AgentEnsemble supports cross-language tool implementations through two remote tool types: ProcessAgentTool (subprocess execution) and HttpAgentTool (REST endpoint wrapping). Both implement AgentTool and are indistinguishable from native Java tools from the agent’s perspective.


  • Python ML models that are impractical to run in-JVM
  • Existing microservices you want to expose as agent capabilities
  • Polyglot teams where tools are maintained in different languages
  • Sandboxing computationally intensive or security-sensitive operations

Executes an external subprocess and communicates via the AgentEnsemble subprocess protocol.

Communication is JSON-over-stdio:

Input — written to the process’s stdin immediately after launch:

{"input": "the agent's input string"}

Success output — the process writes to stdout before exiting with code 0:

{"output": "result text for the LLM", "success": true}

Optionally include a structured payload for programmatic consumers:

{"output": "result text for the LLM", "success": true, "structured": {"key": "value"}}

Failure output — on a logical failure (not an exception):

{"error": "description of what went wrong", "success": false}

Non-zero exit code — treated as a failure; stderr is captured as the error message.

Timeout — configurable; the process is killed with destroyForcibly() if exceeded.

Note: Processes that do not read stdin (e.g., echo) are supported. The stdin write failure is logged at DEBUG level and execution continues normally.

var sentiment = ProcessAgentTool.builder()
.name("sentiment_analysis")
.description("Analyzes the sentiment of a piece of text. Returns positive, negative, or neutral.")
.command("python3", "/opt/tools/sentiment.py")
.timeout(Duration.ofSeconds(30))
.build();
var agent = Agent.builder()
.role("Analyst")
.goal("Analyze customer feedback")
.tools(List.of(sentiment))
.llm(chatModel)
.build();
import sys
import json
def analyze(text):
# Replace with real sentiment logic
words = text.lower().split()
if any(w in words for w in ["good", "great", "excellent"]):
return "positive"
elif any(w in words for w in ["bad", "terrible", "awful"]):
return "negative"
return "neutral"
data = json.loads(sys.stdin.read())
result = analyze(data["input"])
print(json.dumps({"output": result, "success": True}))
OptionRequiredDefaultDescription
nameYes-Tool name shown to the LLM
descriptionYes-Tool description shown to the LLM
commandYes-Program and arguments (varargs or List)
timeoutNo30 secondsMaximum execution time before process kill

Wraps an HTTP endpoint as an agent tool. No subprocess overhead — uses Java’s built-in HttpClient.

  • GET requests: input appended as a query parameter — ?input=<url-encoded-input>
  • POST requests: input sent as the request body
    • Content-Type: application/json when input is valid JSON
    • Content-Type: text/plain; charset=UTF-8 otherwise

The HTTP response body is returned as the tool’s plain-text output to the agent. HTTP 4xx/5xx responses are treated as failures.

var search = HttpAgentTool.get(
"knowledge_base_search",
"Searches the internal knowledge base for relevant articles.",
"https://kb.example.com/search");
var classifier = HttpAgentTool.builder()
.name("text_classifier")
.description("Classifies text into categories: tech, finance, sports, other.")
.url("https://ml.example.com/classify")
.method("POST")
.header("Authorization", "Bearer " + System.getenv("ML_API_KEY"))
.timeout(Duration.ofSeconds(60))
.build();
// When the agent provides JSON input, application/json Content-Type is set automatically
var enricher = HttpAgentTool.post(
"entity_enricher",
"Enriches entity data given a JSON object with 'name' and 'type' fields.",
"https://api.example.com/enrich");
OptionRequiredDefaultDescription
nameYes-Tool name shown to the LLM
descriptionYes-Tool description shown to the LLM
urlYes-The HTTP endpoint URL
methodNoPOSTHTTP method (GET, POST, PUT, etc.)
headerNo(none)Add a request header (can be called multiple times)
timeoutNo30 secondsRequest timeout
const express = require('express');
const app = express();
app.use(express.text());
app.post('/classify', (req, res) => {
const input = req.body;
const category = classify(input); // your logic here
res.send(category);
});
app.listen(8080);

Tools of all types can be combined freely on a single agent:

var agent = Agent.builder()
.role("Data Analyst")
.goal("Analyze sales data using all available tools")
.tools(List.of(
new CalculatorTool(), // Java (in-process)
ProcessAgentTool.builder() // Python subprocess
.name("forecast")
.description("Generates a sales forecast")
.command("python3", "/opt/tools/forecast.py")
.build(),
HttpAgentTool.get( // REST API
"product_data",
"Retrieves product catalog data",
"https://api.example.com/products")
))
.llm(chatModel)
.build();

When the LLM requests multiple tools in a single turn, AgentEnsemble executes them concurrently using the configured tool executor (default: virtual threads). This is especially beneficial for remote tools where I/O latency dominates:

// A parallel tool turn (calculator + Python forecast + HTTP API) runs concurrently
Ensemble.builder()
.agent(agent)
.task(task)
.toolExecutor(Executors.newVirtualThreadPerTaskExecutor()) // default
.build()
.run();

For rate-limited APIs, provide a bounded executor to cap concurrency:

Ensemble.builder()
.agent(agent)
.task(task)
.toolExecutor(Executors.newFixedThreadPool(4))
.build()
.run();