Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,12 @@ public Msg buildFinalMessage() {

// Add thinking content if present
if (thinkingAcc.hasContent()) {
blocks.add(thinkingAcc.buildAggregated());
blocks.addAll(thinkingAcc.buildAllThinkingBlocks());
}

// Add text content if present
if (textAcc.hasContent()) {
blocks.add(textAcc.buildAggregated());
blocks.addAll(textAcc.buildAllTextBlocks());
}

// Add all tool calls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
package io.agentscope.core.agent.accumulator;

import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.ContentBlockMetadataKeys;
import io.agentscope.core.message.TextBlock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Text content accumulator for accumulating streaming text chunks.
Expand All @@ -27,14 +32,34 @@
public class TextAccumulator implements ContentAccumulator<TextBlock> {

private final StringBuilder accumulated = new StringBuilder();
private final Map<String, Object> metadata = new HashMap<>();
private final List<TextBlock> completedBlocks = new ArrayList<>();
private final StringBuilder currentBlock = new StringBuilder();
private final Map<String, Object> currentMetadata = new HashMap<>();

/**
* @hidden
*/
@Override
public void add(TextBlock block) {
if (block != null && block.getText() != null) {
accumulated.append(block.getText());
if (block == null) {
return;
}

accumulated.append(block.getText());
currentBlock.append(block.getText());

Map<String, Object> blockMetadata = block.getMetadata();
if (blockMetadata != null) {
metadata.putAll(blockMetadata);
currentMetadata.putAll(blockMetadata);
}

// A thought signature terminates its provider Part. Keep that boundary so distinct
// signature-bearing Parts can be replayed without merging their metadata.
if (blockMetadata != null
&& blockMetadata.get(ContentBlockMetadataKeys.THOUGHT_SIGNATURE) != null) {
completeCurrentBlock();
}
}

Expand All @@ -43,7 +68,7 @@ public void add(TextBlock block) {
*/
@Override
public boolean hasContent() {
return accumulated.length() > 0;
return accumulated.length() > 0 || !metadata.isEmpty();
}

/**
Expand All @@ -54,7 +79,28 @@ public ContentBlock buildAggregated() {
if (!hasContent()) {
return null;
}
return TextBlock.builder().text(accumulated.toString()).build();
return TextBlock.builder()
.text(accumulated.toString())
.metadata(metadata.isEmpty() ? null : metadata)
.build();
}

/**
* Build accumulated text blocks while preserving thought-signature Part boundaries.
*
* @hidden
* @return accumulated text blocks in their original order
*/
public List<TextBlock> buildAllTextBlocks() {
if (!hasContent()) {
return List.of();
}

List<TextBlock> blocks = new ArrayList<>(completedBlocks);
if (currentBlock.length() > 0 || !currentMetadata.isEmpty()) {
blocks.add(buildCurrentBlock());
}
return List.copyOf(blocks);
}

/**
Expand All @@ -63,6 +109,10 @@ public ContentBlock buildAggregated() {
@Override
public void reset() {
accumulated.setLength(0);
metadata.clear();
completedBlocks.clear();
currentBlock.setLength(0);
currentMetadata.clear();
}

/**
Expand All @@ -74,4 +124,17 @@ public void reset() {
public String getAccumulated() {
return accumulated.toString();
}

private void completeCurrentBlock() {
completedBlocks.add(buildCurrentBlock());
currentBlock.setLength(0);
currentMetadata.clear();
}

private TextBlock buildCurrentBlock() {
return TextBlock.builder()
.text(currentBlock.toString())
.metadata(currentMetadata.isEmpty() ? null : currentMetadata)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
package io.agentscope.core.agent.accumulator;

import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.ContentBlockMetadataKeys;
import io.agentscope.core.message.ThinkingBlock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Thinking content accumulator for accumulating streaming thinking chunks.
Expand All @@ -28,14 +33,34 @@
public class ThinkingAccumulator implements ContentAccumulator<ThinkingBlock> {

private final StringBuilder accumulated = new StringBuilder();
private final Map<String, Object> metadata = new HashMap<>();
private final List<ThinkingBlock> completedBlocks = new ArrayList<>();
private final StringBuilder currentBlock = new StringBuilder();
private final Map<String, Object> currentMetadata = new HashMap<>();

/**
* @hidden
*/
@Override
public void add(ThinkingBlock block) {
if (block != null && block.getThinking() != null) {
accumulated.append(block.getThinking());
if (block == null) {
return;
}

accumulated.append(block.getThinking());
currentBlock.append(block.getThinking());

Map<String, Object> blockMetadata = block.getMetadata();
if (blockMetadata != null) {
metadata.putAll(blockMetadata);
currentMetadata.putAll(blockMetadata);
}

// A thought signature terminates its provider Part. Keep that boundary so distinct
// signature-bearing Parts can be replayed without merging their metadata.
if (blockMetadata != null
&& blockMetadata.get(ContentBlockMetadataKeys.THOUGHT_SIGNATURE) != null) {
completeCurrentBlock();
}
}

Expand All @@ -44,7 +69,7 @@ public void add(ThinkingBlock block) {
*/
@Override
public boolean hasContent() {
return accumulated.length() > 0;
return accumulated.length() > 0 || !metadata.isEmpty();
}

/**
Expand All @@ -55,7 +80,28 @@ public ContentBlock buildAggregated() {
if (!hasContent()) {
return null;
}
return ThinkingBlock.builder().thinking(accumulated.toString()).build();
return ThinkingBlock.builder()
.thinking(accumulated.toString())
.metadata(metadata.isEmpty() ? null : metadata)
.build();
}

/**
* Build accumulated thinking blocks while preserving thought-signature Part boundaries.
*
* @hidden
* @return accumulated thinking blocks in their original order
*/
public List<ThinkingBlock> buildAllThinkingBlocks() {
if (!hasContent()) {
return List.of();
}

List<ThinkingBlock> blocks = new ArrayList<>(completedBlocks);
if (currentBlock.length() > 0 || !currentMetadata.isEmpty()) {
blocks.add(buildCurrentBlock());
}
return List.copyOf(blocks);
}

/**
Expand All @@ -64,6 +110,10 @@ public ContentBlock buildAggregated() {
@Override
public void reset() {
accumulated.setLength(0);
metadata.clear();
completedBlocks.clear();
currentBlock.setLength(0);
currentMetadata.clear();
}

/**
Expand All @@ -75,4 +125,17 @@ public void reset() {
public String getAccumulated() {
return accumulated.toString();
}

private void completeCurrentBlock() {
completedBlocks.add(buildCurrentBlock());
currentBlock.setLength(0);
currentMetadata.clear();
}

private ThinkingBlock buildCurrentBlock() {
return ThinkingBlock.builder()
.thinking(currentBlock.toString())
.metadata(currentMetadata.isEmpty() ? null : currentMetadata)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.core.message;

/** Constants for well-known content block metadata keys. */
public final class ContentBlockMetadataKeys {

private ContentBlockMetadataKeys() {}

/**
* Metadata key for an opaque model thought signature.
*
* <p>The value representation is provider-specific. For example, Gemini stores signatures as
* {@code byte[]} in memory; after a generic JSON round trip, the value is restored as a
* Base64-encoded {@link String}.
*/
public static final String THOUGHT_SIGNATURE = "thoughtSignature";
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
package io.agentscope.core.message;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashMap;
import java.util.Map;

/**
* Represents plain text content in a message.
Expand All @@ -25,21 +28,27 @@
* Text blocks are commonly used for user messages, assistant responses,
* and any other textual communication.
*
* <p>The text content can be empty but never null. The toString() method
* returns the text content for convenience.
* <p>The text content can be empty but never null. Optional metadata preserves provider-specific
* information associated with the text content. The toString() method returns the text content for
* convenience.
*/
public final class TextBlock extends ContentBlock {

private final String text;
private final Map<String, Object> metadata;

/**
* Creates a new text block for JSON deserialization.
*
* @param text The text content (null will be converted to empty string)
* @param metadata Optional provider-specific metadata
*/
@JsonCreator
private TextBlock(@JsonProperty("text") String text) {
private TextBlock(
@JsonProperty("text") String text,
@JsonProperty("metadata") Map<String, Object> metadata) {
this.text = text != null ? text : "";
this.metadata = metadata != null ? new HashMap<>(metadata) : null;
}

/**
Expand All @@ -51,6 +60,16 @@ public String getText() {
return text;
}

/**
* Gets provider-specific metadata associated with this text block.
*
* @return The metadata map, or null if no metadata is set
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public Map<String, Object> getMetadata() {
return metadata;
}

@Override
public String toString() {
return text;
Expand All @@ -71,6 +90,7 @@ public static Builder builder() {
public static class Builder {

private String text;
private Map<String, Object> metadata;

/**
* Sets the text content for the block.
Expand All @@ -83,13 +103,24 @@ public Builder text(String text) {
return this;
}

/**
* Sets provider-specific metadata for the block.
*
* @param metadata The metadata map
* @return This builder for chaining
*/
public Builder metadata(Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}

/**
* Builds a new TextBlock with the configured text.
*
* @return A new TextBlock instance (null text will be converted to empty string)
*/
public TextBlock build() {
return new TextBlock(text != null ? text : "");
return new TextBlock(text != null ? text : "", metadata);
}
}
}
Loading
Loading