Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,13 @@ public Map<String, Object> getExtendedParameters() {
}
return extendedModel.mergeWithBaseSchema(tool.getParameters());
}

/**
* Creates a deep copy of this RegisteredToolFunction.
* Ensures mutable states (like presetParameters) are isolated.
*/
public RegisteredToolFunction copy() {
return new RegisteredToolFunction(
this.tool, this.extendedModel, this.mcpClientName, this.presetParameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,26 @@ class ToolMethodInvoker {
/**
* Invoke tool method asynchronously with custom converter support.
*
* @param toolObject the object containing the method
* @param method the method to invoke
* <p>To support AOP proxies (e.g., Spring CGLIB or JDK Dynamic Proxies), this method strictly
* separates metadata extraction from the actual reflection execution:
* <ul>
* <li>{@code originalMethod}: The unproxied method. Used exclusively to extract annotations,
* parameter names, and generic return types (which are often lost on proxy classes).</li>
* <li>{@code executableMethod}: The method on the proxy object. Used exclusively to ensure
* that aspects (e.g., transactions, security) are correctly triggered.</li>
* </ul>
*
* @param toolObject the object containing the method (can be a proxy instance)
* @param originalMethod the original method used for reading metadata and generic types
* @param executableMethod the actual method to invoke on the toolObject
* @param param the tool call parameters containing input, toolUseBlock, agent, and context
* @param customConverter custom converter for this invocation (null to use default)
* @return Mono containing ToolResultBlock
*/
Mono<ToolResultBlock> invokeAsync(
Object toolObject,
Method method,
Method originalMethod,
Method executableMethod,
ToolCallParam param,
ToolResultConverter customConverter) {
// Use custom converter if provided, otherwise use default
Expand All @@ -64,18 +75,20 @@ Mono<ToolResultBlock> invokeAsync(
ToolExecutionContext context = param.getContext();
ToolEmitter emitter = param.getEmitter();

Class<?> returnType = method.getReturnType();
Class<?> returnType = originalMethod.getReturnType();

if (returnType == CompletableFuture.class) {
// Async method returning CompletableFuture: invoke and convert to Mono
return Mono.fromCallable(
() -> {
method.setAccessible(true);
executableMethod.setAccessible(true);
Object[] args =
convertParameters(method, input, agent, context, emitter);
convertParameters(
originalMethod, input, agent, context, emitter);
@SuppressWarnings("unchecked")
CompletableFuture<Object> future =
(CompletableFuture<Object>) method.invoke(toolObject, args);
(CompletableFuture<Object>)
executableMethod.invoke(toolObject, args);
return future;
})
.flatMap(
Expand All @@ -84,36 +97,47 @@ Mono<ToolResultBlock> invokeAsync(
.map(
r ->
converter.convert(
r, extractGenericType(method)))
r,
extractGenericType(
originalMethod)))
.onErrorResume(this::handleError))
.onErrorResume(this::handleError);

} else if (returnType == Mono.class) {
// Async method returning Mono: invoke and flatMap
return Mono.fromCallable(
() -> {
method.setAccessible(true);
executableMethod.setAccessible(true);
Object[] args =
convertParameters(method, input, agent, context, emitter);
convertParameters(
originalMethod, input, agent, context, emitter);
@SuppressWarnings("unchecked")
Mono<Object> mono = (Mono<Object>) method.invoke(toolObject, args);
Mono<Object> mono =
(Mono<Object>) executableMethod.invoke(toolObject, args);
return mono;
})
.flatMap(
mono ->
mono.map(r -> converter.convert(r, extractGenericType(method)))
mono.map(
r ->
converter.convert(
r,
extractGenericType(
originalMethod)))
.onErrorResume(this::handleError))
.onErrorResume(this::handleError);

} else {
// Sync method: wrap in Mono.fromCallable
return Mono.fromCallable(
() -> {
method.setAccessible(true);
executableMethod.setAccessible(true);
Object[] args =
convertParameters(method, input, agent, context, emitter);
Object result = method.invoke(toolObject, args);
return converter.convert(result, method.getGenericReturnType());
convertParameters(
originalMethod, input, agent, context, emitter);
Object result = executableMethod.invoke(toolObject, args);
return converter.convert(
result, originalMethod.getGenericReturnType());
})
.onErrorResume(this::handleError);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,11 @@ void copyTo(ToolRegistry target) {
String toolName = entry.getKey();
AgentTool tool = entry.getValue();
RegisteredToolFunction registered = registeredTools.get(toolName);
target.registerTool(toolName, tool, registered);
if (registered == null) {
continue;
}

target.registerTool(toolName, tool, registered.copy());
}
}
}
89 changes: 75 additions & 14 deletions agentscope-core/src/main/java/io/agentscope/core/tool/Toolkit.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,32 @@ public ToolRegistration registration() {
* @param toolObject the object containing tool methods
*/
public void registerTool(Object toolObject) {
registerTool(toolObject, null, null, null);
registerTool(toolObject, null, null, null, null);
}

/**
* Register a tool object and optionally specify its original target class.
*
* <p>Specifying the target class is highly recommended when the {@code toolObject} is an AOP
* proxy (e.g., managed by Spring). Since annotations like {@code @Tool} are not typically
* inherited by dynamically generated proxy classes, the toolkit uses the {@code targetClass}
* to scan for metadata, while continuing to route actual executions through the proxy instance
* to preserve aspects (like transactions or logging).
*
* @param toolObject the object containing tool methods (can be a proxy instance)
* @param targetClass the original, unproxied class to scan for @Tool annotations
* (if null, toolObject.getClass() is used)
*/
public void registerTool(Object toolObject, Class<?> targetClass) {
registerTool(toolObject, targetClass, null, null, null);
}

/**
* Internal method: Register a tool object with group, extended model, and preset parameters.
*/
private void registerTool(
Object toolObject,
Class<?> targetClass,
String groupName,
ExtendedModel extendedModel,
Map<String, Map<String, Object>> presetParameters) {
Expand All @@ -179,19 +197,45 @@ private void registerTool(
return;
}

Class<?> clazz = toolObject.getClass();
Method[] methods = clazz.getDeclaredMethods();
Class<?> clazzToScan = targetClass != null ? targetClass : toolObject.getClass();
Method[] methods = clazzToScan.getDeclaredMethods();

for (Method method : methods) {
if (method.isAnnotationPresent(Tool.class)) {
Tool toolAnnotation = method.getAnnotation(Tool.class);
for (Method originalMethod : methods) {
if (originalMethod.isAnnotationPresent(Tool.class)) {
Tool toolAnnotation = originalMethod.getAnnotation(Tool.class);
String toolName =
toolAnnotation.name().isEmpty() ? method.getName() : toolAnnotation.name();
toolAnnotation.name().isEmpty()
? originalMethod.getName()
: toolAnnotation.name();
Map<String, Object> toolPresets =
(presetParameters != null && presetParameters.containsKey(toolName))
? presetParameters.get(toolName)
: null;
registerToolMethod(toolObject, method, groupName, extendedModel, toolPresets);

// Find the corresponding real execution method on the proxy object
Method executableMethod = originalMethod;
if (targetClass != null && toolObject.getClass() != targetClass) {
try {
executableMethod =
toolObject
.getClass()
.getMethod(
originalMethod.getName(),
originalMethod.getParameterTypes());
} catch (NoSuchMethodException e) {
logger.debug(
"Proxy method not found for {}, falling back to original method",
originalMethod.getName());
}
}

registerToolMethod(
toolObject,
originalMethod,
executableMethod,
groupName,
extendedModel,
toolPresets);
}
}
}
Expand Down Expand Up @@ -338,14 +382,15 @@ public List<ToolSchema> getToolSchemas() {
*/
private void registerToolMethod(
Object toolObject,
Method method,
Method originalMethod,
Method executableMethod,
String groupName,
ExtendedModel extendedModel,
Map<String, Object> presetParameters) {
Tool toolAnnotation = method.getAnnotation(Tool.class);
Tool toolAnnotation = originalMethod.getAnnotation(Tool.class);

String toolName =
!toolAnnotation.name().isEmpty() ? toolAnnotation.name() : method.getName();
!toolAnnotation.name().isEmpty() ? toolAnnotation.name() : originalMethod.getName();
String description =
!toolAnnotation.description().isEmpty()
? toolAnnotation.description()
Expand Down Expand Up @@ -373,14 +418,19 @@ public Map<String, Object> getParameters() {
presetParameters != null
? presetParameters.keySet()
: Collections.emptySet();
return schemaGenerator.generateParameterSchema(method, excludeParams);
return schemaGenerator.generateParameterSchema(
originalMethod, excludeParams);
}

@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
// Pass custom converter to method invoker
return methodInvoker.invokeAsync(
toolObject, method, param, customConverter);
toolObject,
originalMethod,
executableMethod,
param,
customConverter);
}
};

Expand Down Expand Up @@ -741,6 +791,7 @@ public Toolkit copy() {
public static class ToolRegistration {
private final Toolkit toolkit;
private Object toolObject;
private Class<?> targetClass;
private AgentTool agentTool;
private McpClientWrapper mcpClientWrapper;
private SubAgentProvider<?> subAgentProvider;
Expand All @@ -766,6 +817,15 @@ public ToolRegistration tool(Object toolObject) {
return this;
}

/**
* Set the tool object and its original target class (useful for AOP proxies).
*/
public ToolRegistration tool(Object toolObject, Class<?> targetClass) {
this.toolObject = toolObject;
this.targetClass = targetClass;
return this;
}

/**
* Set the AgentTool instance to register.
*
Expand Down Expand Up @@ -953,7 +1013,8 @@ public void apply() {
}

if (toolObject != null) {
toolkit.registerTool(toolObject, groupName, extendedModel, presetParameters);
toolkit.registerTool(
toolObject, targetClass, groupName, extendedModel, presetParameters);
} else if (agentTool != null) {
String toolName = agentTool.getName();
Map<String, Object> toolPresets =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private ToolResultBlock invokeWithParam(
ToolUseBlock toolUseBlock = new ToolUseBlock("test-id", method.getName(), input);
ToolCallParam param =
ToolCallParam.builder().toolUseBlock(toolUseBlock).input(input).build();
return invoker.invokeAsync(tools, method, param, null).block();
return invoker.invokeAsync(tools, method, method, param, null).block();
}

// Test tools with various error scenarios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private ToolResultBlock invokeWithParam(
ToolUseBlock toolUseBlock = new ToolUseBlock("test-id", method.getName(), input);
ToolCallParam param =
ToolCallParam.builder().toolUseBlock(toolUseBlock).input(input).build();
return invoker.invokeAsync(tools, method, param, responseConverter).block();
return invoker.invokeAsync(tools, method, method, param, responseConverter).block();
}

// Test class with various method signatures for testing
Expand Down
Loading
Loading