Agents
July 19, 2026 · View on GitHub
Agent is the single orchestration primitive. One agent wraps an LLM plus tools. An agent whose tools include other agents is a multi-agent system — no separate Team or Swarm classes.
Builder
Agent agent = Agent.builder()
.name("my_agent") // required; becomes the Conductor workflow name
.model("anthropic/claude-sonnet-4-6") // required; "provider/model" format
.instructions("You are a helpful agent.") // system prompt
.build();
Every field below is optional.
@AgentDef annotation
Instead of the builder, a method can be annotated with @AgentDef (the Java counterpart of the Python SDK's @agent decorator). The method body returns the instructions; @Tool and @GuardrailDef methods on the same object are attached automatically.
import org.conductoross.conductor.ai.Agent;
import org.conductoross.conductor.ai.annotations.AgentDef;
import org.conductoross.conductor.ai.annotations.Tool;
public class Weather {
@Tool(name = "get_weather", description = "Get weather for a city")
public String getWeather(String city) { return "Sunny, 72F in " + city; }
@AgentDef(model = "openai/gpt-4o")
public String weatherbot() {
return "You are a weather assistant.";
}
}
Agent agent = Agent.fromInstance(new Weather(), "weatherbot");
| Attribute | Default | Description |
|---|---|---|
name | method name | Agent name. |
model | "" | "provider/model". When empty and used as a sub-agent, inherits the parent's model. |
instructions | "" | Static system prompt. A non-empty String returned by the method wins over this attribute. |
tools | {"*"} | Names of @Tool methods on the same object. {"*"} = all, {} = none. |
guardrails | {"*"} | Names of @GuardrailDef methods on the same object. Same wildcard rules. |
agents | {} | Names of other @AgentDef methods on the same object, used as sub-agents. |
strategy | HANDOFF | Multi-agent strategy. |
maxTurns | 25 | Maximum agent loop iterations. |
maxTokens | unset | LLM max_tokens (0 = unset). |
temperature | unset | Sampling temperature (NaN = unset). |
credentials | {} | Agent-level credential names. |
contextWindowBudget | unset | Proactive condensation threshold (0 = unset). |
Method contract. The return type declares what the method provides:
| Return type | Meaning |
|---|---|
void | Nothing — the annotation attributes alone define the agent. |
String | Dynamic instructions. A no-arg method is lazy: re-invoked on every run submission (when the config is serialized), so the prompt can reflect current state. A non-empty result wins over the instructions attribute. |
PromptTemplate | Server-side instructions template (instructionsTemplate); invoked once. |
Agent.Builder | The definition itself — the returned builder is built. |
Agent | The definition itself, returned as-is (full factory, CrewAI-style). |
The method may take no parameters, or a single Agent.Builder parameter — the escape hatch to the full builder API. The builder arrives pre-populated from the annotation and the discovered tools/guardrails/sub-agents; the method body can then apply anything the builder supports, including sub-agents defined in other classes. Builder-param methods are invoked exactly once (a customizer must not be replayed per run):
public class Research {
@AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.")
public void researcher(Agent.Builder builder) {
builder.termination(new MaxMessageTermination(10))
.agents(Agent.fromInstance(new Editing(), "editor"));
}
// full factory — annotation is a discovery marker; attributes other than name are rejected
@AgentDef
public Agent reviewer() {
return Agent.builder().name("reviewer").model("openai/gpt-4o")
.instructions("Review the draft.").build();
}
}
Agent.fromInstance(obj) resolves all @AgentDef methods on an object; Agent.fromInstance(obj, "name") resolves one. For factory methods the lookup name is still the annotation name/method name, while the agent keeps the name the factory set.
Dynamic instructions are also available directly on the builder, without the annotation: Agent.builder().instructions(() -> "Today is " + LocalDate.now()) — the supplier is re-evaluated on each run submission, matching the Python SDK's callable instructions.
Discovery rules. @AgentDef methods must be public (a non-public annotated method throws rather than being silently ignored) and cannot also carry @Tool or @GuardrailDef. Discovery walks the full type hierarchy — superclasses and interfaces (including default methods) — and the nearest annotated declaration wins. An unannotated override does not hide the agent: the ancestor's annotation is used and invocation dispatches to the override, so CGLIB-proxied Spring beans (@Transactional etc.) keep working. In Spring Boot apps, the auto-configured AgentCatalog collects @AgentDef agents from every bean.
Identity
| Builder method | Type | Default | Description |
|---|---|---|---|
name(String) | String | — | Required. Unique workflow name. Use snake_case. |
model(String) | String | — | Required. "provider/model", e.g. "openai/gpt-4o", "anthropic/claude-opus-4-8". |
instructions(String) | String | "" | System prompt. |
instructionsTemplate(PromptTemplate) | PromptTemplate | null | Prompt stored on the server; use PromptTemplate.of("name"). |
introduction(String) | String | null | Injected before the first user message (not the system prompt). |
metadata(Map<String,Object>) | Map | {} | Arbitrary key-value metadata stored with the workflow. |
LLM tuning
| Builder method | Type | Default | Description |
|---|---|---|---|
maxTurns(int) | int | 25 | Maximum agent loop iterations before termination. |
maxTokens(int) | int | null | LLM max_tokens. |
temperature(double) | double | null | LLM sampling temperature. |
thinkingBudgetTokens(int) | int | null | Extended thinking budget (Anthropic only). |
timeoutSeconds(int) | int | 0 | Overall agent execution timeout. 0 lets the server apply its own default. |
Tools
import org.conductoross.conductor.ai.internal.ToolRegistry;
Agent agent = Agent.builder()
.name("tool_agent")
.model("anthropic/claude-sonnet-4-6")
.tools(ToolRegistry.fromInstance(new MyTools())) // from @Tool-annotated POJO
.tools(HttpTool.builder()
.name("search")
.url("https://api.example.com/search")
.method("GET").build()) // HTTP tool
.build();
See Tools for all tool types.
Multi-agent
Agent pipeline = writer.then(editor); // .then() = Strategy.SEQUENTIAL shorthand
Agent team = Agent.builder()
.name("team")
.model("anthropic/claude-sonnet-4-6")
.agents(writer, editor, reviewer)
.strategy(Strategy.PARALLEL)
.build();
See Multi-Agent for all strategies.
Guardrails
import org.conductoross.conductor.ai.guardrail.RegexGuardrail;
import org.conductoross.conductor.ai.guardrail.LLMGuardrail;
import org.conductoross.conductor.ai.enums.Position;
import org.conductoross.conductor.ai.enums.OnFail;
Agent agent = Agent.builder()
.name("safe_agent")
.model("anthropic/claude-sonnet-4-6")
.guardrails(
RegexGuardrail.builder()
.name("no_phone").position(Position.OUTPUT)
.patterns("\\d{10}").onFail(OnFail.RAISE).build(),
LLMGuardrail.builder()
.name("tone_check").position(Position.OUTPUT)
.model("anthropic/claude-sonnet-4-6").policy("The tone must be professional.")
.onFail(OnFail.RETRY).build())
.build();
See Guardrails.
Credentials
Declare which secrets the agent's tools require. A capable Conductor server resolves the declared names at poll time and delivers them to the tool's per-call context; plaintext values never enter workflow input or output.
Agent agent = Agent.builder()
.name("github_agent")
.model("anthropic/claude-sonnet-4-6")
.credentials("GITHUB_TOKEN", "JIRA_API_KEY")
.build();
// In the tool — read the resolved secret off the injected ToolContext:
public String createIssue(String title, ToolContext ctx) {
String token = ctx.getCredential("GITHUB_TOKEN");
// ...
}
Code execution
Agent agent = Agent.builder()
.name("coder")
.model("anthropic/claude-sonnet-4-6")
.localCodeExecution(true)
.allowedLanguages(List.of("python", "javascript"))
.codeExecutionTimeout(30) // seconds per execution
.build();
Callbacks
Intercept the agent loop before or after each model call:
Agent agent = Agent.builder()
.name("observed_agent")
.model("anthropic/claude-sonnet-4-6")
.beforeModelCallback(ctx -> {
System.out.println("Calling LLM with: " + ctx.get("messages"));
return ctx; // return modified context or the original
})
.afterModelCallback(ctx -> {
System.out.println("LLM replied: " + ctx.get("output"));
return ctx;
})
.build();
For composable, reusable hooks — including onToolStart/onToolEnd and agent-level
start/end — use a CallbackHandler. See Callbacks.
Fallback
Run a second agent if the first exceeds fallbackMaxTurns:
Agent agent = Agent.builder()
.name("primary")
.model("anthropic/claude-sonnet-4-6")
.fallback(Agent.builder().name("backup").model("anthropic/claude-haiku-4-5-20251001").build())
.fallbackMaxTurns(5)
.build();
Running an agent
Use AgentRuntime as the SDK entry point and close it when the application stops. For a simple request/response flow:
try (AgentRuntime runtime = new AgentRuntime()) {
AgentResult result = runtime.run(agent, "Hello!");
System.out.println(result.getOutput());
}
AgentResult contains the output, terminal status, execution ID, tool calls, events, token usage, and any error. For background work use start and its AgentHandle; for incremental output use stream. AgentRuntime is thread-safe, so share it instead of creating one per request.
See AgentRuntime for every overload, deployment and serve modes, scheduling, and lifecycle details. See Deploy · Serve · Run · Plan to choose an execution mode.