# agent-sdk **Repository Path**: greycode/agent-sdk ## Basic Information - **Project Name**: agent-sdk - **Description**: Java 环境下最简单的 Agent 框架。没有抽象。没有魔法。仅仅是一个工具调用的 for 循环。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-01-24 - **Last Updated**: 2026-06-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # bu-agent-sdk-java _An agent is just a for-loop._ ![Agent Loop](./static/agent-loop.png) The simplest possible agent framework for Java. No abstractions. No magic. Just a for-loop of tool calls. ## Requirements - JDK 21+ - Maven 3.8+ ## Installation Add to your `pom.xml`: ```xml com.buagent agent-sdk 1.0.0-SNAPSHOT ``` ## Quick Start ### One-Liner (Simplest) ```java import com.buagent.sdk.agent.Agent; // Auto-detects LLM from environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) String answer = Agent.ask("What is 2 + 2?"); ``` ### Quick Builder (Recommended) ```java import com.buagent.sdk.agent.Agent; // Auto-detects LLM, minimal configuration // Use try-with-resources to ensure proper resource cleanup try (Agent agent = Agent.quick() .systemPrompt("You are a helpful assistant.") .tools(new MyTools()) // Auto-scans @AgentTool methods .build()) { String result = agent.query("What is 2 + 3?").join(); System.out.println(result); } // Agent automatically closed ### Provider-Specific Builders ```java // OpenAI (requires OPENAI_API_KEY) Agent agent = Agent.withOpenAI() .model("gpt-4o") .systemPrompt("You are helpful.") .build(); // Anthropic (requires ANTHROPIC_API_KEY) Agent agent = Agent.withAnthropic() .model("claude-sonnet-4-20250514") .systemPrompt("You are helpful.") .build(); // Google (requires GOOGLE_API_KEY) Agent agent = Agent.withGoogle() .model("gemini-2.0-flash") .systemPrompt("You are helpful.") .build(); ``` ### Full Control ```java import com.buagent.sdk.agent.Agent; import com.buagent.sdk.core.exception.TaskComplete; import com.buagent.sdk.tools.core.AgentTool; import com.buagent.sdk.tools.reflect.ToolScanner; import com.buagent.sdk.llm.client.openai.OpenAIChatModel; public class Example { @AgentTool("Add two numbers") public int add(int a, int b) { return a + b; } @AgentTool("Signal task completion") public void done(String message) { throw new TaskComplete(message); } public static void main(String[] args) { // Scan for @AgentTool annotated methods var tools = ToolScanner.scan(new Example()); // Create agent Agent agent = Agent.builder() .llm(new OpenAIChatModel("gpt-4o", System.getenv("OPENAI_API_KEY"))) .tools(tools) .systemPrompt("You are a helpful assistant.") .build(); // Query the agent String result = agent.query("What is 2 + 3?").join(); System.out.println(result); } } ``` ## Philosophy **The Bitter Lesson:** All the value is in the RL'd model, not your 10,000 lines of abstractions. Agent frameworks fail not because models are weak, but because their action spaces are incomplete. Give the LLM as much freedom as possible, then vibe-restrict based on evals. ## Features ### Done Tool Pattern The naive "stop when no tool calls" approach fails. Agents finish prematurely. Force explicit completion: ```java @AgentTool("Signal completion") public void done(String message) { throw new TaskComplete(message); } Agent agent = Agent.builder() .llm(llm) .tools(tools) .requireDoneTool(true) // Autonomous mode .build(); ``` ### Ephemeral Messages Large tool outputs (browser state, screenshots) blow up context. Keep only the last N: ```java @AgentTool(value = "Get browser state", ephemeral = 3) // Keep last 3 only public String getState() { return massiveDomAndScreenshot; } ``` ### Simple LLM Primitives Same interface across providers. Full control: ```java import com.buagent.sdk.llm.client.openai.OpenAIChatModel; // All implement BaseChatModel Agent agent = Agent.builder() .llm(new OpenAIChatModel("gpt-4o", apiKey)) .tools(tools) .build(); ``` ### Dependency Injection FastAPI-style, type-safe: ```java import com.buagent.sdk.tools.di.DependencyContainer; @AgentTool("Query users") public String getUser( int id, @com.buagent.sdk.tools.di.Inject(DatabaseSession.class) DatabaseSession db ) { return db.find(id); } // Configure dependencies DependencyContainer deps = DependencyContainer.create(); deps. register(DatabaseSession .class, () ->new DatabaseSession()); Agent agent = Agent.builder() .llm(llm) .tools(tools) .dependencies(deps) .build(); ``` ### Programmatic Tool Definition Define tools programmatically with the fluent `Tool.Builder` API: ```java import com.buagent.sdk.tools.core.Tool; import com.buagent.sdk.tools.core.ToolParamDef; import java.util.concurrent.CompletableFuture; // Simple tool with inline parameters Tool addTool = Tool.builder() .name("add") .description("Add two numbers") .parameter("a", int.class, "First number", true) .parameter("b", int.class, "Second number", true) .executor(args -> { int a = ((Number) args.get("a")).intValue(); int b = ((Number) args.get("b")).intValue(); return CompletableFuture.completedFuture(a + b); }) .build(); // Advanced tool with ToolParamDef for more control Tool searchTool = Tool.builder() .name("search") .description("Search documents") .parameter(ToolParamDef.builder() .name("query") .type(String.class) .description("Search query") .required(true) .build()) .parameter(ToolParamDef.builder() .name("format") .type(String.class) .description("Output format") .enumValues("json", "xml", "csv") // Enum constraint .required(false) .defaultValue("json") .build()) .ephemeral(3) // Keep only last 3 outputs in context .executor(args -> CompletableFuture.completedFuture("results...")) .build(); // Use with Agent Agent agent = Agent.quick() .tools(addTool, searchTool) .build(); ``` ### Streaming Events Events are organized into categories for easier handling: ```java import com.buagent.sdk.agent.events.*; agent.queryStreamAsync("do something").subscribe(new Flow.Subscriber<>() { @Override public void onNext(AgentEvent event) { // Handle by category if (event instanceof StreamingEvent streaming) { // Real-time chunks: TextChunkEvent, ThinkingChunkEvent, ToolCallChunkEvent switch (streaming) { case TextChunkEvent e -> System.out.print(e.getDelta()); case ThinkingChunkEvent e -> System.out.print("[thinking] " + e.getDelta()); case ToolCallChunkEvent e -> System.out.print("[tool] " + e.getDelta()); } } else if (event instanceof ToolEvent tool) { // Tool interactions: ToolCallEvent, ToolResultEvent switch (tool) { case ToolCallEvent e -> System.out.println("Calling " + e.getTool()); case ToolResultEvent e -> System.out.println(e.getTool() + " -> " + e.getResult()); } } else if (event instanceof ContentEvent content) { // Complete content: TextEvent, ThinkingEvent, FinalResponseEvent switch (content) { case FinalResponseEvent e -> System.out.println("Done: " + e.getContent()); default -> {} } } else if (event instanceof TerminalEvent terminal) { // Errors: ErrorEvent, StreamingCancelledEvent switch (terminal) { case ErrorEvent e -> System.err.println("Error: " + e.getMessage()); case StreamingCancelledEvent e -> System.out.println("Cancelled: " + e.getReason()); } } // Other categories: LifecycleEvent, MetadataEvent } // ... other subscriber methods }); ``` **Event Categories:** - `StreamingEvent` - Real-time streaming chunks (TextChunkEvent, ThinkingChunkEvent, ToolCallChunkEvent) - `ContentEvent` - Complete content (TextEvent, ThinkingEvent, FinalResponseEvent) - `ToolEvent` - Tool interactions (ToolCallEvent, ToolResultEvent) - `LifecycleEvent` - Execution lifecycle (StepStartEvent, StepCompleteEvent, MessageStartEvent, MessageCompleteEvent) - `TerminalEvent` - Execution termination (ErrorEvent, StreamingCancelledEvent) - `MetadataEvent` - Supplementary info (UsageEvent, HiddenUserMessageEvent) ### Configuration Groups Simplify Agent configuration with grouped settings: ```java import com.buagent.sdk.agent.config.RetryConfig; import com.buagent.sdk.agent.config.StreamingConfig; // Grouped configuration (recommended) Agent agent = Agent.builder() .llm(llm) .tools(tools) .retry(RetryConfig.builder() .maxRetries(3) .baseDelay(2.0) .build()) .streaming(StreamingConfig.streaming()) .build(); // Quick creation with factory methods Agent simpleAgent = Agent.simple(llm, "You are a helpful assistant.", tools); // Streaming-enabled builder Agent streamingAgent = Agent.streamingBuilder() .llm(llm) .tools(tools) .build(); ``` **RetryConfig** - LLM retry settings: - `RetryConfig.defaults()` - Default retry (5 retries, 1-60s delay) - `RetryConfig.noRetry()` - Disable retries - `RetryConfig.forRateLimiting()` - Optimized for rate limits (10 retries, longer delays) **StreamingConfig** - Streaming settings: - `StreamingConfig.defaults()` - Streaming disabled - `StreamingConfig.streaming()` - Enable LLM streaming - `StreamingConfig.streaming(options)` - Enable with custom options ### Lifecycle Hooks Intercept Agent execution at key lifecycle points for logging, auditing, validation, or control: ```java import com.buagent.sdk.agent.hook.*; // Create a custom hook AgentHook loggingHook = new AgentHook() { @Override public int priority() { return -100; } // Lower = higher priority @Override public boolean beforeLLMInvoke(BeforeLLMContext context) { System.out.println("LLM call #" + context.getIteration()); context.setPayload("startTime", System.currentTimeMillis()); // Pass data between hooks return true; // Continue execution (false = abort) } @Override public boolean afterLLMInvoke(AfterLLMContext context) { long duration = System.currentTimeMillis() - context.getPayload("startTime", Long.class).orElse(0L); System.out.println("LLM completed in " + duration + "ms"); return true; } @Override public boolean beforeToolExecute(BeforeToolContext context) { System.out.println("Executing tool: " + context.getToolName()); return true; } @Override public boolean afterToolExecute(AfterToolContext context) { System.out.println("Tool " + context.getToolName() + " took " + context.getExecutionTimeMs() + "ms"); return true; } }; // Use with Agent Agent agent = Agent.builder() .llm(llm) .tools(tools) .hook(loggingHook) // Single hook .build(); // Or use HookManager for multiple hooks HookManager hookManager = new HookManager(); hookManager.register(loggingHook); hookManager.register(auditHook); Agent agent = Agent.builder() .llm(llm) .tools(tools) .hookManager(hookManager) // Multiple hooks with priority ordering .build(); ``` **Hook Features:** - **4 lifecycle points**: `beforeLLMInvoke`, `afterLLMInvoke`, `beforeToolExecute`, `afterToolExecute` - **Priority ordering**: Lower priority number executes first - **Execution control**: Return `false` to abort execution - **Payload passing**: Share data between hooks via `setPayload`/`getPayload` - **Rich context**: Access messages, tools, token usage, and more ## Claude Code in ~150 Lines A sandboxed coding assistant with dependency injection: ```java import com.buagent.sdk.agent.Agent; import com.buagent.sdk.core.exception.TaskComplete; import com.buagent.sdk.llm.client.openai.OpenAIChatModel; import com.buagent.sdk.tools.core.AgentTool; import com.buagent.sdk.tools.core.ToolParam; import com.buagent.sdk.tools.di.DependencyContainer; import com.buagent.sdk.tools.di.Inject; import com.buagent.sdk.tools.reflect.ToolScanner; // Sandbox context for secure file operations public class SandboxContext { private final Path rootDir; private Path workingDir; public Path resolvePath(String path) { Path resolved = workingDir.resolve(path).normalize(); if (!resolved.startsWith(rootDir)) { throw new SecurityException("Path escapes sandbox: " + path); } return resolved; } } // Claude Code-style tools public class ClaudeCodeTools { @AgentTool("Execute shell command") public String bash( @ToolParam("The command to execute") String command, @Inject(SandboxContext.class) SandboxContext ctx) { ProcessBuilder pb = new ProcessBuilder("bash", "-c", command); pb.directory(ctx.getWorkingDir().toFile()); // ... execute and return output } @AgentTool("Read file contents") public String read( @ToolParam("Path to the file") String filePath, @Inject(SandboxContext.class) SandboxContext ctx) { return Files.readString(ctx.resolvePath(filePath)); } @AgentTool("Write file contents") public String write( @ToolParam("Path to the file") String filePath, @ToolParam("Content to write") String content, @Inject(SandboxContext.class) SandboxContext ctx) { Files.writeString(ctx.resolvePath(filePath), content); return "Wrote " + content.length() + " bytes"; } @AgentTool("Find files by glob pattern") public String glob( @ToolParam("The glob pattern") String pattern, @Inject(SandboxContext.class) SandboxContext ctx) { // ... glob search implementation } @AgentTool("Signal task completion") public String done(@ToolParam("Completion message") String message) { throw new TaskComplete(message); } } public static void main(String[] args) { SandboxContext ctx = SandboxContext.create(Path.of("./sandbox")); DependencyContainer deps = DependencyContainer.create(); deps.register(SandboxContext.class, () -> ctx); var tools = ToolScanner.scan(new ClaudeCodeTools()); Agent agent = Agent.builder() .llm(new OpenAIChatModel("gpt-4o", System.getenv("OPENAI_API_KEY"))) .tools(tools) .dependencies(deps) .systemPrompt("Coding assistant. Working dir: " + ctx.getWorkingDir()) .build(); // Interactive loop while (true) { String task = new Scanner(System.in).nextLine(); agent.queryStreamAsync(task).subscribe(/* event handler */); } } ``` See [`ClaudeCodeExample.java`](./src/main/java/com/buagent/sdk/examples/ClaudeCodeExample.java) for the full version with grep, edit, and todo tools. ## The Bitter Truth Every abstraction is a liability. Every "helper" is a failure point. The models got good. Really good. They were RL'd on computer use, coding, browsing. They don't need your guardrails. They need: - A complete action space - A for-loop - An explicit exit - Context management **The bitter lesson: The less you build, the more it works.** ## Build ```bash mvn clean compile ``` ## License MIT ## Credits Built by [Browser Use](https://browser-use.com). Inspired by reverse-engineering Claude Code and Gemini CLI.