You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When using BasePlugin (via InMemoryRunner with custom plugins) to monitor an agent's execution flow that involves tool calling, the plugin callbacks are not properly invoked. Specifically, only beforeModelCallback from the first model call and afterModelCallback from the last model call are fired. All other callbacks — including afterModelCallback from the first model call, beforeToolCallback/afterToolCallback for tool execution, and beforeModelCallback from the second model call — are completely skipped.
This issue applies not only to a single tool call scenario but also to multiple tool calls. When an agent executes N tool calls in sequence, the expected flow would involve (N+1) model calls and N tool executions, resulting in (N+1) beforeModelCallback/afterModelCallback pairs and N beforeToolCallback/afterToolCallback pairs. However, only the very first beforeModelCallback and the very last afterModelCallback are ever fired — all intermediate callbacks across all tool executions and model calls are silently dropped.
In a typical agent flow with tool use (single tool call example):
This makes it impossible to monitor or intercept any intermediate steps in the agent execution, which is critical for observability, logging, and debugging in production agent systems.
Steps to Reproduce:
Create an LlmAgent with a FunctionTool and a custom BasePlugin that overrides beforeModelCallback, afterModelCallback, beforeToolCallback, and afterToolCallback with logging.
Create an InMemoryRunner with the agent and the plugin.
Send a user message that triggers the agent to call the tool.
Observe the logs — only beforeModelCallback from the first model call and afterModelCallback from the last model call are logged. All other callbacks (first afterModelCallback, tool callbacks, second beforeModelCallback) are missing.
Expected Behavior:
For an agent flow that involves one tool call, the plugin callbacks should be invoked as follows:
beforeModelCallback — before the 1st model call (user input → model)
afterModelCallback — after the 1st model response (model returns tool call)
beforeToolCallback — before tool execution
afterToolCallback — after tool execution
beforeModelCallback — before the 2nd model call (tool result → model)
afterModelCallback — after the 2nd model response (final answer)
All 6 callbacks should be triggered, providing full observability into the agent's execution lifecycle.
Observed Behavior:
Only beforeModelCallback from the first model call and afterModelCallback from the last model call are fired. All other callbacks are silently skipped: afterModelCallback from the first model call, beforeToolCallback/afterToolCallback for tool execution, and beforeModelCallback from the second model call. This pattern holds for any number of tool calls — all intermediate process callbacks are dropped, regardless of how many tool executions occur in between. The tools ARE executed (the final response reflects tool usage), but the plugin is never notified of any intermediate steps.
Log output shows:
10:25:46.225 [main] INFO ... -- [myTestPlugin] 🧠 beforeModelCallback - input: [{"parts":[{"text":"my input is abc,usu your input to change the input"}],"role":"user"}]
10:25:53.570 [main] INFO ... -- Request completed successfully: model=openai, type=chat, duration=7338ms, tokens=381
10:25:53.575 [main] INFO ... -- [myTestPlugin] 🧠 afterModelCallback - Response: {...}
the result is : I can help you change your input using the available tool!...
Missing logs:
No afterModelCallback for the 1st model call (after model returns tool call request)
No beforeToolCallback / afterToolCallback for the tool execution
No beforeModelCallback for the 2nd model call (before model processes tool result)
Which model is being used: GLM-5 (via OpenAI-compatible API using SpringAI adapter)
🟡 Optional Information
Regression:
Unknown — this is the first version tested.
Logs:
10:25:46.225 [main] INFO c.b.a.t.a.a.SequentialAgentTest$MyTestPlugin -- [myTestPlugin] 🧠 beforeModelCallback -input: [{"parts":[{"codeExecutionResult":null,"executableCode":null,"fileData":null,"functionCall":null,"functionResponse":null,"inlineData":null,"mediaResolution":null,"partMetadata":null,"text":"my input is abc,usu your input to change the input","thought":null,"thoughtSignature":null,"toolCall":null,"toolResponse":null,"videoMetadata":null}],"role":"user"}]
10:25:53.570 [main] INFO c.g.a.m.s.o.SpringAIObservabilityHandler -- Request completed successfully: model=openai, type=chat, duration=7338ms, tokens=381
10:25:53.575 [main] INFO c.b.a.t.a.a.SequentialAgentTest$MyTestPlugin -- [myTestPlugin] 🧠 afterModelCallback - Response: {"avgLogprobs":null,"content":{"parts":[{"codeExecutionResult":null,"executableCode":null,"fileData":null,"functionCall":null,"functionResponse":null,"inlineData":null,"mediaResolution":null,"partMetadata":null,"text":"I can help you change your input using the available tool! Your current input is \"abc\", but I need to know what you'd like to change it to. \n\nWhat would you like your new input to be?","thought":null,"thoughtSignature":null,"toolCall":null,"toolResponse":null,"videoMetadata":null}],"role":"model"},"customMetadata":null,"errorCode":null,"errorMessage":null,"finishReason":null,"groundingMetadata":null,"inputTranscription":null,"interrupted":null,"modelVersion":null,"outputTranscription":null,"partial":false,"turnComplete":true,"usageMetadata":null}
the result is : I can help you change your input using the available tool! Your current input is "abc", but I need to know what you'd like to change it to.
What would you like your new input to be?
Screenshots / Video:
N/A
Additional Context:
This is a critical issue for production agent systems that rely on plugin callbacks for observability, audit logging, rate limiting, and error handling. Without callbacks properly firing for each step (first model call's afterModelCallback, tool execution's beforeToolCallback/afterToolCallback, and subsequent model calls' beforeModelCallback/afterModelCallback), it is impossible to build reliable monitoring around the agent's full execution lifecycle. The problem is especially severe for agents with multiple tool calls, where the vast majority of the execution process is completely invisible to plugins.
Minimal Reproduction Code:
packagecn.bugstack.ai.test.api.agent;
importcom.alibaba.fastjson.JSON;
importcom.fasterxml.jackson.annotation.JsonInclude;
importcom.fasterxml.jackson.annotation.JsonProperty;
importcom.fasterxml.jackson.annotation.JsonPropertyDescription;
importcom.google.adk.agents.*;
importcom.google.adk.events.Event;
importcom.google.adk.models.LlmRequest;
importcom.google.adk.models.LlmResponse;
importcom.google.adk.models.springai.SpringAI;
importcom.google.adk.plugins.BasePlugin;
importcom.google.adk.runner.InMemoryRunner;
importcom.google.adk.sessions.Session;
importcom.google.adk.tools.BaseTool;
importcom.google.adk.tools.ToolContext;
importcom.google.genai.types.Content;
importcom.google.genai.types.Part;
importio.reactivex.rxjava3.core.Flowable;
importio.reactivex.rxjava3.core.Maybe;
importlombok.Data;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.ai.chat.model.ChatModel;
importorg.springframework.ai.openai.OpenAiChatModel;
importorg.springframework.ai.openai.OpenAiChatOptions;
importorg.springframework.ai.openai.api.OpenAiApi;
importorg.springframework.ai.tool.ToolCallback;
importorg.springframework.ai.tool.annotation.Tool;
importorg.springframework.ai.tool.method.MethodToolCallbackProvider;
importjava.util.ArrayList;
importjava.util.List;
importjava.util.Map;
@Slf4jpublicclassSequentialAgentTest {
privatestaticfinalStringAPP_NAME = "CodePipelineAgent";
privatestaticfinalStringUSER_ID = "test_user_456";
publicstaticvoidmain(String[] args) {
OpenAiApiopenAiApi = OpenAiApi.builder()
.baseUrl("https://xxxxx")
.apiKey("sk-xxxxx")
.completionsPath("v1/chat/completions")
.build();
List<ToolCallback> toolCallbackList = newArrayList<>();
toolCallbackList.addAll(List.of(MethodToolCallbackProvider.builder().toolObjects(newMyTestMcpService()).build().getToolCallbacks()));
ChatModelchatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model("modeName")
.toolCallbacks(toolCallbackList)
.build())
.build();
// Create an InMemoryRunnerList<BasePlugin> plugins = newArrayList<>();
plugins.add(newMyTestPlugin("myTestPlugin"));
InMemoryRunnerrunner = newInMemoryRunner(codeWriterAgent(chatModel), APP_NAME,plugins);
// InMemoryRunner automatically creates a session service. Create a session using the serviceSessionsession = runner.sessionService().createSession(APP_NAME, USER_ID).blockingGet();
ContentuserMessage = Content.fromParts(Part.fromText("my input is abc,usu your input to change the input"));
// Run the agentFlowable<Event> eventStream = runner.runAsync(USER_ID, session.id(), userMessage);
// Stream event responseeventStream.blockingForEach(
event -> {
if (event.finalResponse()) {
System.out.println("the result is : "+ event.stringifyContent());
}
});
}
staticLlmAgentcodeWriterAgent(ChatModelchatModel) {
returnLlmAgent.builder()
.model(newSpringAI(chatModel))
.name("myAgent")
.description("you must use tool to change the input")
.instruction(
""" usu your tool to change the input """)
.outputKey("generated_code")
.build();
}
@Slf4jstaticclassMyTestPluginextendsBasePlugin {
publicMyTestPlugin(Stringname) {
super(name);
}
@OverridepublicMaybe<LlmResponse> beforeModelCallback(CallbackContextcallbackContext, LlmRequest.BuilderllmRequestBuilder) {
log.info("[myTestPlugin] 🧠 beforeModelCallback -input: {}", JSON.toJSONString(llmRequestBuilder.build().contents()));
returnsuper.beforeModelCallback(callbackContext, llmRequestBuilder);
}
@OverridepublicMaybe<LlmResponse> afterModelCallback(CallbackContextcallbackContext, LlmResponsellmResponse) {
log.info("[myTestPlugin] 🧠 afterModelCallback - Response: {}", JSON.toJSONString(llmResponse));
returnsuper.afterModelCallback(callbackContext, llmResponse);
}
@OverridepublicMaybe<Map<String, Object>> beforeToolCallback(BaseTooltool, Map<String, Object> toolArgs, ToolContexttoolContext) {
log.info("[myTestPlugin] 🔧 beforeToolCallback - toolName: {}", tool.name());
log.info("[myTestPlugin] 🔧 toolArgs: {}", toolArgs);
returnsuper.beforeToolCallback(tool, toolArgs, toolContext);
}
@OverridepublicMaybe<Map<String, Object>> afterToolCallback(BaseTooltool, Map<String, Object> toolArgs, ToolContexttoolContext, Map<String, Object> result) {
log.info("[myTestPlugin] 🔧 afterToolCallback - toolName: {}", tool.name());
log.info("[myTestPlugin] 🔧 toolArgs: {}", toolArgs);
log.info("[myTestPlugin] 🔧 toolContext: {}", toolContext);
log.info("[myTestPlugin] 🔧 result: {}", result);
returnsuper.afterToolCallback(tool, toolArgs, toolContext, result);
}
}
@Slf4jpublicstaticclassMyTestMcpService {
@Tool(description = "a funtion that you must use when you have input")
publicstaticcn.bugstack.ai.domain.agent.service.armory.matter.mcp.server.MyTestMcpService.XxxResponsechangeInput(cn.bugstack.ai.domain.agent.service.armory.matter.mcp.server.MyTestMcpService.XxxRequestrequest) {
cn.bugstack.ai.domain.agent.service.armory.matter.mcp.server.MyTestMcpService.XxxResponsexxxResponse = newcn.bugstack.ai.domain.agent.service.armory.matter.mcp.server.MyTestMcpService.XxxResponse();
xxxResponse.setContent((request.getWord().toUpperCase() + "123").getBytes().length);
returnxxxResponse;
}
@Data@JsonInclude(JsonInclude.Include.NON_NULL)
publicstaticclassXxxRequest {
@JsonProperty(required = true, value = "word")
@JsonPropertyDescription("input")
privateStringword;
}
@Data@JsonInclude(JsonInclude.Include.NON_NULL)
publicstaticclassXxxResponse {
@JsonProperty(required = true, value = "content")
@JsonPropertyDescription("result")
privateIntegercontent;
}
}
}
Describe the Bug:
When using
BasePlugin(viaInMemoryRunnerwith custom plugins) to monitor an agent's execution flow that involves tool calling, the plugin callbacks are not properly invoked. Specifically, onlybeforeModelCallbackfrom the first model call andafterModelCallbackfrom the last model call are fired. All other callbacks — includingafterModelCallbackfrom the first model call,beforeToolCallback/afterToolCallbackfor tool execution, andbeforeModelCallbackfrom the second model call — are completely skipped.This issue applies not only to a single tool call scenario but also to multiple tool calls. When an agent executes N tool calls in sequence, the expected flow would involve (N+1) model calls and N tool executions, resulting in (N+1)
beforeModelCallback/afterModelCallbackpairs and NbeforeToolCallback/afterToolCallbackpairs. However, only the very firstbeforeModelCallbackand the very lastafterModelCallbackare ever fired — all intermediate callbacks across all tool executions and model calls are silently dropped.In a typical agent flow with tool use (single tool call example):
beforeModelCallbackfired ✅,afterModelCallbackNOT fired ❌beforeToolCallbackandafterToolCallbackNOT fired ❌beforeModelCallbackNOT fired ❌, onlyafterModelCallbackfired ✅This makes it impossible to monitor or intercept any intermediate steps in the agent execution, which is critical for observability, logging, and debugging in production agent systems.
Steps to Reproduce:
LlmAgentwith aFunctionTooland a customBasePluginthat overridesbeforeModelCallback,afterModelCallback,beforeToolCallback, andafterToolCallbackwith logging.InMemoryRunnerwith the agent and the plugin.beforeModelCallbackfrom the first model call andafterModelCallbackfrom the last model call are logged. All other callbacks (firstafterModelCallback, tool callbacks, secondbeforeModelCallback) are missing.Expected Behavior:
For an agent flow that involves one tool call, the plugin callbacks should be invoked as follows:
beforeModelCallback— before the 1st model call (user input → model)afterModelCallback— after the 1st model response (model returns tool call)beforeToolCallback— before tool executionafterToolCallback— after tool executionbeforeModelCallback— before the 2nd model call (tool result → model)afterModelCallback— after the 2nd model response (final answer)All 6 callbacks should be triggered, providing full observability into the agent's execution lifecycle.
Observed Behavior:
Only
beforeModelCallbackfrom the first model call andafterModelCallbackfrom the last model call are fired. All other callbacks are silently skipped:afterModelCallbackfrom the first model call,beforeToolCallback/afterToolCallbackfor tool execution, andbeforeModelCallbackfrom the second model call. This pattern holds for any number of tool calls — all intermediate process callbacks are dropped, regardless of how many tool executions occur in between. The tools ARE executed (the final response reflects tool usage), but the plugin is never notified of any intermediate steps.Log output shows:
Missing logs:
afterModelCallbackfor the 1st model call (after model returns tool call request)beforeToolCallback/afterToolCallbackfor the tool executionbeforeModelCallbackfor the 2nd model call (before model processes tool result)Environment Details:
1.5.0(com.google.adk:google-adk:1.5.0,com.google.adk:google-adk-spring-ai:1.5.0)Model Information:
SpringAIadapter)🟡 Optional Information
Regression:
Unknown — this is the first version tested.
Logs:
Screenshots / Video:
N/A
Additional Context:
This is a critical issue for production agent systems that rely on plugin callbacks for observability, audit logging, rate limiting, and error handling. Without callbacks properly firing for each step (first model call's
afterModelCallback, tool execution'sbeforeToolCallback/afterToolCallback, and subsequent model calls'beforeModelCallback/afterModelCallback), it is impossible to build reliable monitoring around the agent's full execution lifecycle. The problem is especially severe for agents with multiple tool calls, where the vast majority of the execution process is completely invisible to plugins.Minimal Reproduction Code:
How often has this issue occurred?: