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.
Why Remote Tools?
Section titled “Why Remote Tools?”- 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
ProcessAgentTool
Section titled “ProcessAgentTool”Executes an external subprocess and communicates via the AgentEnsemble subprocess protocol.
Subprocess Protocol Specification
Section titled “Subprocess Protocol Specification”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();Example Python Implementation
Section titled “Example Python Implementation”import sysimport 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}))Builder Options
Section titled “Builder Options”| Option | Required | Default | Description |
|---|---|---|---|
name | Yes | - | Tool name shown to the LLM |
description | Yes | - | Tool description shown to the LLM |
command | Yes | - | Program and arguments (varargs or List) |
timeout | No | 30 seconds | Maximum execution time before process kill |
HttpAgentTool
Section titled “HttpAgentTool”Wraps an HTTP endpoint as an agent tool. No subprocess overhead — uses Java’s built-in
HttpClient.
Request Format
Section titled “Request Format”- GET requests: input appended as a query parameter —
?input=<url-encoded-input> - POST requests: input sent as the request body
Content-Type: application/jsonwhen input is valid JSONContent-Type: text/plain; charset=UTF-8otherwise
Response Format
Section titled “Response Format”The HTTP response body is returned as the tool’s plain-text output to the agent. HTTP 4xx/5xx responses are treated as failures.
GET endpoint (simple search API)
Section titled “GET endpoint (simple search API)”var search = HttpAgentTool.get( "knowledge_base_search", "Searches the internal knowledge base for relevant articles.", "https://kb.example.com/search");POST endpoint with custom headers
Section titled “POST endpoint with custom headers”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();POST endpoint with JSON input
Section titled “POST endpoint with JSON input”// When the agent provides JSON input, application/json Content-Type is set automaticallyvar enricher = HttpAgentTool.post( "entity_enricher", "Enriches entity data given a JSON object with 'name' and 'type' fields.", "https://api.example.com/enrich");Builder Options
Section titled “Builder Options”| Option | Required | Default | Description |
|---|---|---|---|
name | Yes | - | Tool name shown to the LLM |
description | Yes | - | Tool description shown to the LLM |
url | Yes | - | The HTTP endpoint URL |
method | No | POST | HTTP method (GET, POST, PUT, etc.) |
header | No | (none) | Add a request header (can be called multiple times) |
timeout | No | 30 seconds | Request timeout |
Example Node.js Service
Section titled “Example Node.js Service”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);Mixing Native and Remote Tools
Section titled “Mixing Native and Remote Tools”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();Parallel Execution
Section titled “Parallel Execution”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 concurrentlyEnsemble.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();