Skip to content
Merged
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ target/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/
.claude/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ public interface AgentTool {
*/
Map<String, Object> getParameters();

/**
* Gets strict mode configuration for this tool schema.
*
* <p>When strict mode is enabled, compatible model providers are expected to enforce stricter
* adherence to the tool parameter schema. Returning {@code null} means no explicit strict mode
* preference is provided.
*
* @return strict mode value ({@code true}/{@code false}) or {@code null} when unspecified
*/
default Boolean getStrict() {
return null;
}

/**
* Gets the optional output schema for this tool in JSON Schema format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.model.ToolSchema;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -59,38 +60,59 @@ public class SchemaOnlyTool implements AgentTool {
private final String name;
private final String description;
private final Map<String, Object> parameters;
private final Boolean strict;

/**
* Creates a new SchemaOnlyTool from a ToolSchema.
*
* @param schema The tool schema containing name, description, and parameters
* @param schema The tool schema containing name, description, parameters, and strict mode
* @throws NullPointerException if schema is null
*/
public SchemaOnlyTool(ToolSchema schema) {
Objects.requireNonNull(schema, "schema cannot be null");
this.name = schema.getName();
this.description = schema.getDescription();
this.parameters =
schema.getParameters() != null
? Collections.unmodifiableMap(schema.getParameters())
: Collections.emptyMap();
this(
Objects.requireNonNull(schema, "schema cannot be null").getName(),
schema.getDescription(),
schema.getParameters(),
schema.getStrict());
}

/**
* Creates a new SchemaOnlyTool with the specified name, description, and parameters.
*
* <p>Strict mode is set to null (unspecified).
*
* @param name The tool name
* @param description The tool description
* @param parameters The tool parameters schema
* @throws NullPointerException if name or description is null
*/
public SchemaOnlyTool(String name, String description, Map<String, Object> parameters) {
this(name, description, parameters, null);
}

/**
* Creates a new SchemaOnlyTool with the specified name, description, parameters, and strict
* mode configuration.
*
* <p>The parameters map is defensively copied to prevent external mutations from affecting
* the tool's internal state.
*
* @param name The tool name
* @param description The tool description
* @param parameters The tool parameters schema
* @param strict Whether the tool should use strict schema validation (null if unspecified)
* @throws NullPointerException if name or description is null
*/
public SchemaOnlyTool(
String name, String description, Map<String, Object> parameters, Boolean strict) {
this.name = Objects.requireNonNull(name, "name cannot be null");
this.description = Objects.requireNonNull(description, "description cannot be null");
// Defensive copy: create a new HashMap from the provided parameters, then wrap it
this.parameters =
parameters != null
? Collections.unmodifiableMap(parameters)
? Collections.unmodifiableMap(new HashMap<>(parameters))
: Collections.emptyMap();
this.strict = strict;
}

@Override
Expand All @@ -108,6 +130,11 @@ public Map<String, Object> getParameters() {
return parameters;
}

@Override
public Boolean getStrict() {
return strict;
}

/**
* Throws a ToolSuspendException to signal that this tool requires external execution.
*
Expand Down
10 changes: 10 additions & 0 deletions agentscope-core/src/main/java/io/agentscope/core/tool/Tool.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
*/
String description() default "";

/**
* Whether to enable strict schema mode for this tool.
*
* <p>When enabled, compatible model providers can enforce stronger adherence to the declared
* JSON schema for tool arguments.
*
* @return true to enable strict mode for this tool
*/
boolean strict() default false;

/**
* Custom result converter for this tool.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ List<ToolSchema> getToolSchemas() {
.name(toolName)
.description(tool.getDescription())
.parameters(registered.getExtendedParameters())
.strict(tool.getStrict())
.outputSchema(tool.getOutputSchema())
.build();
schemas.add(schema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ public Map<String, Object> getParameters() {
return schemaGenerator.generateParameterSchema(method, excludeParams);
}

@Override
public Boolean getStrict() {
return toolAnnotation.strict() ? Boolean.TRUE : null;
}

@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
// Pass custom converter to method invoker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.agentscope.core.message.ToolResultBlock;
Expand Down Expand Up @@ -160,6 +161,41 @@ void testExternalToolInSchemas() {
assertEquals("db_query", schemas.get(0).getName());
}

@Test
@DisplayName("Should preserve strict when registering external schema")
void testExternalSchemaStrictPreserved() {
ToolSchema strictSchema =
ToolSchema.builder()
.name("strict_tool")
.description("Strict external tool")
.parameters(Map.of("type", "object"))
.strict(true)
.build();

toolkit.registerSchema(strictSchema);

List<ToolSchema> schemas = toolkit.getToolSchemas();
assertEquals(1, schemas.size());
assertEquals(Boolean.TRUE, schemas.get(0).getStrict());
}

@Test
@DisplayName("Should keep strict as null when schema strict is not set")
void testExternalSchemaStrictNullPreserved() {
ToolSchema schema =
ToolSchema.builder()
.name("null_strict_tool")
.description("Null strict external tool")
.parameters(Map.of("type", "object"))
.build();

toolkit.registerSchema(schema);

List<ToolSchema> schemas = toolkit.getToolSchemas();
assertEquals(1, schemas.size());
assertNull(schemas.get(0).getStrict());
}

@Test
@DisplayName("Should handle null schemas list gracefully")
void testRegisterSchemasNull() {
Expand Down Expand Up @@ -206,4 +242,22 @@ public String calculate(@ToolParam(name = "expression") String expression) {
return "result";
}
}

static class StrictInternalToolExample {
@Tool(name = "strict_calculator", description = "Calculate expression", strict = true)
public String calculate(@ToolParam(name = "expression") String expression) {
return "result";
}
}

@Test
@DisplayName("Should propagate strict from @Tool annotation")
void testAnnotationStrictPropagation() {
toolkit.registerTool(new StrictInternalToolExample());

List<ToolSchema> schemas = toolkit.getToolSchemas();
assertEquals(1, schemas.size());
assertEquals("strict_calculator", schemas.get(0).getName());
assertEquals(Boolean.TRUE, schemas.get(0).getStrict());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -57,6 +58,7 @@ void setUp() {
Map.of("sql", Map.of("type", "string")),
"required",
List.of("sql")))
.strict(true)
.build();
schemaOnlyTool = new SchemaOnlyTool(testSchema);
}
Expand All @@ -69,6 +71,7 @@ void testCreateFromToolSchema() {
assertEquals("Query an external database", schemaOnlyTool.getDescription());
assertNotNull(schemaOnlyTool.getParameters());
assertEquals("object", schemaOnlyTool.getParameters().get("type"));
assertEquals(Boolean.TRUE, schemaOnlyTool.getStrict());
}

@Test
Expand All @@ -82,6 +85,7 @@ void testCreateWithParameters() {
assertEquals("get_user", tool.getName());
assertEquals("Get user by ID", tool.getDescription());
assertEquals(params, tool.getParameters());
assertNull(tool.getStrict());
}

@Test
Expand Down Expand Up @@ -136,4 +140,56 @@ void testParametersImmutability() {
Map<String, Object> parameters = schemaOnlyTool.getParameters();
assertThrows(UnsupportedOperationException.class, () -> parameters.put("new_key", "value"));
}

@Test
@DisplayName("Should support strict mode configuration via 4-arg constructor")
void testStrictModeConfiguration() {
Map<String, Object> params =
Map.of("type", "object", "properties", Map.of("id", Map.of("type", "integer")));

SchemaOnlyTool toolWithStrict = new SchemaOnlyTool("get_user", "Get user", params, true);
assertEquals(Boolean.TRUE, toolWithStrict.getStrict());

SchemaOnlyTool toolWithoutStrict =
new SchemaOnlyTool("get_user", "Get user", params, false);
assertEquals(Boolean.FALSE, toolWithoutStrict.getStrict());

SchemaOnlyTool toolUnspecifiedStrict =
new SchemaOnlyTool("get_user", "Get user", params, null);
assertNull(toolUnspecifiedStrict.getStrict());
}

@Test
@DisplayName("Should defensively copy parameters map to prevent external mutations")
void testDefensiveCopyOfParameters() {
Map<String, Object> originalParams = new java.util.HashMap<>();
originalParams.put("type", "object");
originalParams.put("sql", "SELECT 1");

// Create tool with the original parameters map
SchemaOnlyTool tool = new SchemaOnlyTool("query", "Query tool", originalParams);

// Mutate the original parameters map
originalParams.put("sql", "MODIFIED");
originalParams.put("newKey", "newValue");

// Verify that the tool's parameters were not affected by external mutations
Map<String, Object> toolParams = tool.getParameters();
assertEquals("SELECT 1", toolParams.get("sql"), "Original value should be preserved");
assertNull(toolParams.get("newKey"), "External mutation should not be visible");

// Verify exactly the expected entries are in the tool's parameters
assertEquals(2, toolParams.size(), "Tool should have only the original 2 entries");
}

@Test
@DisplayName("Should preserve strict mode when delegating through 3-arg constructor")
void testStrictModePreservedInThreeArgConstructor() {
Map<String, Object> params = Map.of("type", "object");

SchemaOnlyTool tool = new SchemaOnlyTool("test_tool", "Test tool", params);

// Verify that 3-arg constructor sets strict to null (unspecified)
assertNull(tool.getStrict());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,42 @@ void testGetToolSchemasMultipleTools() {
assertTrue(toolNames.contains("tool2"));
assertTrue(toolNames.contains("tool3"));
}

@Test
void testGetToolSchemasPreserveStrictFromAgentTool() {
AgentTool strictTool =
new AgentTool() {
@Override
public String getName() {
return "strict_tool";
}

@Override
public String getDescription() {
return "Strict tool";
}

@Override
public Map<String, Object> getParameters() {
return Map.of("type", "object");
}

@Override
public Boolean getStrict() {
return true;
}

@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam input) {
return Mono.just(ToolResultBlock.text("ok"));
}
};

RegisteredToolFunction registered = new RegisteredToolFunction(strictTool, null, null);
registry.registerTool("strict_tool", strictTool, registered);

List<ToolSchema> schemas = schemaProvider.getToolSchemas();
assertEquals(1, schemas.size());
assertEquals(Boolean.TRUE, schemas.get(0).getStrict());
}
}
Loading
Loading