Skip to content

Commit 2bc3080

Browse files
authored
Merge branch 'main' into fix/unstable-computehash
2 parents bec4729 + 6c39c7b commit 2bc3080

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/tool/ToolCallParam.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Collections;
2222
import java.util.HashMap;
2323
import java.util.Map;
24+
import java.util.Objects;
2425

2526
/**
2627
* Parameters for tool invocation.
@@ -126,6 +127,34 @@ public static Builder builder() {
126127
return new Builder();
127128
}
128129

130+
/**
131+
* Creates a new builder initialized with values from an existing ToolCallParam.
132+
*
133+
* <p>This is useful for creating a modified copy of an existing parameter object.
134+
* The builder copies all values from the source object, allowing selective
135+
* modifications before building a new instance.
136+
*
137+
* <p><b>Example usage:</b>
138+
* <pre>{@code
139+
* ToolCallParam original = ...;
140+
* ToolCallParam modified = ToolCallParam.builder(original)
141+
* .input(Map.of("newKey", "newValue"))
142+
* .build();
143+
* }</pre>
144+
*
145+
* <p>Note: The input map structure is copied so that entries can be modified
146+
* independently, but nested values (typed as Object) remain shared.
147+
* Immutable fields (toolUseBlock, agent, context, emitter) are shared by reference.
148+
*
149+
* @param source The existing ToolCallParam to copy values from
150+
* @return A new builder pre-populated with the source's values
151+
* @throws NullPointerException if source is null
152+
*/
153+
public static Builder builder(ToolCallParam source) {
154+
Objects.requireNonNull(source, "source must not be null");
155+
return new Builder(source);
156+
}
157+
129158
/**
130159
* Builder for ToolCallParam.
131160
*/
@@ -138,6 +167,14 @@ public static class Builder {
138167

139168
private Builder() {}
140169

170+
private Builder(ToolCallParam source) {
171+
this.toolUseBlock = source.toolUseBlock;
172+
this.input = source.input.isEmpty() ? null : source.input;
173+
this.agent = source.agent;
174+
this.context = source.context;
175+
this.emitter = source.emitter;
176+
}
177+
141178
/**
142179
* Sets the tool use block.
143180
*

agentscope-core/src/test/java/io/agentscope/core/tool/ToolCallParamTest.java

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,202 @@ void testBuilderChaining() {
118118
}
119119
}
120120

121+
@Nested
122+
@DisplayName("Copy Builder pattern")
123+
class CopyBuilderTests {
124+
125+
@Test
126+
@DisplayName("Should copy all fields from source")
127+
void testCopyAllFields() {
128+
ToolUseBlock toolUseBlock =
129+
ToolUseBlock.builder()
130+
.id("test-id")
131+
.name("test_tool")
132+
.input(Map.of("key", "value"))
133+
.build();
134+
135+
Agent mockAgent = mock(Agent.class);
136+
when(mockAgent.getName()).thenReturn("TestAgent");
137+
138+
ToolExecutionContext context =
139+
ToolExecutionContext.builder().register("testContext").build();
140+
141+
List<ToolResultBlock> emittedChunks = new ArrayList<>();
142+
ToolEmitter emitter = emittedChunks::add;
143+
144+
Map<String, Object> input = new HashMap<>();
145+
input.put("param1", "value1");
146+
147+
ToolCallParam original =
148+
ToolCallParam.builder()
149+
.toolUseBlock(toolUseBlock)
150+
.input(input)
151+
.agent(mockAgent)
152+
.context(context)
153+
.emitter(emitter)
154+
.build();
155+
156+
// Create copy
157+
ToolCallParam copy = ToolCallParam.builder(original).build();
158+
159+
assertNotNull(copy);
160+
assertEquals(original.getToolUseBlock(), copy.getToolUseBlock());
161+
assertEquals(original.getInput(), copy.getInput());
162+
assertEquals(original.getAgent(), copy.getAgent());
163+
assertEquals(original.getContext(), copy.getContext());
164+
assertSame(original.getEmitter(), copy.getEmitter());
165+
}
166+
167+
@Test
168+
@DisplayName("Should allow modifying copied fields")
169+
void testModifyCopiedFields() {
170+
ToolUseBlock toolUseBlock =
171+
ToolUseBlock.builder()
172+
.id("test-id")
173+
.name("test_tool")
174+
.input(Map.of("key", "value"))
175+
.build();
176+
177+
Map<String, Object> input = new HashMap<>();
178+
input.put("param1", "value1");
179+
180+
ToolCallParam original =
181+
ToolCallParam.builder().toolUseBlock(toolUseBlock).input(input).build();
182+
183+
// Create copy with modified input
184+
Map<String, Object> newInput = new HashMap<>();
185+
newInput.put("param2", "value2");
186+
ToolCallParam modified = ToolCallParam.builder(original).input(newInput).build();
187+
188+
// Original should be unchanged
189+
assertEquals("value1", original.getInput().get("param1"));
190+
assertNull(original.getInput().get("param2"));
191+
192+
// Modified should have new value
193+
assertEquals("value2", modified.getInput().get("param2"));
194+
assertNull(modified.getInput().get("param1"));
195+
}
196+
197+
@Test
198+
@DisplayName("Should shallow copy input map (entries independent, nested values shared)")
199+
@SuppressWarnings("unchecked")
200+
void testShallowCopyInputMap() {
201+
ToolUseBlock toolUseBlock =
202+
ToolUseBlock.builder().id("id").name("tool").input(Map.of()).build();
203+
204+
// Use a nested mutable value to verify shallow copy semantics
205+
List<String> nestedList = new ArrayList<>(List.of("a", "b"));
206+
Map<String, Object> input = new HashMap<>();
207+
input.put("list", nestedList);
208+
209+
ToolCallParam original =
210+
ToolCallParam.builder().toolUseBlock(toolUseBlock).input(input).build();
211+
212+
// Create copy (no input override)
213+
ToolCallParam copy = ToolCallParam.builder(original).build();
214+
215+
// Input maps should be equal
216+
assertEquals(original.getInput(), copy.getInput());
217+
218+
// Nested mutable value should be the SAME reference (shallow copy)
219+
assertSame(
220+
original.getInput().get("list"),
221+
copy.getInput().get("list"),
222+
"Nested values should be shared (shallow copy)");
223+
224+
// But top-level entries should be independent:
225+
// adding a new top-level key to copy's input should NOT affect original
226+
// (because ToolCallParam constructor does new HashMap<>(builder.input))
227+
Map<String, Object> modifiedInput = new HashMap<>(copy.getInput());
228+
modifiedInput.put("newKey", "newValue");
229+
ToolCallParam modified = ToolCallParam.builder(original).input(modifiedInput).build();
230+
231+
assertNull(
232+
original.getInput().get("newKey"),
233+
"Original should not have the new key added to the modified copy");
234+
assertEquals("newValue", modified.getInput().get("newKey"));
235+
}
236+
237+
@Test
238+
@DisplayName("Should copy with null fields")
239+
void testCopyWithNullFields() {
240+
ToolUseBlock toolUseBlock =
241+
ToolUseBlock.builder().id("id").name("tool").input(Map.of()).build();
242+
243+
ToolCallParam original = ToolCallParam.builder().toolUseBlock(toolUseBlock).build();
244+
245+
// Create copy
246+
ToolCallParam copy = ToolCallParam.builder(original).build();
247+
248+
assertNotNull(copy);
249+
assertEquals(original.getToolUseBlock(), copy.getToolUseBlock());
250+
assertTrue(copy.getInput().isEmpty());
251+
assertNull(copy.getAgent());
252+
assertNull(copy.getContext());
253+
}
254+
255+
@Test
256+
@DisplayName("Should throw NPE when source is null")
257+
void testCopyWithNullSource() {
258+
org.junit.jupiter.api.Assertions.assertThrows(
259+
NullPointerException.class, () -> ToolCallParam.builder((ToolCallParam) null));
260+
}
261+
262+
@Test
263+
@DisplayName("Should preserve immutable fields as references")
264+
void testPreserveImmutableFields() {
265+
ToolUseBlock toolUseBlock =
266+
ToolUseBlock.builder()
267+
.id("test-id")
268+
.name("test_tool")
269+
.input(Map.of("key", "value"))
270+
.build();
271+
272+
Agent mockAgent = mock(Agent.class);
273+
when(mockAgent.getName()).thenReturn("TestAgent");
274+
275+
ToolExecutionContext context =
276+
ToolExecutionContext.builder().register("testContext").build();
277+
278+
ToolCallParam original =
279+
ToolCallParam.builder()
280+
.toolUseBlock(toolUseBlock)
281+
.agent(mockAgent)
282+
.context(context)
283+
.build();
284+
285+
ToolCallParam copy = ToolCallParam.builder(original).build();
286+
287+
// Immutable fields should be the same references
288+
assertSame(original.getToolUseBlock(), copy.getToolUseBlock());
289+
assertSame(original.getAgent(), copy.getAgent());
290+
assertSame(original.getContext(), copy.getContext());
291+
}
292+
293+
@Test
294+
@DisplayName("Should allow replacing immutable fields in copy")
295+
void testReplaceImmutableFieldsInCopy() {
296+
ToolUseBlock originalToolUseBlock =
297+
ToolUseBlock.builder().id("id1").name("tool1").input(Map.of()).build();
298+
ToolUseBlock newToolUseBlock =
299+
ToolUseBlock.builder().id("id2").name("tool2").input(Map.of()).build();
300+
301+
ToolCallParam original =
302+
ToolCallParam.builder().toolUseBlock(originalToolUseBlock).build();
303+
304+
ToolCallParam modified =
305+
ToolCallParam.builder(original).toolUseBlock(newToolUseBlock).build();
306+
307+
// Original should be unchanged
308+
assertEquals("id1", original.getToolUseBlock().getId());
309+
assertEquals("tool1", original.getToolUseBlock().getName());
310+
311+
// Modified should have new values
312+
assertEquals("id2", modified.getToolUseBlock().getId());
313+
assertEquals("tool2", modified.getToolUseBlock().getName());
314+
}
315+
}
316+
121317
@Nested
122318
@DisplayName("Emitter functionality")
123319
class EmitterTests {

0 commit comments

Comments
 (0)