Skip to content

Commit 7f2a1ed

Browse files
authored
Merge branch 'main' into fix/951-react-agent-tool-execution-error-handling
2 parents a6fd753 + 5de85f4 commit 7f2a1ed

214 files changed

Lines changed: 15064 additions & 1154 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: "Close stale PRs"
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *'
6+
workflow_dispatch:
7+
8+
jobs:
9+
stale:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
pull-requests: write
13+
contents: read
14+
steps:
15+
- uses: actions/stale@v9
16+
with:
17+
only-labels: 'wait-for-response'
18+
days-before-stale: 7
19+
days-before-close: 0
20+
stale-pr-message: >
21+
Closing due to inactivity. Feel free to reopen when ready.
22+
stale-pr-label: 'stale'
23+
days-before-issue-stale: -1
24+
days-before-issue-close: -1

agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public class ReActAgent extends StructuredOutputCapableAgent {
138138
private final int maxIters;
139139
private final ExecutionConfig modelExecutionConfig;
140140
private final ExecutionConfig toolExecutionConfig;
141+
private final GenerateOptions generateOptions;
141142
private final PlanNotebook planNotebook;
142143
private final ToolExecutionContext toolExecutionContext;
143144
private final StatePersistence statePersistence;
@@ -159,6 +160,7 @@ private ReActAgent(Builder builder, Toolkit agentToolkit) {
159160
this.maxIters = builder.maxIters;
160161
this.modelExecutionConfig = builder.modelExecutionConfig;
161162
this.toolExecutionConfig = builder.toolExecutionConfig;
163+
this.generateOptions = builder.generateOptions;
162164
this.planNotebook = builder.planNotebook;
163165
this.toolExecutionContext = builder.toolExecutionContext;
164166
this.statePersistence =
@@ -575,8 +577,9 @@ private Mono<Msg> acting(int iter) {
575577
return executeIteration(iter + 1);
576578
}
577579

578-
// Set chunk callback for streaming tool responses
579-
toolkit.setChunkCallback((toolUse, chunk) -> notifyActingChunk(toolUse, chunk).subscribe());
580+
// Forward tool chunks into ActingChunkEvent hooks without overwriting user callbacks.
581+
toolkit.setInternalChunkCallback(
582+
(toolUse, chunk) -> notifyActingChunk(toolUse, chunk).subscribe());
580583

581584
// Execute only pending tools (those without results in memory)
582585
return notifyPreActingHooks(pendingToolCalls)
@@ -887,11 +890,17 @@ private List<ToolUseBlock> extractPendingToolCalls() {
887890

888891
@Override
889892
protected GenerateOptions buildGenerateOptions() {
890-
GenerateOptions.Builder builder = GenerateOptions.builder();
893+
// Start with user-configured generateOptions if available
894+
GenerateOptions baseOptions = generateOptions;
895+
896+
// If modelExecutionConfig is set, merge it into the options
891897
if (modelExecutionConfig != null) {
892-
builder.executionConfig(modelExecutionConfig);
898+
GenerateOptions execConfigOptions =
899+
GenerateOptions.builder().executionConfig(modelExecutionConfig).build();
900+
baseOptions = GenerateOptions.mergeOptions(execConfigOptions, baseOptions);
893901
}
894-
return builder.build();
902+
903+
return baseOptions != null ? baseOptions : GenerateOptions.builder().build();
895904
}
896905

897906
// ==================== Hook Notification Methods ====================
@@ -1063,6 +1072,15 @@ public PlanNotebook getPlanNotebook() {
10631072
return planNotebook;
10641073
}
10651074

1075+
/**
1076+
* Gets the configured generation options for this agent.
1077+
*
1078+
* @return The generation options, or null if not configured
1079+
*/
1080+
public GenerateOptions getGenerateOptions() {
1081+
return generateOptions;
1082+
}
1083+
10661084
public static Builder builder() {
10671085
return new Builder();
10681086
}
@@ -1080,6 +1098,7 @@ public static class Builder {
10801098
private int maxIters = 10;
10811099
private ExecutionConfig modelExecutionConfig;
10821100
private ExecutionConfig toolExecutionConfig;
1101+
private GenerateOptions generateOptions;
10831102
private final Set<Hook> hooks = new LinkedHashSet<>();
10841103
private boolean enableMetaTool = false;
10851104
private StructuredOutputReminder structuredOutputReminder =
@@ -1257,6 +1276,39 @@ public Builder toolExecutionConfig(ExecutionConfig toolExecutionConfig) {
12571276
return this;
12581277
}
12591278

1279+
/**
1280+
* Sets the generation options for model API calls.
1281+
*
1282+
* <p>This configuration controls LLM generation parameters such as temperature, topP,
1283+
* maxTokens, frequencyPenalty, presencePenalty, etc. These options are passed to the
1284+
* model during the reasoning phase.
1285+
*
1286+
* <p><b>Example usage:</b>
1287+
* <pre>{@code
1288+
* ReActAgent agent = ReActAgent.builder()
1289+
* .name("assistant")
1290+
* .model(model)
1291+
* .generateOptions(GenerateOptions.builder()
1292+
* .temperature(0.7)
1293+
* .topP(0.9)
1294+
* .maxTokens(1000)
1295+
* .build())
1296+
* .build();
1297+
* }</pre>
1298+
*
1299+
* <p><b>Note:</b> If both generateOptions and modelExecutionConfig are set,
1300+
* the modelExecutionConfig's executionConfig will be merged into the generateOptions,
1301+
* with modelExecutionConfig taking precedence for execution settings.
1302+
*
1303+
* @param generateOptions The generation options for model calls, can be null
1304+
* @return This builder instance for method chaining
1305+
* @see GenerateOptions
1306+
*/
1307+
public Builder generateOptions(GenerateOptions generateOptions) {
1308+
this.generateOptions = generateOptions;
1309+
return this;
1310+
}
1311+
12601312
/**
12611313
* Sets the structured output enforcement mode.
12621314
*
@@ -1539,7 +1591,7 @@ private void configureRAG(Toolkit agentToolkit) {
15391591
case AGENTIC -> {
15401592
// Register knowledge retrieval tools
15411593
KnowledgeRetrievalTools tools =
1542-
new KnowledgeRetrievalTools(aggregatedKnowledge);
1594+
new KnowledgeRetrievalTools(aggregatedKnowledge, retrieveConfig);
15431595
agentToolkit.registerTool(tools);
15441596
}
15451597
case NONE -> {

agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,19 @@ public final Flux<Event> stream(
675675
return createEventStream(options, () -> call(msgs, structuredModel));
676676
}
677677

678+
/**
679+
* Stream with multiple input messages using a JSON schema.
680+
*
681+
* @param msgs Input messages
682+
* @param options Stream configuration options
683+
* @param schema JSON schema defining the structure of the response
684+
* @return Flux of events emitted during execution
685+
*/
686+
@Override
687+
public final Flux<Event> stream(List<Msg> msgs, StreamOptions options, JsonNode schema) {
688+
return createEventStream(options, () -> call(msgs, schema));
689+
}
690+
678691
/**
679692
* Helper method to create an event stream with proper hook lifecycle management.
680693
*

agentscope-core/src/main/java/io/agentscope/core/agent/StreamableAgent.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.agentscope.core.agent;
1717

18+
import com.fasterxml.jackson.databind.JsonNode;
1819
import io.agentscope.core.message.Msg;
1920
import java.util.List;
2021
import reactor.core.publisher.Flux;
@@ -98,6 +99,18 @@ default Flux<Event> stream(Msg msg, StreamOptions options, Class<?> structuredMo
9899
return stream(List.of(msg), options, structuredModel);
99100
}
100101

102+
/**
103+
* Stream execution events for a single message with JSON schema support.
104+
*
105+
* @param msg Input message
106+
* @param options Stream configuration options
107+
* @param schema JSON schema defining the structure
108+
* @return Flux of events emitted during execution
109+
*/
110+
default Flux<Event> stream(Msg msg, StreamOptions options, JsonNode schema) {
111+
return stream(List.of(msg), options, schema);
112+
}
113+
101114
/**
102115
* Stream execution events for multiple messages with default options.
103116
*
@@ -126,4 +139,14 @@ default Flux<Event> stream(List<Msg> msgs) {
126139
* @return Flux of events emitted during execution
127140
*/
128141
Flux<Event> stream(List<Msg> msgs, StreamOptions options, Class<?> structuredModel);
142+
143+
/**
144+
* Stream execution events with JSON schema support.
145+
*
146+
* @param msgs Input messages
147+
* @param options Stream configuration options
148+
* @param schema JSON schema defining the structure
149+
* @return Flux of events emitted during execution
150+
*/
151+
Flux<Event> stream(List<Msg> msgs, StreamOptions options, JsonNode schema);
129152
}

agentscope-core/src/main/java/io/agentscope/core/agent/StructuredOutputCapableAgent.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ private Mono<Msg> executeWithStructuredOutput(
189189
})
190190
.doFinally(
191191
signal -> {
192-
// Cleanup: remove hook and unregister tool
193192
removeHook(hook);
194-
toolkit.removeTool(STRUCTURED_OUTPUT_TOOL_NAME);
193+
toolkit.removeToolIfSame(
194+
STRUCTURED_OUTPUT_TOOL_NAME, structuredOutputTool);
195195
});
196196
});
197197
}

agentscope-core/src/main/java/io/agentscope/core/formatter/anthropic/AnthropicResponseParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static ChatResponse parseMessage(Message message, Instant startTime) {
5050
List<ContentBlock> contentBlocks = new ArrayList<>();
5151

5252
// Process content blocks
53-
for (com.anthropic.models.messages.ContentBlock block : message.content()) {
53+
for (var block : message.content()) {
5454
// Text block
5555
block.text()
5656
.ifPresent(

agentscope-core/src/main/java/io/agentscope/core/formatter/anthropic/AnthropicToolsHelper.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package io.agentscope.core.formatter.anthropic;
1717

18+
import static com.anthropic.models.messages.ToolChoice.ofAny;
19+
import static com.anthropic.models.messages.ToolChoice.ofAuto;
20+
import static com.anthropic.models.messages.ToolChoice.ofTool;
21+
1822
import com.anthropic.core.JsonValue;
1923
import com.anthropic.core.ObjectMappers;
2024
import com.anthropic.models.messages.MessageCreateParams;
@@ -27,6 +31,7 @@
2731
import io.agentscope.core.model.ToolSchema;
2832
import java.util.List;
2933
import java.util.Map;
34+
import java.util.function.Function;
3035
import org.slf4j.Logger;
3136
import org.slf4j.LoggerFactory;
3237

@@ -86,27 +91,19 @@ private static JsonValue convertToJsonValue(Object parameters) {
8691
private static void applyToolChoice(
8792
MessageCreateParams.Builder builder, ToolChoice toolChoice) {
8893
if (toolChoice instanceof ToolChoice.Auto) {
89-
builder.toolChoice(
90-
com.anthropic.models.messages.ToolChoice.ofAuto(
91-
ToolChoiceAuto.builder().build()));
94+
builder.toolChoice(ofAuto(ToolChoiceAuto.builder().build()));
9295
} else if (toolChoice instanceof ToolChoice.None) {
9396
// Anthropic doesn't have None, use Any instead
94-
builder.toolChoice(
95-
com.anthropic.models.messages.ToolChoice.ofAny(
96-
ToolChoiceAny.builder().build()));
97+
builder.toolChoice(ofAny(ToolChoiceAny.builder().build()));
9798
} else if (toolChoice instanceof ToolChoice.Required) {
9899
// Anthropic doesn't have a direct "required" option, use "any" which forces tool
99100
// use
100101
log.warn(
101102
"Anthropic API doesn't support ToolChoice.Required directly, using 'any'"
102103
+ " instead");
103-
builder.toolChoice(
104-
com.anthropic.models.messages.ToolChoice.ofAny(
105-
ToolChoiceAny.builder().build()));
104+
builder.toolChoice(ofAny(ToolChoiceAny.builder().build()));
106105
} else if (toolChoice instanceof ToolChoice.Specific specific) {
107-
builder.toolChoice(
108-
com.anthropic.models.messages.ToolChoice.ofTool(
109-
ToolChoiceTool.builder().name(specific.toolName()).build()));
106+
builder.toolChoice(ofTool(ToolChoiceTool.builder().name(specific.toolName()).build()));
110107
} else {
111108
log.warn("Unknown tool choice type: {}", toolChoice);
112109
}
@@ -203,7 +200,7 @@ private static void applyAdditionalQueryParams(
203200
private static <T> T getOption(
204201
GenerateOptions options,
205202
GenerateOptions defaultOptions,
206-
java.util.function.Function<GenerateOptions, T> getter) {
203+
Function<GenerateOptions, T> getter) {
207204
if (options != null) {
208205
T value = getter.apply(options);
209206
if (value != null) {

agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeChatFormatter.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.time.Instant;
3030
import java.util.ArrayList;
3131
import java.util.List;
32+
import java.util.Map;
3233
import java.util.stream.Collectors;
3334

3435
/**
@@ -41,6 +42,8 @@
4142
public class DashScopeChatFormatter
4243
extends AbstractBaseFormatter<DashScopeMessage, DashScopeResponse, DashScopeRequest> {
4344

45+
private static final Map<String, String> EPHEMERAL_CACHE_CONTROL = Map.of("type", "ephemeral");
46+
4447
private final DashScopeMessageConverter messageConverter;
4548
private final DashScopeResponseParser responseParser;
4649
private final DashScopeToolsHelper toolsHelper;
@@ -168,4 +171,37 @@ public DashScopeRequest buildRequest(
168171

169172
return request;
170173
}
174+
175+
/**
176+
* Apply cache control to DashScope messages.
177+
*
178+
* <p>Adds <code>cache_control: {"type": "ephemeral"}</code> to all system messages and the last
179+
* message in the list. Messages that already have cache_control set (e.g., via manual metadata
180+
* marking) will not be overwritten.
181+
*
182+
* @param messages the list of formatted DashScope messages
183+
*/
184+
public void applyCacheControl(List<DashScopeMessage> messages) {
185+
if (messages == null || messages.isEmpty()) {
186+
return;
187+
}
188+
for (DashScopeMessage msg : messages) {
189+
if ("system".equals(msg.getRole()) && msg.getCacheControl() == null) {
190+
msg.setCacheControl(EPHEMERAL_CACHE_CONTROL);
191+
}
192+
}
193+
DashScopeMessage lastMsg = messages.get(messages.size() - 1);
194+
if (lastMsg.getCacheControl() == null) {
195+
lastMsg.setCacheControl(EPHEMERAL_CACHE_CONTROL);
196+
}
197+
}
198+
199+
/**
200+
* Get the ephemeral cache control constant.
201+
*
202+
* @return unmodifiable map representing ephemeral cache control
203+
*/
204+
static Map<String, String> getEphemeralCacheControl() {
205+
return EPHEMERAL_CACHE_CONTROL;
206+
}
171207
}

agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMessageConverter.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.agentscope.core.message.AudioBlock;
2121
import io.agentscope.core.message.ContentBlock;
2222
import io.agentscope.core.message.ImageBlock;
23+
import io.agentscope.core.message.MessageMetadataKeys;
2324
import io.agentscope.core.message.Msg;
2425
import io.agentscope.core.message.MsgRole;
2526
import io.agentscope.core.message.TextBlock;
@@ -64,11 +65,17 @@ public DashScopeMessageConverter(Function<List<ContentBlock>, String> toolResult
6465
* @return The converted DashScopeMessage
6566
*/
6667
public DashScopeMessage convertToMessage(Msg msg, boolean useMultimodalFormat) {
68+
DashScopeMessage result;
6769
if (useMultimodalFormat) {
68-
return convertToMultimodalContent(msg);
70+
result = convertToMultimodalContent(msg);
6971
} else {
70-
return convertToSimpleContent(msg);
72+
result = convertToSimpleContent(msg);
7173
}
74+
75+
// Apply cache_control from message metadata if manually marked
76+
applyCacheControlFromMetadata(msg, result);
77+
78+
return result;
7279
}
7380

7481
/**
@@ -237,4 +244,20 @@ private String extractTextContent(Msg msg) {
237244
.map(block -> ((TextBlock) block).getText())
238245
.reduce("", (a, b) -> a.isEmpty() ? b : a + "\n" + b);
239246
}
247+
248+
/**
249+
* Apply cache_control from Msg metadata to the converted DashScopeMessage.
250+
*
251+
* @param msg the source message with metadata
252+
* @param result the converted DashScope message
253+
*/
254+
private void applyCacheControlFromMetadata(Msg msg, DashScopeMessage result) {
255+
if (msg.getMetadata() == null) {
256+
return;
257+
}
258+
Object cacheFlag = msg.getMetadata().get(MessageMetadataKeys.CACHE_CONTROL);
259+
if (Boolean.TRUE.equals(cacheFlag)) {
260+
result.setCacheControl(DashScopeChatFormatter.getEphemeralCacheControl());
261+
}
262+
}
240263
}

0 commit comments

Comments
 (0)