Skip to content

feat: connect tool-calling loop + baseUrl config - makes agent runnable#2

Merged
Sj295 merged 1 commit into
mainfrom
feat/runnable-tool-loop
Jul 17, 2026
Merged

feat: connect tool-calling loop + baseUrl config - makes agent runnable#2
Sj295 merged 1 commit into
mainfrom
feat/runnable-tool-loop

Conversation

@Sj295

@Sj295 Sj295 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

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 AgentInvoker only extracted .getText() from the response, ignoring tool_use blocks. No tools were registered on the ChatClient, so the API never even returned tool calls.

What this PR does

1. Tool callback wiring (the key fix)

  • AppConfiguration now converts ToolRegistry.all()ToolCallbackAdapterList<ToolCallback> and registers them on the ChatClient via .defaultToolCallbacks()
  • Spring AI 2.0.0's DefaultChatClient auto-registers ToolCallingAdvisor, which drives the full tool-calling loop:
    • detect tool_use → execute ToolCallbackAdapter.call() → send tool_result → repeat until end_turn
  • No manual loop implementation needed — Spring AI handles it all

2. baseUrl configuration

  • AppSettings gains a baseUrl field
  • SettingsLoader loads it from CCJ_BASE_URL / ANTHROPIC_BASE_URL / OPENAI_BASE_URL env vars
  • AnthropicModelFactory accepts baseUrl (was hardcoded null)
  • All 3 factories (Anthropic/OpenAI Chat/OpenAI Response) now pass baseUrl through

3. Advisor loop-safety

  • ClaudeCodeRequestAdvisor.before() uses a context flag to skip history re-injection during ToolCallingAdvisor loop iterations (prevents duplicate system prompts + history)
  • ClaudeCodeRequestAdvisor.after() only writes the final (non-tool-call) response to memory

4. Circular dependency resolution

  • chatClient depends on toolRegistry (for tool callbacks)
  • toolRegistry depends on chatClient (for AgentTool, WebSearchTool)
  • Resolved with @Lazy injection on the chatClient parameter of toolRegistry

How to run

# Anthropic official
export ANTHROPIC_API_KEY=sk-ant-xxx
mvn -pl ccc-app spring-boot:run

# Third-party OpenAI-compatible API (e.g. OpenRouter)
export OPENAI_API_KEY=sk-xxx
export CCJ_PROVIDER=openai-chat
export CCJ_BASE_URL=https://openrouter.ai/api/v1
export CCJ_MODEL=anthropic/claude-sonnet-4
mvn -pl ccc-app spring-boot:run

# Anthropic proxy
export ANTHROPIC_API_KEY=sk-ant-xxx
export CCJ_BASE_URL=https://my-proxy.com/v1
mvn -pl ccc-app spring-boot:run

Test Results

  • 252 tests pass (246 existing + 6 new)
  • New ToolCallbackAdapterWiringTest: verifies ToolRegistry → ToolCallbackAdapter → ToolCallback conversion, tool execution via callback, disabled tool filtering, baseUrl config

…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)
Copilot AI review requested due to automatic review settings July 17, 2026 10:36
@Sj295
Sj295 merged commit 9539d3d into main Jul 17, 2026
@Sj295
Sj295 deleted the feat/runnable-tool-loop branch July 17, 2026 10:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ToolRegistry tools as Spring AI ToolCallbacks on the ChatClient, enabling ToolCallingAdvisor-driven tool execution loops.
  • Adds baseUrl to AppSettings and loads it from environment variables, passing it through to all model factories.
  • Adds loop-safety in ClaudeCodeRequestAdvisor to 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();
Sj295 added a commit that referenced this pull request Jul 17, 2026
fix: address all Copilot review issues (PR #1, #2, #3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants