feat: connect tool-calling loop + baseUrl config - makes agent runnable#2
Merged
Conversation
…e agent actually runnable This is the critical 'glue' that connects the existing tool implementations to Spring AI's automatic tool-calling loop, making the Java Claude Code actually capable of using tools (Read, Bash, Edit, etc.) autonomously. Changes: - AppSettings: add baseUrl field + getter/setter - SettingsLoader: load baseUrl from CCJ_BASE_URL / ANTHROPIC_BASE_URL / OPENAI_BASE_URL env vars - AnthropicModelFactory: add baseUrl param to createChatModel (was hardcoded null) - ChatClientFactory: pass baseUrl to all 3 factories (Anthropic/OpenAI Chat/ OpenAI Response); new createChatClientWithAdvisorAndTools() method that registers ToolCallbacks via .defaultToolCallbacks() - AppConfiguration: wire ToolRegistry.all() -> ToolCallbackAdapter -> List<ToolCallback> -> ChatClient; use @lazy to break chatClient <-> toolRegistry circular dependency - ClaudeCodeRequestAdvisor: skip history re-injection during ToolCallingAdvisor loop iterations (context flag); only write final (non-tool-call) response to memory How it works: Spring AI 2.0.0's DefaultChatClient auto-registers ToolCallingAdvisor when ToolCallbacks are present. ToolCallingAdvisor drives the full loop: detect tool_use -> execute ToolCallbackAdapter.call() -> send tool_result -> repeat until end_turn No manual loop implementation needed. Tests: +6 new (ToolCallbackAdapterWiringTest) All 252 tests green (246 existing + 6 new)
There was a problem hiding this comment.
Pull request overview
This PR wires the existing Tool implementations into Spring AI’s automatic tool-calling loop so the agent can actually execute tools, and adds configurable baseUrl support for Anthropic/OpenAI(-compatible) endpoints.
Changes:
- Registers
ToolRegistrytools as Spring AIToolCallbacks on theChatClient, enablingToolCallingAdvisor-driven tool execution loops. - Adds
baseUrltoAppSettingsand loads it from environment variables, passing it through to all model factories. - Adds loop-safety in
ClaudeCodeRequestAdvisorto avoid re-injecting history/system prompts during tool-calling iterations and to only persist final responses.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| ccc-test/src/test/java/com/ccj/test/ToolCallbackAdapterWiringTest.java | Adds tests validating tool callback wiring and baseUrl getter/setter behavior. |
| ccc-models/src/main/java/com/ccj/models/factory/ChatClientFactory.java | Passes baseUrl into model factories and adds a ChatClient builder variant that registers tool callbacks. |
| ccc-models/src/main/java/com/ccj/models/anthropic/AnthropicModelFactory.java | Accepts and forwards baseUrl to the Anthropic client setup. |
| ccc-config/src/main/java/com/ccj/config/SettingsLoader.java | Loads baseUrl from env vars into AppSettings. |
| ccc-config/src/main/java/com/ccj/config/AppSettings.java | Introduces baseUrl configuration field with accessors. |
| ccc-app/src/main/java/com/ccj/app/AppConfiguration.java | Wires ToolRegistry into ChatClient via ToolCallbackAdapter and resolves circular dependency with @Lazy. |
| ccc-app/src/main/java/com/ccj/app/advisor/ClaudeCodeRequestAdvisor.java | Prevents duplicate history/system injection during tool-calling loops and only persists final non-tool-call responses. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+91
to
+97
| com.ccj.core.tool.ToolUseContext ctxTemplate = new com.ccj.core.tool.ToolUseContext( | ||
| session.workingDirectory(), java.util.List.of(), () -> false, | ||
| new java.util.concurrent.ConcurrentHashMap<>(), session.id(), session); | ||
| java.util.List<org.springframework.ai.tool.ToolCallback> toolCallbacks = toolRegistry.all().stream() | ||
| .filter(Tool::isEnabled) | ||
| .map(t -> (org.springframework.ai.tool.ToolCallback) new com.ccj.tools.adapter.ToolCallbackAdapter(t, ctxTemplate)) | ||
| .toList(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the critical "glue" that connects the existing tool implementations to Spring AI's automatic tool-calling loop, making the Java Claude Code actually capable of using tools (Read, Bash, Edit, etc.) autonomously.
What was broken
The agent could send messages to the API and receive responses, but tool calls were silently dropped. The
AgentInvokeronly extracted.getText()from the response, ignoringtool_useblocks. No tools were registered on theChatClient, so the API never even returned tool calls.What this PR does
1. Tool callback wiring (the key fix)
AppConfigurationnow convertsToolRegistry.all()→ToolCallbackAdapter→List<ToolCallback>and registers them on theChatClientvia.defaultToolCallbacks()DefaultChatClientauto-registersToolCallingAdvisor, which drives the full tool-calling loop:tool_use→ executeToolCallbackAdapter.call()→ sendtool_result→ repeat untilend_turn2. baseUrl configuration
AppSettingsgains abaseUrlfieldSettingsLoaderloads it fromCCJ_BASE_URL/ANTHROPIC_BASE_URL/OPENAI_BASE_URLenv varsAnthropicModelFactoryacceptsbaseUrl(was hardcodednull)baseUrlthrough3. Advisor loop-safety
ClaudeCodeRequestAdvisor.before()uses a context flag to skip history re-injection duringToolCallingAdvisorloop iterations (prevents duplicate system prompts + history)ClaudeCodeRequestAdvisor.after()only writes the final (non-tool-call) response to memory4. Circular dependency resolution
chatClientdepends ontoolRegistry(for tool callbacks)toolRegistrydepends onchatClient(forAgentTool,WebSearchTool)@Lazyinjection on thechatClientparameter oftoolRegistryHow to run
Test Results
ToolCallbackAdapterWiringTest: verifies ToolRegistry → ToolCallbackAdapter → ToolCallback conversion, tool execution via callback, disabled tool filtering, baseUrl config