Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ target/
build/
!**/src/main/**/build/
!**/src/test/**/build/

.devcontainer/
Comment thread
BEASTSHRIRAM marked this conversation as resolved.
Outdated
### 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;
}

/**
* Execute the tool with the given parameters (asynchronous).
*
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;
}
Comment thread
BEASTSHRIRAM marked this conversation as resolved.

@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())
.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 @@ -224,4 +224,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