Skip to content

Commit ba13a16

Browse files
authored
feat: add reasoning chunk and result filtering options to StreamOptions (#269)
1 parent 311dbd7 commit ba13a16

2 files changed

Lines changed: 174 additions & 18 deletions

File tree

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

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package io.agentscope.core.agent;
1718

1819
import java.util.Arrays;
@@ -24,6 +25,16 @@
2425
*
2526
* <p>Controls which event types to receive and how streaming content is delivered.
2627
*
28+
* <p><b>Reasoning filtering (Issue #265):</b>
29+
* Some streaming backends emit both:
30+
* <ul>
31+
* <li><b>Reasoning chunks</b>: incremental deltas during the reasoning process</li>
32+
* <li><b>Reasoning result</b>: the final consolidated reasoning output</li>
33+
* </ul>
34+
*
35+
* <p>Use {@link #isIncludeReasoningChunk()} and {@link #isIncludeReasoningResult()} to filter
36+
* these reasoning-related emissions when {@link EventType#REASONING} is enabled.
37+
*
2738
* <p><b>Example usage:</b>
2839
*
2940
* <pre>{@code
@@ -33,11 +44,12 @@
3344
* .incremental(true)
3445
* .build();
3546
*
36-
* // All events including final result, cumulative mode
47+
* // Reasoning events, but hide intermediate deltas and only keep the final reasoning result
3748
* StreamOptions options = StreamOptions.builder()
38-
* .eventTypes(EventType.ALL)
39-
* .includeAgentResult(true)
40-
* .incremental(false)
49+
* .eventTypes(EventType.REASONING)
50+
* .includeReasoningChunk(false)
51+
* .includeReasoningResult(true)
52+
* .incremental(true)
4153
* .build();
4254
*
4355
* // Multiple specific types
@@ -52,6 +64,22 @@ public class StreamOptions {
5264
private final Set<EventType> eventTypes;
5365
private final boolean incremental;
5466

67+
/**
68+
* Whether to include the incremental delta of the reasoning process during streaming.
69+
* <p>
70+
* If false, intermediate reasoning chunk emissions should be filtered out by the stream
71+
* implementation.
72+
*/
73+
private final boolean includeReasoningChunk;
74+
75+
/**
76+
* Whether to include the final consolidated reasoning output in the response.
77+
* <p>
78+
* If false, final reasoning result emissions should be filtered out by the stream
79+
* implementation.
80+
*/
81+
private final boolean includeReasoningResult;
82+
5583
/**
5684
* Private constructor called by the builder.
5785
*
@@ -60,10 +88,12 @@ public class StreamOptions {
6088
private StreamOptions(Builder builder) {
6189
this.eventTypes = builder.eventTypes;
6290
this.incremental = builder.incremental;
91+
this.includeReasoningChunk = builder.includeReasoningChunk;
92+
this.includeReasoningResult = builder.includeReasoningResult;
6393
}
6494

6595
/**
66-
* Default options: All event types (except AGENT_RESULT), incremental mode.
96+
* Default options: All event types, incremental mode, include both reasoning chunk and reasoning result.
6797
*
6898
* @return StreamOptions with default configuration
6999
*/
@@ -83,8 +113,7 @@ public static Builder builder() {
83113
/**
84114
* Get the set of event types that should be streamed.
85115
*
86-
* <p>If the set contains {@link EventType#ALL}, all event types (except AGENT_RESULT unless
87-
* explicitly opted-in) will be streamed.
116+
* <p>If the set contains {@link EventType#ALL}, all event types will be streamed.
88117
*
89118
* @return The set of event types to stream
90119
*/
@@ -104,6 +133,28 @@ public boolean isIncremental() {
104133
return incremental;
105134
}
106135

136+
/**
137+
* Whether reasoning "chunk" emissions should be included.
138+
*
139+
* <p>Reasoning chunks are the incremental delta of the reasoning process during streaming.</p>
140+
*
141+
* @return true if reasoning chunks should be included
142+
*/
143+
public boolean isIncludeReasoningChunk() {
144+
return includeReasoningChunk;
145+
}
146+
147+
/**
148+
* Whether the final reasoning result should be included.
149+
*
150+
* <p>The reasoning result is the final consolidated reasoning output in the response.</p>
151+
*
152+
* @return true if the final reasoning result should be included
153+
*/
154+
public boolean isIncludeReasoningResult() {
155+
return includeReasoningResult;
156+
}
157+
107158
/**
108159
* Check if a specific event type should be streamed.
109160
*
@@ -114,11 +165,28 @@ public boolean shouldStream(EventType type) {
114165
return eventTypes.contains(EventType.ALL) || eventTypes.contains(type);
115166
}
116167

168+
/**
169+
* Convenience method for stream implementations to decide whether to emit a reasoning subtype.
170+
*
171+
* <p><b>TODO (Issue #265):</b> Thread these flags through the stream event mapping layer where
172+
* reasoning events are converted into Flux emissions (e.g., when distinguishing chunk vs result).
173+
*
174+
* @param isChunk true if the reasoning emission is an incremental chunk, false if it is the final result
175+
* @return true if this reasoning emission should be included
176+
*/
177+
public boolean shouldIncludeReasoningEmission(boolean isChunk) {
178+
return isChunk ? includeReasoningChunk : includeReasoningResult;
179+
}
180+
117181
/** Builder for {@link StreamOptions}. */
118182
public static class Builder {
119183
private Set<EventType> eventTypes = EnumSet.of(EventType.ALL);
120184
private boolean incremental = true;
121185

186+
// Defaults are "true" to preserve existing behavior.
187+
private boolean includeReasoningChunk = true;
188+
private boolean includeReasoningResult = true;
189+
122190
/**
123191
* Set which event types to stream.
124192
*
@@ -150,6 +218,34 @@ public Builder incremental(boolean incremental) {
150218
return this;
151219
}
152220

221+
/**
222+
* Include or exclude incremental reasoning chunk emissions.
223+
*
224+
* <p>When {@link EventType#REASONING} is enabled, some providers emit reasoning deltas (chunks)
225+
* as the model thinks. Set to false to hide these.</p>
226+
*
227+
* @param includeReasoningChunk true to include chunk emissions, false to filter them out
228+
* @return this builder
229+
*/
230+
public Builder includeReasoningChunk(boolean includeReasoningChunk) {
231+
this.includeReasoningChunk = includeReasoningChunk;
232+
return this;
233+
}
234+
235+
/**
236+
* Include or exclude the final consolidated reasoning result emission.
237+
*
238+
* <p>When {@link EventType#REASONING} is enabled, some providers emit a final reasoning result.
239+
* Set to false to hide it.</p>
240+
*
241+
* @param includeReasoningResult true to include the final reasoning result, false to filter it out
242+
* @return this builder
243+
*/
244+
public Builder includeReasoningResult(boolean includeReasoningResult) {
245+
this.includeReasoningResult = includeReasoningResult;
246+
return this;
247+
}
248+
153249
public StreamOptions build() {
154250
return new StreamOptions(this);
155251
}

agentscope-core/src/test/java/io/agentscope/core/agent/StreamOptionsTest.java

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,14 @@
2121

2222
import org.junit.jupiter.api.Test;
2323

24-
/**
25-
* Tests for {@link StreamOptions}.
26-
*/
24+
/** Tests for {@link StreamOptions}. */
2725
class StreamOptionsTest {
2826

2927
@Test
3028
void testDefaults() {
3129
StreamOptions options = StreamOptions.defaults();
3230

33-
// Default should include all event types except AGENT_RESULT
31+
// Default should include ALL event types (based on current StreamOptions implementation)
3432
assertTrue(options.shouldStream(EventType.REASONING));
3533
assertTrue(options.shouldStream(EventType.TOOL_RESULT));
3634
assertTrue(options.shouldStream(EventType.HINT));
@@ -39,6 +37,14 @@ void testDefaults() {
3937

4038
// Default should be incremental mode
4139
assertTrue(options.isIncremental());
40+
41+
// Default should include both reasoning chunk and reasoning result
42+
assertTrue(options.isIncludeReasoningChunk());
43+
assertTrue(options.isIncludeReasoningResult());
44+
45+
// Convenience helper should respect defaults
46+
assertTrue(options.shouldIncludeReasoningEmission(true)); // chunk
47+
assertTrue(options.shouldIncludeReasoningEmission(false)); // result
4248
}
4349

4450
@Test
@@ -50,6 +56,7 @@ void testBuilderEventTypes() {
5056
assertFalse(options.shouldStream(EventType.TOOL_RESULT));
5157
assertFalse(options.shouldStream(EventType.HINT));
5258
assertFalse(options.shouldStream(EventType.SUMMARY));
59+
assertFalse(options.shouldStream(EventType.AGENT_RESULT));
5360
}
5461

5562
@Test
@@ -64,6 +71,7 @@ void testBuilderMultipleEventTypes() {
6471
assertTrue(options.shouldStream(EventType.TOOL_RESULT));
6572
assertFalse(options.shouldStream(EventType.HINT));
6673
assertFalse(options.shouldStream(EventType.SUMMARY));
74+
assertFalse(options.shouldStream(EventType.AGENT_RESULT));
6775
}
6876

6977
@Test
@@ -89,13 +97,6 @@ void testBuilderIncrementalMode() {
8997
assertFalse(cumulativeOptions.isIncremental());
9098
}
9199

92-
@Test
93-
void testAgentResultRequiresExplicitOptIn() {
94-
// Even with ALL event types, AGENT_RESULT is not included by default
95-
StreamOptions options = StreamOptions.builder().eventTypes(EventType.ALL).build();
96-
assertTrue(options.shouldStream(EventType.AGENT_RESULT));
97-
}
98-
99100
@Test
100101
void testGetEventTypes() {
101102
StreamOptions options =
@@ -137,4 +138,63 @@ void testFilteringByEventType() {
137138
assertFalse(reasoningOnly.shouldStream(EventType.SUMMARY));
138139
assertFalse(reasoningOnly.shouldStream(EventType.AGENT_RESULT));
139140
}
141+
142+
@Test
143+
void testReasoningChunkAndResultFlags_DefaultsTrue() {
144+
StreamOptions options = StreamOptions.builder().eventTypes(EventType.REASONING).build();
145+
146+
assertTrue(options.isIncludeReasoningChunk());
147+
assertTrue(options.isIncludeReasoningResult());
148+
149+
assertTrue(options.shouldIncludeReasoningEmission(true)); // chunk
150+
assertTrue(options.shouldIncludeReasoningEmission(false)); // result
151+
}
152+
153+
@Test
154+
void testReasoningChunkDisabled_ResultEnabled() {
155+
StreamOptions options =
156+
StreamOptions.builder()
157+
.eventTypes(EventType.REASONING)
158+
.includeReasoningChunk(false)
159+
.includeReasoningResult(true)
160+
.build();
161+
162+
assertFalse(options.isIncludeReasoningChunk());
163+
assertTrue(options.isIncludeReasoningResult());
164+
165+
assertFalse(options.shouldIncludeReasoningEmission(true)); // chunk filtered
166+
assertTrue(options.shouldIncludeReasoningEmission(false)); // result allowed
167+
}
168+
169+
@Test
170+
void testReasoningChunkEnabled_ResultDisabled() {
171+
StreamOptions options =
172+
StreamOptions.builder()
173+
.eventTypes(EventType.REASONING)
174+
.includeReasoningChunk(true)
175+
.includeReasoningResult(false)
176+
.build();
177+
178+
assertTrue(options.isIncludeReasoningChunk());
179+
assertFalse(options.isIncludeReasoningResult());
180+
181+
assertTrue(options.shouldIncludeReasoningEmission(true)); // chunk allowed
182+
assertFalse(options.shouldIncludeReasoningEmission(false)); // result filtered
183+
}
184+
185+
@Test
186+
void testReasoningChunkAndResultBothDisabled() {
187+
StreamOptions options =
188+
StreamOptions.builder()
189+
.eventTypes(EventType.REASONING)
190+
.includeReasoningChunk(false)
191+
.includeReasoningResult(false)
192+
.build();
193+
194+
assertFalse(options.isIncludeReasoningChunk());
195+
assertFalse(options.isIncludeReasoningResult());
196+
197+
assertFalse(options.shouldIncludeReasoningEmission(true)); // chunk filtered
198+
assertFalse(options.shouldIncludeReasoningEmission(false)); // result filtered
199+
}
140200
}

0 commit comments

Comments
 (0)